FEATURE: Admin interface for editing email templates

This commit is contained in:
Robin Ward 2015-11-12 16:08:19 -05:00
parent e168c5fde3
commit f5b34d5f53
26 changed files with 240 additions and 60 deletions

View File

@ -0,0 +1,18 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: ['controls'],
buttonDisabled: Ember.computed.or('model.isSaving', 'saveDisabled'),
@computed('model.isSaving')
savingText(saving) {
return saving ? 'saving' : 'save';
},
actions: {
saveChanges() {
this.sendAction();
}
}
});

View File

@ -0,0 +1,16 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { bufferedProperty } from 'discourse/mixins/buffered-content';
export default Ember.Controller.extend(bufferedProperty('emailTemplate'), {
saved: false,
actions: {
saveChanges() {
const model = this.get('emailTemplate');
const buffered = this.get('buffered');
model.save(buffered.getProperties('subject', 'body')).then(() => {
this.set('saved', true);
}).catch(popupAjaxError);
}
}
});

View File

@ -0,0 +1,6 @@
export default Ember.Controller.extend({
titleSorting: ['title'],
emailTemplates: null,
sortedTemplates: Ember.computed.sort('emailTemplates', 'titleSorting')
});

View File

@ -2,9 +2,7 @@ export default Ember.Controller.extend({
saved: false,
saveDisabled: function() {
if (this.get('model.isSaving')) { return true; }
if ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value'))) { return true; }
return false;
return ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value')));
}.property('model.iSaving', 'model.value'),
actions: {

View File

@ -0,0 +1,13 @@
import { scrollTop } from 'discourse/mixins/scroll-top';
export default Ember.Route.extend({
model(params) {
const all = this.modelFor('adminCustomizeEmailTemplates');
return all.findProperty('id', params.id);
},
setupController(controller, model) {
controller.set('emailTemplate', model);
scrollTop();
}
});

View File

@ -0,0 +1,9 @@
export default Ember.Route.extend({
model() {
return this.store.findAll('email-template');
},
setupController(controller, model) {
controller.set('emailTemplates', model);
}
});

View File

@ -28,6 +28,9 @@ export default {
this.resource('adminEmojis', { path: '/emojis' });
this.resource('adminPermalinks', { path: '/permalinks' });
this.resource('adminEmbedding', { path: '/embedding' });
this.resource('adminCustomizeEmailTemplates', { path: '/email_templates' }, function() {
this.route('edit', { path: '/:id' });
});
});
this.route('api');

View File

@ -0,0 +1,2 @@
{{d-button action="saveChanges" disabled=buttonDisabled label=savingText}}
{{#if saved}}{{i18n 'saved'}}{{/if}}

View File

@ -0,0 +1,13 @@
<div class='email-template'>
<label>
{{i18n "admin.customize.email_templates.subject"}}
{{input value=buffered.subject}}
</label>
<label>
{{i18n "admin.customize.email_templates.body"}}
{{d-editor value=buffered.body}}
</label>
{{save-controls model=emailTemplate action="saveChanges" saved=saved}}
</div>

View File

@ -0,0 +1 @@
<p>{{i18n "admin.customize.email_templates.none_selected"}}</p>

View File

@ -0,0 +1,15 @@
<div class='row'>
<div class='content-list span6'>
<ul>
{{#each sortedTemplates as |et|}}
<li>
{{#link-to 'adminCustomizeEmailTemplates.edit' et}}{{et.title}}{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class='content-editor'>
{{outlet}}
</div>
</div>

View File

@ -1,13 +1,16 @@
{{#admin-nav}}
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
{{nav-item route='adminCustomizeCssHtml.index' label='admin.customize.css_html.title'}}
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
{{/admin-nav}}
<div class='customize'>
{{#admin-nav}}
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
{{nav-item route='adminCustomizeCssHtml.index' label='admin.customize.css_html.title'}}
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}
<div class="admin-container">
{{outlet}}
</div>
</div>

View File

@ -4,6 +4,7 @@
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -14,13 +14,4 @@
{{ace-editor content=model.value mode="css"}}
{{/if}}
<div class='controls'>
<button class='btn' {{action "saveChanges"}} disabled={{saveDisabled}}>
{{#if model.isSaving}}
{{i18n 'saving'}}
{{else}}
{{i18n 'save'}}
{{/if}}
</button>
{{#if saved}}{{i18n 'saved'}}{{/if}}
</div>
{{save-controls model=model action="saveChanges" saveDisabled=saveDisabled saved=saved}}

View File

@ -1,3 +0,0 @@
export default Ember.View.extend({
classNames: ['customize']
});

View File

@ -0,0 +1,7 @@
import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({
basePath() {
return "/admin/customize/";
}
});

View File

@ -1,7 +1,6 @@
import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({
find(store, type, findArgs) {
if (findArgs.similar) {
return Discourse.ajax("/topics/similar_to", { data: findArgs.similar });

View File

@ -215,6 +215,8 @@ export default Ember.Component.extend({
},
_updatePreview() {
if (this._state !== "inDOM") { return; }
const value = this.get('value');
const markdownOptions = this.get('markdownOptions') || {};
markdownOptions.sanitize = true;

View File

@ -1,6 +1,6 @@
/* global BufferedProxy: true */
export function bufferedProperty(property) {
return Ember.Mixin.create({
const mixin = {
buffered: function() {
return Em.ObjectProxy.extend(BufferedProxy).create({
content: this.get(property)
@ -14,7 +14,12 @@ export function bufferedProperty(property) {
commitBuffer: function() {
this.get('buffered').applyBufferedChanges();
}
});
};
// It's a good idea to null out fields when declaring objects
mixin.property = null;
return Ember.Mixin.create(mixin);
}
export default bufferedProperty('content');

View File

@ -1208,6 +1208,16 @@ table.api-keys {
}
.email-template {
input {
width: 100%;
}
label {
font-weight: bold;
}
}
.row.groups {
input[type='text'] {
width: 500px;

View File

@ -0,0 +1,51 @@
class Admin::EmailTemplatesController < Admin::AdminController
def self.email_keys
@email_keys ||= ["invite_forum_mailer", "invite_mailer", "invite_password_instructions",
"new_version_mailer", "new_version_mailer_with_notes", "queued_posts_reminder",
"system_messages.backup_failed", "system_messages.backup_succeeded",
"system_messages.blocked_by_staff", "system_messages.bulk_invite_failed",
"system_messages.bulk_invite_succeeded", "system_messages.csv_export_failed",
"system_messages.csv_export_succeeded", "system_messages.download_remote_images_disabled",
"system_messages.email_error_notification", "system_messages.email_reject_auto_generated",
"system_messages.email_reject_destination", "system_messages.email_reject_empty",
"system_messages.email_reject_invalid_access", "system_messages.email_reject_no_account",
"system_messages.email_reject_parsing", "system_messages.email_reject_post_error",
"system_messages.email_reject_post_error_specified",
"system_messages.email_reject_reply_key", "system_messages.email_reject_topic_closed",
"system_messages.email_reject_topic_not_found", "system_messages.email_reject_trust_level",
"system_messages.pending_users_reminder", "system_messages.post_hidden",
"system_messages.restore_failed", "system_messages.restore_succeeded",
"system_messages.spam_post_blocked", "system_messages.too_many_spam_flags",
"system_messages.unblocked", "system_messages.user_automatically_blocked",
"system_messages.welcome_invite", "system_messages.welcome_user", "test_mailer",
"user_notifications.account_created", "user_notifications.admin_login",
"user_notifications.authorize_email", "user_notifications.forgot_password",
"user_notifications.set_password", "user_notifications.signup",
"user_notifications.signup_after_approval",
"user_notifications.user_invited_to_private_message_pm",
"user_notifications.user_invited_to_topic", "user_notifications.user_mentioned",
"user_notifications.user_posted", "user_notifications.user_posted_pm",
"user_notifications.user_quoted", "user_notifications.user_replied"]
end
def show
end
def update
et = params[:email_template]
key = params[:id]
raise Discourse::NotFound unless self.class.email_keys.include?(params[:id])
TranslationOverride.upsert!(I18n.locale, "#{key}.subject_template", et[:subject])
TranslationOverride.upsert!(I18n.locale, "#{key}.text_body_template", et[:body])
render_serialized(key, AdminEmailTemplateSerializer, root: 'email_template', rest_serializer: true)
end
def index
render_serialized(self.class.email_keys, AdminEmailTemplateSerializer, root: 'email_templates', rest_serializer: true)
end
end

View File

@ -3,8 +3,8 @@ require_dependency 'site_text_class_methods'
require_dependency 'distributed_cache'
class SiteText < ActiveRecord::Base
extend SiteTextClassMethods
self.primary_key = 'text_type'
validates_presence_of :value

View File

@ -0,0 +1,19 @@
class AdminEmailTemplateSerializer < ApplicationSerializer
attributes :id, :title, :subject, :body
def id
object
end
def title
object.gsub(/.*\./, '').titleize
end
def subject
I18n.t("#{object}.subject_template")
end
def body
I18n.t("#{object}.text_body_template")
end
end

View File

@ -2087,6 +2087,12 @@ en:
color: "Color"
opacity: "Opacity"
copy: "Copy"
email_templates:
title: "Email Templates"
subject: "Subject"
body: "Body"
none_selected: "Select an email template to begin editing."
css_html:
title: "CSS/HTML"
long_title: "CSS and HTML Customizations"

View File

@ -158,6 +158,11 @@ Discourse::Application.routes.draw do
resources :site_text_types, constraints: AdminConstraint.new
resources :user_fields, constraints: AdminConstraint.new
resources :emojis, constraints: AdminConstraint.new
# They have periods in their URLs often:
get 'email_templates' => 'email_templates#index'
match 'email_templates/(:id)' => 'email_templates#show', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :get
match 'email_templates/(:id)' => 'email_templates#update', :constraints => { :id => /[0-9a-z\_\.]+/ }, via: :put
end
resources :embeddable_hosts, constraints: AdminConstraint.new

View File

@ -18,17 +18,6 @@ module I18n
super
end
def overrides_for(locale)
@overrides ||= {}
site_overrides = @overrides[RailsMultisite::ConnectionManagement.current_db] ||= {}
return site_overrides[locale] if site_overrides[locale]
locale_overrides = site_overrides[locale] = {}
locale_overrides
end
# force explicit loading
def load_translations(*filenames)
unless filenames.empty?
@ -40,24 +29,6 @@ module I18n
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
end
def lookup(locale, key, scope = [], options = {})
# Support interpolation and pluralization of overrides
if options[:overrides]
if options[:count]
result = {}
options[:overrides].each do |k, v|
result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key.to_s)
end
return result if result.size > 0
end
return options[:overrides][key] if options[:overrides][key]
end
super(locale, key, scope, options)
end
def exists?(locale, key)
fallbacks(locale).each do |fallback|
begin
@ -70,6 +41,25 @@ module I18n
false
end
protected
def lookup(locale, key, scope = [], options = {})
# Support interpolation and pluralization of overrides
if options[:overrides]
if options[:count]
result = {}
options[:overrides].each do |k, v|
result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key.to_s)
end
return result if result.size > 0
end
return options[:overrides][key] if options[:overrides][key]
end
super(locale, key, scope, options)
end
end
end
end