diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js.es6 new file mode 100644 index 00000000000..5e40d157fba --- /dev/null +++ b/app/assets/javascripts/admin/components/themes-list.js.es6 @@ -0,0 +1,4 @@ +export default Ember.Component.extend({ + classNames: ["themes-list"], + hasThemes: Ember.computed.gt("themes.length", 0) +}); 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 33b46886dbe..7cbe893fe56 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 @@ -53,11 +53,6 @@ export default Ember.Controller.extend({ return this.shouldShow("mobile"); }, - @computed("onlyOverridden", "model.remote_theme") - showSettings() { - return false; - }, - @observes("onlyOverridden") onlyOverriddenChanged() { if (this.get("onlyOverridden")) { 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 955d5a6710c..40ce1b37c7b 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 @@ -8,6 +8,7 @@ import showModal from "discourse/lib/show-modal"; import ThemeSettings from "admin/models/theme-settings"; const THEME_UPLOAD_VAR = 2; +const SETTINGS_TYPE_ID = 5; export default Ember.Controller.extend({ editRouteName: "adminCustomizeThemes.edit", @@ -24,8 +25,11 @@ export default Ember.Controller.extend({ } }, - @computed("model", "allThemes") + @computed("model", "allThemes", "model.component") parentThemes(model, allThemes) { + if (!model.get("component")) { + return null; + } let parents = allThemes.filter(theme => _.contains(theme.get("childThemes"), model) ); @@ -34,7 +38,9 @@ export default Ember.Controller.extend({ @computed("model.theme_fields.@each") hasEditedFields(fields) { - return fields.any(f => !Em.isBlank(f.value)); + return fields.any( + f => !Em.isBlank(f.value) && f.type_id !== SETTINGS_TYPE_ID + ); }, @computed("model.theme_fields.@each") @@ -72,36 +78,31 @@ export default Ember.Controller.extend({ "model", "allowChildThemes" ) - selectableChildThemes(available, childThemes, model, allowChildThemes) { + selectableChildThemes(available, childThemes, allowChildThemes) { if (!allowChildThemes && (!childThemes || childThemes.length === 0)) { return null; } let themes = []; available.forEach(t => { - if ( - (!childThemes || childThemes.indexOf(t) === -1) && - Em.isEmpty(t.get("childThemes")) && - !t.get("user_selectable") && - !t.get("default") - ) { + if (!childThemes || childThemes.indexOf(t) === -1) { themes.push(t); } }); return themes.length === 0 ? null : themes; }, - @computed("allThemes", "allThemes.length", "model", "parentThemes") - availableChildThemes(allThemes, count) { - if (count === 1 || this.get("parentThemes")) { + @computed("allThemes", "allThemes.length", "model.component", "model") + availableChildThemes(allThemes, count, component) { + if (count === 1 || component) { return null; } - let excludeIds = [this.get("model.id")]; + const themeId = this.get("model.id"); let themes = []; allThemes.forEach(theme => { - if (excludeIds.indexOf(theme.get("id")) === -1) { + if (themeId !== theme.get("id") && theme.get("component")) { themes.push(theme); } }); @@ -109,6 +110,12 @@ export default Ember.Controller.extend({ return themes; }, + @computed("model.component") + switchKey(component) { + const type = component ? "component" : "theme"; + return `admin.customize.theme.switch_${type}`; + }, + @computed("model.settings") settings(settings) { return settings.map(setting => ThemeSettings.create(setting)); @@ -254,6 +261,33 @@ 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); + } + } + ); } } }); 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 ecd80018955..8468634a0ad 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 @@ -1,14 +1,19 @@ import { default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ - @computed("model", "model.@each") - sortedThemes(themes) { - return _.sortBy(themes.content, t => { + @computed("model", "model.@each", "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() ]; }); + }, + + @computed("model", "model.@each", "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 new file mode 100644 index 00000000000..343be61eb1e --- /dev/null +++ b/app/assets/javascripts/admin/controllers/modals/admin-create-theme.js.es6 @@ -0,0 +1,36 @@ +import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const COMPONENT = "component"; + +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"), + themesController: Ember.inject.controller("adminCustomizeThemes"), + loading: false, + + @computed("selectedType") + component(type) { + return type === COMPONENT; + }, + + actions: { + createTheme() { + this.set("loading", true); + const theme = this.store.createRecord("theme"); + theme + .save({ name: this.get("name"), component: this.get("component") }) + .then(() => { + this.get("themesController").send("addTheme", theme); + this.send("closeModal"); + }) + .catch(popupAjaxError) + .finally(() => this.set("loading", false)); + } + } +}); 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 7827b605768..e9ca55bb382 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,3 +1,5 @@ +import { scrollTop } from "discourse/mixins/scroll-top"; + export default Ember.Route.extend({ serialize(model) { return { theme_id: model.get("id") }; @@ -10,14 +12,37 @@ 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")); + + this.handleHighlight(model); + controller.set( "colorSchemes", parentController.get("model.extras.color_schemes") ); controller.set("colorSchemeId", model.get("color_scheme_id")); + }, + + deactivate() { + this.handleHighlight(); + }, + + handleHighlight(theme) { + this.get("controller.allThemes").forEach(t => t.set("active", false)); + if (theme) { + theme.set("active", true); + } + }, + + actions: { + didTransition() { + scrollTop(); + } } }); diff --git a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 index 4b772ffd069..24ee2312f34 100644 --- a/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-customize-themes.js.es6 @@ -1,5 +1,4 @@ import showModal from "discourse/lib/show-modal"; -import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Route.extend({ model() { @@ -22,16 +21,8 @@ export default Ember.Route.extend({ this.transitionTo("adminCustomizeThemes.show", theme.get("id")); }, - newTheme(obj) { - obj = obj || { name: I18n.t("admin.customize.new_style") }; - const item = this.store.createRecord("theme"); - - item - .save(obj) - .then(() => { - this.send("addTheme", item); - }) - .catch(popupAjaxError); + showCreateModal() { + showModal("admin-create-theme", { admin: true }); } } }); diff --git a/app/assets/javascripts/admin/templates/components/themes-list.hbs b/app/assets/javascripts/admin/templates/components/themes-list.hbs new file mode 100644 index 00000000000..147f34808e2 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/themes-list.hbs @@ -0,0 +1,26 @@ +
+ {{I18n title}} +
+ +
+ {{#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}} + {{else}} +
+ {{I18n "admin.customize.theme.empty"}} +
+ {{/if}} +
diff --git a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs index c798836b767..0ec171dfa8f 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-edit.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-edit.hbs @@ -31,14 +31,6 @@ {{/link-to}} {{/if}} - {{#if showSettings}} -
  • - {{#link-to 'adminCustomizeThemes.edit' model.id 'settings' fieldName replace=true}} - {{i18n 'admin.customize.theme.settings'}} - {{d-icon 'cog'}} - {{/link-to}} -
  • - {{/if}}
    diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index 91644922640..e796f5cbb5c 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -1,25 +1,15 @@ {{#unless editingTheme}}

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

    - - {{d-button label="admin.customize.new" icon="plus" action="newTheme" 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"}} +
    {{/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 new file mode 100644 index 00000000000..3fb6a43c19c --- /dev/null +++ b/app/assets/javascripts/admin/templates/modal/admin-create-theme.hbs @@ -0,0 +1,24 @@ +{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}} +
    + + {{I18n "admin.customize.theme.create_name"}} + + + {{input value=name}} + +
    + +
    + + {{I18n "admin.customize.theme.create_type"}} + + + {{combo-box valueAttribute="value" content=types value=selectedType}} + +
    +{{/d-modal-body}} + + diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 2faa1b39422..1001755386a 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -44,6 +44,16 @@ } } +.create-theme-modal { + div.input { + margin-bottom: 12px; + .label { + width: 20%; + display: inline-block; + } + } +} + .admin-customize { h1 { margin-bottom: 10px; @@ -101,6 +111,62 @@ } } + .create-actions { + margin-bottom: 10px; + } + + .themes-list { + margin-bottom: 20px; + } + + .themes-list-header { + font-size: $font-up-1; + padding: 10px; + background-color: $primary-low; + } + + .themes-list-container { + max-height: 280px; + overflow-y: scroll; + + &::-webkit-scrollbar-track { + border-radius: 10px; + background-color: $secondary; + } + + &::-webkit-scrollbar { + width: 5px; + } + + &::-webkit-scrollbar-thumb { + border-radius: 10px; + background-color: $primary-low-mid; + } + + .themes-list-item { + color: $primary; + border-bottom: 1px solid $primary-low; + display: flex; + + &:hover { + background-color: $tertiary-low; + } + + &.active { + color: $secondary; + font-weight: bold; + background-color: $tertiary; + } + + a, + span.empty { + color: inherit; + width: 100%; + padding: 10px; + } + } + } + .theme.settings { .theme-setting { padding-bottom: 0; diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index 08a4d10b766..7207392b0f8 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -95,13 +95,13 @@ class Admin::ThemesController < Admin::AdminController end def index - @theme = Theme.order(:name).includes(:theme_fields, :remote_theme) + @themes = Theme.order(:name).includes(:theme_fields, :remote_theme) @color_schemes = ColorScheme.all.to_a light = ColorScheme.new(name: I18n.t("color_schemes.light")) @color_schemes.unshift(light) payload = { - themes: ActiveModel::ArraySerializer.new(@theme, each_serializer: ThemeSerializer), + themes: ActiveModel::ArraySerializer.new(@themes, each_serializer: ThemeSerializer), extras: { color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer) } @@ -116,7 +116,8 @@ class Admin::ThemesController < Admin::AdminController @theme = Theme.new(name: theme_params[:name], user_id: current_user.id, user_selectable: theme_params[:user_selectable] || false, - color_scheme_id: theme_params[:color_scheme_id]) + color_scheme_id: theme_params[:color_scheme_id], + component: [true, "true"].include?(theme_params[:component])) set_fields respond_to do |format| @@ -155,11 +156,11 @@ class Admin::ThemesController < Admin::AdminController Theme.where(id: expected).each do |theme| @theme.add_child_theme!(theme) end - end set_fields update_settings + handle_switch save_remote = false if params[:theme][:remote_check] @@ -247,6 +248,7 @@ class Admin::ThemesController < Admin::AdminController :color_scheme_id, :default, :user_selectable, + :component, settings: {}, theme_fields: [:name, :target, :value, :upload_id, :type_id], child_theme_ids: [] @@ -280,4 +282,12 @@ class Admin::ThemesController < Admin::AdminController StaffActionLogger.new(current_user).log_theme_change(old_record, new_record) end + def handle_switch + param = theme_params[:component] + if param.to_s == "false" && @theme.component? + @theme.switch_to_theme! + elsif param.to_s == "true" && !@theme.component? + @theme.switch_to_component! + end + end end diff --git a/app/models/child_theme.rb b/app/models/child_theme.rb index c37e417dbed..c281f87f061 100644 --- a/app/models/child_theme.rb +++ b/app/models/child_theme.rb @@ -7,17 +7,12 @@ class ChildTheme < ActiveRecord::Base private def child_validations - if ChildTheme.exists?(["parent_theme_id = ? OR child_theme_id = ?", child_theme_id, parent_theme_id]) + if Theme.where( + "(component IS true AND id = :parent) OR (component IS false AND id = :child)", + parent: parent_theme_id, child: child_theme_id + ).exists? errors.add(:base, I18n.t("themes.errors.no_multilevels_components")) end - - if Theme.exists?(id: child_theme_id, user_selectable: true) - errors.add(:base, I18n.t("themes.errors.component_no_user_selectable")) - end - - if child_theme_id == SiteSetting.default_theme_id - errors.add(:base, I18n.t("themes.errors.component_no_default")) - end end end diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 95889f76bc0..32e53fbec17 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -40,7 +40,8 @@ class RemoteTheme < ActiveRecord::Base importer.import! theme_info = JSON.parse(importer["about.json"]) - theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"]) + component = [true, "true"].include?(theme_info["component"]) + theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"], component: component) remote_theme = new theme.remote_theme = remote_theme @@ -142,7 +143,7 @@ class RemoteTheme < ActiveRecord::Base self.commits_behind = 0 end - update_theme_color_schemes(theme, theme_info["color_schemes"]) + update_theme_color_schemes(theme, theme_info["color_schemes"]) unless theme.component self ensure diff --git a/app/models/theme.rb b/app/models/theme.rb index 039d18953b8..71a4f793d5a 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -20,7 +20,7 @@ class Theme < ActiveRecord::Base has_many :color_schemes belongs_to :remote_theme - validate :user_selectable_validation + validate :component_validations scope :user_selectable, ->() { where('user_selectable OR id = ?', SiteSetting.default_theme_id) @@ -128,7 +128,7 @@ class Theme < ActiveRecord::Base end def set_default! - if component? + if component raise Discourse::InvalidParameters.new( I18n.t("themes.errors.component_no_default") ) @@ -141,13 +141,36 @@ class Theme < ActiveRecord::Base SiteSetting.default_theme_id == id end - def component? - ChildTheme.exists?(child_theme_id: id) + def component_validations + return unless component + + errors.add(:base, I18n.t("themes.errors.component_no_color_scheme")) if color_scheme_id.present? + errors.add(:base, I18n.t("themes.errors.component_no_user_selectable")) if user_selectable + errors.add(:base, I18n.t("themes.errors.component_no_default")) if default? end - def user_selectable_validation - if component? && user_selectable - errors.add(:base, I18n.t("themes.errors.component_no_user_selectable")) + def switch_to_component! + return if component + + Theme.transaction do + self.component = true + + self.color_scheme_id = nil + self.user_selectable = false + Theme.clear_default! if default? + + ChildTheme.where("parent_theme_id = ?", id).destroy_all + self.save! + end + end + + def switch_to_theme! + return unless component + + Theme.transaction do + self.component = false + ChildTheme.where("child_theme_id = ?", id).destroy_all + self.save! end end diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb index fc2ce44cc89..d8e6f5e6626 100644 --- a/app/serializers/theme_serializer.rb +++ b/app/serializers/theme_serializer.rb @@ -33,7 +33,7 @@ class ThemeFieldSerializer < ApplicationSerializer end class ChildThemeSerializer < ApplicationSerializer - attributes :id, :name, :created_at, :updated_at, :default + attributes :id, :name, :created_at, :updated_at, :default, :component def include_default? object.id == SiteSetting.default_theme_id @@ -76,6 +76,10 @@ class ThemeSerializer < ChildThemeSerializer def settings object.settings.map { |setting| ThemeSettingsSerializer.new(setting, root: false) } end + + def include_child_themes? + !object.component? + end end class ThemeFieldWithEmbeddedUploadsSerializer < ThemeFieldSerializer diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c339487ea79..7a6885ed157 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3192,9 +3192,16 @@ en: revert_confirm: "Are you sure you want to revert your changes?" theme: + theme: "Theme" + component: "Component" + components: "Components" import_theme: "Import Theme" customize_desc: "Customize:" title: "Themes" + modal_title: "Create Theme" + create: "Create" + create_type: "Type:" + create_name: "Name:" 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." @@ -3209,6 +3216,10 @@ 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." uploads: "Uploads" no_uploads: "You can upload assets associated with your theme such as fonts and images" add_upload: "Add Upload" @@ -3231,7 +3242,7 @@ en: public_key: "Grant the following public key access to the repo:" about_theme: "About Theme" license: "License" - component_of: "Theme is a component of:" + component_of: "Component of:" update_to_latest: "Update to Latest" check_for_updates: "Check for Updates" updating: "Updating..." @@ -3239,6 +3250,7 @@ en: add: "Add" theme_settings: "Theme Settings" no_settings: "This theme has no settings." + empty: "No items" commits_behind: one: "Theme is 1 commit behind!" other: "Theme is {{count}} commits behind!" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 8d63436398f..3e1e7ef4677 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -63,6 +63,7 @@ en: errors: component_no_user_selectable: "Theme components can't be user-selectable" component_no_default: "Theme components can't be default theme" + component_no_color_scheme: "Theme components can't have color scheme" no_multilevels_components: "Themes with child themes can't be child themes themselves" settings_errors: invalid_yaml: "Provided YAML is invalid." diff --git a/db/migrate/20180813074843_add_component_to_themes.rb b/db/migrate/20180813074843_add_component_to_themes.rb new file mode 100644 index 00000000000..54b2d39f9e1 --- /dev/null +++ b/db/migrate/20180813074843_add_component_to_themes.rb @@ -0,0 +1,26 @@ +class AddComponentToThemes < ActiveRecord::Migration[5.2] + def up + add_column :themes, :component, :boolean, null: false, default: false + + execute(" + UPDATE themes + SET component = true, color_scheme_id = NULL, user_selectable = false + WHERE id IN (SELECT child_theme_id FROM child_themes) + ") + + execute(" + UPDATE site_settings + SET value = -1 + WHERE name = 'default_theme_id' AND value::integer IN (SELECT id FROM themes WHERE component) + ") + + execute(" + DELETE FROM child_themes + WHERE parent_theme_id IN (SELECT id FROM themes WHERE component) + ") + end + + def down + remove_column :themes, :component + end +end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index a99111f77c9..13ab35f426f 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -2609,7 +2609,7 @@ describe Guardian do theme2.update!(user_selectable: true) expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(false) - theme2.update!(user_selectable: false) + theme2.update!(user_selectable: false, component: true) theme.add_child_theme!(theme2) expect(user_guardian.allow_themes?([theme.id, theme2.id])).to eq(true) expect(user_guardian.allow_themes?([theme2.id])).to eq(false) diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb index 2df206297b5..541a4696bde 100644 --- a/spec/components/stylesheet/manager_spec.rb +++ b/spec/components/stylesheet/manager_spec.rb @@ -25,7 +25,7 @@ describe Stylesheet::Manager do theme.save! - child_theme = Fabricate(:theme) + child_theme = Fabricate(:theme, component: true) child_theme.set_field(target: :common, name: "scss", value: ".child_common{.scss{color: red;}}") child_theme.set_field(target: :desktop, name: "scss", value: ".child_desktop{.scss{color: red;}}") diff --git a/spec/models/child_theme_spec.rb b/spec/models/child_theme_spec.rb index 2c13206cb16..8925accb300 100644 --- a/spec/models/child_theme_spec.rb +++ b/spec/models/child_theme_spec.rb @@ -4,13 +4,13 @@ describe ChildTheme do describe "validations" do it "doesn't allow children to become parents or parents to become children" do theme = Fabricate(:theme) - child = Fabricate(:theme) + child = Fabricate(:theme, component: true) child_theme = ChildTheme.new(parent_theme: theme, child_theme: child) expect(child_theme.valid?).to eq(true) child_theme.save! - grandchild = Fabricate(:theme) + grandchild = Fabricate(:theme, component: true) child_theme = ChildTheme.new(parent_theme: child, child_theme: grandchild) expect(child_theme.valid?).to eq(false) expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.no_multilevels_components")) @@ -20,24 +20,5 @@ describe ChildTheme do expect(child_theme.valid?).to eq(false) expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.no_multilevels_components")) end - - it "doesn't allow a user selectable theme to be a child" do - parent = Fabricate(:theme) - selectable_theme = Fabricate(:theme, user_selectable: true) - - child_theme = ChildTheme.new(parent_theme: parent, child_theme: selectable_theme) - expect(child_theme.valid?).to eq(false) - expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_user_selectable")) - end - - it "doesn't allow a default theme to be child" do - parent = Fabricate(:theme) - default = Fabricate(:theme) - default.set_default! - - child_theme = ChildTheme.new(parent_theme: parent, child_theme: default) - expect(child_theme.valid?).to eq(false) - expect(child_theme.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_default")) - end end end diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 31ad460fb39..40ae4f164df 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -23,7 +23,7 @@ describe Theme do end let(:theme) { Fabricate(:theme, user: user) } - let(:child) { Fabricate(:theme, user: user) } + let(:child) { Fabricate(:theme, user: user, component: true) } it 'can properly clean up color schemes' do scheme = ColorScheme.create!(theme_id: theme.id, name: 'test') scheme2 = ColorScheme.create!(theme_id: theme.id, name: 'test2') @@ -76,7 +76,6 @@ describe Theme do grandchild = Fabricate(:theme, user: user) grandparent = Fabricate(:theme, user: user) - theme.add_child_theme!(child) expect do child.add_child_theme!(grandchild) end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.no_multilevels_components")) @@ -87,18 +86,22 @@ describe Theme do end it "doesn't allow a child to be user selectable" do - theme.add_child_theme!(child) child.update(user_selectable: true) expect(child.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_user_selectable")) end it "doesn't allow a child to be set as the default theme" do - theme.add_child_theme!(child) expect do child.set_default! end.to raise_error(Discourse::InvalidParameters, I18n.t("themes.errors.component_no_default")) end + it "doesn't allow a component to have color scheme" do + scheme = ColorScheme.create!(name: "test") + child.update(color_scheme: scheme) + expect(child.errors.full_messages).to contain_exactly(I18n.t("themes.errors.component_no_color_scheme")) + end + it 'should correct bad html in body_tag_baked and head_tag_baked' do theme.set_field(target: :common, name: "head_tag", value: "I am bold") theme.save! @@ -146,14 +149,49 @@ HTML expect(field).to match(/theme2test<\/b>/) end - describe ".transform_ids" do - it "adds the child themes of the parent" do - child = Fabricate(:theme) - child2 = Fabricate(:theme) - sorted = [child.id, child2.id].sort + describe "#switch_to_component!" do + it "correctly converts a theme to component" do + theme.add_child_theme!(child) + scheme = ColorScheme.create!(name: 'test') + theme.update!(color_scheme_id: scheme.id, user_selectable: true) + theme.set_default! + theme.switch_to_component! + theme.reload + + expect(theme.component).to eq(true) + expect(theme.user_selectable).to eq(false) + expect(theme.default?).to eq(false) + expect(theme.color_scheme_id).to eq(nil) + expect(ChildTheme.where(parent_theme: theme).exists?).to eq(false) + end + end + + describe "#switch_to_theme!" do + it "correctly converts a component to theme" do + theme.add_child_theme!(child) + + child.switch_to_theme! + theme.reload + child.reload + + expect(child.component).to eq(false) + expect(ChildTheme.where(child_theme: child).exists?).to eq(false) + end + end + + describe ".transform_ids" do + let!(:child) { Fabricate(:theme, component: true) } + let!(:child2) { Fabricate(:theme, component: true) } + + before do theme.add_child_theme!(child) theme.add_child_theme!(child2) + end + + it "adds the child themes of the parent" do + sorted = [child.id, child2.id].sort + expect(Theme.transform_ids([theme.id])).to eq([theme.id, *sorted]) fake_id = [child.id, child2.id, theme.id].min - 5 @@ -164,12 +202,6 @@ HTML end it "doesn't insert children when extend is false" do - child = Fabricate(:theme) - child2 = Fabricate(:theme) - - theme.add_child_theme!(child) - theme.add_child_theme!(child2) - fake_id = theme.id + 1 fake_id2 = fake_id + 2 fake_id3 = fake_id2 + 3 @@ -219,6 +251,7 @@ HTML theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; }') theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var) theme.set_field(target: :common, name: :not_red, value: 'red', type: :theme_var) + theme.component = true theme.save parent_theme = Fabricate(:theme) diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb index 9d5a6de5baa..cff93367358 100644 --- a/spec/requests/admin/themes_controller_spec.rb +++ b/spec/requests/admin/themes_controller_spec.rb @@ -169,7 +169,7 @@ describe Admin::ThemesController do theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') theme.save - child_theme = Fabricate(:theme) + child_theme = Fabricate(:theme, component: true) upload = Fabricate(:upload) @@ -200,8 +200,7 @@ describe Admin::ThemesController do end it 'returns the right error message' do - parent = Fabricate(:theme) - parent.add_child_theme!(theme) + theme.update!(component: true) put "/admin/themes/#{theme.id}.json", params: { theme: { default: true } diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 3b9344d0e1d..03a998965dd 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -122,7 +122,7 @@ RSpec.describe ApplicationController do expect(response.status).to eq(200) expect(controller.theme_ids).to eq([theme2.id]) - theme2.update!(user_selectable: false) + theme2.update!(user_selectable: false, component: true) theme.add_child_theme!(theme2) cookies['theme_ids'] = "#{theme.id},#{theme2.id}|#{user.user_option.theme_key_seq}" diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index 34daad1b64b..49e3023ca8a 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -143,7 +143,7 @@ describe UserUpdater do expect(user.user_option.theme_ids).to eq([]) theme = Fabricate(:theme) - child = Fabricate(:theme) + child = Fabricate(:theme, component: true) theme.add_child_theme!(child) theme.set_default! 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 2611c4b1c74..a2c290a710c 100644 --- a/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 @@ -8,7 +8,7 @@ moduleFor("controller:admin-customize-themes", { needs: ["controller:adminUser"] }); -QUnit.test("can list sorted themes", function(assert) { +QUnit.test("can list themes correctly", function(assert) { const defaultTheme = Theme.create({ id: 2, default: true, name: "default" }); const userTheme = Theme.create({ id: 3, @@ -17,16 +17,25 @@ QUnit.test("can list sorted themes", function(assert) { }); const strayTheme1 = Theme.create({ id: 4, name: "stray1" }); const strayTheme2 = Theme.create({ id: 5, name: "stray2" }); + const componentTheme = Theme.create({ + id: 6, + name: "component", + component: true + }); const controller = this.subject({ - model: { - content: [strayTheme2, strayTheme1, userTheme, defaultTheme] - } + model: [strayTheme2, strayTheme1, userTheme, defaultTheme, componentTheme] }); assert.deepEqual( - controller.get("sortedThemes").map(t => t.get("name")), + controller.get("fullThemes").map(t => t.get("name")), [defaultTheme, userTheme, strayTheme1, strayTheme2].map(t => t.get("name")), "sorts themes correctly" ); + + assert.deepEqual( + controller.get("childThemes").map(t => t.get("name")), + [componentTheme].map(t => t.get("name")), + "separate components from themes" + ); });