FEATURE: customization of html emails (#7934)

FEATURE: customization of html emails (#7934)

This feature adds the ability to customize the HTML part of all emails using a custom HTML template and optionally some CSS to style it. The CSS will be parsed and converted into inline styles because CSS is poorly supported by email clients. When writing the custom HTML and CSS, be aware of what email clients support. Keep customizations very simple.

Customizations can be added and edited in Admin > Customize > Email Style.

Since the summary email is already heavily styled, there is a setting to disable custom styles for summary emails called “apply custom styles to digest” found in Admin > Settings > Email.

As part of this work, RTL locales are now rendered correctly for all emails.

diff --git a/Gemfile b/Gemfile
index 2db7fff..dde65dc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -78,6 +78,7 @@ gem 'discourse_image_optim', require: 'image_optim'
 gem 'multi_json'
 gem 'mustache'
 gem 'nokogiri'
+gem 'css_parser', require: false
 
 gem 'omniauth'
 gem 'omniauth-openid'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3e50a65a..c0438f2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -88,6 +88,8 @@ GEM
     crack (0.4.3)
       safe_yaml (~> 1.0.0)
     crass (1.0.4)
+    css_parser (1.7.0)
+      addressable
     debug_inspector (0.0.3)
     diff-lcs (1.3)
     diffy (3.3.0)
@@ -438,6 +440,7 @@ DEPENDENCIES
   certified
   colored2
   cppjieba_rb
+  css_parser
   diffy
   discourse-ember-source (~> 3.10.0)
   discourse_image_optim
diff --git a/app/assets/javascripts/admin/adapters/email-style.js.es6 b/app/assets/javascripts/admin/adapters/email-style.js.es6
new file mode 100644
index 0000000..c9f3865
--- /dev/null
+++ b/app/assets/javascripts/admin/adapters/email-style.js.es6
@@ -0,0 +1,7 @@
+import RestAdapter from "discourse/adapters/rest";
+
+export default RestAdapter.extend({
+  pathFor() {
+    return "/admin/customize/email_style";
+  }
+});
diff --git a/app/assets/javascripts/admin/components/email-styles-editor.js.es6 b/app/assets/javascripts/admin/components/email-styles-editor.js.es6
new file mode 100644
index 0000000..d0e5694
--- /dev/null
+++ b/app/assets/javascripts/admin/components/email-styles-editor.js.es6
@@ -0,0 +1,45 @@
+import computed from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+  editorId: Ember.computed.reads("fieldName"),
+
+  @computed("fieldName", "styles.html", "styles.css")
+  resetDisabled(fieldName) {
+    return (
+      this.get(`styles.${fieldName}`) ===
+      this.get(`styles.default_${fieldName}`)
+    );
+  },
+
+  @computed("styles", "fieldName")
+  editorContents: {
+    get(styles, fieldName) {
+      return styles[fieldName];
+    },
+    set(value, styles, fieldName) {
+      styles.setField(fieldName, value);
+      return value;
+    }
+  },
+
+  actions: {
+    reset() {
+      bootbox.confirm(
+        I18n.t("admin.customize.email_style.reset_confirm", {
+          fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`)
+        }),
+        I18n.t("no_value"),
+        I18n.t("yes_value"),
+        result => {
+          if (result) {
+            this.styles.setField(
+              this.fieldName,
+              this.styles.get(`default_${this.fieldName}`)
+            );
+            this.notifyPropertyChange("editorContents");
+          }
+        }
+      );
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6
new file mode 100644
index 0000000..b8054c8
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/admin-customize-email-style-edit.js.es6
@@ -0,0 +1,33 @@
+import computed from "ember-addons/ember-computed-decorators";
+
+export default Ember.Controller.extend({
+  @computed("model.isSaving")
+  saveButtonText(isSaving) {
+    return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save");
+  },
+
+  @computed("model.changed", "model.isSaving")
+  saveDisabled(changed, isSaving) {
+    return !changed || isSaving;
+  },
+
+  actions: {
+    save() {
+      if (!this.model.saving) {
+        this.set("saving", true);
+        this.model
+          .update(this.model.getProperties("html", "css"))
+          .catch(e => {
+            const msg =
+              e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
+                ? I18n.t("admin.customize.email_style.save_error_with_reason", {
+                    error: e.jqXHR.responseJSON.errors.join(". ")
+                  })
+                : I18n.t("generic_error");
+            bootbox.alert(msg);
+          })
+          .finally(() => this.set("model.changed", false));
+      }
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/models/email-style.js.es6 b/app/assets/javascripts/admin/models/email-style.js.es6
new file mode 100644
index 0000000..29d7568
--- /dev/null
+++ b/app/assets/javascripts/admin/models/email-style.js.es6
@@ -0,0 +1,10 @@
+import RestModel from "discourse/models/rest";
+
+export default RestModel.extend({
+  changed: false,
+
+  setField(fieldName, value) {
+    this.set(`${fieldName}`, value);
+    this.set("changed", true);
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6
new file mode 100644
index 0000000..74649c1
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-customize-email-style-edit.js.es6
@@ -0,0 +1,39 @@
+export default Ember.Route.extend({
+  model(params) {
+    return {
+      model: this.modelFor("adminCustomizeEmailStyle"),
+      fieldName: params.field_name
+    };
+  },
+
+  setupController(controller, model) {
+    controller.setProperties({
+      fieldName: model.fieldName,
+      model: model.model
+    });
+    this._shouldAlertUnsavedChanges = true;
+  },
+
+  actions: {
+    willTransition(transition) {
+      if (
+        this.get("controller.model.changed") &&
+        this._shouldAlertUnsavedChanges &&
+        transition.intent.name !== this.routeName
+      ) {
+        transition.abort();
+        bootbox.confirm(
+          I18n.t("admin.customize.theme.unsaved_changes_alert"),
+          I18n.t("admin.customize.theme.discard"),
+          I18n.t("admin.customize.theme.stay"),
+          result => {
+            if (!result) {
+              this._shouldAlertUnsavedChanges = false;
+              transition.retry();
+            }
+          }
+        );
+      }
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-style.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-style.js.es6
new file mode 100644
index 0000000..8e202e6
--- /dev/null
+++ b/app/assets/javascripts/admin/routes/admin-customize-email-style.js.es6
@@ -0,0 +1,9 @@
+export default Ember.Route.extend({
+  model() {
+    return this.store.find("email-style");
+  },
+
+  redirect() {
+    this.transitionTo("adminCustomizeEmailStyle.edit", "html");
+  }
+});
diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
index a20165d..478a851 100644
--- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6
@@ -90,6 +90,13 @@ export default function() {
           path: "/robots",
           resetNamespace: true
         });
+        this.route(
+          "adminCustomizeEmailStyle",
+          { path: "/email_style", resetNamespace: true },
+          function() {
+            this.route("edit", { path: "/:field_name" });
+          }
+        );
       }
     );
 
diff --git a/app/assets/javascripts/admin/templates/components/email-styles-editor.hbs b/app/assets/javascripts/admin/templates/components/email-styles-editor.hbs
new file mode 100644
index 0000000..8831496
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/email-styles-editor.hbs
@@ -0,0 +1,20 @@
+<div class='row'>
+  <div class='admin-controls'>
+    <nav>
+      <ul class='nav nav-pills'>
+        <li>{{#link-to 'adminCustomizeEmailStyle.edit' 'html' replace=true}}{{i18n 'admin.customize.email_style.html'}}{{/link-to}}</li>
+        <li>{{#link-to 'adminCustomizeEmailStyle.edit' 'css' replace=true}}{{i18n 'admin.customize.email_style.css'}}{{/link-to}}</li>
+      </ul>
+    </nav>
+  </div>
+</div>
+
+{{ace-editor content=editorContents mode=fieldName editorId=editorId}}
+
+<div class='admin-footer'>
+  <div class='buttons'>
+    {{#d-button action=(action "reset") disabled=resetDisabled class='btn-default'}}
+      {{i18n 'admin.customize.email_style.reset'}}
+    {{/d-button}}
+  </div>
+</div>

[... diff too long, it was truncated ...]

GitHub sha: 9656a21f

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there:

This commit has been mentioned on Discourse Meta. There might be relevant details there: