FEATURE: themes and components split

* FEATURE: themes and components split

* two seperate methods to switch theme type

* use strict equality operator
This commit is contained in:
Osama Sayegh 2018-08-24 04:30:00 +03:00 committed by Sam
parent 4a552fb967
commit e0cc29d658
29 changed files with 445 additions and 157 deletions

View File

@ -0,0 +1,4 @@
export default Ember.Component.extend({
classNames: ["themes-list"],
hasThemes: Ember.computed.gt("themes.length", 0)
});

View File

@ -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")) {

View File

@ -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);
}
}
);
}
}
});

View File

@ -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"));
}
});

View File

@ -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));
}
}
});

View File

@ -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();
}
}
});

View File

@ -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 });
}
}
});

View File

@ -0,0 +1,26 @@
<div class="themes-list-header">
<b>{{I18n title}}</b>
</div>
<div class="themes-list-container">
{{#if hasThemes}}
{{#each themes as |theme|}}
<div class="themes-list-item {{if theme.active 'active' ''}}">
{{#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}}
</div>
{{/each}}
{{else}}
<div class="themes-list-item">
<span class="empty">{{I18n "admin.customize.theme.empty"}}</span>
</div>
{{/if}}
</div>

View File

@ -31,14 +31,6 @@
{{/link-to}}
</li>
{{/if}}
{{#if showSettings}}
<li class='theme-settings'>
{{#link-to 'adminCustomizeThemes.edit' model.id 'settings' fieldName replace=true}}
{{i18n 'admin.customize.theme.settings'}}
{{d-icon 'cog'}}
{{/link-to}}
</li>
{{/if}}
</ul>
<div class='show-overidden'>
<label>

View File

@ -22,32 +22,34 @@
{{#if parentThemes}}
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
<ul>
{{#each parentThemes as |theme|}}
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
{{/each}}
</ul>
{{else}}
<p>
{{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}}
</p>
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
<p>{{combo-box content=colorSchemes
filterable=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}}
</p>
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
<ul>
{{#each parentThemes as |theme|}}
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
{{/each}}
</ul>
{{/if}}
{{#unless model.component}}
<p>
{{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}}
</p>
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
<p>{{combo-box content=colorSchemes
filterable=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}}
</p>
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
{{/unless}}
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
{{#if hasEditedFields}}
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
@ -61,15 +63,18 @@
{{i18n "admin.customize.theme.edit_css_html_help"}}
</p>
{{/if}}
<p>
{{#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}}
{{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
{{else}}
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
{{/if}}
{{/if}}
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
{{#if model.remote_theme}}
<span class='status-message'>
{{#if updatingRemote}}
@ -146,5 +151,6 @@
<a href='{{previewUrl}}' title="{{i18n 'admin.customize.explain_preview'}}" target='_blank' class='btn'>{{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}}</a>
<a class="btn export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
{{d-button action="switchType" label=switchKey icon="arrows-h" class="btn-danger"}}
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
</div>

View File

@ -1,25 +1,15 @@
{{#unless editingTheme}}
<div class='content-list'>
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
<ul>
{{#each sortedThemes as |theme|}}
<li>
{{#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}}
</li>
{{/each}}
</ul>
{{d-button label="admin.customize.new" icon="plus" action="newTheme" class="btn-primary"}}
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
<div class="create-actions">
{{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}}
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
</div>
{{themes-list themes=fullThemes title="admin.customize.theme.title"}}
{{themes-list themes=childThemes title="admin.customize.theme.components"}}
</div>
{{/unless}}
{{outlet}}

View File

@ -0,0 +1,24 @@
{{#d-modal-body class="create-theme-modal" title="admin.customize.theme.modal_title"}}
<div class="input">
<span class="label">
{{I18n "admin.customize.theme.create_name"}}
</span>
<span class="control">
{{input value=name}}
</span>
</div>
<div class="input">
<span class="label">
{{I18n "admin.customize.theme.create_type"}}
</span>
<span class="control">
{{combo-box valueAttribute="value" content=types value=selectedType}}
</span>
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn btn-primary" label="admin.customize.theme.create" action="createTheme" disabled=loading}}
{{d-modal-cancel close=(action "closeModal")}}
</div>

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!"

View File

@ -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."

View File

@ -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

View File

@ -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)

View File

@ -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;}}")

View File

@ -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

View File

@ -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: "<b>I am bold")
theme.save!
@ -146,14 +149,49 @@ HTML
expect(field).to match(/<b>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)

View File

@ -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 }

View File

@ -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}"

View File

@ -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!

View File

@ -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"
);
});