mirror of
https://github.com/discourse/discourse.git
synced 2025-02-22 22:16:25 +08:00
FEATURE: Admin interface for editing email templates
This commit is contained in:
parent
e168c5fde3
commit
f5b34d5f53
18
app/assets/javascripts/admin/components/save-controls.js.es6
Normal file
18
app/assets/javascripts/admin/components/save-controls.js.es6
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -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,
|
saved: false,
|
||||||
|
|
||||||
saveDisabled: function() {
|
saveDisabled: function() {
|
||||||
if (this.get('model.isSaving')) { return true; }
|
return ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value')));
|
||||||
if ((!this.get('allow_blank')) && Ember.isEmpty(this.get('model.value'))) { return true; }
|
|
||||||
return false;
|
|
||||||
}.property('model.iSaving', 'model.value'),
|
}.property('model.iSaving', 'model.value'),
|
||||||
|
|
||||||
actions: {
|
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('adminEmojis', { path: '/emojis' });
|
||||||
this.resource('adminPermalinks', { path: '/permalinks' });
|
this.resource('adminPermalinks', { path: '/permalinks' });
|
||||||
this.resource('adminEmbedding', { path: '/embedding' });
|
this.resource('adminEmbedding', { path: '/embedding' });
|
||||||
|
this.resource('adminCustomizeEmailTemplates', { path: '/email_templates' }, function() {
|
||||||
|
this.route('edit', { path: '/:id' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.route('api');
|
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}}
|
<div class='customize'>
|
||||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
{{#admin-nav}}
|
||||||
{{nav-item route='adminCustomizeCssHtml.index' label='admin.customize.css_html.title'}}
|
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
{{nav-item route='adminCustomizeCssHtml.index' label='admin.customize.css_html.title'}}
|
||||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||||
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||||
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||||
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
|
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
|
||||||
{{/admin-nav}}
|
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
|
||||||
|
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
|
||||||
|
{{/admin-nav}}
|
||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
|
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
|
||||||
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
|
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
|
||||||
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
|
||||||
|
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||||
{{/admin-nav}}
|
{{/admin-nav}}
|
||||||
|
|
||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
|
@ -14,13 +14,4 @@
|
|||||||
{{ace-editor content=model.value mode="css"}}
|
{{ace-editor content=model.value mode="css"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class='controls'>
|
{{save-controls model=model action="saveChanges" saveDisabled=saveDisabled saved=saved}}
|
||||||
<button class='btn' {{action "saveChanges"}} disabled={{saveDisabled}}>
|
|
||||||
{{#if model.isSaving}}
|
|
||||||
{{i18n 'saving'}}
|
|
||||||
{{else}}
|
|
||||||
{{i18n 'save'}}
|
|
||||||
{{/if}}
|
|
||||||
</button>
|
|
||||||
{{#if saved}}{{i18n 'saved'}}{{/if}}
|
|
||||||
</div>
|
|
||||||
|
@ -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';
|
import RestAdapter from 'discourse/adapters/rest';
|
||||||
|
|
||||||
export default RestAdapter.extend({
|
export default RestAdapter.extend({
|
||||||
|
|
||||||
find(store, type, findArgs) {
|
find(store, type, findArgs) {
|
||||||
if (findArgs.similar) {
|
if (findArgs.similar) {
|
||||||
return Discourse.ajax("/topics/similar_to", { data: findArgs.similar });
|
return Discourse.ajax("/topics/similar_to", { data: findArgs.similar });
|
||||||
|
@ -215,6 +215,8 @@ export default Ember.Component.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
_updatePreview() {
|
_updatePreview() {
|
||||||
|
if (this._state !== "inDOM") { return; }
|
||||||
|
|
||||||
const value = this.get('value');
|
const value = this.get('value');
|
||||||
const markdownOptions = this.get('markdownOptions') || {};
|
const markdownOptions = this.get('markdownOptions') || {};
|
||||||
markdownOptions.sanitize = true;
|
markdownOptions.sanitize = true;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* global BufferedProxy: true */
|
/* global BufferedProxy: true */
|
||||||
export function bufferedProperty(property) {
|
export function bufferedProperty(property) {
|
||||||
return Ember.Mixin.create({
|
const mixin = {
|
||||||
buffered: function() {
|
buffered: function() {
|
||||||
return Em.ObjectProxy.extend(BufferedProxy).create({
|
return Em.ObjectProxy.extend(BufferedProxy).create({
|
||||||
content: this.get(property)
|
content: this.get(property)
|
||||||
@ -14,7 +14,12 @@ export function bufferedProperty(property) {
|
|||||||
commitBuffer: function() {
|
commitBuffer: function() {
|
||||||
this.get('buffered').applyBufferedChanges();
|
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');
|
export default bufferedProperty('content');
|
||||||
|
@ -1208,6 +1208,16 @@ table.api-keys {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email-template {
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.row.groups {
|
.row.groups {
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
51
app/controllers/admin/email_templates_controller.rb
Normal file
51
app/controllers/admin/email_templates_controller.rb
Normal 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
|
@ -3,8 +3,8 @@ require_dependency 'site_text_class_methods'
|
|||||||
require_dependency 'distributed_cache'
|
require_dependency 'distributed_cache'
|
||||||
|
|
||||||
class SiteText < ActiveRecord::Base
|
class SiteText < ActiveRecord::Base
|
||||||
|
|
||||||
extend SiteTextClassMethods
|
extend SiteTextClassMethods
|
||||||
|
|
||||||
self.primary_key = 'text_type'
|
self.primary_key = 'text_type'
|
||||||
|
|
||||||
validates_presence_of :value
|
validates_presence_of :value
|
||||||
|
19
app/serializers/admin_email_template_serializer.rb
Normal file
19
app/serializers/admin_email_template_serializer.rb
Normal 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
|
@ -2087,6 +2087,12 @@ en:
|
|||||||
color: "Color"
|
color: "Color"
|
||||||
opacity: "Opacity"
|
opacity: "Opacity"
|
||||||
copy: "Copy"
|
copy: "Copy"
|
||||||
|
email_templates:
|
||||||
|
title: "Email Templates"
|
||||||
|
subject: "Subject"
|
||||||
|
body: "Body"
|
||||||
|
none_selected: "Select an email template to begin editing."
|
||||||
|
|
||||||
css_html:
|
css_html:
|
||||||
title: "CSS/HTML"
|
title: "CSS/HTML"
|
||||||
long_title: "CSS and HTML Customizations"
|
long_title: "CSS and HTML Customizations"
|
||||||
|
@ -158,6 +158,11 @@ Discourse::Application.routes.draw do
|
|||||||
resources :site_text_types, constraints: AdminConstraint.new
|
resources :site_text_types, constraints: AdminConstraint.new
|
||||||
resources :user_fields, constraints: AdminConstraint.new
|
resources :user_fields, constraints: AdminConstraint.new
|
||||||
resources :emojis, 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
|
end
|
||||||
|
|
||||||
resources :embeddable_hosts, constraints: AdminConstraint.new
|
resources :embeddable_hosts, constraints: AdminConstraint.new
|
||||||
|
@ -18,17 +18,6 @@ module I18n
|
|||||||
super
|
super
|
||||||
end
|
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
|
# force explicit loading
|
||||||
def load_translations(*filenames)
|
def load_translations(*filenames)
|
||||||
unless filenames.empty?
|
unless filenames.empty?
|
||||||
@ -40,24 +29,6 @@ module I18n
|
|||||||
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
|
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
|
||||||
end
|
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)
|
def exists?(locale, key)
|
||||||
fallbacks(locale).each do |fallback|
|
fallbacks(locale).each do |fallback|
|
||||||
begin
|
begin
|
||||||
@ -70,6 +41,25 @@ module I18n
|
|||||||
false
|
false
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user