FEATURE: Admin interface for editing email templates
This commit is contained in:
parent
e168c5fde3
commit
f5b34d5f53
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export default Ember.Controller.extend({
|
||||
titleSorting: ['title'],
|
||||
emailTemplates: null,
|
||||
|
||||
sortedTemplates: Ember.computed.sort('emailTemplates', 'titleSorting')
|
||||
});
|
|
@ -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: {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('email-template');
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set('emailTemplates', model);
|
||||
}
|
||||
});
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
{{d-button action="saveChanges" disabled=buttonDisabled label=savingText}}
|
||||
{{#if saved}}{{i18n 'saved'}}{{/if}}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
<p>{{i18n "admin.customize.email_templates.none_selected"}}</p>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export default Ember.View.extend({
|
||||
classNames: ['customize']
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/customize/";
|
||||
}
|
||||
});
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -1208,6 +1208,16 @@ table.api-keys {
|
|||
|
||||
}
|
||||
|
||||
.email-template {
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.row.groups {
|
||||
input[type='text'] {
|
||||
width: 500px;
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue