diff --git a/app/assets/javascripts/admin/components/themes-list-item.js.es6 b/app/assets/javascripts/admin/components/themes-list-item.js.es6 new file mode 100644 index 00000000000..fe5243585bf --- /dev/null +++ b/app/assets/javascripts/admin/components/themes-list-item.js.es6 @@ -0,0 +1,32 @@ +import { default as computed } from "ember-addons/ember-computed-decorators"; + +const MAX_COMPONENTS = 4; + +export default Ember.Component.extend({ + classNames: ["themes-list-item"], + classNameBindings: ["theme.active:active"], + hasComponents: Em.computed.gt("children.length", 0), + hasMore: Em.computed.gt("moreCount", 0), + + @computed( + "theme.component", + "theme.childThemes.@each.name", + "theme.childThemes.length" + ) + children() { + const theme = this.get("theme"); + const children = theme.get("childThemes"); + if (theme.get("component") || !children) { + return []; + } + return children.slice(0, MAX_COMPONENTS).map(t => t.get("name")); + }, + + @computed("theme.childThemes.length", "theme.component", "children.length") + moreCount(childrenCount, component) { + if (component || !childrenCount) { + return 0; + } + return childrenCount - MAX_COMPONENTS; + } +}); diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js.es6 index 5e40d157fba..cf41e4dd33c 100644 --- a/app/assets/javascripts/admin/components/themes-list.js.es6 +++ b/app/assets/javascripts/admin/components/themes-list.js.es6 @@ -1,4 +1,84 @@ +import { THEMES, COMPONENTS } from "admin/models/theme"; +import { default as computed } from "ember-addons/ember-computed-decorators"; + +const NUM_ENTRIES = 8; + export default Ember.Component.extend({ + THEMES: THEMES, + COMPONENTS: COMPONENTS, + classNames: ["themes-list"], - hasThemes: Ember.computed.gt("themes.length", 0) + + hasThemes: Em.computed.gt("themesList.length", 0), + hasUserThemes: Em.computed.gt("userThemes.length", 0), + hasInactiveThemes: Em.computed.gt("inactiveThemes.length", 0), + + themesTabActive: Em.computed.equal("currentTab", THEMES), + componentsTabActive: Em.computed.equal("currentTab", COMPONENTS), + + @computed("themes", "components", "currentTab") + themesList(themes, components) { + if (this.get("themesTabActive")) { + return themes; + } else { + return components; + } + }, + + @computed( + "themesList", + "currentTab", + "themesList.@each.user_selectable", + "themesList.@each.default" + ) + inactiveThemes(themes) { + if (this.get("componentsTabActive")) { + return []; + } + return themes.filter( + theme => !theme.get("user_selectable") && !theme.get("default") + ); + }, + + @computed( + "themesList", + "currentTab", + "themesList.@each.user_selectable", + "themesList.@each.default" + ) + userThemes(themes) { + if (this.get("componentsTabActive")) { + return []; + } + themes = themes.filter( + theme => theme.get("user_selectable") || theme.get("default") + ); + return _.sortBy(themes, t => { + return [ + !t.get("default"), + !t.get("user_selectable"), + t.get("name").toLowerCase() + ]; + }); + }, + + didRender() { + let height = -1; + this.$(".themes-list-item") + .slice(0, NUM_ENTRIES) + .each(function() { + height += $(this).outerHeight(); + }); + if (height >= 485 && height <= 800) { + this.$(".themes-list-container").css("max-height", `${height}px`); + } + }, + + actions: { + changeView(newTab) { + if (newTab !== this.get("currentTab")) { + this.set("currentTab", newTab); + } + } + } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index 40ce1b37c7b..c73d4467fd4 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -6,6 +6,7 @@ import { url } from "discourse/lib/computed"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; import ThemeSettings from "admin/models/theme-settings"; +import { THEMES, COMPONENTS } from "admin/models/theme"; const THEME_UPLOAD_VAR = 2; const SETTINGS_TYPE_ID = 5; @@ -111,9 +112,20 @@ export default Ember.Controller.extend({ }, @computed("model.component") - switchKey(component) { + convertKey(component) { const type = component ? "component" : "theme"; - return `admin.customize.theme.switch_${type}`; + return `admin.customize.theme.convert_${type}`; + }, + + @computed("model.component") + convertIcon(component) { + return component ? "cube" : ""; + }, + + @computed("model.component") + convertTooltip(component) { + const type = component ? "component" : "theme"; + return `admin.customize.theme.convert_${type}_tooltip`; }, @computed("model.settings") @@ -128,6 +140,48 @@ export default Ember.Controller.extend({ downloadUrl: url("model.id", "/admin/themes/%@"), + commitSwitchType() { + const model = this.get("model"); + const newValue = !model.get("component"); + model.set("component", newValue); + + if (newValue) { + // component + this.set("parentController.currentTab", COMPONENTS); + } else { + this.set("parentController.currentTab", THEMES); + } + + model + .saveChanges("component") + .then(() => { + this.set("colorSchemeId", null); + + model.setProperties({ + default: false, + color_scheme_id: null, + user_selectable: false, + child_themes: [], + childThemes: [] + }); + + this.get("parentController.model.content").forEach(theme => { + const children = Array.from(theme.get("childThemes")); + const rawChildren = Array.from(theme.get("child_themes") || []); + const index = children ? children.indexOf(model) : -1; + if (index > -1) { + children.splice(index, 1); + rawChildren.splice(index, 1); + theme.setProperties({ + childThemes: children, + child_themes: rawChildren + }); + } + }); + }) + .catch(popupAjaxError); + }, + actions: { updateToLatest() { this.set("updatingRemote", true); @@ -264,30 +318,26 @@ export default Ember.Controller.extend({ }, switchType() { - return bootbox.confirm( - I18n.t(`${this.get("switchKey")}_alert`), - I18n.t("no_value"), - I18n.t("yes_value"), - result => { - if (result) { - const model = this.get("model"); - model.set("component", !model.get("component")); - model - .saveChanges("component") - .then(() => { - this.set("colorSchemeId", null); - model.setProperties({ - default: false, - color_scheme_id: null, - user_selectable: false, - child_themes: [], - childThemes: [] - }); - }) - .catch(popupAjaxError); + const relatives = this.get("model.component") + ? this.get("parentThemes") + : this.get("model.childThemes"); + if (relatives && relatives.length > 0) { + const names = relatives.map(relative => relative.get("name")); + bootbox.confirm( + I18n.t(`${this.get("convertKey")}_alert`, { + relatives: names.join(", ") + }), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + this.commitSwitchType(); + } } - } - ); + ); + } else { + this.commitSwitchType(); + } } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 index 8468634a0ad..641ec628260 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 @@ -1,18 +1,15 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; +import { THEMES } from "admin/models/theme"; export default Ember.Controller.extend({ - @computed("model", "model.@each", "model.@each.component") + currentTab: THEMES, + + @computed("model", "model.@each.component") fullThemes(themes) { - return _.sortBy(themes.filter(t => !t.get("component")), t => { - return [ - !t.get("default"), - !t.get("user_selectable"), - t.get("name").toLowerCase() - ]; - }); + return themes.filter(t => !t.get("component")); }, - @computed("model", "model.@each", "model.@each.component") + @computed("model", "model.@each.component") childThemes(themes) { return themes.filter(t => t.get("component")); } diff --git a/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 index 343be61eb1e..4b25e006c25 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 @@ -1,27 +1,56 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { default as computed } from "ember-addons/ember-computed-decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { THEMES, COMPONENTS } from "admin/models/theme"; -const COMPONENT = "component"; +const MIN_NAME_LENGTH = 4; export default Ember.Controller.extend(ModalFunctionality, { - types: [ - { name: I18n.t("admin.customize.theme.theme"), value: "theme" }, - { name: I18n.t("admin.customize.theme.component"), value: COMPONENT } - ], - selectedType: "theme", - name: I18n.t("admin.customize.new_style"), + saving: false, + triggerError: false, themesController: Ember.inject.controller("adminCustomizeThemes"), - loading: false, + types: [ + { name: I18n.t("admin.customize.theme.theme"), value: THEMES }, + { name: I18n.t("admin.customize.theme.component"), value: COMPONENTS } + ], + + @computed("triggerError", "nameTooShort") + showError(trigger, tooShort) { + return trigger && tooShort; + }, + + @computed("name") + nameTooShort(name) { + return !name || name.length < MIN_NAME_LENGTH; + }, + + @computed("component") + placeholder(component) { + if (component) { + return I18n.t("admin.customize.theme.component_name"); + } else { + return I18n.t("admin.customize.theme.theme_name"); + } + }, + + @computed("themesController.currentTab") + selectedType(tab) { + return tab; + }, @computed("selectedType") component(type) { - return type === COMPONENT; + return type === COMPONENTS; }, actions: { createTheme() { - this.set("loading", true); + if (this.get("nameTooShort")) { + this.set("triggerError", true); + return; + } + + this.set("saving", true); const theme = this.store.createRecord("theme"); theme .save({ name: this.get("name"), component: this.get("component") }) @@ -30,7 +59,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.send("closeModal"); }) .catch(popupAjaxError) - .finally(() => this.set("loading", false)); + .finally(() => this.set("saving", false)); } } }); diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index c3d7fd800da..fa37798fc01 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -4,6 +4,9 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; const THEME_UPLOAD_VAR = 2; +export const THEMES = "themes"; +export const COMPONENTS = "components"; + const Theme = RestModel.extend({ FIELDS_IDS: [0, 1], @@ -33,6 +36,18 @@ const Theme = RestModel.extend({ ); }, + @computed("theme_fields", "theme_fields.@each.error") + isBroken(fields) { + return ( + fields && fields.some(field => field.error && field.error.length > 0) + ); + }, + + @computed("remote_theme", "remote_theme.commits_behind") + isPendingUpdates(remote, commitsBehind) { + return remote && commitsBehind && commitsBehind > 0; + }, + getKey(field) { return `${field.target} ${field.name}`; }, diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 index e9ca55bb382..f3736ffa74a 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-show.js.es6 @@ -1,4 +1,5 @@ import { scrollTop } from "discourse/mixins/scroll-top"; +import { THEMES, COMPONENTS } from "admin/models/theme"; export default Ember.Route.extend({ serialize(model) { @@ -14,19 +15,21 @@ export default Ember.Route.extend({ setupController(controller, model) { this._super(...arguments); - controller.set("model", model); - const parentController = this.controllerFor("adminCustomizeThemes"); - parentController.set("editingTheme", false); - controller.set("allThemes", parentController.get("model")); + parentController.setProperties({ + editingTheme: false, + currentTab: model.get("component") ? COMPONENTS : THEMES + }); + + controller.setProperties({ + model: model, + parentController: parentController, + allThemes: parentController.get("model"), + colorSchemeId: model.get("color_scheme_id"), + colorSchemes: parentController.get("model.extras.color_schemes") + }); this.handleHighlight(model); - - controller.set( - "colorSchemes", - parentController.get("model.extras.color_schemes") - ); - controller.set("colorSchemeId", model.get("color_scheme_id")); }, deactivate() { diff --git a/app/assets/javascripts/admin/templates/components/themes-list-item.hbs b/app/assets/javascripts/admin/templates/components/themes-list-item.hbs new file mode 100644 index 00000000000..3cde5084b0d --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/themes-list-item.hbs @@ -0,0 +1,34 @@ +{{#link-to 'adminCustomizeThemes.show' theme replace=true}} + {{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}} + +
+ + {{theme.name}} + + + + {{#if theme.default}} + {{d-icon "check" class="default-indicator" title="admin.customize.theme.default_theme_tooltip"}} + {{/if}} + {{#if theme.isPendingUpdates}} + {{d-icon "refresh" title="admin.customize.theme.updates_available_tooltip" class="light-grey-icon"}} + {{/if}} + {{#if theme.isBroken}} + {{d-icon "exclamation-circle" class="broken-indicator" title="admin.customize.theme.broken_theme_tooltip"}} + {{/if}} + +
+ + {{#if hasComponents}} +
+ {{#each children as |child|}} + + {{child}} + + {{/each}} + {{#if hasMore}} + {{I18n "admin.customize.theme.and_x_more" count=moreCount}} + {{/if}} +
+ {{/if}} +{{/link-to}} diff --git a/app/assets/javascripts/admin/templates/components/themes-list.hbs b/app/assets/javascripts/admin/templates/components/themes-list.hbs index 147f34808e2..53a404b0fea 100644 --- a/app/assets/javascripts/admin/templates/components/themes-list.hbs +++ b/app/assets/javascripts/admin/templates/components/themes-list.hbs @@ -1,23 +1,37 @@
- {{I18n title}} +
+ {{d-icon "cube"}} + {{I18n "admin.customize.theme.title"}} +
+ {{I18n "admin.customize.theme.components"}} +
-
+
{{#if hasThemes}} - {{#each themes as |theme|}} -
- {{#link-to 'adminCustomizeThemes.show' theme replace=true}} - {{plugin-outlet name="admin-customize-themes-list-item" connectorTagName='span' args=(hash theme=theme)}} - {{theme.name}} - {{#if theme.user_selectable}} - {{d-icon "user"}} - {{/if}} - {{#if theme.default}} - {{d-icon "asterisk"}} - {{/if}} - {{/link-to}} -
- {{/each}} + {{#if componentsTabActive}} + {{#each themesList as |theme|}} + {{themes-list-item theme=theme}} + {{/each}} + {{else}} + {{#if hasUserThemes}} + {{#each userThemes as |theme|}} + {{themes-list-item theme=theme}} + {{/each}} + + {{#if hasInactiveThemes}} +
+ {{I18n "admin.customize.theme.inactive_themes"}} +
+ {{/if}} + {{/if}} + + {{#if hasInactiveThemes}} + {{#each inactiveThemes as |theme|}} + {{themes-list-item theme=theme}} + {{/each}} + {{/if}} + {{/if}} {{else}}
{{I18n "admin.customize.theme.empty"}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-index.hbs b/app/assets/javascripts/admin/templates/customize-themes-index.hbs deleted file mode 100644 index 781d063c7f2..00000000000 --- a/app/assets/javascripts/admin/templates/customize-themes-index.hbs +++ /dev/null @@ -1 +0,0 @@ -

{{i18n 'admin.customize.about'}}

diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 21090a03e92..1d490ade5e1 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -1,5 +1,5 @@
-

+
{{#if editingName}} {{text-field value=model.name autofocus="true"}} {{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}} @@ -7,65 +7,64 @@ {{else}} {{model.name}} {{d-icon "pencil"}} {{/if}} -

+
{{#if model.remote_theme}} -

- {{i18n "admin.customize.theme.about_theme"}} -

- {{#if model.remote_theme.license_url}} -

- {{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}} -

+ {{i18n "admin.customize.theme.about_theme"}} + {{#if model.remote_theme.license_url}} + {{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}} + {{/if}} {{/if}} - {{/if}} - {{#if parentThemes}} -

{{i18n "admin.customize.theme.component_of"}}

-
    - {{#each parentThemes as |theme|}} -
  • {{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}
  • - {{/each}} -
+
+
{{i18n "admin.customize.theme.component_of"}}
+
    + {{#each parentThemes as |theme|}} +
  • {{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}
  • + {{/each}} +
+
{{/if}} {{#unless model.component}} -

+

{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}} {{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}} -

+
-

{{i18n "admin.customize.theme.color_scheme"}}

-

{{i18n "admin.customize.theme.color_scheme_select"}}

-

{{combo-box content=colorSchemes - filterable=true - forceEscape=true - value=colorSchemeId - icon="paint-brush"}} - {{#if colorSchemeChanged}} - {{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}} - {{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}} - {{/if}} -

- {{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}} +
+
{{i18n "admin.customize.theme.color_scheme"}}
+
{{i18n "admin.customize.theme.color_scheme_select"}}
+
{{combo-box content=colorSchemes + filterable=true + forceEscape=true + value=colorSchemeId + icon="paint-brush"}} + {{#if colorSchemeChanged}} + {{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}} + {{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}} + {{/if}} +
+ {{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}} +
{{/unless}} -

{{i18n "admin.customize.theme.css_html"}}

- {{#if hasEditedFields}} -

{{i18n "admin.customize.theme.custom_sections"}}

-
    - {{#each editedDescriptions as |desc|}} -
  • {{desc}}
  • - {{/each}} -
- {{else}} -

- {{i18n "admin.customize.theme.edit_css_html_help"}} -

- {{/if}} +
+
{{i18n "admin.customize.theme.css_html"}}
+ {{#if hasEditedFields}} +
{{i18n "admin.customize.theme.custom_sections"}}
+
    + {{#each editedDescriptions as |desc|}} +
  • {{desc}}
  • + {{/each}} +
+ {{else}} +
+ {{i18n "admin.customize.theme.edit_css_html_help"}} +
+ {{/if}} -

{{#if model.remote_theme}} {{#if model.remote_theme.commits_behind}} {{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} @@ -75,7 +74,6 @@ {{/if}} {{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} - {{#if model.remote_theme}} {{#if updatingRemote}} @@ -94,64 +92,67 @@ {{/if}} {{/if}} -

+
- -

{{i18n "admin.customize.theme.uploads"}}

- {{#if model.uploads}} -
    - {{#each model.uploads as |upload|}} -
  • - ${{upload.name}}: {{upload.filename}} - - {{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}} - -
  • - {{/each}} -
- {{else}} -

{{i18n "admin.customize.theme.no_uploads"}}

- {{/if}} -

+

+
{{i18n "admin.customize.theme.uploads"}}
+ {{#if model.uploads}} +
    + {{#each model.uploads as |upload|}} +
  • + ${{upload.name}}: {{upload.filename}} + + {{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}} + +
  • + {{/each}} +
+ {{else}} +
{{i18n "admin.customize.theme.no_uploads"}}
+ {{/if}} {{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} -

+
{{#if hasSettings}} -

{{i18n "admin.customize.theme.theme_settings"}}

- {{#d-section class="form-horizontal theme settings"}} - {{#each settings as |setting|}} - {{theme-setting setting=setting model=model class="theme-setting"}} - {{/each}} - {{/d-section}} +
+
{{i18n "admin.customize.theme.theme_settings"}}
+ {{#d-section class="form-horizontal theme settings"}} + {{#each settings as |setting|}} + {{theme-setting setting=setting model=model class="theme-setting"}} + {{/each}} + {{/d-section}} +
{{/if}} {{#if availableChildThemes}} -

{{i18n "admin.customize.theme.theme_components"}}

- {{#unless model.childThemes.length}} -

- -

- {{else}} -
    - {{#each model.childThemes as |child|}} -
  • {{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" icon="times"}}
  • - {{/each}} -
- {{/unless}} - {{#if selectableChildThemes}} -

- {{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId}} - {{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} -

- {{/if}} +
+
{{i18n "admin.customize.theme.theme_components"}}
+ {{#unless model.childThemes.length}} +
+ +
+ {{else}} +
    + {{#each model.childThemes as |child|}} +
  • {{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" icon="times"}}
  • + {{/each}} +
+ {{/unless}} + {{#if selectableChildThemes}} +
+ {{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId}} + {{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} +
+ {{/if}} +
{{/if}} {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} - {{d-button action="switchType" label=switchKey icon="arrows-h" class="btn-danger"}} + {{d-button action="switchType" label="admin.customize.theme.convert" icon=convertIcon class="btn-normal" title=convertTooltip}} {{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index e796f5cbb5c..68f12bc4044 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,15 +1,14 @@ {{#unless editingTheme}} -
-

{{i18n 'admin.customize.theme.long_title'}}

+
+
+

{{i18n 'admin.customize.theme.long_title'}}

+
-
- {{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}} - {{d-button action="importModal" icon="upload" label="admin.customize.import"}} +
+ {{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}} + {{d-button action="importModal" icon="upload" label="admin.customize.import"}} +
- - {{themes-list themes=fullThemes title="admin.customize.theme.title"}} - {{themes-list themes=childThemes title="admin.customize.theme.components"}} - -
+ {{themes-list themes=fullThemes components=childThemes currentTab=currentTab}} {{/unless}} {{outlet}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs index 3fb6a43c19c..2d5c8d69391 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs @@ -1,10 +1,10 @@ -{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}} +{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.create"}}
{{I18n "admin.customize.theme.create_name"}} - {{input value=name}} + {{input value=name placeholder=placeholder}}
@@ -16,9 +16,15 @@ {{combo-box valueAttribute="value" content=types value=selectedType}}
+ {{#if showError}} +
+ {{d-icon "warning"}} + {{I18n "admin.customize.theme.name_too_short"}} +
+ {{/if}} {{/d-modal-body}} diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 66aee294028..2ad408cac33 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -93,7 +93,7 @@ registerIconRenderer({ let tagName = params.tagName || "i"; let html = `<${tagName} class='${faClasses(icon, params)}'`; if (params.title) { - html += ` title='${I18n.t(params.title)}'`; + html += ` title='${I18n.t(params.title).replace(/'/g, "'")}'`; } if (params.label) { html += " aria-hidden='true'"; diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 1001755386a..ccd02cedf63 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -52,6 +52,30 @@ display: inline-block; } } + + .error { + color: $danger; + } +} + +.admin-customize.admin-customize-themes { + .customize-themes-header { + border-bottom: 1px solid $primary-low; + padding-bottom: 8px; + display: flex; + align-items: center; + + .title { + color: $primary-medium; + flex-grow: 1; + h3 { + margin-bottom: 0; + } + } + } + .admin-container { + padding: 0; + } } .admin-customize { @@ -99,70 +123,179 @@ } } .show-current-style { - margin-left: 20px; + .url { + margin-bottom: 10px; + } + + .title { + font-size: $font-up-4; + font-weight: bold; + margin-bottom: 10px; + } + .about-url, + license-url { + display: block; + margin-bottom: 10px; + } + .mini-title { + font-size: $font-up-1; + font-weight: bold; + margin-bottom: 7px; + } + .control-unit { + margin-bottom: 25px; + margin-top: 15px; + } + .control { + margin-bottom: 10px; + } + .description { + margin-bottom: 12px; + } + + padding-left: 15px; + padding-top: 10px; float: left; width: 70%; - h2 { - margin-bottom: 15px; - } - h3 { - margin-bottom: 10px; - margin-top: 30px; - } - } - - .create-actions { - margin-bottom: 10px; } .themes-list { - margin-bottom: 20px; + border-right: 1px solid $primary-low; + border-bottom: 1px solid $primary-low; + float: left; + width: 28%; } .themes-list-header { - font-size: $font-up-1; - padding: 10px; - background-color: $primary-low; + width: 100%; + border-bottom: 1px solid $primary-low; + + .tab { + display: inline-block; + padding: 10px; + width: 50%; + box-sizing: border-box; + text-align: center; + border-left: 1px solid $primary-low; + + &.active { + font-weight: bold; + color: $secondary; + background-color: $tertiary; + } + + &:not(.active) { + cursor: pointer; + + &:hover { + background-color: $tertiary-low; + } + } + } } .themes-list-container { - max-height: 280px; - overflow-y: scroll; + max-height: 485px; + overflow-y: auto; &::-webkit-scrollbar-track { - border-radius: 10px; background-color: $secondary; } - &::-webkit-scrollbar { width: 5px; } - &::-webkit-scrollbar-thumb { - border-radius: 10px; - background-color: $primary-low-mid; + background-color: $primary-low; + } + .themes-list-item:last-child { + border-bottom: none; } - .themes-list-item { color: $primary; border-bottom: 1px solid $primary-low; display: flex; + border-left: 1px solid $primary-low; - &:hover { + &:not(.inactive-indicator):not(.active):hover { background-color: $tertiary-low; + .component { + border-color: $primary-low-mid; + } } - &.active { color: $secondary; - font-weight: bold; background-color: $tertiary; + .fa { + color: inherit; + } + } + .light-grey-icon { + color: $primary-medium; + } + .info { + overflow: hidden; + font-weight: bold; + font-size: $font-up-1; + + .name { + float: left; + } + + .icons { + float: right; + } + } + + .components-list { + margin-top: 5px; + display: flex; + flex-wrap: wrap; + font-size: $font-down-1; + align-items: baseline; + + .component { + display: flex; + padding: 3px 5px 3px 5px; + border-radius: 2px; + border: 1px solid $primary-low; + margin-right: 5px; + margin-bottom: 5px; + } + } + &:not(.active) { + .broken-indicator { + color: $danger; + } + + .default-indicator { + color: $success; + } + } + + a { + padding: 10px; + } + + &.inactive-indicator { + border-right: 0; + border-left: 0; + font-weight: bold; + color: $primary-medium; + + span.empty { + padding-left: 5px; + padding-top: 15px; + } + } + + span.empty { + padding: 3px 10px 3px 10px; } a, span.empty { color: inherit; width: 100%; - padding: 10px; } } } @@ -170,7 +303,7 @@ .theme.settings { .theme-setting { padding-bottom: 0; - padding-top: 18px; + margin-top: 18px; min-height: 35px; } .setting-label { diff --git a/app/views/qunit/index.html.erb b/app/views/qunit/index.html.erb index 0515dfc0d65..eef411cfd57 100644 --- a/app/views/qunit/index.html.erb +++ b/app/views/qunit/index.html.erb @@ -5,6 +5,7 @@ <%= stylesheet_link_tag "test_helper" %> <%= javascript_include_tag "test_helper" %> <%= csrf_meta_tags %> +
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fc28a6e444e..df1eb640bd9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3212,13 +3212,15 @@ en: theme: "Theme" component: "Component" components: "Components" + theme_name: "Theme name" + component_name: "Component name" import_theme: "Import Theme" customize_desc: "Customize:" title: "Themes" - modal_title: "Create Theme" create: "Create" create_type: "Type:" create_name: "Name:" + name_too_short: "The name must be at least 4 characters long." long_title: "Amend colors, CSS and HTML contents of your site" edit: "Edit" edit_confirm: "This is a remote theme, if you edit CSS/HTML your changes will be erased next time you update the theme." @@ -3233,10 +3235,16 @@ en: color_scheme_select: "Select colors to be used by theme" custom_sections: "Custom sections:" theme_components: "Theme Components" - switch_component: "Make theme" - switch_component_alert: "Are you sure you want to convert this component to theme? This will make it an independant theme and it will be removed as a child from all themes." - switch_theme: "Make component" - switch_theme_alert: "Are you sure you want to convert this theme to component? It will be removed as a parent from all components." + convert: "Convert" + convert_component_alert: "Are you sure you want to convert this component to theme? It will be removed as a component from %{relatives}." + convert_component_tooltip: "Convert this component to theme" + convert_theme_alert: "Are you sure you want to convert this theme to component? It will be removed as a parent from %{relatives}." + convert_theme_tooltip: "Convert this theme to component" + inactive_themes: "Inactive themes:" + broken_theme_tooltip: "This theme has errors in its CSS, HTML or YAML" + default_theme_tooltip: "This theme is the site's default theme" + updates_available_tooltip: "Updates are available for this theme" + and_x_more: "and {{count}} more." uploads: "Uploads" no_uploads: "You can upload assets associated with your theme such as fonts and images" add_upload: "Add Upload" diff --git a/test/javascripts/admin/components/themes-list-item-test.js.es6 b/test/javascripts/admin/components/themes-list-item-test.js.es6 new file mode 100644 index 00000000000..da7eda3c6b9 --- /dev/null +++ b/test/javascripts/admin/components/themes-list-item-test.js.es6 @@ -0,0 +1,84 @@ +import componentTest from "helpers/component-test"; +import Theme from "admin/models/theme"; + +moduleForComponent("themes-list-item", { integration: true }); + +componentTest("default theme", { + template: "{{themes-list-item theme=theme}}", + beforeEach() { + this.set("theme", Theme.create({ name: "Test", default: true })); + }, + + test(assert) { + assert.expect(1); + assert.equal(this.$(".fa-check").length, 1, "shows default theme icon"); + } +}); + +componentTest("pending updates", { + template: "{{themes-list-item theme=theme}}", + beforeEach() { + this.set( + "theme", + Theme.create({ name: "Test", remote_theme: { commits_behind: 6 } }) + ); + }, + + test(assert) { + assert.expect(1); + assert.equal(this.$(".fa-refresh").length, 1, "shows pending update icon"); + } +}); + +componentTest("borken theme", { + template: "{{themes-list-item theme=theme}}", + beforeEach() { + this.set( + "theme", + Theme.create({ + name: "Test", + theme_fields: [{ name: "scss", type_id: 1, error: "something" }] + }) + ); + }, + + test(assert) { + assert.expect(1); + assert.equal( + this.$(".fa-exclamation-circle").length, + 1, + "shows broken theme icon" + ); + } +}); + +const childrenList = [1, 2, 3, 4, 5].map(num => + Theme.create({ name: `Child ${num}`, component: true }) +); + +componentTest("with children", { + template: "{{themes-list-item theme=theme}}", + + beforeEach() { + this.set( + "theme", + Theme.create({ name: "Test", childThemes: childrenList }) + ); + }, + + test(assert) { + assert.expect(2); + assert.deepEqual( + Array.from(this.$(".component")).map(node => node.innerText.trim()), + childrenList.splice(0, 4).map(theme => theme.get("name")), + "lists the first 4 children" + ); + assert.deepEqual( + this.$(".others-count") + .text() + .trim(), + I18n.t("admin.customize.theme.and_x_more", { count: 1 }), + "shows count of remaining children" + ); + } +}); diff --git a/test/javascripts/admin/components/themes-list-test.js.es6 b/test/javascripts/admin/components/themes-list-test.js.es6 new file mode 100644 index 00000000000..040b9772b6a --- /dev/null +++ b/test/javascripts/admin/components/themes-list-test.js.es6 @@ -0,0 +1,132 @@ +import componentTest from "helpers/component-test"; +import { default as Theme, THEMES, COMPONENTS } from "admin/models/theme"; + +moduleForComponent("themes-list", { integration: true }); + +const themes = [1, 2, 3, 4, 5].map(num => + Theme.create({ name: `Theme ${num}` }) +); +const components = [1, 2, 3, 4, 5].map(num => + Theme.create({ name: `Child ${num}`, component: true }) +); + +componentTest("current tab is themes", { + template: + "{{themes-list themes=themes components=components currentTab=currentTab}}", + beforeEach() { + this.setProperties({ + themes, + components, + currentTab: THEMES + }); + }, + + test(assert) { + assert.equal( + this.$(".themes-tab").hasClass("active"), + true, + "themes tab is active" + ); + assert.equal( + this.$(".components-tab").hasClass("active"), + false, + "components tab is not active" + ); + + assert.equal( + this.$(".inactive-indicator").index(), + -1, + "there is no inactive themes separator when all themes are inactive" + ); + assert.equal(this.$(".themes-list-item").length, 5, "displays all themes"); + + [2, 3].forEach(num => themes[num].set("user_selectable", true)); + themes[4].set("default", true); + this.set("themes", themes); + const names = [4, 2, 3, 0, 1].map(num => themes[num].get("name")); // default theme always on top, followed by user-selectable ones and then the rest + assert.deepEqual( + Array.from(this.$(".themes-list-item").find(".name")).map(node => + node.innerText.trim() + ), + names, + "sorts themes correctly" + ); + assert.equal( + this.$(".inactive-indicator").index(), + 3, + "the separator is in the right location" + ); + + themes.forEach(theme => theme.set("user_selectable", true)); + this.set("themes", themes); + assert.equal( + this.$(".inactive-indicator").index(), + -1, + "there is no inactive themes separator when all themes are user-selectable" + ); + + this.set("themes", []); + assert.equal( + this.$(".themes-list-item").length, + 1, + "shows one entry with a message when there is nothing to display" + ); + assert.equal( + this.$(".themes-list-item span.empty") + .text() + .trim(), + I18n.t("admin.customize.theme.empty"), + "displays the right message" + ); + } +}); + +componentTest("current tab is components", { + template: + "{{themes-list themes=themes components=components currentTab=currentTab}}", + beforeEach() { + this.setProperties({ + themes, + components, + currentTab: COMPONENTS + }); + }, + + test(assert) { + assert.equal( + this.$(".components-tab").hasClass("active"), + true, + "components tab is active" + ); + assert.equal( + this.$(".themes-tab").hasClass("active"), + false, + "themes tab is not active" + ); + + assert.equal( + this.$(".inactive-indicator").index(), + -1, + "there is no separator" + ); + assert.equal( + this.$(".themes-list-item").length, + 5, + "displays all components" + ); + + this.set("components", []); + assert.equal( + this.$(".themes-list-item").length, + 1, + "shows one entry with a message when there is nothing to display" + ); + assert.equal( + this.$(".themes-list-item span.empty") + .text() + .trim(), + I18n.t("admin.customize.theme.empty"), + "displays the right message" + ); + } +}); diff --git a/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 b/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 index a2c290a710c..a07f0432036 100644 --- a/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 @@ -29,8 +29,8 @@ QUnit.test("can list themes correctly", function(assert) { assert.deepEqual( controller.get("fullThemes").map(t => t.get("name")), - [defaultTheme, userTheme, strayTheme1, strayTheme2].map(t => t.get("name")), - "sorts themes correctly" + [strayTheme2, strayTheme1, userTheme, defaultTheme].map(t => t.get("name")), + "returns a list of themes without components" ); assert.deepEqual(