diff --git a/app/assets/javascripts/admin/components/themes-list-item.js.es6 b/app/assets/javascripts/admin/components/themes-list-item.js.es6 index fe5243585b..44f337dd76 100644 --- a/app/assets/javascripts/admin/components/themes-list-item.js.es6 +++ b/app/assets/javascripts/admin/components/themes-list-item.js.es6 @@ -1,32 +1,136 @@ -import { default as computed } from "ember-addons/ember-computed-decorators"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; const MAX_COMPONENTS = 4; export default Ember.Component.extend({ + childrenExpanded: false, classNames: ["themes-list-item"], - classNameBindings: ["theme.active:active"], + classNameBindings: ["theme.selected:selected"], hasComponents: Em.computed.gt("children.length", 0), - hasMore: Em.computed.gt("moreCount", 0), + displayComponents: Em.computed.and("hasComponents", "theme.isActive"), + displayHasMore: Em.computed.gt("theme.childThemes.length", MAX_COMPONENTS), + + click(e) { + if (!$(e.target).hasClass("others-count")) { + this.navigateToTheme(); + } + }, + + init() { + this._super(...arguments); + this.scheduleAnimation(); + }, + + @observes("theme.selected") + triggerAnimation() { + this.animate(); + }, + + scheduleAnimation() { + Ember.run.schedule("afterRender", () => { + this.animate(true); + }); + }, + + animate(isInitial) { + const $container = this.$(); + const $list = this.$(".components-list"); + if ($list.length === 0 || Ember.testing) { + return; + } + const duration = 300; + if (this.get("theme.selected")) { + this.collapseComponentsList($container, $list, duration); + } else if (!isInitial) { + this.expandComponentsList($container, $list, duration); + } + }, @computed( "theme.component", "theme.childThemes.@each.name", - "theme.childThemes.length" + "theme.childThemes.length", + "childrenExpanded" ) children() { const theme = this.get("theme"); - const children = theme.get("childThemes"); + let children = theme.get("childThemes"); if (theme.get("component") || !children) { return []; } - return children.slice(0, MAX_COMPONENTS).map(t => t.get("name")); + children = this.get("childrenExpanded") + ? children + : children.slice(0, MAX_COMPONENTS); + return children.map(t => t.get("name")); }, - @computed("theme.childThemes.length", "theme.component", "children.length") - moreCount(childrenCount, component) { - if (component || !childrenCount) { + @computed( + "theme.childThemes.length", + "theme.component", + "childrenExpanded", + "children.length" + ) + moreCount(childrenCount, component, expanded) { + if (component || !childrenCount || expanded) { return 0; } return childrenCount - MAX_COMPONENTS; + }, + + expandComponentsList($container, $list, duration) { + $container.css("height", `${$container.height()}px`); + $list.css("display", ""); + $container.animate( + { + height: `${$container.height() + $list.outerHeight(true)}px` + }, + { + duration, + done: () => { + $list.css("display", ""); + $container.css("height", ""); + } + } + ); + $list.animate( + { + opacity: 1 + }, + { + duration + } + ); + }, + + collapseComponentsList($container, $list, duration) { + $container.animate( + { + height: `${$container.height() - $list.outerHeight(true)}px` + }, + { + duration, + done: () => { + $list.css("display", "none"); + $container.css("height", ""); + } + } + ); + $list.animate( + { + opacity: 0 + }, + { + duration + } + ); + }, + + actions: { + toggleChildrenExpanded() { + this.toggleProperty("childrenExpanded"); + } } }); diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js.es6 index cf41e4dd33..c9498566c3 100644 --- a/app/assets/javascripts/admin/components/themes-list.js.es6 +++ b/app/assets/javascripts/admin/components/themes-list.js.es6 @@ -1,7 +1,7 @@ import { THEMES, COMPONENTS } from "admin/models/theme"; import { default as computed } from "ember-addons/ember-computed-decorators"; -const NUM_ENTRIES = 8; +const MAX_LIST_HEIGHT = 700; export default Ember.Component.extend({ THEMES: THEMES, @@ -63,15 +63,24 @@ export default Ember.Component.extend({ }, 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`); + this._super(...arguments); + + // hide scrollbar + const $container = this.$(".themes-list-container"); + const containerNode = $container[0]; + if (containerNode) { + const width = containerNode.offsetWidth - containerNode.clientWidth; + $container.css("width", `calc(100% + ${width}px)`); } + + let height = -1; + Array.from(this.$(".themes-list-item")).forEach(node => { + const nodeHeight = $(node).outerHeight(); + if (height + nodeHeight <= MAX_LIST_HEIGHT) { + height += nodeHeight; + } + }); + $container.css("max-height", `${height}px`); }, actions: { @@ -79,6 +88,11 @@ export default Ember.Component.extend({ if (newTab !== this.get("currentTab")) { this.set("currentTab", newTab); } + }, + navigateToTheme(theme) { + Em.getOwner(this) + .lookup("router:main") + .transitionTo("adminCustomizeThemes.show", theme); } } }); 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 7cbe893fe5..c8082cc84d 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 @@ -5,8 +5,10 @@ import { } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ - maximized: false, section: null, + currentTarget: 0, + maximized: false, + previewUrl: url("model.id", "/admin/themes/%@/preview"), editRouteName: "adminCustomizeThemes.edit", @@ -86,8 +88,6 @@ export default Ember.Controller.extend({ return this.get("model").hasEdited(target); }, - currentTarget: 0, - setTargetName: function(name) { const target = this.get("targets").find(t => t.name === name); this.set("currentTarget", target && target.id); @@ -152,21 +152,20 @@ export default Ember.Controller.extend({ }); }, - previewUrl: url("model.id", "/admin/themes/%@/preview"), + @computed("maximized") + maximizeIcon(maximized) { + return maximized ? "compress" : "expand"; + }, - maximizeIcon: function() { - return this.get("maximized") ? "compress" : "expand"; - }.property("maximized"), + @computed("model.isSaving") + saveButtonText(isSaving) { + return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); + }, - saveButtonText: function() { - return this.get("model.isSaving") - ? I18n.t("saving") - : I18n.t("admin.customize.save"); - }.property("model.isSaving"), - - saveDisabled: function() { - return !this.get("model.changed") || this.get("model.isSaving"); - }.property("model.changed", "model.isSaving"), + @computed("model.changed", "model.isSaving") + saveDisabled(changed, isSaving) { + return !changed || isSaving; + }, actions: { save() { 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 c73d4467fd..47b59317f1 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 @@ -9,106 +9,65 @@ import ThemeSettings from "admin/models/theme-settings"; import { THEMES, COMPONENTS } from "admin/models/theme"; const THEME_UPLOAD_VAR = 2; -const SETTINGS_TYPE_ID = 5; export default Ember.Controller.extend({ - editRouteName: "adminCustomizeThemes.edit", - - @observes("allowChildThemes") - setSelectedThemeId() { - const available = this.get("selectableChildThemes"); - if ( - !this.get("selectedChildThemeId") && - available && - available.length > 0 - ) { - this.set("selectedChildThemeId", available[0].get("id")); - } - }, + downloadUrl: url("model.id", "/admin/themes/%@"), + previewUrl: url("model.id", "/admin/themes/%@/preview"), + addButtonDisabled: Em.computed.empty("selectedChildThemeId"), @computed("model", "allThemes", "model.component") parentThemes(model, allThemes) { if (!model.get("component")) { return null; } - let parents = allThemes.filter(theme => + const parents = allThemes.filter(theme => _.contains(theme.get("childThemes"), model) ); return parents.length === 0 ? null : parents; }, - @computed("model.theme_fields.@each") - hasEditedFields(fields) { - return fields.any( - f => !Em.isBlank(f.value) && f.type_id !== SETTINGS_TYPE_ID - ); - }, - - @computed("model.theme_fields.@each") - editedDescriptions(fields) { - let descriptions = []; - let description = target => { - let current = fields.filter( - field => field.target === target && !Em.isBlank(field.value) - ); - if (current.length > 0) { - let text = I18n.t("admin.customize.theme." + target); - let localized = current.map(f => - I18n.t("admin.customize.theme." + f.name + ".text") - ); - return text + ": " + localized.join(" , "); - } - }; + @computed("model.editedFields") + editedFieldsFormatted(fields) { + const descriptions = []; ["common", "desktop", "mobile"].forEach(target => { - descriptions.push(description(target)); + const fields = this.editedFieldsForTarget(target); + if (fields.length < 1) { + return; + } + let resultString = I18n.t("admin.customize.theme." + target); + const formattedFields = fields + .map(f => I18n.t("admin.customize.theme." + f.name + ".text")) + .join(" , "); + resultString += `: ${formattedFields}`; + descriptions.push(resultString); }); - return descriptions.reject(d => Em.isBlank(d)); + return descriptions; }, - previewUrl: url("model.id", "/admin/themes/%@/preview"), - @computed("colorSchemeId", "model.color_scheme_id") colorSchemeChanged(colorSchemeId, existingId) { colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId); return colorSchemeId !== existingId; }, - @computed( - "availableChildThemes", - "model.childThemes.@each", - "model", - "allowChildThemes" - ) - selectableChildThemes(available, childThemes, allowChildThemes) { - if (!allowChildThemes && (!childThemes || childThemes.length === 0)) { - return null; + @computed("availableChildThemes", "model.childThemes.@each", "model") + selectableChildThemes(available, childThemes) { + if (available) { + const themes = !childThemes + ? available + : available.filter(theme => childThemes.indexOf(theme) === -1); + return themes.length === 0 ? null : themes; } - - let themes = []; - available.forEach(t => { - if (!childThemes || childThemes.indexOf(t) === -1) { - themes.push(t); - } - }); - return themes.length === 0 ? null : themes; }, - @computed("allThemes", "allThemes.length", "model.component", "model") - availableChildThemes(allThemes, count, component) { - if (count === 1 || component) { - return null; + @computed("allThemes", "model.component", "model") + availableChildThemes(allThemes) { + if (!this.get("model.component")) { + const themeId = this.get("model.id"); + return allThemes.filter( + theme => theme.get("id") !== themeId && theme.get("component") + ); } - - const themeId = this.get("model.id"); - - let themes = []; - allThemes.forEach(theme => { - if (themeId !== theme.get("id") && theme.get("component")) { - themes.push(theme); - } - }); - - return themes; }, @computed("model.component") @@ -137,8 +96,11 @@ export default Ember.Controller.extend({ hasSettings(settings) { return settings.length > 0; }, - - downloadUrl: url("model.id", "/admin/themes/%@"), + editedFieldsForTarget(target) { + return this.get("model.editedFields").filter( + field => field.target === target + ); + }, commitSwitchType() { const model = this.get("model"); @@ -146,7 +108,6 @@ export default Ember.Controller.extend({ model.set("component", newValue); if (newValue) { - // component this.set("parentController.currentTab", COMPONENTS); } else { this.set("parentController.currentTab", THEMES); @@ -166,8 +127,8 @@ export default Ember.Controller.extend({ }); this.get("parentController.model.content").forEach(theme => { - const children = Array.from(theme.get("childThemes")); - const rawChildren = Array.from(theme.get("child_themes") || []); + const children = _.toArray(theme.get("childThemes")); + const rawChildren = _.toArray(theme.get("child_themes") || []); const index = children ? children.indexOf(model) : -1; if (index > -1) { children.splice(index, 1); @@ -181,7 +142,14 @@ export default Ember.Controller.extend({ }) .catch(popupAjaxError); }, - + transitionToEditRoute() { + this.transitionToRoute( + "adminCustomizeThemes.edit", + this.get("model.id"), + "common", + "scss" + ); + }, actions: { updateToLatest() { this.set("updatingRemote", true); @@ -238,25 +206,17 @@ export default Ember.Controller.extend({ }, editTheme() { - let edit = () => - this.transitionToRoute( - this.get("editRouteName"), - this.get("model.id"), - "common", - "scss" - ); - if (this.get("model.remote_theme")) { bootbox.confirm( I18n.t("admin.customize.theme.edit_confirm"), result => { if (result) { - edit(); + this.transitionToEditRoute(); } } ); } else { - edit(); + this.transitionToEditRoute(); } }, diff --git a/app/assets/javascripts/admin/models/theme.js.es6 b/app/assets/javascripts/admin/models/theme.js.es6 index fa37798fc0..413d9a93c6 100644 --- a/app/assets/javascripts/admin/models/theme.js.es6 +++ b/app/assets/javascripts/admin/models/theme.js.es6 @@ -6,9 +6,13 @@ const THEME_UPLOAD_VAR = 2; export const THEMES = "themes"; export const COMPONENTS = "components"; +const SETTINGS_TYPE_ID = 5; const Theme = RestModel.extend({ FIELDS_IDS: [0, 1], + isActive: Em.computed.or("default", "user_selectable"), + isPendingUpdates: Em.computed.gt("remote_theme.commits_behind", 0), + hasEditedFields: Em.computed.gt("editedFields.length", 0), @computed("theme_fields") themeFields(fields) { @@ -43,9 +47,11 @@ const Theme = RestModel.extend({ ); }, - @computed("remote_theme", "remote_theme.commits_behind") - isPendingUpdates(remote, commitsBehind) { - return remote && commitsBehind && commitsBehind > 0; + @computed("theme_fields.@each") + editedFields(fields) { + return fields.filter( + field => !Em.isBlank(field.value) && field.type_id !== SETTINGS_TYPE_ID + ); }, getKey(field) { 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 7b63658ac0..5bab290091 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 @@ -35,5 +35,29 @@ export default Ember.Route.extend({ controller.setTargetName(wrapper.target || "common"); controller.set("fieldName", wrapper.field_name || "scss"); this.controllerFor("adminCustomizeThemes").set("editingTheme", true); + this.set("shouldAlertUnsavedChanges", true); + }, + + actions: { + willTransition(transition) { + if ( + this.get("controller.model.changed") && + this.get("shouldAlertUnsavedChanges") && + transition.intent.name !== this.routeName + ) { + transition.abort(); + bootbox.confirm( + I18n.t("admin.customize.theme.unsaved_changes_alert"), + I18n.t("admin.customize.theme.discard"), + I18n.t("admin.customize.theme.stay"), + result => { + if (!result) { + this.set("shouldAlertUnsavedChanges", false); + transition.retry(); + } + } + ); + } + } } }); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 index e1a5bf51db..e29ef8659e 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes-index.js.es6 @@ -1,5 +1,25 @@ +const externalResources = [ + { + key: "admin.customize.theme.beginners_guide_title", + link: "https://meta.discourse.org/t/91966", + icon: "book" + }, + { + key: "admin.customize.theme.developers_guide_title", + link: "https://meta.discourse.org/t/93648", + icon: "book" + }, + { + key: "admin.customize.theme.browse_themes", + link: "https://meta.discourse.org/c/theme", + icon: "paint-brush" + } +]; + export default Ember.Route.extend({ - setupController() { + setupController(controller, model) { + this._super(...arguments); this.controllerFor("adminCustomizeThemes").set("editingTheme", false); + controller.set("externalResources", externalResources); } }); 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 f3736ffa74..f39b747ef7 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 @@ -37,9 +37,11 @@ export default Ember.Route.extend({ }, handleHighlight(theme) { - this.get("controller.allThemes").forEach(t => t.set("active", false)); + this.get("controller.allThemes") + .filter(t => t.get("selected")) + .forEach(t => t.set("selected", false)); if (theme) { - theme.set("active", true); + theme.set("selected", true); } }, diff --git a/app/assets/javascripts/admin/templates/components/themes-list-item.hbs b/app/assets/javascripts/admin/templates/components/themes-list-item.hbs index 3cde5084b0..a12ddb4123 100644 --- a/app/assets/javascripts/admin/templates/components/themes-list-item.hbs +++ b/app/assets/javascripts/admin/templates/components/themes-list-item.hbs @@ -1,4 +1,4 @@ -{{#link-to 'adminCustomizeThemes.show' theme replace=true}} +