From 05ee1d1aba3d1238867b59d91b72327688358439 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 19 Feb 2019 12:56:01 +0000 Subject: [PATCH] FEATURE: Added settings/translations support to theme editor UI (#7026) - These advanced fields are hidden behind an 'advanced' button, so will not affect normal use - The editor has been refactored into a component, and styling cleaned up so menu items do not overlap on small screens - Styling has been added to indicate which fields are in use for a theme - Icons have been added to identify which fields have errors --- .../components/admin-theme-editor.js.es6 | 96 +++++++++ .../admin-customize-themes-edit.js.es6 | 184 ++++-------------- .../javascripts/admin/models/theme.js.es6 | 97 ++++++++- .../routes/admin-customize-themes-edit.js.es6 | 2 +- .../components/admin-theme-editor.hbs | 94 +++++++++ .../admin/templates/customize-themes-edit.hbs | 71 ++----- .../stylesheets/common/admin/customize.scss | 83 ++++---- config/locales/client.en.yml | 4 + lib/svg_sprite/svg_sprite.rb | 2 + 9 files changed, 380 insertions(+), 253 deletions(-) create mode 100644 app/assets/javascripts/admin/components/admin-theme-editor.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs diff --git a/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 new file mode 100644 index 00000000000..af661e49cfc --- /dev/null +++ b/app/assets/javascripts/admin/components/admin-theme-editor.js.es6 @@ -0,0 +1,96 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + @computed("theme.targets", "onlyOverridden", "showAdvanced") + visibleTargets(targets, onlyOverridden, showAdvanced) { + return targets.filter(target => { + if (target.advanced && !showAdvanced) { + return false; + } + if (!onlyOverridden) { + return true; + } + return target.edited; + }); + }, + + @computed("currentTargetName", "onlyOverridden", "theme.fields") + visibleFields(targetName, onlyOverridden, fields) { + fields = fields[targetName]; + if (onlyOverridden) { + fields = fields.filter(field => field.edited); + } + return fields; + }, + + @computed("currentTargetName", "fieldName") + activeSectionMode(targetName, fieldName) { + if (["settings", "translations"].includes(targetName)) return "yaml"; + return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; + }, + + @computed("fieldName", "currentTargetName", "theme") + activeSection: { + get(fieldName, target, model) { + return model.getField(target, fieldName); + }, + set(value, fieldName, target, model) { + model.setField(target, fieldName, value); + return value; + } + }, + + @computed("fieldName", "currentTargetName") + editorId(fieldName, currentTarget) { + return fieldName + "|" + currentTarget; + }, + + @computed("maximized") + maximizeIcon(maximized) { + return maximized ? "discourse-compress" : "discourse-expand"; + }, + + @computed("currentTargetName", "theme.targets") + showAddField(currentTargetName, targets) { + return targets.find(t => t.name === currentTargetName).customNames; + }, + + @computed("currentTargetName", "fieldName", "theme.theme_fields.@each.error") + error(target, fieldName) { + return this.get("theme").getError(target, fieldName); + }, + + actions: { + toggleShowAdvanced() { + this.toggleProperty("showAdvanced"); + }, + + toggleAddField() { + this.toggleProperty("addingField"); + }, + + cancelAddField() { + this.set("addingField", false); + }, + + addField(name) { + if (!name) return; + name = name.replace(/\W/g, ""); + this.get("theme").setField(this.get("currentTargetName"), name, ""); + this.set("newFieldName", ""); + this.set("addingField", false); + this.fieldAdded(this.get("currentTargetName"), name); + }, + + toggleMaximize: function() { + this.toggleProperty("maximized"); + Ember.run.next(() => { + this.appEvents.trigger("ace:resize"); + }); + }, + + onlyOverriddenChanged(value) { + this.onlyOverriddenChanged(value); + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index 807c21e768e..9c33c27392a 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -1,163 +1,28 @@ import { url } from "discourse/lib/computed"; -import { - default as computed, - observes -} from "ember-addons/ember-computed-decorators"; +import { default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ section: null, currentTarget: 0, maximized: false, previewUrl: url("model.id", "/admin/themes/%@/preview"), - + showAdvanced: false, editRouteName: "adminCustomizeThemes.edit", - - targets: [ - { id: 0, name: "common" }, - { id: 1, name: "desktop" }, - { id: 2, name: "mobile" }, - { id: 3, name: "settings" }, - { id: 4, name: "translations" } - ], - - fieldsForTarget: function(target) { - const common = [ - "scss", - "head_tag", - "header", - "after_header", - "body_tag", - "footer" - ]; - switch (target) { - case "common": - return [...common, "embedded_scss"]; - case "desktop": - return common; - case "mobile": - return common; - case "settings": - return ["yaml"]; - } - }, - - @computed("onlyOverridden") - showCommon() { - return this.shouldShow("common"); - }, - - @computed("onlyOverridden") - showDesktop() { - return this.shouldShow("desktop"); - }, - - @computed("onlyOverridden") - showMobile() { - return this.shouldShow("mobile"); - }, - - @observes("onlyOverridden") - onlyOverriddenChanged() { - if (this.get("onlyOverridden")) { - if ( - !this.get("model").hasEdited( - this.get("currentTargetName"), - this.get("fieldName") - ) - ) { - let target = - (this.get("showCommon") && "common") || - (this.get("showDesktop") && "desktop") || - (this.get("showMobile") && "mobile"); - - let fields = this.get("model.theme_fields"); - let field = fields && fields.find(f => f.target === target); - this.replaceRoute( - this.get("editRouteName"), - this.get("model.id"), - target, - field && field.name - ); - } - } - }, - - shouldShow(target) { - if (!this.get("onlyOverridden")) { - return true; - } - return this.get("model").hasEdited(target); - }, + showRouteName: "adminCustomizeThemes.show", setTargetName: function(name) { - const target = this.get("targets").find(t => t.name === name); + const target = this.get("model.targets").find(t => t.name === name); this.set("currentTarget", target && target.id); }, @computed("currentTarget") currentTargetName(id) { - const target = this.get("targets").find(t => t.id === parseInt(id, 10)); + const target = this.get("model.targets").find( + t => t.id === parseInt(id, 10) + ); return target && target.name; }, - @computed("fieldName") - activeSectionMode(fieldName) { - if (fieldName === "yaml") return "yaml"; - return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html"; - }, - - @computed("currentTargetName", "fieldName", "saving") - error(target, fieldName) { - return this.get("model").getError(target, fieldName); - }, - - @computed("fieldName", "currentTargetName") - editorId(fieldName, currentTarget) { - return fieldName + "|" + currentTarget; - }, - - @computed("fieldName", "currentTargetName", "model") - activeSection: { - get(fieldName, target, model) { - return model.getField(target, fieldName); - }, - set(value, fieldName, target, model) { - model.setField(target, fieldName, value); - return value; - } - }, - - @computed("currentTargetName", "onlyOverridden") - fields(target, onlyOverridden) { - let fields = this.fieldsForTarget(target); - - if (onlyOverridden) { - const model = this.get("model"); - const targetName = this.get("currentTargetName"); - fields = fields.filter(name => model.hasEdited(targetName, name)); - } - - return fields.map(name => { - let hash = { - key: `admin.customize.theme.${name}.text`, - name: name - }; - - if (name.indexOf("_tag") > 0) { - hash.icon = "file-text-o"; - } - - hash.title = I18n.t(`admin.customize.theme.${name}.title`); - - return hash; - }); - }, - - @computed("maximized") - maximizeIcon(maximized) { - return maximized ? "discourse-compress" : "discourse-expand"; - }, - @computed("model.isSaving") saveButtonText(isSaving) { return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); @@ -178,11 +43,36 @@ export default Ember.Controller.extend({ }); }, - toggleMaximize: function() { - this.toggleProperty("maximized"); - Ember.run.next(() => { - this.appEvents.trigger("ace:resize"); - }); + fieldAdded(target, name) { + this.replaceRoute( + this.get("editRouteName"), + this.get("model.id"), + target, + name + ); + }, + + onlyOverriddenChanged(onlyShowOverridden) { + if (onlyShowOverridden) { + if ( + !this.get("model").hasEdited( + this.get("currentTargetName"), + this.get("fieldName") + ) + ) { + let firstTarget = this.get("model.targets").find(t => t.edited); + let firstField = this.get(`model.fields.${firstTarget.name}`).find( + f => f.edited + ); + + this.replaceRoute( + this.get("editRouteName"), + this.get("model.id"), + firstTarget.name, + firstField.name + ); + } + } } } }); diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index a3ae2308202..25612ed9f1c 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -9,11 +9,87 @@ export const COMPONENTS = "components"; const SETTINGS_TYPE_ID = 5; const Theme = RestModel.extend({ - FIELDS_IDS: [0, 1], + FIELDS_IDS: [0, 1, 5], isActive: Ember.computed.or("default", "user_selectable"), isPendingUpdates: Ember.computed.gt("remote_theme.commits_behind", 0), hasEditedFields: Ember.computed.gt("editedFields.length", 0), + @computed("theme_fields.[]") + targets() { + return [ + { id: 0, name: "common" }, + { id: 1, name: "desktop", icon: "desktop" }, + { id: 2, name: "mobile", icon: "mobile-alt" }, + { id: 3, name: "settings", icon: "cog", advanced: true }, + { + id: 4, + name: "translations", + icon: "globe", + advanced: true, + customNames: true + } + ].map(target => { + target["edited"] = this.hasEdited(target.name); + target["error"] = this.hasError(target.name); + return target; + }); + }, + + @computed("theme_fields.[]") + fieldNames() { + const common = [ + "scss", + "head_tag", + "header", + "after_header", + "body_tag", + "footer" + ]; + + return { + common: [...common, "embedded_scss"], + desktop: common, + mobile: common, + settings: ["yaml"], + translations: [ + "en", + ...(this.get("theme_fields") || []) + .filter(f => f.target === "translations" && f.name !== "en") + .map(f => f.name) + ] + }; + }, + + @computed("fieldNames", "theme_fields.@each.error") + fields(fieldNames) { + const hash = {}; + Object.keys(fieldNames).forEach(target => { + hash[target] = fieldNames[target].map(fieldName => { + const field = { + name: fieldName, + edited: this.hasEdited(target, fieldName), + error: this.hasError(target, fieldName) + }; + + if (target === "translations") { + field.translatedName = fieldName; + } else { + field.translatedName = I18n.t( + `admin.customize.theme.${fieldName}.text` + ); + field.title = I18n.t(`admin.customize.theme.${fieldName}.title`); + } + + if (fieldName.indexOf("_tag") > 0) { + field.icon = "far-file-alt"; + } + + return field; + }); + }); + return hash; + }, + @computed("theme_fields") themeFields(fields) { if (!fields) { @@ -76,6 +152,14 @@ const Theme = RestModel.extend({ } }, + hasError(target, name) { + return this.get("theme_fields") + .filter(f => { + return f.target === target && (!name || name === f.name); + }) + .any(f => f.error); + }, + getError(target, name) { let themeFields = this.get("themeFields"); let key = this.getKey({ target, name }); @@ -114,7 +198,7 @@ const Theme = RestModel.extend({ existing.value = value; existing.upload_id = upload_id; } else { - fields.push(field); + fields.pushObject(field); } return; } @@ -123,10 +207,15 @@ const Theme = RestModel.extend({ let key = this.getKey({ target, name }); let existingField = themeFields[key]; if (!existingField) { - this.theme_fields.push(field); + this.theme_fields.pushObject(field); themeFields[key] = field; } else { - existingField.value = value; + if (Ember.isEmpty(value)) { + this.theme_fields.removeObject(themeFields[key]); + themeFields[key] = null; + } else { + existingField.value = value; + } } }, diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 index 5bab2900915..818a75e6e4f 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-edit.js.es6 @@ -21,7 +21,7 @@ export default Ember.Route.extend({ }, setupController(controller, wrapper) { - const fields = controller.fieldsForTarget(wrapper.target); + const fields = wrapper.model.get("fields")[wrapper.target].map(f => f.name); if (!fields.includes(wrapper.field_name)) { this.transitionTo( "adminCustomizeThemes.edit", diff --git a/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs new file mode 100644 index 00000000000..029fd209c9c --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-theme-editor.hbs @@ -0,0 +1,94 @@ +
+ +
+ +
+ +
+ + +{{#if error}} +
{{error}}
+{{/if}} + +{{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index 4807209639d..d17ab8d2f3b 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -1,66 +1,17 @@
-

{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}

- - {{#if error}} -
{{error}}
- {{/if}} - -
- -
- -
-
-
- -
- -
- - {{ace-editor content=activeSection editorId=editorId mode=activeSectionMode autofocus="true"}} +

{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to showRouteName model.id replace=true}}{{model.name}}{{/link-to}}

+ {{admin-theme-editor + theme=model + editRouteName=editRouteName + currentTargetName=currentTargetName + fieldName=fieldName + fieldAdded=(action 'fieldAdded') + maximized=maximized + onlyOverriddenChanged=(action 'onlyOverriddenChanged') + }} +