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 @@
+
+
+
+ {{#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'}}
-
- {{#each sortedThemes 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}}
-
- {{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"
+ );
});