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