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 +};