From 8e54eebdcda1362834607d4f4a28c5c0fe0757cc Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Thu, 20 Oct 2016 17:04:40 -0700 Subject: [PATCH 001/110] Register template (#1362) * added email templates package Added ability to register html and react templates with new helper Reaction.registerTemplate. * Schema updates and render render functions Added handlebars render functions, as well as the ability to register a custom template renderer. Added caching for handlebars templates. Updated templates schema with more properties. * Updated tests and template registration functionality * add handlebars --- .../email-templates/lib/templates/standard.js | 12 ++ .../included/email-templates/register.js | 0 .../included/email-templates/server/index.js | 9 + lib/collections/schemas/templates.js | 41 ++++ lib/core/templates.js | 5 + package.json | 1 + server/api/core/core.js | 18 ++ server/api/core/import.js | 18 ++ server/api/core/templates.app-test.js | 97 +++++++++ server/api/core/templates.js | 184 ++++++++++++++++++ 10 files changed, 385 insertions(+) create mode 100644 imports/plugins/included/email-templates/lib/templates/standard.js create mode 100644 imports/plugins/included/email-templates/register.js create mode 100644 imports/plugins/included/email-templates/server/index.js create mode 100644 lib/core/templates.js create mode 100644 server/api/core/templates.app-test.js create mode 100644 server/api/core/templates.js diff --git a/imports/plugins/included/email-templates/lib/templates/standard.js b/imports/plugins/included/email-templates/lib/templates/standard.js new file mode 100644 index 00000000000..c1b5b1f6ea9 --- /dev/null +++ b/imports/plugins/included/email-templates/lib/templates/standard.js @@ -0,0 +1,12 @@ +import { html } from "/lib/core/templates"; + +// Standard HTML Email templste to be processed by Handlebars +const StandardTemplate = html` + + + "Great!!!" {{title}} + + +`; + +export default StandardTemplate; diff --git a/imports/plugins/included/email-templates/register.js b/imports/plugins/included/email-templates/register.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/imports/plugins/included/email-templates/server/index.js b/imports/plugins/included/email-templates/server/index.js new file mode 100644 index 00000000000..ea06c9ad494 --- /dev/null +++ b/imports/plugins/included/email-templates/server/index.js @@ -0,0 +1,9 @@ +import { Reaction } from "/server/api"; +import StandardTemplate from "../lib/templates/standard"; + +Reaction.registerTemplate({ + title: "Standard Email", + name: "standard-email", + type: "email", + template: StandardTemplate +}); diff --git a/lib/collections/schemas/templates.js b/lib/collections/schemas/templates.js index 567031b65c6..47ea3f69e9e 100644 --- a/lib/collections/schemas/templates.js +++ b/lib/collections/schemas/templates.js @@ -1,7 +1,48 @@ import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import { Audience } from "./layouts"; export const Templates = new SimpleSchema({ + name: { + type: String, + index: true + }, + priority: { + type: Number, + optional: true, + defaultValue: 1 + }, + enabled: { + type: Boolean, + defaultValue: true + }, + route: { + type: String, + optional: true + }, + audience: { + type: [Audience], + optional: true + }, + type: { + type: String + }, + provides: { + type: String, + defaultValue: "template" + }, + block: { + type: String, + optional: true + }, + defaultData: { + type: Object, + blackbox: true + }, template: { + type: String, + optional: true + }, + parser: { type: String }, language: { diff --git a/lib/core/templates.js b/lib/core/templates.js new file mode 100644 index 00000000000..2db1de4af69 --- /dev/null +++ b/lib/core/templates.js @@ -0,0 +1,5 @@ + +// Template literal for html strings. +export function html(strings, ...values) { + return strings.raw[0]; +} diff --git a/package.json b/package.json index a50f397436e..1e3536c459b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "fibers": "^1.0.14", "font-awesome": "^4.6.3", "griddle-react": "^0.6.1", + "handlebars": "^4.0.5", "i18next": "^3.4.3", "i18next-browser-languagedetector": "^1.0.0", "i18next-localstorage-cache": "^0.3.0", diff --git a/server/api/core/core.js b/server/api/core/core.js index b3966081f9e..043df9f0f1a 100644 --- a/server/api/core/core.js +++ b/server/api/core/core.js @@ -6,6 +6,7 @@ import { Jobs, Packages, Shops } from "/lib/collections"; import { Hooks, Logger } from "/server/api"; import ProcessJobs from "/server/jobs"; import { getRegistryDomain } from "./setDomain"; +import { registerTemplate } from "./templates"; import { sendVerificationEmail } from "./accounts"; import { getMailUrl } from "./email/config"; @@ -46,6 +47,23 @@ export default { return registeredPackage; }, + registerTemplate(templateInfo, shopIds) { + if (typeof shopIds === "string") { + // Register template with supplied, single shopId + registerTemplate(templateInfo, shopIds); + } else if (Array.isArray(shopIds)) { + // Register template for all supplied shopIds + for (const shopId of shopIds) { + registerTemplate(templateInfo, shopId); + } + } + + // Otherwise template for all available shops + return Shops.find().forEach((shop) => { + registerTemplate(templateInfo, shop._id); + }); + }, + /** * hasPermission - server * server permissions checks diff --git a/server/api/core/import.js b/server/api/core/import.js index a75a0ce66b5..6e7a31425c3 100644 --- a/server/api/core/import.js +++ b/server/api/core/import.js @@ -250,6 +250,24 @@ Import.package = function (pkg, shopId) { // server/startup/i18n.js // +/** + * @summary Store a template in the import buffer. + * @param {Object} tempalteInfo The template data to be updated + * @param {String} shopId The package data to be updated + * @returns {undefined} + */ +Import.template = function (templateInfo, shopId) { + check(templateInfo, Object); + check(shopId, String); + + const key = { + name: templateInfo.name, + shopId: shopId + }; + + return this.object(Collections.Templates, key, templateInfo); +}; + /** * @summary Store a translation in the import buffer. * @param {Object} key A key to look up the translation diff --git a/server/api/core/templates.app-test.js b/server/api/core/templates.app-test.js new file mode 100644 index 00000000000..4e7b2a17004 --- /dev/null +++ b/server/api/core/templates.app-test.js @@ -0,0 +1,97 @@ +import React from "react"; +import { expect } from "meteor/practicalmeteor:chai"; +import Reaction from "./"; +import { + registerTemplate, + getTemplateByName, + renderTemplate, + resetRegisteredTemplates, + processTemplateInfoForMemoryCache, + TEMPLATE_PARSER_REACT, + TEMPLATE_PARSER_HANDLEBARS +} from "./templates"; +import { Templates } from "/lib/collections"; + + +function sampleReactComponent() { + return ( +
{"Test"}
+ ); +} + +describe("Templates:", function () { + beforeEach(function () { + Templates.direct.remove(); + resetRegisteredTemplates(); + }); + + it("It should process a handlebars template for memory cache", function () { + const expectedTemplate = processTemplateInfoForMemoryCache({ + name: "test-template", + template: "
Test
" + }); + + expect(expectedTemplate.name).to.be.equal("test-template"); + expect(expectedTemplate.parser).to.be.equal(TEMPLATE_PARSER_HANDLEBARS); + }); + + it("It should process a react component for memory cache", function () { + const expectedTemplate = processTemplateInfoForMemoryCache({ + name: "test-template", + template: sampleReactComponent + }); + + expect(expectedTemplate.name).to.be.equal("test-template"); + expect(expectedTemplate.parser).to.be.equal(TEMPLATE_PARSER_REACT); + expect(expectedTemplate.template).to.be.a("function"); + }); + + it("It should register Handlebars template", function () { + const shopId = Reaction.getShopId(); + // Register template + const sampleTemplate = { + name: "test-template", + template: "
Test
" + }; + registerTemplate(sampleTemplate, shopId); + + const actualTemplate = getTemplateByName("test-template", shopId); + expect(sampleTemplate.name).to.be.equal(actualTemplate.name); + expect(actualTemplate.parser).to.be.equal(TEMPLATE_PARSER_HANDLEBARS); + }); + + it("It should register Handlebars template and render to a string", function () { + const shopId = Reaction.getShopId(); + // Register template + const sampleTemplate = { + name: "test-template", + template: "
Test
" + }; + + registerTemplate(sampleTemplate, shopId); + + const actualTemplate = getTemplateByName("test-template", shopId); + expect(sampleTemplate.name).to.be.equal(actualTemplate.name); + expect(actualTemplate.parser).to.be.equal(TEMPLATE_PARSER_HANDLEBARS); + + // Compile template to string + const renderedHtmlString = renderTemplate(actualTemplate); + expect(renderedHtmlString).to.be.a("string"); + }); + + it("It should register a React component", function () { + const shopId = Reaction.getShopId(); + const sampleTemplate = { + name: "test-template-react", + template: sampleReactComponent + }; + + registerTemplate(sampleTemplate, shopId); + + const actualTemplate = getTemplateByName("test-template-react", shopId); + + expect(sampleTemplate.name).to.be.equal(actualTemplate.name); + expect(actualTemplate.parser).to.be.equal(TEMPLATE_PARSER_REACT); + expect(actualTemplate.template).to.be.a("function"); + }); +}); diff --git a/server/api/core/templates.js b/server/api/core/templates.js new file mode 100644 index 00000000000..abfffc1d850 --- /dev/null +++ b/server/api/core/templates.js @@ -0,0 +1,184 @@ +import React from "react"; +import ReactDOMServer from "react-dom/server"; +import Handlebars from "handlebars"; +import Import from "./import"; +import Immutable from "immutable"; +import { Templates } from "/lib/collections"; + +let registeredTemplates = Immutable.OrderedMap(); +let templateCache = Immutable.Map(); +let templateParsers = Immutable.Map(); + +// var ReactComponentPrototype = React.Component.prototype +// var ReactClassComponentPrototype = (Object.getPrototypeOf(Object.getPrototypeOf(new (React.createClass({ render () {} }))()))) + +export const TEMPLATE_PARSER_REACT = "react"; +export const TEMPLATE_PARSER_HANDLEBARS = "handlebars"; + +export function registerTemplate(templateInfo, shopId, insertImmediately = false) { + const literal = registerTemplateForMemoryCache(templateInfo, shopId); + const reference = registerTemplateForDatabase(templateInfo, shopId, insertImmediately); + + return { + templateLiteral: literal, + templateReference: reference + }; +} + +export function registerTemplateForMemoryCache(templateInfo, shopId) { + // Process template info and cache in memory. + // This allows us to have function and class references for the templates for + // React and other custom parsers + const templateInfoForMemoryCache = processTemplateInfoForMemoryCache(templateInfo); + + + let shopTemplates = registeredTemplates.get(shopId); + + if (!shopTemplates) { + shopTemplates = {}; + } + + shopTemplates[templateInfo.name] = templateInfoForMemoryCache; + registeredTemplates = registeredTemplates.set(shopId, shopTemplates); + + return templateInfoForMemoryCache; +} + +export function registerTemplateForDatabase(templateInfo, shopId, insertImmediately = false) { + // Process template info for use in a database + // Namely, any literals like functions are stripped as they cannot be safetly, + // and should not stored in the database + const templateInfoForDatabase = processTemplateInfoForDatabase(templateInfo); + + Import.template(templateInfoForDatabase, shopId); + + if (insertImmediately) { + Import.flush(); + } + + // Return template data crafted for entry into a database + return templateInfoForDatabase; +} + +export function getTemplateByName(templateName, shopId) { + const registeredTemplate = registeredTemplates.get(shopId)[templateName]; + + if (registeredTemplate) { + return registeredTemplate; + } + + const templateInfo = Templates.findOne({ + name: templateName, + shopId + }); + + return registerTemplateForMemoryCache(templateInfo); +} + +export function processTemplateInfoForMemoryCache(templateInfo) { + // Avoid mutating the original passed in param + const info = Immutable.Map(templateInfo); + + if (typeof templateInfo.template === "string") { + // Set the template parser to Handlebars for string based templates + return info.set("parser", TEMPLATE_PARSER_HANDLEBARS).toObject(); + } else if (typeof templateInfo.template === "function") { + // Set the parser to react for React components + return info.set("parser", TEMPLATE_PARSER_REACT).toObject(); + } + + return null; +} + +export function processTemplateInfoForDatabase(templateInfo) { + const templateData = { + name: templateInfo.name, + title: templateInfo.title, + type: templateInfo.type, + templateData: templateInfo.template + }; + + + if (typeof templateInfo.template === "string") { + templateData.template = templateInfo.template; + templateData.parser = TEMPLATE_PARSER_HANDLEBARS; + } else if (typeof templateInfo.template === "function") { + templateData.parser = TEMPLATE_PARSER_REACT; + } + + return templateData; +} + + +export function registerTemplateParser(name, renderFunction) { + templateParsers = templateParsers.set(name, renderFunction); +} + +export function renderTemplate(templateInfo, data = {}) { + if (templateInfo.parser === TEMPLATE_PARSER_REACT) { + return null; + } else if (templateInfo.parser === TEMPLATE_PARSER_HANDLEBARS) { + return renderHandlebarsTemplate(templateInfo, data); + } + + if (typeof templateParsers.get(name) === "function") { + return templateParsers.get(name)(templateInfo, data); + } + + return false; +} + +/** + * Compile and cache Handlebars template + * @param {String} name Name of template to register amd save to cache + * @param {String} template markup + * @return {Function} Compiled handlebars template. + */ +export function compileHandlebarsTemplate(name, template) { + const compiledTemplate = Handlebars.compile(template); + templateCache = templateCache.set(name, compiledTemplate); + return compiledTemplate; +} + +export function renderHandlebarsTemplate(templateInfo, data) { + if (templateCache[templateInfo.name] === undefined) { + compileHandlebarsTemplate(templateInfo.name, templateInfo.template); + } + + const compiledTemplate = templateCache.get(templateInfo.name); + return compiledTemplate(data); +} + +export function renderTemplateToStaticMarkup(template, props) { + return ReactDOMServer.renderToStaticMarkup( + React.createElement(template, props) + ); +} + +/** + * Reset regestered templates + * This is mostly useful for aiding in unit testing + * @return {Immutable.OrderedMap} immultable.js OrderedMap + */ +export function resetRegisteredTemplates() { + registeredTemplates = Immutable.OrderedMap(); +} + +export default { + get registeredTemplates() { + return registeredTemplates; + }, + get templateCache() { + return templateCache; + }, + get templateParsers() { + return templateParsers; + }, + registerTemplate, + getTemplateByName, + processTemplateInfoForDatabase, + processTemplateInfoForMemoryCache, + compileHandlebarsTemplate, + renderHandlebarsTemplate, + renderTemplateToStaticMarkup +}; From e48f46632d6856d5d268f04ef28bdfcf00883e20 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Fri, 21 Oct 2016 10:39:54 -0700 Subject: [PATCH 002/110] Quick fixes for template registration (#1518) Fixed import of "Import" module. Fixed type in jsDoc comments for "Import.template" function. --- server/api/core/import.js | 2 +- server/api/core/templates.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api/core/import.js b/server/api/core/import.js index 6e7a31425c3..f0203498660 100644 --- a/server/api/core/import.js +++ b/server/api/core/import.js @@ -252,7 +252,7 @@ Import.package = function (pkg, shopId) { /** * @summary Store a template in the import buffer. - * @param {Object} tempalteInfo The template data to be updated + * @param {Object} templateInfo The template data to be updated * @param {String} shopId The package data to be updated * @returns {undefined} */ diff --git a/server/api/core/templates.js b/server/api/core/templates.js index abfffc1d850..164aa8c7e0a 100644 --- a/server/api/core/templates.js +++ b/server/api/core/templates.js @@ -1,8 +1,8 @@ import React from "react"; import ReactDOMServer from "react-dom/server"; import Handlebars from "handlebars"; -import Import from "./import"; import Immutable from "immutable"; +import { Import } from "./import"; import { Templates } from "/lib/collections"; let registeredTemplates = Immutable.OrderedMap(); From 665432d59d384101b963d30299e8bf1b11e8f5b0 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Sun, 23 Oct 2016 10:52:33 +0800 Subject: [PATCH 003/110] Remove unused imports --- .../client/templates/searchModal/searchModal.html | 10 ---------- .../client/templates/searchModal/searchModal.js | 5 +---- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.html b/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.html index 263502ba5e6..836b9dbf98f 100644 --- a/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.html +++ b/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.html @@ -1,21 +1,11 @@ diff --git a/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.js b/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.js index cb48d2be165..cb0c6ca0566 100644 --- a/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.js +++ b/imports/plugins/included/ui-search/client/templates/searchModal/searchModal.js @@ -1,10 +1,7 @@ import _ from "lodash"; -import React from "react"; -import { DataType } from "react-taco-table"; import { Template } from "meteor/templating"; -import { i18next } from "/client/api"; import { ProductSearch, Tags, OrderSearch, AccountSearch } from "/lib/collections"; -import { IconButton, SortableTable } from "/imports/plugins/core/ui/client/components"; +import { IconButton } from "/imports/plugins/core/ui/client/components"; /* * searchModal extra functions From 3789059664ede076deb4cd0b1b5767d35e9a82b3 Mon Sep 17 00:00:00 2001 From: Mike Murray Date: Mon, 24 Oct 2016 16:01:20 -0700 Subject: [PATCH 004/110] Add templates to database (#1522) * Register all email templates * Fetch email templates from database * Add helper to get email template file without any extra dependencies * Update method for fetching email template file * Fix template paths * Re-added missing comment * Fixed path to core order "new" email template --- .../included/email-templates/lib/paths.js | 16 ++++ .../included/email-templates/server/index.js | 89 ++++++++++++++++++- server/api/core/email/email.js | 23 +++-- 3 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 imports/plugins/included/email-templates/lib/paths.js diff --git a/imports/plugins/included/email-templates/lib/paths.js b/imports/plugins/included/email-templates/lib/paths.js new file mode 100644 index 00000000000..b2e53fec1b2 --- /dev/null +++ b/imports/plugins/included/email-templates/lib/paths.js @@ -0,0 +1,16 @@ +// Tempalte paths relative to private/email/templates +export const coreDefaultTemplate = "coreDefault"; + +export const resetPaswordTemplate = "accounts/reset_password"; +export const inviteShopMemberTemplate = "accounts/inviteShopMember"; +export const welcomeEmailTemplate = "accounts/sendWelcomeEmail"; +export const verifyEmailTemplate = "accounts/verify_email"; + +export const checkoutLoginTemplate = "checkout/checkoutLogin"; + +export const coreOrderCompletedTemplate = "orders/coreOrderCompleted"; +export const coreOrderCreatedTemplate = "orders/coreOrderCreated"; +export const coreOrderShippingInvoiceTemplate = "orders/coreOrderShippingInvoice"; +export const coreOrderShippingSummaryTemplate = "orders/coreOrderShippingSummary"; +export const coreOrderShippingTrackingTemplate = "orders/coreOrderShippingTracking"; +export const coreOrderNewTemplate = "orders/new"; diff --git a/imports/plugins/included/email-templates/server/index.js b/imports/plugins/included/email-templates/server/index.js index ea06c9ad494..80072c5da37 100644 --- a/imports/plugins/included/email-templates/server/index.js +++ b/imports/plugins/included/email-templates/server/index.js @@ -1,9 +1,90 @@ import { Reaction } from "/server/api"; -import StandardTemplate from "../lib/templates/standard"; +import * as TemplatePaths from "../lib/paths.js"; +// Default Reaction.registerTemplate({ - title: "Standard Email", - name: "standard-email", + title: "Default", + name: TemplatePaths.coreDefaultTemplate, type: "email", - template: StandardTemplate + template: Reaction.Email.getTemplateFile(TemplatePaths.coreDefaultTemplate) +}); + +// Accounts +Reaction.registerTemplate({ + title: "Reset Password", + name: TemplatePaths.resetPaswordTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.resetPaswordTemplate) +}); + +Reaction.registerTemplate({ + title: "Invite Shop Member", + name: TemplatePaths.inviteShopMemberTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.inviteShopMemberTemplate) +}); + +Reaction.registerTemplate({ + title: "Welcome Email", + name: TemplatePaths.welcomeEmailTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.welcomeEmailTemplate) +}); + +Reaction.registerTemplate({ + title: "Verify Account", + name: TemplatePaths.verifyEmailTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.verifyEmailTemplate) +}); + +// Checkout +Reaction.registerTemplate({ + title: "Checkout Login", + name: TemplatePaths.checkoutLoginTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.checkoutLoginTemplate) +}); + +// Order workflow +Reaction.registerTemplate({ + title: "Order Completed", + name: TemplatePaths.coreOrderCompletedTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderCompletedTemplate) +}); + +Reaction.registerTemplate({ + title: "Order Created", + name: TemplatePaths.coreOrderCreatedTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderCreatedTemplate) +}); + +Reaction.registerTemplate({ + title: "Shipping Invoice", + name: TemplatePaths.coreOrderShippingInvoiceTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderShippingInvoiceTemplate) +}); + +Reaction.registerTemplate({ + title: "Shipping Summary", + name: TemplatePaths.coreOrderShippingSummaryTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderShippingSummaryTemplate) +}); + +Reaction.registerTemplate({ + title: "Shipping Tracking", + name: TemplatePaths.coreOrderShippingTrackingTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderShippingTrackingTemplate) +}); + +Reaction.registerTemplate({ + title: "New Order Started Processing", + name: TemplatePaths.coreOrderNewTemplate, + type: "email", + template: Reaction.Email.getTemplateFile(TemplatePaths.coreOrderNewTemplate) }); diff --git a/server/api/core/email/email.js b/server/api/core/email/email.js index b931bfb3779..4ef485b4481 100644 --- a/server/api/core/email/email.js +++ b/server/api/core/email/email.js @@ -43,18 +43,31 @@ export function getTemplate(template) { } // check database for a matching template - const tmpl = Templates.findOne({ template, language }); + const tmpl = Templates.findOne({ name: template, language }); // use that template if found - if (tmpl && tmpl.source) { - return tmpl.source; + if (tmpl && tmpl.template) { + return tmpl.template; } // otherwise, use the default template from the filesystem - const file = `email/templates/${template}.html`; + return getTemplateFile(template); +} + +/** + * Reaction.Email.getTemplateFile + * @param {String} file name of the template on file system + * @return {String} returns source + */ +export function getTemplateFile(file) { + if (typeof file !== "string") { + const msg = "Reaction.Email.getTemplateFile() requires a template name"; + Logger.error(msg); + throw new Meteor.Error("no-template-name", msg); + } try { - return Assets.getText(file); + return Assets.getText(`email/templates/${file}.html`); } catch (e) { Logger.warn(`Template not found: ${file}. Falling back to coreDefault.html`); return Assets.getText("email/templates/coreDefault.html"); From 1cb8eb867eef8ca6417c71c2342ef2dea4306f16 Mon Sep 17 00:00:00 2001 From: Erik Kieckhafer Date: Mon, 24 Oct 2016 16:21:28 -0700 Subject: [PATCH 005/110] Admin Invite & Password reset emails (#1523) * added some comments to break up html table * reorganized data for email template removed all [0]. type variables from new.html email template, so all variables are set in orders.js for easy manipulation * welcome to the shop email template * extract social from template, add on/off switch * invite shop member email * shopName variable updates * update reaction address This was driving me crazy seeing the wrong address in all these emails being tested. * add Collections imports for Accounts, Cart, Shopts * password reset emails * update subject of order confirmation email * lint fix * renamed file for email template * removed test text --- private/data/Shops.json | 6 +- .../templates/accounts/inviteShopMember.html | 1028 +++------------- .../templates/accounts/resetPassword.html | 172 +++ .../templates/accounts/reset_password.html | 7 - .../templates/accounts/sendWelcomeEmail.html | 1030 +++-------------- private/email/templates/orders/new.html | 53 +- server/api/core/accounts/password.js | 61 +- server/methods/accounts/accounts.js | 135 ++- server/methods/core/orders.js | 59 +- 9 files changed, 736 insertions(+), 1815 deletions(-) create mode 100644 private/email/templates/accounts/resetPassword.html delete mode 100644 private/email/templates/accounts/reset_password.html diff --git a/private/data/Shops.json b/private/data/Shops.json index 5db4b311c98..dcb382afa51 100755 --- a/private/data/Shops.json +++ b/private/data/Shops.json @@ -3064,11 +3064,11 @@ "addressBook": [{ "company": "Reaction Commerce", "fullName": "Ongo Works", - "address1": "1119 Colorado Ave", - "address2": "Ste. 7", + "address1": "2110 Main Street", + "address2": "Suite 207", "city": "Santa Monica", "region": "CA", - "postal": "90401", + "postal": "90405", "country": "US", "phone": "8885551212", "isCommercial": true, diff --git a/private/email/templates/accounts/inviteShopMember.html b/private/email/templates/accounts/inviteShopMember.html index 6104df3c3e0..39335b147bc 100644 --- a/private/email/templates/accounts/inviteShopMember.html +++ b/private/email/templates/accounts/inviteShopMember.html @@ -1,900 +1,172 @@ - - - - + + + +Basic Email + + + - - - - + +
-
- - - - - -
-
- - - - -
- - - - - - -
- {{shop.name}} -
- -
-
-
-
+ + + + + +
+ + +
+ + + + + - -
 
+ + - -
+ + + + + + + + + + + + -
 
logo
 
- - + + + + + + + + + + + + + + + - -
+ +
Hi, {{invitedUserName}}.
 
{{currentUserName}} has invited you to join {{shopName}}.
 
+ + + + +
Get Started 
+ - - -
- + + + + + + + + + + + + + {{#if socialLinks.display}} + + + + + -
 
 
 
You received this email because a user of {{shopName}} invited you to create an account. Questions or suggestions? Email us at {{contactEmail}}
 
+ - - - -
-

Hi, {{invitedUserName}}

+ {{#if socialLinks.twitter.display}} +
+ twt_icon
- - - + {{/if}} + {{#if socialLinks.facebook.display}} + + + {{/if}} + {{#if socialLinks.googlePlus.display}} + - + {{/if}}
  + fb_icon +   -

{{currentUserName}} invited you to join {{shop.name}}.

+ g_plus_icon
-
- - - + -
- - - - - - -
-

You're invited!

-

If you don't want to accept this invitation or think you were mistaken for someone else please let us know!

-
-
 
- + {{else}} - + - -


- + {{/if}} + - + + + + + + + + + + + + + + + + + -
+  
 
© {{copyrightDate}} {{legalName}}. All rights reserved
 
{{physicalAddress.address}}, {{physicalAddress.city}}, {{physicalAddress.region}} {{physicalAddress.postal}}
 
- - - - -
-
-

Sent by {{shop.name}}

-
-
-
- -
- -
+
+ + + + + + diff --git a/private/email/templates/accounts/resetPassword.html b/private/email/templates/accounts/resetPassword.html new file mode 100644 index 00000000000..a8a675fa41d --- /dev/null +++ b/private/email/templates/accounts/resetPassword.html @@ -0,0 +1,172 @@ + + + + +Basic Email + + + + + + + + + + +
+ + + + +
+ + + + + + +
 
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{#if socialLinks.display}} + + + + + + + + + + {{else}} + + + + {{/if}} + + + + + + + + + + + + + + + + + + + + + + +
 
logo
 
Reset Your Password
 
We've received a request to reset your password. If you didn't make the request, you can just ignore this email.
 
+ + + + +
Reset Password 
 
 
 
You received this email because you have an account with {{shopName}}. Questions or suggestions? Email us at {{contactEmail}}
 
+ + + {{#if socialLinks.twitter.display}} + + {{/if}} + {{#if socialLinks.facebook.display}} + + + {{/if}} + {{#if socialLinks.googlePlus.display}} + + + {{/if}} + +
+ twt_icon +   + fb_icon +   + g_plus_icon +
+
 
 
 
 
© {{copyrightDate}} {{legalName}}. All rights reserved
 
{{physicalAddress.address}}, {{physicalAddress.city}}, {{physicalAddress.region}} {{physicalAddress.postal}}
 
+
+ + diff --git a/private/email/templates/accounts/reset_password.html b/private/email/templates/accounts/reset_password.html deleted file mode 100644 index d756b426325..00000000000 --- a/private/email/templates/accounts/reset_password.html +++ /dev/null @@ -1,7 +0,0 @@ - -

Hello,

- -

To reset your password at {{shopName}}, click the link below:

- -

{{url}}

- diff --git a/private/email/templates/accounts/sendWelcomeEmail.html b/private/email/templates/accounts/sendWelcomeEmail.html index 1818477285d..716b912648a 100644 --- a/private/email/templates/accounts/sendWelcomeEmail.html +++ b/private/email/templates/accounts/sendWelcomeEmail.html @@ -1,902 +1,172 @@ - - - - + + + +Basic Email + + + - - - - + +
-
- - - - - -
-
- - - - -
- - - - - - - -
- {{shop.name}} -
-
-
-
-
+ + + + + +
+ + +
+ + + + + - -
 
+ + - -
+ + + + + + + + + + + + -
 
logo
 
- - + + + + + + + + + + + + + + + - -
+ +
Hello!
 
Welcome to {{shopName}}.
 
+ + + + +
Get Started 
+ + + + + + + + + + - - -
 
 
 
- + + + + {{#if socialLinks.display}} + + + + + -
You received this email because you have an account with {{shopName}}. Questions or suggestions? Email us at {{contactEmail}}
 
+ - - - -
-

Hi, {{user.username}}

+ {{#if socialLinks.twitter.display}} +
+ twt_icon
- - - + {{/if}} + {{#if socialLinks.facebook.display}} + + + {{/if}} + {{#if socialLinks.googlePlus.display}} + - + {{/if}}
  + fb_icon +   -

Welcome to {{shop.name}}.

+ g_plus_icon
-
- - - + -
- - - - - - -
- -

We're excited to have you at {{shop.name}}!

-

Thank you for taking the time to register at {{shop.name}}.

-

We'll keep this introduction simple. Welcome aboard!

-
-
 
- + {{else}} - + - -


- + {{/if}} + - + + + + + + + + + + + + + + + + + -
+  
 
© {{copyrightDate}} {{legalName}}. All rights reserved
 
{{physicalAddress.address}}, {{physicalAddress.city}}, {{physicalAddress.region}} {{physicalAddress.postal}}
 
- - - - -
-
-

Sent by {{shop.name}}

-
-
-
- -
- -
+
+ + + + + + diff --git a/private/email/templates/orders/new.html b/private/email/templates/orders/new.html index 2626eb78b96..2fc82149cd3 100644 --- a/private/email/templates/orders/new.html +++ b/private/email/templates/orders/new.html @@ -127,6 +127,8 @@ + + @@ -142,6 +144,9 @@ + + + + + - + - + + + + + + + + + {{#if socialLinks.display}} @@ -354,20 +368,26 @@ @@ -375,6 +395,12 @@ + {{else}} + + + + {{/if}} + @@ -382,15 +408,16 @@ - + - + + +
 
Order Confirmation
@@ -220,11 +225,11 @@
- - - + + @@ -339,14 +344,23 @@
{{shipping.address.address1}},
{{shipping.address.city}}, +
{{shipping.address.address}},
{{shipping.address.city}}, {{shipping.address.region}} {{shipping.address.postal}}
{{order.billing.[0].address.address1}},
{{order.billing.[0].address.city}}, - {{order.billing.[0].address.region}} {{order.billing.[0].address.postal}}
{{order.billing.[0].paymentMethod.storedCard}}{{billing.address.address}},
{{billing.address.city}}, + {{billing.address.region}} {{billing.address.postal}}
{{billing.paymentMethod}}
 
  
You received this email because you created an order with {{shop.addressBook.[0].company}}. If you have any questions, concerns, or suggestions please email us: - {{shop.emails.[0].address}} 
 
You received this email because you created an order with {{shopName}}. Questions or suggestions? Email us at {{contactEmail}}
 
+ {{#if socialLinks.twitter.display}} + {{/if}} + {{#if socialLinks.facebook.display}} + {{/if}} + {{#if socialLinks.googlePlus.display}} + {{/if}}
- twt_icon   - fb_icon   - g_plus_icon
 
 
 
 
© {{copyrightDate}} {{shop.addressBook.[0].company}}. All rights reserved© {{copyrightDate}} {{legalName}}. All rights reserved
 
{{shop.addressBook.[0].address1}} {{shop.addressBook.[0].address2}}, {{shop.addressBook.[0].city}}, - {{shop.addressBook.[0].region}} {{shop.addressBook.[0].postal}}{{physicalAddress.address}}, {{physicalAddress.city}}, {{physicalAddress.region}} {{physicalAddress.postal}}
diff --git a/server/api/core/accounts/password.js b/server/api/core/accounts/password.js index 50d1e76c8fe..c806a635131 100644 --- a/server/api/core/accounts/password.js +++ b/server/api/core/accounts/password.js @@ -2,6 +2,7 @@ import _ from "lodash"; import { Meteor } from "meteor/meteor"; import { Accounts } from "meteor/accounts-base"; import { SSR } from "meteor/meteorhacks:ssr"; +import { Media, Shops } from "/lib/collections"; import { Reaction, Logger } from "/server/api"; @@ -35,6 +36,7 @@ export function sendResetPasswordEmail(userId, optionalEmail) { throw new Meteor.Error("email-not-found", "Email not found"); } + // Create token for password reset const token = Random.secret(); const when = new Date(); const tokenObj = { token, email, when }; @@ -47,16 +49,65 @@ export function sendResetPasswordEmail(userId, optionalEmail) { Meteor._ensure(user, "services", "password").reset = tokenObj; - SSR.compileTemplate("resetPassword", Reaction.Email.getTemplate("accounts/reset_password")); + const tpl = "accounts/resetPassword"; + SSR.compileTemplate(tpl, Reaction.Email.getTemplate(tpl)); - const shopName = Reaction.getShopName(); - const url = Accounts.urls.resetPassword(token); + // Get shop data for email display + const shop = Shops.findOne(Reaction.getShopId()); + + // Get shop logo, if available. If not, use default logo from file-system + let emailLogo; + if (Array.isArray(shop.brandAssets)) { + const brandAsset = _.find(shop.brandAssets, (asset) => asset.type === "navbarBrandImage"); + const mediaId = Media.findOne(brandAsset.mediaId); + emailLogo = path.join(Meteor.absoluteUrl(), mediaId.url()); + } else { + emailLogo = Meteor.absoluteUrl() + "resources/email-templates/shop-logo.png"; + } + + const dataForEmail = { + // Shop Data + shop: shop, + contactEmail: shop.emails[0].address, + homepage: Meteor.absoluteUrl(), + emailLogo: emailLogo, + copyrightDate: moment().format("YYYY"), + legalName: shop.addressBook[0].company, + physicalAddress: { + address: shop.addressBook[0].address1 + " " + shop.addressBook[0].address2, + city: shop.addressBook[0].city, + region: shop.addressBook[0].region, + postal: shop.addressBook[0].postal + }, + shopName: shop.name, + socialLinks: { + display: true, + facebook: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/facebook-icon.png", + link: "https://www.facebook.com" + }, + googlePlus: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/google-plus-icon.png", + link: "https://plus.google.com" + }, + twitter: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/twitter-icon.png", + link: "https://www.twitter.com" + } + }, + // Account Data + passwordResetUrl: Accounts.urls.resetPassword(token), + user: user + }; return Reaction.Email.send({ to: email, from: Reaction.getShopEmail(), - subject: `${shopName} - Reset your password`, - html: SSR.render("resetPassword", { shopName, user, url }) + subject: `${dataForEmail.shopName}: Here's your password reset link`, + html: SSR.render(tpl, dataForEmail) }); } diff --git a/server/methods/accounts/accounts.js b/server/methods/accounts/accounts.js index da9140c9a55..a3dee38f226 100644 --- a/server/methods/accounts/accounts.js +++ b/server/methods/accounts/accounts.js @@ -1,4 +1,9 @@ +import _ from "lodash"; +import moment from "moment"; +import path from "path"; import * as Collections from "/lib/collections"; +// TODO: Change all imports from collections to only pull in needed? +// import { Accounts, Cart, Media, Shops } from "/lib/collections"; import * as Schemas from "/lib/collections/schemas"; import { Logger, Reaction } from "/server/api"; @@ -274,8 +279,8 @@ Meteor.methods({ "emails.address": email }); - const tmpl = "accounts/inviteShopMember"; - SSR.compileTemplate("accounts/inviteShopMember", Reaction.Email.getTemplate(tmpl)); + const tpl = "accounts/inviteShopMember"; + SSR.compileTemplate(tpl, Reaction.Email.getTemplate(tpl)); if (!user) { const userId = Accounts.createUser({ @@ -297,24 +302,71 @@ Meteor.methods({ } }); + // Get shop logo, if available. If not, use default logo from file-system + let emailLogo; + if (Array.isArray(shop.brandAssets)) { + const brandAsset = _.find(shop.brandAssets, (asset) => asset.type === "navbarBrandImage"); + const mediaId = Collections.Media.findOne(brandAsset.mediaId); + emailLogo = path.join(Meteor.absoluteUrl(), mediaId.url()); + } else { + emailLogo = Meteor.absoluteUrl() + "resources/email-templates/shop-logo.png"; + } + + const dataForEmail = { + // Shop Data + shop: shop, + contactEmail: shop.emails[0].address, + homepage: Meteor.absoluteUrl(), + emailLogo: emailLogo, + copyrightDate: moment().format("YYYY"), + legalName: shop.addressBook[0].company, + physicalAddress: { + address: shop.addressBook[0].address1 + " " + shop.addressBook[0].address2, + city: shop.addressBook[0].city, + region: shop.addressBook[0].region, + postal: shop.addressBook[0].postal + }, + shopName: shop.name, + socialLinks: { + display: true, + facebook: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/facebook-icon.png", + link: "https://www.facebook.com" + }, + googlePlus: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/google-plus-icon.png", + link: "https://plus.google.com" + }, + twitter: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/twitter-icon.png", + link: "https://www.twitter.com" + } + }, + // Account Data + user: Meteor.user(), + + + + currentUserName, + invitedUserName: name, + url: Accounts.urls.enrollAccount(token) + }; + Reaction.Email.send({ to: email, from: `${shop.name} <${shop.emails[0].address}>`, - subject: `You have been invited to join ${shop.name}`, - html: SSR.render("accounts/inviteShopMember", { - homepage: Meteor.absoluteUrl(), - shop, - currentUserName, - invitedUserName: name, - url: Accounts.urls.enrollAccount(token) - }) + subject: `You've been invited to join ${shop.name}`, + html: SSR.render(tpl, dataForEmail) }); } else { Reaction.Email.send({ to: email, from: `${shop.name} <${shop.emails[0].address}>`, - subject: `You have been invited to join ${shop.name}`, - html: SSR.render("accounts/inviteShopMember", { + subject: `You've been invited to join ${shop.name}`, + html: SSR.render(tpl, { homepage: Meteor.absoluteUrl(), shop, currentUserName, @@ -342,6 +394,53 @@ Meteor.methods({ const user = Collections.Accounts.findOne(userId); const shop = Collections.Shops.findOne(shopId); + // Get shop logo, if available. If not, use default logo from file-system + let emailLogo; + if (Array.isArray(shop.brandAssets)) { + const brandAsset = _.find(shop.brandAssets, (asset) => asset.type === "navbarBrandImage"); + const mediaId = Collections.Media.findOne(brandAsset.mediaId); + emailLogo = path.join(Meteor.absoluteUrl(), mediaId.url()); + } else { + emailLogo = Meteor.absoluteUrl() + "resources/email-templates/shop-logo.png"; + } + + const dataForEmail = { + // Shop Data + shop: shop, + contactEmail: shop.emails[0].address, + homepage: Meteor.absoluteUrl(), + emailLogo: emailLogo, + copyrightDate: moment().format("YYYY"), + legalName: shop.addressBook[0].company, + physicalAddress: { + address: shop.addressBook[0].address1 + " " + shop.addressBook[0].address2, + city: shop.addressBook[0].city, + region: shop.addressBook[0].region, + postal: shop.addressBook[0].postal + }, + shopName: shop.name, + socialLinks: { + display: true, + facebook: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/facebook-icon.png", + link: "https://www.facebook.com" + }, + googlePlus: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/google-plus-icon.png", + link: "https://plus.google.com" + }, + twitter: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/twitter-icon.png", + link: "https://www.twitter.com" + } + }, + // Account Data + user: Meteor.user() + }; + // anonymous users arent welcome here if (!user.emails || !user.emails.length > 0) { return true; @@ -358,18 +457,14 @@ Meteor.methods({ shopEmail = shop.emails[0].address; } - const tmpl = "accounts/sendWelcomeEmail"; - SSR.compileTemplate("accounts/sendWelcomeEmail", Reaction.Email.getTemplate(tmpl)); + const tpl = "accounts/sendWelcomeEmail"; + SSR.compileTemplate(tpl, Reaction.Email.getTemplate(tpl)); Reaction.Email.send({ to: userEmail, from: `${shop.name} <${shopEmail}>`, - subject: `Welcome to ${shop.name}!`, - html: SSR.render("accounts/sendWelcomeEmail", { - homepage: Meteor.absoluteUrl(), - shop: shop, - user: Meteor.user() - }) + subject: `You're In. Welcome to ${shop.name}!`, + html: SSR.render(tpl, dataForEmail) }); return true; diff --git a/server/methods/core/orders.js b/server/methods/core/orders.js index c1124488eab..e99ae9b3db3 100644 --- a/server/methods/core/orders.js +++ b/server/methods/core/orders.js @@ -334,7 +334,6 @@ Meteor.methods({ // Get Shop information const shop = Shops.findOne(order.shopId); - const shopContact = shop.addressBook[0]; // Get shop logo, if available let emailLogo; @@ -391,24 +390,66 @@ Meteor.methods({ } // Merge data into single object to pass to email template - const dataForOrderEmail = { + const dataForEmail = { + // Shop Data + shop: shop, + contactEmail: shop.emails[0].address, homepage: Meteor.absoluteUrl(), emailLogo: emailLogo, copyrightDate: moment().format("YYYY"), - shop: shop, - shopContact: shopContact, + legalName: shop.addressBook[0].company, + physicalAddress: { + address: shop.addressBook[0].address1 + " " + shop.addressBook[0].address2, + city: shop.addressBook[0].city, + region: shop.addressBook[0].region, + postal: shop.addressBook[0].postal + }, + shopName: shop.name, + socialLinks: { + display: true, + facebook: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/facebook-icon.png", + link: "https://www.facebook.com" + }, + googlePlus: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/google-plus-icon.png", + link: "https://plus.google.com" + }, + twitter: { + display: true, + icon: Meteor.absoluteUrl() + "resources/email-templates/twitter-icon.png", + link: "https://www.twitter.com" + } + }, + // Order Data order: order, - orderDate: moment(order.createdAt).format("MM/DD/YYYY"), billing: { + address: { + address: order.billing[0].address.address1, + city: order.billing[0].address.city, + region: order.billing[0].address.region, + postal: order.billing[0].address.postal + }, + paymentMethod: order.billing[0].paymentMethod.storedCard, subtotal: accounting.toFixed(order.billing[0].invoice.subtotal, 2), shipping: accounting.toFixed(order.billing[0].invoice.shipping, 2), taxes: accounting.toFixed(order.billing[0].invoice.taxes, 2), discounts: accounting.toFixed(order.billing[0].invoice.discounts, 2), total: accounting.toFixed(order.billing[0].invoice.total, 2) }, - shipping: order.shipping[0], + combinedItems: combinedItems, + orderDate: moment(order.createdAt).format("MM/DD/YYYY"), orderUrl: getSlug(shop.name) + "/cart/completed?_id=" + order.cartId, - combinedItems: combinedItems + shipping: { + address: { + address: order.shipping[0].address.address1, + city: order.shipping[0].address.city, + region: order.shipping[0].address.region, + postal: order.shipping[0].address.postal + } + } }; Logger.info(`orders/sendNotification status: ${order.workflow.status}`); @@ -434,9 +475,9 @@ Meteor.methods({ Reaction.Email.send({ to: order.email, from: `${shop.name} <${shop.emails[0].address}>`, - subject: `Your order is confirmed`, + subject: `${shop.name}: Your order is confirmed`, // subject: `Order update from ${shop.name}`, - html: SSR.render(tpl, dataForOrderEmail) + html: SSR.render(tpl, dataForEmail) }); return true; From 9e7fdca8b9a1da56ef4d3593b7937a3fb013acd8 Mon Sep 17 00:00:00 2001 From: Erik Kieckhafer Date: Tue, 25 Oct 2016 13:21:27 -0700 Subject: [PATCH 006/110] update const to conform to new file name (#1528) Will take care of `WARN Reaction: Template not found: accounts/reset_password. Falling back to coreDefault.html` error --- imports/plugins/included/email-templates/lib/paths.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/email-templates/lib/paths.js b/imports/plugins/included/email-templates/lib/paths.js index b2e53fec1b2..6b637ca7db7 100644 --- a/imports/plugins/included/email-templates/lib/paths.js +++ b/imports/plugins/included/email-templates/lib/paths.js @@ -1,7 +1,7 @@ // Tempalte paths relative to private/email/templates export const coreDefaultTemplate = "coreDefault"; -export const resetPaswordTemplate = "accounts/reset_password"; +export const resetPaswordTemplate = "accounts/resetPassword"; export const inviteShopMemberTemplate = "accounts/inviteShopMember"; export const welcomeEmailTemplate = "accounts/sendWelcomeEmail"; export const verifyEmailTemplate = "accounts/verify_email"; From c575fb53533d281671e28e2e950ebe77ce49bdf5 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 08:56:51 +0800 Subject: [PATCH 007/110] Add registry settings to package --- imports/plugins/included/ui-search/register.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/imports/plugins/included/ui-search/register.js b/imports/plugins/included/ui-search/register.js index 82d8b0d3e20..a83d2c615b4 100644 --- a/imports/plugins/included/ui-search/register.js +++ b/imports/plugins/included/ui-search/register.js @@ -4,5 +4,11 @@ Reaction.registerPackage({ label: "Search UI", name: "reaction-ui-search", icon: "fa fa-search", - autoEnable: true + autoEnable: true, + registry: [ + { + provides: "search-ui", + template: "searchModal" + } + ] }); From a2460a55b87f9706ecc9afc0ef0c85926e122000 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 08:57:27 +0800 Subject: [PATCH 008/110] Conditionally show search --- .../core/ui-navbar/client/components/navbar/navbar.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html index 0cd53e5ec97..c6e119282ad 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.html @@ -6,8 +6,9 @@ - - + {{#if isSearchEnabled}} + + {{/if}}
{{> accounts tpl="loginDropdown"}}
{{> cartIcon}}
From 9c8a0af6ff49318e4771d57129d165ef7bce46df Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 08:58:26 +0800 Subject: [PATCH 009/110] Make naming consistent --- imports/plugins/included/ui-search/register.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/included/ui-search/register.js b/imports/plugins/included/ui-search/register.js index a83d2c615b4..ebf9a0b304e 100644 --- a/imports/plugins/included/ui-search/register.js +++ b/imports/plugins/included/ui-search/register.js @@ -7,7 +7,7 @@ Reaction.registerPackage({ autoEnable: true, registry: [ { - provides: "search-ui", + provides: "ui-search", template: "searchModal" } ] From ceb5395bdd2f5c4a45f48eec227cedb8a34b6c3a Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 08:59:51 +0800 Subject: [PATCH 010/110] Look up in registry to determine whether to show search, and if so which template --- .../client/components/navbar/navbar.js | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js index ae94de7ab93..df26092062a 100644 --- a/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js +++ b/imports/plugins/core/ui-navbar/client/components/navbar/navbar.js @@ -1,9 +1,26 @@ import { FlatButton } from "/imports/plugins/core/ui/client/components"; import { Reaction } from "/client/api"; -import { Tags } from "/lib/collections"; +import { Tags, Packages } from "/lib/collections"; + +function getSearchPackage() { + const searchPackage = Packages.findOne({ + shopId: Reaction.getShopId(), + "registry.provides": "ui-search", + enabled: true + }); + return searchPackage; +} + Template.CoreNavigationBar.onCreated(function () { this.state = new ReactiveDict(); + const searchPackage = getSearchPackage(); + if (searchPackage) { + this.state.set("searchEnabled", true); + this.state.set("searchTemplate", searchPackage.registry[0].template); + } else { + this.state.set("searchEnabled", false); + } }); /** @@ -19,7 +36,10 @@ Template.CoreNavigationBar.events({ return $(".dashboard-navbar-packages ul li").removeClass("active"); }, "click .search": function () { - Blaze.renderWithData(Template.searchModal, { + const instance = Template.instance(); + const searchTemplateName = instance.state.get("searchTemplate"); + const searchTemplate = Template[searchTemplateName]; + Blaze.renderWithData(searchTemplate, { }, $("body").get(0)); $("body").css("overflow", "hidden"); $("#search-input").focus(); @@ -27,17 +47,23 @@ Template.CoreNavigationBar.events({ }); Template.CoreNavigationBar.helpers({ + isSearchEnabled() { + const instance = Template.instance(); + return instance.state.get("searchEnabled"); + }, + + searchTemplate() { + const instance = Template.instance(); + if (instance.state.get("searchEnabled")) { + return instance.state.get("searchTemplate"); + } + }, + IconButtonComponent() { return { component: FlatButton, icon: "fa fa-search", kind: "flat" - // onClick() { - // Blaze.renderWithData(Template.searchModal, { - // }, $("body").get(0)); - // $("body").css("overflow-y", "hidden"); - // $("#search-input").focus(); - // } }; }, onMenuButtonClick() { @@ -51,9 +77,7 @@ Template.CoreNavigationBar.helpers({ tagNavProps() { const instance = Template.instance(); - let tags = []; - - tags = Tags.find({ + const tags = Tags.find({ isTopLevel: true }, { sort: { From ac2e3139f5a467cea776c0977c226a04396cc797 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 09:56:14 +0800 Subject: [PATCH 011/110] Add migration to update existing registries --- .../migrations/2_add_key_to_search_ui.js | 24 +++++++++++++++++++ .../core/versions/server/migrations/index.js | 1 + 2 files changed, 25 insertions(+) create mode 100644 imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js new file mode 100644 index 00000000000..ca3cd328d99 --- /dev/null +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -0,0 +1,24 @@ +import { Migrations } from "/imports/plugins/core/versions"; +import { Packages } from "/lib/collections"; + + +// Add keys to search so that stock search is enabled by default +Migrations.add({ + version: 2, + up() { + while (!Packages.find({Name: "reaction-ui-search"})) { + Packages.update({name: "reaction-ui-search"}, + { + $set: { + registry: [{ + provides: "ui-search", + template: "searchModal" + } + + ] + } + } + ); + } + } +}); diff --git a/imports/plugins/core/versions/server/migrations/index.js b/imports/plugins/core/versions/server/migrations/index.js index 238d8131c02..333be6112bc 100644 --- a/imports/plugins/core/versions/server/migrations/index.js +++ b/imports/plugins/core/versions/server/migrations/index.js @@ -1 +1,2 @@ import "./1_rebuild_account_and_order_search_collections"; +import "./2_add_key_to_search_ui"; From c9a56b2ec3f48e2731c25f8f0d3f2253da8a5103 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 10:15:45 +0800 Subject: [PATCH 012/110] Modify all search-ui record --- .../core/versions/server/migrations/2_add_key_to_search_ui.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index ca3cd328d99..32a1f611798 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -17,7 +17,8 @@ Migrations.add({ ] } - } + }, + { multi: true} ); } } From 2a40c577bdc70e80677046fa0acaa217c15bc2c5 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 10:23:36 +0800 Subject: [PATCH 013/110] Loop until missing record is available to modify --- .../core/versions/server/migrations/2_add_key_to_search_ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index 32a1f611798..9a87a3560cd 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -6,7 +6,7 @@ import { Packages } from "/lib/collections"; Migrations.add({ version: 2, up() { - while (!Packages.find({Name: "reaction-ui-search"})) { + while (!Packages.find({Name: "reaction-ui-search", "registry.provides": "ui-search"})) { Packages.update({name: "reaction-ui-search"}, { $set: { From 19621010d7740864bc37d946c7478eae7b4c4151 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 10:33:53 +0800 Subject: [PATCH 014/110] Logging --- .../versions/server/migrations/2_add_key_to_search_ui.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index 9a87a3560cd..27fc6711006 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -1,12 +1,13 @@ import { Migrations } from "/imports/plugins/core/versions"; import { Packages } from "/lib/collections"; - +import { Hooks, Logger } from "/server/api"; // Add keys to search so that stock search is enabled by default Migrations.add({ version: 2, up() { - while (!Packages.find({Name: "reaction-ui-search", "registry.provides": "ui-search"})) { + Hooks.Events.add("afterCoreInit", () => { + Logger.info("Running search package update"); Packages.update({name: "reaction-ui-search"}, { $set: { @@ -20,6 +21,6 @@ Migrations.add({ }, { multi: true} ); - } + }); } }); From 164a02425b5d68ee487e697efaedbbb2ee6a6376 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 11:08:21 +0800 Subject: [PATCH 015/110] No hook, no conditional --- .../migrations/2_add_key_to_search_ui.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index 27fc6711006..f25937b78fc 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -6,21 +6,19 @@ import { Hooks, Logger } from "/server/api"; Migrations.add({ version: 2, up() { - Hooks.Events.add("afterCoreInit", () => { - Logger.info("Running search package update"); - Packages.update({name: "reaction-ui-search"}, - { - $set: { - registry: [{ - provides: "ui-search", - template: "searchModal" - } - - ] + Logger.info("Running search package update"); + Packages.update({name: "reaction-ui-search"}, + { + $set: { + registry: [{ + provides: "ui-search", + template: "searchModal" } - }, - { multi: true} - ); - }); + + ] + } + }, + { multi: true} + ); } }); From 13fe738cf1c44daf43ed6ebee503cad96d048c87 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 11:26:35 +0800 Subject: [PATCH 016/110] Make field label more explicit --- lib/collections/schemas/registry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/collections/schemas/registry.js b/lib/collections/schemas/registry.js index 9ca5086344c..2e9cde80b73 100644 --- a/lib/collections/schemas/registry.js +++ b/lib/collections/schemas/registry.js @@ -31,6 +31,7 @@ export const Registry = new SimpleSchema({ }, name: { type: String, + label: "Registry Name", index: true }, template: { From 41d1afff73d1a47232cdbd8efbe54a599dac4ef0 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 11:26:50 +0800 Subject: [PATCH 017/110] Set name which is required for some reason --- .../versions/server/migrations/2_add_key_to_search_ui.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index f25937b78fc..df5d1b3759c 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -1,6 +1,6 @@ import { Migrations } from "/imports/plugins/core/versions"; import { Packages } from "/lib/collections"; -import { Hooks, Logger } from "/server/api"; +import { Logger } from "/server/api"; // Add keys to search so that stock search is enabled by default Migrations.add({ @@ -11,11 +11,10 @@ Migrations.add({ { $set: { registry: [{ + name: "Search Modal", provides: "ui-search", template: "searchModal" - } - - ] + }] } }, { multi: true} From a504444406c0177d780e8f483b0e1e8dbc87d02c Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 11:33:52 +0800 Subject: [PATCH 018/110] Make registry consistent with migration. I guess schema is not validated here either? --- imports/plugins/included/ui-search/register.js | 1 + 1 file changed, 1 insertion(+) diff --git a/imports/plugins/included/ui-search/register.js b/imports/plugins/included/ui-search/register.js index ebf9a0b304e..92c9f6e0683 100644 --- a/imports/plugins/included/ui-search/register.js +++ b/imports/plugins/included/ui-search/register.js @@ -7,6 +7,7 @@ Reaction.registerPackage({ autoEnable: true, registry: [ { + name: "Search Modal", provides: "ui-search", template: "searchModal" } From 0473ffd83d2d15248ba2dfef35db29283743f072 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 11:40:31 +0800 Subject: [PATCH 019/110] Remove logging --- .../core/versions/server/migrations/2_add_key_to_search_ui.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js index df5d1b3759c..8c22682e39a 100644 --- a/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js +++ b/imports/plugins/core/versions/server/migrations/2_add_key_to_search_ui.js @@ -1,12 +1,10 @@ import { Migrations } from "/imports/plugins/core/versions"; import { Packages } from "/lib/collections"; -import { Logger } from "/server/api"; // Add keys to search so that stock search is enabled by default Migrations.add({ version: 2, up() { - Logger.info("Running search package update"); Packages.update({name: "reaction-ui-search"}, { $set: { From d8078af5d5aaad5c4d34ced455bcfb9e8c04c30a Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Thu, 10 Nov 2016 13:02:52 +0800 Subject: [PATCH 020/110] Update package.json to reflect release number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b0d1600db4..7c0d603fd8f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "reaction", "description": "Reaction is a modern reactive, real-time event driven ecommerce platform.", - "version": "0.17.1", + "version": "0.18.0", "main": "main.js", "directories": { "test": "tests" From 8e5eb79991cc8dfd2f8a35a4a0fc6c98078a6469 Mon Sep 17 00:00:00 2001 From: Aaron Judd Date: Thu, 10 Nov 2016 13:36:10 -0800 Subject: [PATCH 021/110] Discounts (#1556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * discount skeleton - remove from core - add core/discounts - add discounts-rates - add discount-codes - new discount schemas [WIP] * local module import of translations - adds `loadTranslations` and `loadTranslation` methods - removes necessity to add translations to core - `import { loadTranslations } from "/server/startup/i18n”;` - multiple file import illustrated in reaction-taxes - single import illustrated in reaction-discounts - introduces mergeDeep helper in i18n/startup and updates to translation resource structure to accommodate additional namespaces for package (uses package name). * updated i18n namespace and handling - add i18n handling for griddle column headers - add local i18n imports to discounts - updated i18n taxSettings namespace (into packages + admin) * change info logging to debug * schemas, forms, ui, i18n for discount modules * updated Discount schemas - implement client collections for DiscountCodes, DiscountRates - implement multi-schema on server Discounts collection * updated discounts pub - split security out * Meteor 1.4.2.1 updated to Meteor 1.4.2.1. * group payments packages - resolves #1408 - consolidates all payment packages into a single admin group - rename include payment package folders `payment-` - add initial local i18n, moved from en.json (TODO, other lang). - move “default method” setting from shop to new `core/payments` package. - move checkout/payment templates from core to `core/payments` package. - expose icon in public registry --- .meteor/packages | 4 +- .meteor/release | 2 +- .meteor/versions | 6 +- client/modules/core/helpers/apps.js | 3 +- client/modules/i18n/main.js | 2 +- client/modules/i18n/startup.js | 55 +++-- imports/plugins/core/catalog/register.js | 2 +- imports/plugins/core/checkout/client/index.js | 3 - .../templates/shop/settings/settings.html | 22 -- .../templates/shop/settings/settings.js | 31 --- .../plugins/core/discounts/client/index.js | 2 + .../discounts/client/settings/settings.html | 22 ++ .../discounts/client/settings/settings.js | 118 ++++++++++ .../discounts/lib/collections/collections.js | 13 ++ .../core/discounts/lib/collections/index.js | 1 + .../lib/collections/schemas/config.js | 25 ++ .../lib/collections/schemas/discounts.js | 87 +++++++ .../lib/collections/schemas/index.js | 2 + imports/plugins/core/discounts/register.js | 38 +++ .../core/discounts/server/api/import.js | 20 ++ .../core/discounts/server/api/index.js | 3 + .../discounts/server/hooks/collections.js | 47 ++++ .../core/discounts/server/hooks/index.js | 1 + .../core/discounts/server/i18n/en.json | 22 ++ .../core/discounts/server/i18n/index.js | 7 + .../plugins/core/discounts/server/index.js | 6 + .../core/discounts/server/methods/index.js | 1 + .../server/methods/methods.app-test.js | 30 +++ .../core/discounts/server/methods/methods.js | 82 +++++++ .../server/publications/discounts.js | 41 ++++ .../discounts/server/security/discounts.js | 13 ++ .../core/payments/client/checkout/index.js | 1 + .../payments/client/checkout/payment/index.js | 3 + .../checkout/payment/methods/cards.html | 2 - .../client}/checkout/payment/methods/cards.js | 18 +- .../client}/checkout/payment/payment.html | 0 imports/plugins/core/payments/client/index.js | 2 + .../core/payments/client/settings/index.js | 2 + .../payments/client/settings/settings.html | 44 ++++ .../core/payments/client/settings/settings.js | 97 ++++++++ imports/plugins/core/payments/register.js | 26 +++ .../plugins/core/payments/server/i18n/en.json | 34 +++ .../core/payments/server/i18n/index.js | 19 ++ imports/plugins/core/payments/server/index.js | 1 + .../core/taxes/client/settings/custom.js | 32 ++- imports/plugins/core/taxes/register.js | 2 +- .../plugins/core/taxes/server/i18n/ar.json | 37 +++ .../plugins/core/taxes/server/i18n/de.json | 37 +++ .../plugins/core/taxes/server/i18n/en.json | 45 ++++ .../plugins/core/taxes/server/i18n/es.json | 37 +++ .../plugins/core/taxes/server/i18n/fr.json | 37 +++ .../plugins/core/taxes/server/i18n/he.json | 37 +++ .../core/taxes/server/i18n/i18n/en.json | 15 ++ .../core/taxes/server/i18n/i18n/index.js | 7 + .../plugins/core/taxes/server/i18n/index.js | 19 ++ .../plugins/core/taxes/server/i18n/pt.json | 36 +++ .../plugins/core/taxes/server/i18n/ro.json | 37 +++ .../plugins/core/taxes/server/i18n/ru.json | 37 +++ .../plugins/core/taxes/server/i18n/zh.json | 37 +++ imports/plugins/core/taxes/server/index.js | 1 + .../plugins/included/braintree/register.js | 50 ---- .../client/collections/codes.js | 10 + .../included/discount-codes/client/index.js | 3 + .../discount-codes/client/settings/codes.html | 72 ++++++ .../discount-codes/client/settings/codes.js | 215 +++++++++++++++++ .../discount-codes/lib/collections/index.js | 3 + .../lib/collections/schemas/codes.js | 33 +++ .../lib/collections/schemas/config.js | 22 ++ .../lib/collections/schemas/index.js | 2 + .../included/discount-codes/register.js | 21 ++ .../discount-codes/server/hooks/hooks.js | 20 ++ .../discount-codes/server/hooks/index.js | 1 + .../discount-codes/server/i18n/en.json | 27 +++ .../discount-codes/server/i18n/index.js | 7 + .../included/discount-codes/server/index.js | 5 + .../discount-codes/server/methods/index.js | 1 + .../server/methods/methods.app-test.js | 30 +++ .../discount-codes/server/methods/methods.js | 39 ++++ .../server/publications/discounts.js | 43 ++++ .../server/security/discounts.js | 13 ++ .../client/collections/rates.js | 9 + .../included/discount-rates/client/index.js | 3 + .../discount-rates/client/settings/rates.html | 66 ++++++ .../discount-rates/client/settings/rates.js | 216 ++++++++++++++++++ .../discount-rates/lib/collections/index.js | 3 + .../lib/collections/schemas/config.js | 22 ++ .../lib/collections/schemas/index.js | 2 + .../lib/collections/schemas/rates.js | 18 ++ .../included/discount-rates/register.js | 21 ++ .../discount-rates/server/hooks/hooks.js | 19 ++ .../discount-rates/server/hooks/index.js | 1 + .../discount-rates/server/i18n/en.json | 23 ++ .../discount-rates/server/i18n/index.js | 7 + .../included/discount-rates/server/index.js | 5 + .../discount-rates/server/methods/index.js | 1 + .../server/methods/methods.app-test.js | 30 +++ .../discount-rates/server/methods/methods.js | 38 +++ .../server/publications/discounts.js | 43 ++++ .../server/security/discounts.js | 13 ++ .../inventory/server/methods/inventory.js | 2 +- .../jobcontrol/server/jobs/exchangerates.js | 2 +- .../client/api/authnet.js | 0 .../client/api/index.js | 0 .../client/checkout/authnet.html | 0 .../client/checkout/authnet.js | 0 .../client/dashboard/authnet.html | 0 .../client/dashboard/authnet.js | 0 .../client/index.js | 0 .../client/settings/authnet.html | 0 .../client/settings/authnet.js | 0 .../lib/collections/schemas/index.js | 0 .../lib/collections/schemas/package.js | 0 .../{authnet => payments-authnet}/register.js | 22 +- .../payments-authnet/server/i18n/en.json | 24 ++ .../payments-authnet/server/i18n/index.js | 19 ++ .../server/index.js | 1 + .../server/methods/authnet.js | 0 .../client/api/braintree.js | 0 .../client/checkout/braintree.html | 0 .../client/checkout/braintree.js | 0 .../client/index.js | 0 .../client/settings/braintree.html | 0 .../client/settings/braintree.js | 0 .../lib/collections/schemas/braintree.js | 0 .../lib/collections/schemas/index.js | 0 .../included/payments-braintree/register.js | 31 +++ .../payments-braintree/server/i18n/en.json | 31 +++ .../payments-braintree/server/i18n/index.js | 19 ++ .../server/index.js | 1 + .../server/methods/braintree.js | 0 .../server/methods/braintreeApi.js | 0 .../server/methods/braintreeMethods.js | 0 .../braintreeapi-methods-refund.app-test.js | 0 .../client/checkout/example.html | 0 .../client/checkout/example.js | 0 .../client/index.js | 0 .../client/settings/example.html | 8 +- .../client/settings/example.js | 0 .../lib/api/example.js | 0 .../lib/api/index.js | 0 .../lib/collections/schemas/example.js | 0 .../lib/collections/schemas/index.js | 0 .../register.js | 19 +- .../payments-example/server/i18n/en.json | 26 +++ .../payments-example/server/i18n/index.js | 19 ++ .../server/index.js | 1 + .../example-payment-methods.app-test.js | 0 .../server/methods/example.js | 0 .../server/methods/exampleapi.js | 0 .../client/index.js | 0 .../client/lib/paypalRestApi.js | 0 .../client/paypal.less | 0 .../templates/checkout/checkoutButton.html | 0 .../templates/checkout/checkoutButton.js | 0 .../templates/checkout/payflowForm.html | 0 .../client/templates/checkout/payflowForm.js | 0 .../templates/checkout/paymentForm.html | 0 .../client/templates/checkout/paymentForm.js | 0 .../templates/checkout/return/cancel.html | 0 .../templates/checkout/return/cancel.js | 0 .../templates/checkout/return/done.html | 0 .../client/templates/checkout/return/done.js | 0 .../client/templates/dashboard/dashboard.html | 0 .../client/templates/dashboard/dashboard.js | 0 .../client/templates/settings/settings.html | 0 .../client/templates/settings/settings.js | 0 .../lib/api/index.js | 0 .../lib/api/paypal.js | 0 .../lib/collections/schemas/index.js | 0 .../lib/collections/schemas/paypal.js | 0 .../{paypal => payments-paypal}/register.js | 19 +- .../payments-paypal/server/i18n/en.json | 23 ++ .../payments-paypal/server/i18n/index.js | 19 ++ .../server/index.js | 1 + .../server/methods/express.js | 0 .../server/methods/payflow.js | 0 .../payflowpro-methods-refund.app-test.js | 0 .../server/methods/payflowproApi.js | 0 .../server/methods/payflowproMethods.js | 0 .../server/security/paypal.js | 0 .../client/checkout/stripe.html | 0 .../client/checkout/stripe.js | 0 .../client/index.js | 0 .../client/settings/stripe.html | 0 .../client/settings/stripe.js | 0 .../lib/api/index.js | 0 .../lib/api/stripe.js | 0 .../lib/collections/schemas/index.js | 0 .../lib/collections/schemas/stripe.js | 0 .../{stripe => payments-stripe}/register.js | 20 +- .../payments-stripe/server/i18n/en.json | 22 ++ .../payments-stripe/server/i18n/index.js | 19 ++ .../server/index.js | 1 + .../server/methods/stripe.js | 0 .../stripeapi-integrationtest.app-test.js | 0 .../stripeapi-methods-capture.app-test.js | 0 .../stripeapi-methods-charge.app-test.js | 0 .../stripeapi-methods-refund.app-test.js | 0 .../stripeapi-methods-refundlist.app-test.js | 0 .../server/methods/stripeapi.js | 0 .../taxes-avalara/client/settings/avalara.js | 4 +- .../client/settings/taxcloud.js | 4 +- .../taxes-taxjar/client/settings/taxjar.js | 4 +- lib/collections/collections.js | 8 - lib/collections/schemas/discounts.js | 81 ------- lib/collections/schemas/index.js | 1 - private/data/i18n/ar.json | 21 +- private/data/i18n/de.json | 21 +- private/data/i18n/en.json | 50 ---- private/data/i18n/es.json | 21 +- private/data/i18n/fr.json | 21 +- private/data/i18n/he.json | 21 +- private/data/i18n/pt.json | 21 +- private/data/i18n/ro.json | 21 +- private/data/i18n/ru.json | 21 +- private/data/i18n/zh.json | 21 +- server/methods/core/cart.js | 2 +- server/publications/collections/discounts.js | 16 -- server/publications/collections/packages.js | 3 +- server/security/collections.js | 1 - server/startup/i18n.js | 59 ++++- 221 files changed, 2768 insertions(+), 573 deletions(-) create mode 100644 imports/plugins/core/discounts/client/index.js create mode 100644 imports/plugins/core/discounts/client/settings/settings.html create mode 100644 imports/plugins/core/discounts/client/settings/settings.js create mode 100644 imports/plugins/core/discounts/lib/collections/collections.js create mode 100644 imports/plugins/core/discounts/lib/collections/index.js create mode 100644 imports/plugins/core/discounts/lib/collections/schemas/config.js create mode 100644 imports/plugins/core/discounts/lib/collections/schemas/discounts.js create mode 100644 imports/plugins/core/discounts/lib/collections/schemas/index.js create mode 100644 imports/plugins/core/discounts/register.js create mode 100644 imports/plugins/core/discounts/server/api/import.js create mode 100644 imports/plugins/core/discounts/server/api/index.js create mode 100644 imports/plugins/core/discounts/server/hooks/collections.js create mode 100644 imports/plugins/core/discounts/server/hooks/index.js create mode 100644 imports/plugins/core/discounts/server/i18n/en.json create mode 100644 imports/plugins/core/discounts/server/i18n/index.js create mode 100644 imports/plugins/core/discounts/server/index.js create mode 100644 imports/plugins/core/discounts/server/methods/index.js create mode 100644 imports/plugins/core/discounts/server/methods/methods.app-test.js create mode 100644 imports/plugins/core/discounts/server/methods/methods.js create mode 100644 imports/plugins/core/discounts/server/publications/discounts.js create mode 100644 imports/plugins/core/discounts/server/security/discounts.js create mode 100644 imports/plugins/core/payments/client/checkout/index.js create mode 100644 imports/plugins/core/payments/client/checkout/payment/index.js rename imports/plugins/core/{checkout/client/templates => payments/client}/checkout/payment/methods/cards.html (96%) rename imports/plugins/core/{checkout/client/templates => payments/client}/checkout/payment/methods/cards.js (62%) rename imports/plugins/core/{checkout/client/templates => payments/client}/checkout/payment/payment.html (100%) create mode 100644 imports/plugins/core/payments/client/index.js create mode 100644 imports/plugins/core/payments/client/settings/index.js create mode 100644 imports/plugins/core/payments/client/settings/settings.html create mode 100644 imports/plugins/core/payments/client/settings/settings.js create mode 100644 imports/plugins/core/payments/register.js create mode 100644 imports/plugins/core/payments/server/i18n/en.json create mode 100644 imports/plugins/core/payments/server/i18n/index.js create mode 100644 imports/plugins/core/payments/server/index.js create mode 100644 imports/plugins/core/taxes/server/i18n/ar.json create mode 100644 imports/plugins/core/taxes/server/i18n/de.json create mode 100644 imports/plugins/core/taxes/server/i18n/en.json create mode 100644 imports/plugins/core/taxes/server/i18n/es.json create mode 100644 imports/plugins/core/taxes/server/i18n/fr.json create mode 100644 imports/plugins/core/taxes/server/i18n/he.json create mode 100644 imports/plugins/core/taxes/server/i18n/i18n/en.json create mode 100644 imports/plugins/core/taxes/server/i18n/i18n/index.js create mode 100644 imports/plugins/core/taxes/server/i18n/index.js create mode 100644 imports/plugins/core/taxes/server/i18n/pt.json create mode 100644 imports/plugins/core/taxes/server/i18n/ro.json create mode 100644 imports/plugins/core/taxes/server/i18n/ru.json create mode 100644 imports/plugins/core/taxes/server/i18n/zh.json delete mode 100644 imports/plugins/included/braintree/register.js create mode 100644 imports/plugins/included/discount-codes/client/collections/codes.js create mode 100644 imports/plugins/included/discount-codes/client/index.js create mode 100644 imports/plugins/included/discount-codes/client/settings/codes.html create mode 100644 imports/plugins/included/discount-codes/client/settings/codes.js create mode 100644 imports/plugins/included/discount-codes/lib/collections/index.js create mode 100644 imports/plugins/included/discount-codes/lib/collections/schemas/codes.js create mode 100644 imports/plugins/included/discount-codes/lib/collections/schemas/config.js create mode 100644 imports/plugins/included/discount-codes/lib/collections/schemas/index.js create mode 100644 imports/plugins/included/discount-codes/register.js create mode 100644 imports/plugins/included/discount-codes/server/hooks/hooks.js create mode 100644 imports/plugins/included/discount-codes/server/hooks/index.js create mode 100644 imports/plugins/included/discount-codes/server/i18n/en.json create mode 100644 imports/plugins/included/discount-codes/server/i18n/index.js create mode 100644 imports/plugins/included/discount-codes/server/index.js create mode 100644 imports/plugins/included/discount-codes/server/methods/index.js create mode 100644 imports/plugins/included/discount-codes/server/methods/methods.app-test.js create mode 100644 imports/plugins/included/discount-codes/server/methods/methods.js create mode 100644 imports/plugins/included/discount-codes/server/publications/discounts.js create mode 100644 imports/plugins/included/discount-codes/server/security/discounts.js create mode 100644 imports/plugins/included/discount-rates/client/collections/rates.js create mode 100644 imports/plugins/included/discount-rates/client/index.js create mode 100644 imports/plugins/included/discount-rates/client/settings/rates.html create mode 100644 imports/plugins/included/discount-rates/client/settings/rates.js create mode 100644 imports/plugins/included/discount-rates/lib/collections/index.js create mode 100644 imports/plugins/included/discount-rates/lib/collections/schemas/config.js create mode 100644 imports/plugins/included/discount-rates/lib/collections/schemas/index.js create mode 100644 imports/plugins/included/discount-rates/lib/collections/schemas/rates.js create mode 100644 imports/plugins/included/discount-rates/register.js create mode 100644 imports/plugins/included/discount-rates/server/hooks/hooks.js create mode 100644 imports/plugins/included/discount-rates/server/hooks/index.js create mode 100644 imports/plugins/included/discount-rates/server/i18n/en.json create mode 100644 imports/plugins/included/discount-rates/server/i18n/index.js create mode 100644 imports/plugins/included/discount-rates/server/index.js create mode 100644 imports/plugins/included/discount-rates/server/methods/index.js create mode 100644 imports/plugins/included/discount-rates/server/methods/methods.app-test.js create mode 100644 imports/plugins/included/discount-rates/server/methods/methods.js create mode 100644 imports/plugins/included/discount-rates/server/publications/discounts.js create mode 100644 imports/plugins/included/discount-rates/server/security/discounts.js rename imports/plugins/included/{authnet => payments-authnet}/client/api/authnet.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/api/index.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/checkout/authnet.html (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/checkout/authnet.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/dashboard/authnet.html (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/dashboard/authnet.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/index.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/settings/authnet.html (100%) rename imports/plugins/included/{authnet => payments-authnet}/client/settings/authnet.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/lib/collections/schemas/index.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/lib/collections/schemas/package.js (100%) rename imports/plugins/included/{authnet => payments-authnet}/register.js (53%) create mode 100644 imports/plugins/included/payments-authnet/server/i18n/en.json create mode 100644 imports/plugins/included/payments-authnet/server/i18n/index.js rename imports/plugins/included/{authnet => payments-authnet}/server/index.js (62%) rename imports/plugins/included/{authnet => payments-authnet}/server/methods/authnet.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/api/braintree.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/checkout/braintree.html (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/checkout/braintree.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/index.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/settings/braintree.html (100%) rename imports/plugins/included/{braintree => payments-braintree}/client/settings/braintree.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/lib/collections/schemas/braintree.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/lib/collections/schemas/index.js (100%) create mode 100644 imports/plugins/included/payments-braintree/register.js create mode 100644 imports/plugins/included/payments-braintree/server/i18n/en.json create mode 100644 imports/plugins/included/payments-braintree/server/i18n/index.js rename imports/plugins/included/{braintree => payments-braintree}/server/index.js (80%) rename imports/plugins/included/{braintree => payments-braintree}/server/methods/braintree.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/server/methods/braintreeApi.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/server/methods/braintreeMethods.js (100%) rename imports/plugins/included/{braintree => payments-braintree}/server/methods/braintreeapi-methods-refund.app-test.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/client/checkout/example.html (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/client/checkout/example.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/client/index.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/client/settings/example.html (70%) rename imports/plugins/included/{example-paymentmethod => payments-example}/client/settings/example.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/lib/api/example.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/lib/api/index.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/lib/collections/schemas/example.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/lib/collections/schemas/index.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/register.js (51%) create mode 100644 imports/plugins/included/payments-example/server/i18n/en.json create mode 100644 imports/plugins/included/payments-example/server/i18n/index.js rename imports/plugins/included/{example-paymentmethod => payments-example}/server/index.js (79%) rename imports/plugins/included/{example-paymentmethod => payments-example}/server/methods/example-payment-methods.app-test.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/server/methods/example.js (100%) rename imports/plugins/included/{example-paymentmethod => payments-example}/server/methods/exampleapi.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/index.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/lib/paypalRestApi.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/paypal.less (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/checkoutButton.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/checkoutButton.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/payflowForm.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/payflowForm.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/paymentForm.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/paymentForm.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/return/cancel.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/return/cancel.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/return/done.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/checkout/return/done.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/dashboard/dashboard.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/dashboard/dashboard.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/settings/settings.html (100%) rename imports/plugins/included/{paypal => payments-paypal}/client/templates/settings/settings.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/lib/api/index.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/lib/api/paypal.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/lib/collections/schemas/index.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/lib/collections/schemas/paypal.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/register.js (58%) create mode 100644 imports/plugins/included/payments-paypal/server/i18n/en.json create mode 100644 imports/plugins/included/payments-paypal/server/i18n/index.js rename imports/plugins/included/{paypal => payments-paypal}/server/index.js (83%) rename imports/plugins/included/{paypal => payments-paypal}/server/methods/express.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/server/methods/payflow.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/server/methods/payflowpro-methods-refund.app-test.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/server/methods/payflowproApi.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/server/methods/payflowproMethods.js (100%) rename imports/plugins/included/{paypal => payments-paypal}/server/security/paypal.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/client/checkout/stripe.html (100%) rename imports/plugins/included/{stripe => payments-stripe}/client/checkout/stripe.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/client/index.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/client/settings/stripe.html (100%) rename imports/plugins/included/{stripe => payments-stripe}/client/settings/stripe.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/lib/api/index.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/lib/api/stripe.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/lib/collections/schemas/index.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/lib/collections/schemas/stripe.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/register.js (57%) create mode 100644 imports/plugins/included/payments-stripe/server/i18n/en.json create mode 100644 imports/plugins/included/payments-stripe/server/i18n/index.js rename imports/plugins/included/{stripe => payments-stripe}/server/index.js (79%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripe.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi-integrationtest.app-test.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi-methods-capture.app-test.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi-methods-charge.app-test.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi-methods-refund.app-test.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi-methods-refundlist.app-test.js (100%) rename imports/plugins/included/{stripe => payments-stripe}/server/methods/stripeapi.js (100%) delete mode 100644 lib/collections/schemas/discounts.js delete mode 100644 server/publications/collections/discounts.js diff --git a/.meteor/packages b/.meteor/packages index fff98875a3a..ac55fe93463 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -29,7 +29,7 @@ underscore@1.0.10 logging@1.1.16 reload@1.1.11 ejson@1.0.13 -less@2.7.6 +less@2.7.7 service-configuration@1.0.11 amplify mdg:validated-method @@ -37,7 +37,7 @@ shell-server@0.2.1 # Meteor Auth Packages accounts-base@1.2.14 -accounts-password@1.3.1 +accounts-password@1.3.2 accounts-facebook@1.0.11 accounts-google@1.0.11 accounts-twitter@1.1.12 diff --git a/.meteor/release b/.meteor/release index c26084673d3..65e8c3928f8 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.4.2 +METEOR@1.4.2.1 diff --git a/.meteor/versions b/.meteor/versions index e43518af313..3055a7e4bfb 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -2,7 +2,7 @@ accounts-base@1.2.14 accounts-facebook@1.0.11 accounts-google@1.0.11 accounts-oauth@1.1.14 -accounts-password@1.3.1 +accounts-password@1.3.2 accounts-twitter@1.1.12 alanning:roles@1.2.15 aldeed:autoform@5.8.1 @@ -28,7 +28,7 @@ browser-policy@1.0.9 browser-policy-common@1.0.11 browser-policy-content@1.0.12 browser-policy-framing@1.0.12 -caching-compiler@1.1.8 +caching-compiler@1.1.9 caching-html-compiler@1.0.7 callback-hook@1.0.10 cfs:access-point@0.1.49 @@ -90,7 +90,7 @@ kadira:blaze-layout@2.3.0 kadira:dochead@1.5.0 kadira:flow-router-ssr@3.13.0 launch-screen@1.1.0 -less@2.7.6 +less@2.7.7 livedata@1.0.18 localstorage@1.0.12 logging@1.1.16 diff --git a/client/modules/core/helpers/apps.js b/client/modules/core/helpers/apps.js index 6b98758fcbd..f158ff241fe 100644 --- a/client/modules/core/helpers/apps.js +++ b/client/modules/core/helpers/apps.js @@ -93,7 +93,8 @@ export function Apps(optionHash) { enabled: 1, registry: 1, name: 1, - provides: 1 + provides: 1, + icon: 1 }; // fetch the packages diff --git a/client/modules/i18n/main.js b/client/modules/i18n/main.js index 4c743558c50..806faf0eb28 100644 --- a/client/modules/i18n/main.js +++ b/client/modules/i18n/main.js @@ -3,7 +3,7 @@ import { Meteor } from "meteor/meteor"; import { Tracker } from "meteor/tracker"; import { SimpleSchema } from "meteor/aldeed:simple-schema"; import { Reaction } from "/client/api"; -import { Packages, Translations } from "/lib/collections"; +import { Packages } from "/lib/collections"; // // Reaction i18n Translations, RTL and Currency Exchange Support diff --git a/client/modules/i18n/startup.js b/client/modules/i18n/startup.js index 8be073e69e8..d1f230e07bb 100644 --- a/client/modules/i18n/startup.js +++ b/client/modules/i18n/startup.js @@ -32,6 +32,34 @@ const options = { htmlTag: document.documentElement }; +/** + * Simple is object check. + * @param {Object} item item to check if is an object + * @returns {boolean} return true if object + */ +function isObject(item) { + return (item && typeof item === "object" && !Array.isArray(item) && item !== null); +} + +/** + * Helper for Deep merge two objects. + * @param {Object} target deep merge into this object + * @param {Object} source merge this object + * @returns {Object} return deep merged object + */ +function mergeDeep(target, source) { + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach(key => { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + }); + } + return target; +} Meteor.startup(() => { // use tracker autorun to detect language changes @@ -55,18 +83,16 @@ Meteor.startup(() => { } }).fetch(); - - // map reduce translations into i18next formatting - const resources = translations.reduce(function (x, y) { - const ns = Object.keys(y.translation)[0]; - // first creating the structure, when add additional namespaces - if (x[y.i18n]) { - x[y.i18n][ns] = y.translation[ns]; - } else { - x[y.i18n] = y.translation; - } - return x; - }, {}); + // + // reduce and merge translations + // into i18next resource format + // + let resources = {}; + translations.forEach(function (translation) { + const resource = {}; + resource[translation.i18n] = translation.translation; + resources = mergeDeep(resources, resource); + }); // // initialize i18next @@ -81,13 +107,10 @@ Meteor.startup(() => { debug: false, ns: packageNamespaces, // translation namespace for every package defaultNS: "core", // reaction "core" is the default namespace + fallbackNS: packageNamespaces, lng: language, // user session language fallbackLng: shop ? shop.language : null, // Shop language resources: resources - // saveMissing: true, - // missingKeyHandler: function (lng, ns, key, fallbackValue) { - // Meteor.call("i18n/addTranslation", lng, ns, key, fallbackValue); - // } }, () => { // someday this should work // see: https://github.com/aldeed/meteor-simple-schema/issues/494 diff --git a/imports/plugins/core/catalog/register.js b/imports/plugins/core/catalog/register.js index 65610c8a905..a7cf808b6a1 100644 --- a/imports/plugins/core/catalog/register.js +++ b/imports/plugins/core/catalog/register.js @@ -14,7 +14,7 @@ Reaction.registerPackage({ label: "Catalog", description: "Product catalog", icon: "fa fa-archive", - priority: 2, + priority: 1, container: "core" }, { diff --git a/imports/plugins/core/checkout/client/index.js b/imports/plugins/core/checkout/client/index.js index 342a17e2d97..e476dfa118a 100644 --- a/imports/plugins/core/checkout/client/index.js +++ b/imports/plugins/core/checkout/client/index.js @@ -20,9 +20,6 @@ import "./templates/checkout/completed/completed.js"; import "./templates/checkout/header/header.html"; import "./templates/checkout/login/login.html"; import "./templates/checkout/login/login.js"; -import "./templates/checkout/payment/methods/cards.html"; -import "./templates/checkout/payment/methods/cards.js"; -import "./templates/checkout/payment/payment.html"; import "./templates/checkout/progressBar/progressBar.html"; import "./templates/checkout/progressBar/progressBar.js"; import "./templates/checkout/review/review.html"; diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html index 4c33ffa52db..afb07a20296 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.html @@ -82,28 +82,6 @@ -
-
- -
-
-
- {{#autoForm collection=Collections.Shops doc=shop id="shopEditPaymentMethodsForm" type="update" - autosave=true}} - {{> afQuickField name='defaultPaymentMethod' options=paymentMethodOptions}} - {{/autoForm}} -
-
-
-
diff --git a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js index e784e177db2..7d1ee80584c 100644 --- a/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js +++ b/imports/plugins/core/dashboard/client/templates/shop/settings/settings.js @@ -133,23 +133,6 @@ Template.shopSettings.helpers({ addressBook: function () { const address = Shops.findOne().addressBook; return address[0]; - }, - paymentMethodOptions() { - const paymentMethods = Reaction.Apps({provides: "paymentMethod"}); - const options = [{ - label: i18next.t("app.auto"), - value: "none" - }]; - - if (paymentMethods && _.isArray(paymentMethods)) { - for (const method of paymentMethods) { - options.push({ - label: i18next.t(method.i18nKeyLabel), - value: method.packageName - }); - } - } - return options; } }); @@ -214,17 +197,3 @@ AutoForm.hooks({ } } }); - -AutoForm.hooks({ - shopEditPaymentMethodsForm: { - onSuccess: function () { - return Alerts.toast(i18next.t("shopSettings.shopPaymentMethodsSaved"), - "success"); - }, - onError: function (operation, error) { - return Alerts.toast( - `${i18next.t("shopSettings.shopPaymentMethodsFailed")} ${error}`, "error" - ); - } - } -}); diff --git a/imports/plugins/core/discounts/client/index.js b/imports/plugins/core/discounts/client/index.js new file mode 100644 index 00000000000..c244a3fc8e5 --- /dev/null +++ b/imports/plugins/core/discounts/client/index.js @@ -0,0 +1,2 @@ +import "./settings/settings.html"; +import "./settings/settings.js"; diff --git a/imports/plugins/core/discounts/client/settings/settings.html b/imports/plugins/core/discounts/client/settings/settings.html new file mode 100644 index 00000000000..b717351a464 --- /dev/null +++ b/imports/plugins/core/discounts/client/settings/settings.html @@ -0,0 +1,22 @@ + diff --git a/imports/plugins/core/discounts/client/settings/settings.js b/imports/plugins/core/discounts/client/settings/settings.js new file mode 100644 index 00000000000..5ccf54eac00 --- /dev/null +++ b/imports/plugins/core/discounts/client/settings/settings.js @@ -0,0 +1,118 @@ +import { Template } from "meteor/templating"; +import { Packages } from "/lib/collections"; +// import { DiscountCodes } from "../../lib/collections"; +// import { i18next } from "/client/api"; +import { DiscountPackageConfig } from "../../lib/collections/schemas"; + +// /* +// * Template discountses Helpers +// */ +// Template.discountSettings.onCreated(function () { +// this.autorun(() => { +// this.subscribe("DiscountCodes"); +// }); +// }); + +Template.discountSettings.helpers({ + packageConfigSchema() { + return DiscountPackageConfig; + }, + // + // check if this package setting is enabled + // + checked(pkg) { + let enabled; + const pkgData = Packages.findOne(pkg.packageId); + const setting = pkg.name.split("/").splice(-1); + + if (pkgData && pkgData.settings) { + if (pkgData.settings[setting]) { + enabled = pkgData.settings[setting].enabled; + } + } + return enabled === true ? "checked" : ""; + }, + // + // get current packages settings data + // + packageData() { + return Packages.findOne({ + name: "reaction-discounts" + }); + }, + // + // prepare and return discountsCodes + // for default shop value + // + // taxCodes() { + // const instance = Template.instance(); + // if (instance.subscriptionsReady()) { + // const discountsCodes = DiscountCodes.find().fetch(); + // const options = [{ + // label: i18next.t("app.auto"), + // value: "none" + // }]; + // + // for (const discountsCode of discountsCodes) { + // options.push({ + // label: i18next.t(discountsCode.label), + // value: discountsCode.id + // }); + // } + // return options; + // } + // return undefined; + // }, + // + // Template helper to add a hidden class if the condition is false + // + shown(pkg) { + let enabled; + const pkgData = Packages.findOne(pkg.packageId); + const setting = pkg.name.split("/").splice(-1); + + if (pkgData && pkgData.settings) { + if (pkgData.settings[setting]) { + enabled = pkgData.settings[setting].enabled; + } + } + + return enabled !== true ? "hidden" : ""; + } +}); + +Template.discountSettings.events({ + /** + * discountSettings settings update enabled status for discounts service on change + * @param {event} event jQuery Event + * @return {void} + */ + "change input[name=enabled]": (event) => { + const name = event.target.value; + const packageId = event.target.getAttribute("data-id"); + const fields = [{ + property: "enabled", + value: event.target.checked + }]; + + Meteor.call("registry/update", packageId, name, fields); + }, + + /** + * discountSettings settings show/hide secret key for a discounts service + * @param {event} event jQuery Event + * @return {void} + */ + "click [data-event-action=showSecret]": (event) => { + const button = $(event.currentTarget); + const input = button.closest(".form-group").find("input[name=secret]"); + + if (input.attr("type") === "password") { + input.attr("type", "text"); + button.html("Hide"); + } else { + input.attr("type", "password"); + button.html("Show"); + } + } +}); diff --git a/imports/plugins/core/discounts/lib/collections/collections.js b/imports/plugins/core/discounts/lib/collections/collections.js new file mode 100644 index 00000000000..82b32e8cac1 --- /dev/null +++ b/imports/plugins/core/discounts/lib/collections/collections.js @@ -0,0 +1,13 @@ +import { Mongo } from "meteor/mongo"; +import * as Schemas from "./schemas"; + +/** +* Discounts Collection +* @type {Object} +* @desc Collection for custom discount rates +* for dollar, percentage, and shipping +* discount rates. +*/ +export const Discounts = new Mongo.Collection("Discounts"); + +Discounts.attachSchema(Schemas.Discounts); diff --git a/imports/plugins/core/discounts/lib/collections/index.js b/imports/plugins/core/discounts/lib/collections/index.js new file mode 100644 index 00000000000..45e94450ebd --- /dev/null +++ b/imports/plugins/core/discounts/lib/collections/index.js @@ -0,0 +1 @@ +export * from "./collections"; diff --git a/imports/plugins/core/discounts/lib/collections/schemas/config.js b/imports/plugins/core/discounts/lib/collections/schemas/config.js new file mode 100644 index 00000000000..d9368dcb2ad --- /dev/null +++ b/imports/plugins/core/discounts/lib/collections/schemas/config.js @@ -0,0 +1,25 @@ +import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import { PackageConfig } from "/lib/collections/schemas/registry"; +import { Discounts } from "./discounts"; + +/** +* DiscountsPackageConfig Schema +*/ + +export const DiscountsPackageConfig = new SimpleSchema([ + PackageConfig, { + "settings.rates": { + type: Object, + optional: true + }, + "settings.rates.enabled": { + type: Boolean, + optional: true, + defaultValue: false + }, + "settings.rates.discounts": { + type: [Discounts], + optional: true + } + } +]); diff --git a/imports/plugins/core/discounts/lib/collections/schemas/discounts.js b/imports/plugins/core/discounts/lib/collections/schemas/discounts.js new file mode 100644 index 00000000000..692a81ed397 --- /dev/null +++ b/imports/plugins/core/discounts/lib/collections/schemas/discounts.js @@ -0,0 +1,87 @@ +import { SimpleSchema } from "meteor/aldeed:simple-schema"; +import { shopIdAutoValue } from "/lib/collections/schemas/helpers"; + +/* +* Discounts Schema +*/ + +export const Discounts = new SimpleSchema({ + "shopId": { + type: String, + autoValue: shopIdAutoValue, + index: 1, + label: "Discounts shopId" + }, + "label": { + type: String + }, + "description": { + type: String, + optional: true + }, + "discountMethod": { + label: "Calculation Method", + type: String, + index: 1 + }, + "discount": { + type: String, + optional: true + }, + "conditions": { + type: Object, + optional: true, + label: "Conditions" + }, + "conditions.order": { + type: Object + }, + "conditions.order.min": { + type: Number, + label: "Mininum", + decimal: true, + defaultValue: 0.00 + }, + "conditions.order.max": { + type: Number, + label: "Maximum", + decimal: true, + optional: true + }, + "conditions.order.startDate": { + type: Date, + label: "Start", + optional: true + }, + "conditions.order.endDate": { + type: Date, + label: "End", + optional: true + }, + "conditions.enabled": { + type: Boolean, + label: "Enabled", + defaultValue: true, + optional: true + }, + "conditions.audience": { + type: [String], + optional: true, + label: "Audience" + }, + "conditions.permissions": { + type: [String], + optional: true, + label: "Permissions" + }, + "conditions.products": { + type: [String], + optional: true, + label: "Products" + }, + "conditions.tags": { + type: [String], + optional: true, + label: "Tags" + } +}); diff --git a/imports/plugins/core/discounts/lib/collections/schemas/index.js b/imports/plugins/core/discounts/lib/collections/schemas/index.js new file mode 100644 index 00000000000..9534ec432ce --- /dev/null +++ b/imports/plugins/core/discounts/lib/collections/schemas/index.js @@ -0,0 +1,2 @@ +export * from "./discounts"; +export * from "./config"; diff --git a/imports/plugins/core/discounts/register.js b/imports/plugins/core/discounts/register.js new file mode 100644 index 00000000000..be327b9d763 --- /dev/null +++ b/imports/plugins/core/discounts/register.js @@ -0,0 +1,38 @@ +import { Reaction } from "/server/api"; + +Reaction.registerPackage({ + label: "Discounts", + name: "reaction-discounts", + icon: "fa fa-gift", + autoEnable: true, + settings: { + custom: { + enabled: true + }, + rates: { + enabled: false + } + }, + registry: [ + { + provides: "dashboard", + name: "discounts", + label: "Discounts", + description: "Provide discount rates", + icon: "fa fa-gift", + priority: 1, + container: "core", + workflow: "coreDashboardWorkflow" + }, + { + label: "DiscountSettings", + name: "discounts/settings", + provides: "settings", + template: "discountSettings" + }, + { + template: "flatRateCheckoutDiscounts", + provides: "discountMethod" + } + ] +}); diff --git a/imports/plugins/core/discounts/server/api/import.js b/imports/plugins/core/discounts/server/api/import.js new file mode 100644 index 00000000000..f0bf04e19e2 --- /dev/null +++ b/imports/plugins/core/discounts/server/api/import.js @@ -0,0 +1,20 @@ +import { Reaction } from "/server/api"; +import { Import } from "/server/api/core/import"; +import * as Collections from "../../lib/collections"; + +// plugin Import helpers +const DiscountImport = Import; + +// Import helper to store a discountRate in the import buffer. +DiscountImport.discountRate = function (key, discountRate) { + return this.object(Collections.Discounts, key, discountRate); +}; + +// configure Import key detection +DiscountImport.indication("discount", Collections.Discounts, 0.5); + +// should assign to global +Object.assign(Reaction.Import, DiscountImport); + +// exports Reaction.Import with new discount helper +export default Reaction; diff --git a/imports/plugins/core/discounts/server/api/index.js b/imports/plugins/core/discounts/server/api/index.js new file mode 100644 index 00000000000..4b7505b122f --- /dev/null +++ b/imports/plugins/core/discounts/server/api/index.js @@ -0,0 +1,3 @@ +import Reaction from "./import"; + +export default Reaction; diff --git a/imports/plugins/core/discounts/server/hooks/collections.js b/imports/plugins/core/discounts/server/hooks/collections.js new file mode 100644 index 00000000000..658bd8088ed --- /dev/null +++ b/imports/plugins/core/discounts/server/hooks/collections.js @@ -0,0 +1,47 @@ +import { Cart } from "/lib/collections"; +import { Logger } from "/server/api"; + +/** +* Discounts Collection Hooks +* @type {Object} +* @desc After cart update apply discounts. +* if items are changed, recalculating discounts +* we could have done this in the core/cart transform +* but this way this file controls the events from +* the core/discounts plugin. +* @todo just move so a single Hook.event and move all the +* cart hooks to a single location. +*/ +Cart.after.update((userId, cart, fieldNames, modifier) => { + // adding quantity + if (modifier.$inc) { + Logger.debug("incrementing cart - recalculating discounts"); + Meteor.call("discounts/calculate", cart._id); + } + + // adding new items + if (modifier.$addToSet) { + if (modifier.$addToSet.items) { + Logger.debug("adding to cart - recalculating discounts"); + Meteor.call("discounts/calculate", cart._id); + } + } + + // altering the cart shipping + // or billing address we'll update discounts + // ie: shipping/getShippingRates + if (modifier.$set) { + if (modifier.$set["shipping.$.shipmentMethod"] || modifier.$set["shipping.$.address"]) { + Logger.debug("updated shipping info - recalculating discounts"); + Meteor.call("discounts/calculate", cart._id); + } + } + + // removing items + if (modifier.$pull) { + if (modifier.$pull.items) { + Logger.debug("removing from cart - recalculating discounts"); + Meteor.call("discounts/calculate", cart._id); + } + } +}); diff --git a/imports/plugins/core/discounts/server/hooks/index.js b/imports/plugins/core/discounts/server/hooks/index.js new file mode 100644 index 00000000000..0c4003816c7 --- /dev/null +++ b/imports/plugins/core/discounts/server/hooks/index.js @@ -0,0 +1 @@ +import "./collections"; diff --git a/imports/plugins/core/discounts/server/i18n/en.json b/imports/plugins/core/discounts/server/i18n/en.json new file mode 100644 index 00000000000..52d1c4eb1f6 --- /dev/null +++ b/imports/plugins/core/discounts/server/i18n/en.json @@ -0,0 +1,22 @@ +[{ + "language": "English", + "i18n": "en", + "ns": "reaction-discounts", + "translation": { + "reaction-discounts": { + "admin": { + "settings": { + "discountsettingsLabel": "Discount Settings" + }, + "shortcut": { + "discountsLabel": "Discounts", + "discountsDescription": "Provide promotions and discounts." + }, + "dashboard": { + "discountsLabel": "Discounts", + "discountsDescription": "Provide promotions and discounts." + } + } + } + } +}] diff --git a/imports/plugins/core/discounts/server/i18n/index.js b/imports/plugins/core/discounts/server/i18n/index.js new file mode 100644 index 00000000000..02d738aa5bc --- /dev/null +++ b/imports/plugins/core/discounts/server/i18n/index.js @@ -0,0 +1,7 @@ +import { loadTranslation } from "/server/startup/i18n"; +import json from "./en.json"; + +// +// import local translations +// +loadTranslation(json); diff --git a/imports/plugins/core/discounts/server/index.js b/imports/plugins/core/discounts/server/index.js new file mode 100644 index 00000000000..943926f070a --- /dev/null +++ b/imports/plugins/core/discounts/server/index.js @@ -0,0 +1,6 @@ +// assemble server api +import "./i18n"; +import "./hooks/collections"; +import "./security/discounts"; +import "./publications/discounts"; +import "./methods"; diff --git a/imports/plugins/core/discounts/server/methods/index.js b/imports/plugins/core/discounts/server/methods/index.js new file mode 100644 index 00000000000..c8abe684b3c --- /dev/null +++ b/imports/plugins/core/discounts/server/methods/index.js @@ -0,0 +1 @@ +import "./methods"; diff --git a/imports/plugins/core/discounts/server/methods/methods.app-test.js b/imports/plugins/core/discounts/server/methods/methods.app-test.js new file mode 100644 index 00000000000..161d2bcd237 --- /dev/null +++ b/imports/plugins/core/discounts/server/methods/methods.app-test.js @@ -0,0 +1,30 @@ +import { Meteor } from "meteor/meteor"; +import { Roles } from "meteor/alanning:roles"; +import { expect } from "meteor/practicalmeteor:chai"; +import { sinon } from "meteor/practicalmeteor:sinon"; + +before(function () { + this.timeout(10000); + Meteor._sleepForMs(7000); +}); + +describe("discounts methods", function () { + let sandbox; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe("discounts/deleteRate", function () { + it("should throw 403 error with discounts permission", function (done) { + sandbox.stub(Roles, "userIsInRole", () => false); + // this should actually trigger a whole lot of things + expect(() => Meteor.call("discounts/deleteRate", "dummystring")).to.throw(Meteor.Error, /Access Denied/); + return done(); + }); + }); +}); diff --git a/imports/plugins/core/discounts/server/methods/methods.js b/imports/plugins/core/discounts/server/methods/methods.js new file mode 100644 index 00000000000..e87e6a9afcf --- /dev/null +++ b/imports/plugins/core/discounts/server/methods/methods.js @@ -0,0 +1,82 @@ +import { Meteor } from "meteor/meteor"; +import { Match, check } from "meteor/check"; +import { Cart, Packages } from "/lib/collections"; +import { Discounts } from "../../lib/collections"; +import Reaction from "../api"; +import { Logger } from "/server/api"; + +// +// make all discount methods available +// +export const methods = { + /** + * discounts/deleteRate + * @param {String} discountId discount id to delete + * @return {String} returns update/insert result + */ + "discounts/deleteRate": function (discountId) { + check(discountId, String); + + // check permissions to delete + if (!Reaction.hasPermission("discounts")) { + throw new Meteor.Error(403, "Access Denied"); + } + + return Discounts.remove(discountId); + }, + + /** + * discounts/setRate + * @param {String} cartId cartId + * @param {Number} discountRate discountRate + * @param {Object} discounts discounts + * @return {Number} returns update result + */ + "discounts/setRate": function (cartId, discountRate, discounts) { + check(cartId, String); + check(discountRate, Number); + check(discounts, Match.Optional(Array)); + + return Cart.update(cartId, { + $set: { + discounts: discounts, + discount: discountRate + } + }); + }, + + /** + * discounts/calculate + * @param {String} cartId cartId + * @return {Object} returns discount object + */ + "discounts/calculate": function (cartId) { + check(cartId, String); + const cartToCalc = Cart.findOne(cartId); + const shopId = cartToCalc.shopId; + const discountRate = 0; + + // get all discount packages + const pkg = Packages.findOne({ + shopId: shopId, + name: "reaction-discounts" + }); + + // + // custom rates + // check if plugin is enabled and this calculation method is enabled + if (pkg && pkg.enabled === true && pkg.settings.rates.enabled === true) { + Logger.info("Calculating custom discount rates"); + Meteor.call("discounts/setRate", cartToCalc._id, discountRate); + } else { + // we are here because the custom rate package is disabled. + // we're going to set an inital rate of 0 + // all methods that trigger when discounts/calculate will + // recalculate this rate as needed. + // Meteor.call("discounts/setRate", cartToCalc._id, discountRate); + } + } // end discounts/calculate +}; + +// export methods to Meteor +Meteor.methods(methods); diff --git a/imports/plugins/core/discounts/server/publications/discounts.js b/imports/plugins/core/discounts/server/publications/discounts.js new file mode 100644 index 00000000000..f334c585f4e --- /dev/null +++ b/imports/plugins/core/discounts/server/publications/discounts.js @@ -0,0 +1,41 @@ +import { Meteor } from "meteor/meteor"; +import { Match, check} from "meteor/check"; +import { Counts } from "meteor/tmeasday:publish-counts"; +import { Discounts} from "../../lib/collections"; +import { Reaction } from "/server/api"; + +/** + * Discounts + * @type {Publication} + * @param {Object} query + * @param {Object} options + */ +Meteor.publish("Discounts", function (query, options) { + check(query, Match.Optional(Object)); + check(options, Match.Optional(Object)); + + // check shopId + const shopId = Reaction.getShopId(); + if (!shopId && query) { + return this.ready(); + } + + const select = query || {}; + // append shopId to query + // applicable discounts are published + // for this users cartId; + select.shopId = shopId; + // select.cartId = cartId; + + // appends a count to the collection + // we're doing this for use with griddleTable + Counts.publish(this, "discounts-count", Discounts.find( + select, + options + )); + + return Discounts.find( + select, + options + ); +}); diff --git a/imports/plugins/core/discounts/server/security/discounts.js b/imports/plugins/core/discounts/server/security/discounts.js new file mode 100644 index 00000000000..d142c5a6b7e --- /dev/null +++ b/imports/plugins/core/discounts/server/security/discounts.js @@ -0,0 +1,13 @@ +import { Security } from "meteor/ongoworks:security"; +import { Discounts } from "../../lib/collections"; +import { Reaction } from "/server/api"; + +// +// Security definitions +// +Security.permit(["read", "insert", "update", "remove"]).collections([ + Discounts +]).ifHasRole({ + role: "discounts", + group: Reaction.getShopId() +}); diff --git a/imports/plugins/core/payments/client/checkout/index.js b/imports/plugins/core/payments/client/checkout/index.js new file mode 100644 index 00000000000..90aef2610a5 --- /dev/null +++ b/imports/plugins/core/payments/client/checkout/index.js @@ -0,0 +1 @@ +import "./payment"; diff --git a/imports/plugins/core/payments/client/checkout/payment/index.js b/imports/plugins/core/payments/client/checkout/payment/index.js new file mode 100644 index 00000000000..c985fe13913 --- /dev/null +++ b/imports/plugins/core/payments/client/checkout/payment/index.js @@ -0,0 +1,3 @@ +import "./methods/cards.html"; +import "./methods/cards.js"; +import "./payment.html"; diff --git a/imports/plugins/core/checkout/client/templates/checkout/payment/methods/cards.html b/imports/plugins/core/payments/client/checkout/payment/methods/cards.html similarity index 96% rename from imports/plugins/core/checkout/client/templates/checkout/payment/methods/cards.html rename to imports/plugins/core/payments/client/checkout/payment/methods/cards.html index 4e4adbcffac..de79539a3a0 100644 --- a/imports/plugins/core/checkout/client/templates/checkout/payment/methods/cards.html +++ b/imports/plugins/core/payments/client/checkout/payment/methods/cards.html @@ -1,7 +1,6 @@