diff --git a/app/assets/javascripts/admin/components/save-controls.js.es6 b/app/assets/javascripts/admin/components/save-controls.js.es6 new file mode 100644 index 00000000000..414bbf3661f --- /dev/null +++ b/app/assets/javascripts/admin/components/save-controls.js.es6 @@ -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(); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 new file mode 100644 index 00000000000..4545cd3ef65 --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 @@ -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); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 new file mode 100644 index 00000000000..fe9cd69804c --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 @@ -0,0 +1,6 @@ +export default Ember.Controller.extend({ + titleSorting: ['title'], + emailTemplates: null, + + sortedTemplates: Ember.computed.sort('emailTemplates', 'titleSorting') +}); diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 index 943317b1bfe..159d26e89b5 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-text-edit.js.es6 @@ -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: { diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js.es6 new file mode 100644 index 00000000000..4e049afba69 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-email-templates-edit.js.es6 @@ -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(); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-customize-email-templates.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-email-templates.js.es6 new file mode 100644 index 00000000000..8ad73730803 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-customize-email-templates.js.es6 @@ -0,0 +1,9 @@ +export default Ember.Route.extend({ + model() { + return this.store.findAll('email-template'); + }, + + setupController(controller, model) { + controller.set('emailTemplates', model); + } +}); 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 dd758556c60..b7c7144187c 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -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'); diff --git a/app/assets/javascripts/admin/templates/components/save-controls.hbs b/app/assets/javascripts/admin/templates/components/save-controls.hbs new file mode 100644 index 00000000000..e54a090d796 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/save-controls.hbs @@ -0,0 +1,2 @@ +{{d-button action="saveChanges" disabled=buttonDisabled label=savingText}} +{{#if saved}}{{i18n 'saved'}}{{/if}} diff --git a/app/assets/javascripts/admin/templates/customize-email-templates-edit.hbs b/app/assets/javascripts/admin/templates/customize-email-templates-edit.hbs new file mode 100644 index 00000000000..8f264cf88bf --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-email-templates-edit.hbs @@ -0,0 +1,13 @@ +
+ + + + + {{save-controls model=emailTemplate action="saveChanges" saved=saved}} +
diff --git a/app/assets/javascripts/admin/templates/customize-email-templates-index.hbs b/app/assets/javascripts/admin/templates/customize-email-templates-index.hbs new file mode 100644 index 00000000000..46dcce149fa --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-email-templates-index.hbs @@ -0,0 +1 @@ +

{{i18n "admin.customize.email_templates.none_selected"}}

diff --git a/app/assets/javascripts/admin/templates/customize-email-templates.hbs b/app/assets/javascripts/admin/templates/customize-email-templates.hbs new file mode 100644 index 00000000000..152c02e44a2 --- /dev/null +++ b/app/assets/javascripts/admin/templates/customize-email-templates.hbs @@ -0,0 +1,15 @@ +
+
+ +
+ +
+ {{outlet}} +
+
diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 8ab4c662e74..adfbc08a36c 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -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}} +
+ {{#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}} -
- {{outlet}} +
+ {{outlet}} +
diff --git a/app/assets/javascripts/admin/templates/email.hbs b/app/assets/javascripts/admin/templates/email.hbs index 73886537778..e14d8ba4b5e 100644 --- a/app/assets/javascripts/admin/templates/email.hbs +++ b/app/assets/javascripts/admin/templates/email.hbs @@ -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}}
diff --git a/app/assets/javascripts/admin/templates/site-text-edit.hbs b/app/assets/javascripts/admin/templates/site-text-edit.hbs index 2a6693e8f3f..5ff141d0c93 100644 --- a/app/assets/javascripts/admin/templates/site-text-edit.hbs +++ b/app/assets/javascripts/admin/templates/site-text-edit.hbs @@ -14,13 +14,4 @@ {{ace-editor content=model.value mode="css"}} {{/if}} -
- - {{#if saved}}{{i18n 'saved'}}{{/if}} -
+{{save-controls model=model action="saveChanges" saveDisabled=saveDisabled saved=saved}} diff --git a/app/assets/javascripts/admin/views/admin-customize.js.es6 b/app/assets/javascripts/admin/views/admin-customize.js.es6 deleted file mode 100644 index 240ed0ac73f..00000000000 --- a/app/assets/javascripts/admin/views/admin-customize.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -export default Ember.View.extend({ - classNames: ['customize'] -}); diff --git a/app/assets/javascripts/discourse/adapters/email-template.js.es6 b/app/assets/javascripts/discourse/adapters/email-template.js.es6 new file mode 100644 index 00000000000..f57240a116f --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/email-template.js.es6 @@ -0,0 +1,7 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + basePath() { + return "/admin/customize/"; + } +}); diff --git a/app/assets/javascripts/discourse/adapters/topic.js.es6 b/app/assets/javascripts/discourse/adapters/topic.js.es6 index 858eee01eb8..290dfff4b96 100644 --- a/app/assets/javascripts/discourse/adapters/topic.js.es6 +++ b/app/assets/javascripts/discourse/adapters/topic.js.es6 @@ -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 }); diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 3cf7eadff21..019a5d8a72a 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -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; diff --git a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 index fe6cb765d74..9983d6a3753 100644 --- a/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 +++ b/app/assets/javascripts/discourse/mixins/buffered-content.js.es6 @@ -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'); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index e361da98342..ee6ba203672 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1208,6 +1208,16 @@ table.api-keys { } +.email-template { + input { + width: 100%; + } + + label { + font-weight: bold; + } +} + .row.groups { input[type='text'] { width: 500px; diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb new file mode 100644 index 00000000000..27fd11d7fe8 --- /dev/null +++ b/app/controllers/admin/email_templates_controller.rb @@ -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 diff --git a/app/models/site_text.rb b/app/models/site_text.rb index 88620766a66..82a0f37407b 100644 --- a/app/models/site_text.rb +++ b/app/models/site_text.rb @@ -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 diff --git a/app/serializers/admin_email_template_serializer.rb b/app/serializers/admin_email_template_serializer.rb new file mode 100644 index 00000000000..1c8618cfec8 --- /dev/null +++ b/app/serializers/admin_email_template_serializer.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 72c88369cc3..8c5efd5c318 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 7c02453cbd9..bf0ecb589b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lib/i18n/backend/discourse_i18n.rb b/lib/i18n/backend/discourse_i18n.rb index 6abe29ca2ba..a59b252b42d 100644 --- a/lib/i18n/backend/discourse_i18n.rb +++ b/lib/i18n/backend/discourse_i18n.rb @@ -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