mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:31:12 +08:00
FEATURE: Theme settings (2) (#5611)
Allows theme authors to specify custom theme settings for the theme. Centralizes the theme/site settings into a single construct
This commit is contained in:
@ -1,96 +1,10 @@
|
|||||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||||
import SiteSetting from 'admin/models/site-setting';
|
import SiteSetting from 'admin/models/site-setting';
|
||||||
import { propertyNotEqual } from 'discourse/lib/computed';
|
import SettingComponent from 'admin/mixins/setting-component';
|
||||||
import computed from 'ember-addons/ember-computed-decorators';
|
|
||||||
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
|
||||||
|
|
||||||
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
|
|
||||||
|
|
||||||
export default Ember.Component.extend(BufferedContent, {
|
|
||||||
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
|
||||||
content: Ember.computed.alias('setting'),
|
|
||||||
dirty: propertyNotEqual('buffered.value', 'setting.value'),
|
|
||||||
validationMessage: null,
|
|
||||||
|
|
||||||
@computed("setting", "buffered.value")
|
|
||||||
preview(setting, value) {
|
|
||||||
// A bit hacky, but allows us to use helpers
|
|
||||||
if (setting.get('setting') === 'category_style') {
|
|
||||||
let category = this.site.get('categories.firstObject');
|
|
||||||
if (category) {
|
|
||||||
return categoryLinkHTML(category, {
|
|
||||||
categoryStyle: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let preview = setting.get('preview');
|
|
||||||
if (preview) {
|
|
||||||
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed('componentType')
|
|
||||||
typeClass(componentType) {
|
|
||||||
return componentType.replace(/\_/g, '-');
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("setting.setting")
|
|
||||||
settingName(setting) {
|
|
||||||
return setting.replace(/\_/g, ' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("setting.type")
|
|
||||||
componentType(type) {
|
|
||||||
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
|
|
||||||
},
|
|
||||||
|
|
||||||
@computed("typeClass")
|
|
||||||
componentName(typeClass) {
|
|
||||||
return "site-settings/" + typeClass;
|
|
||||||
},
|
|
||||||
|
|
||||||
_watchEnterKey: function() {
|
|
||||||
const self = this;
|
|
||||||
this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
|
|
||||||
if (e.keyCode === 13) { // enter key
|
|
||||||
self._save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}.on('didInsertElement'),
|
|
||||||
|
|
||||||
_removeBindings: function() {
|
|
||||||
this.$().off("keydown.site-setting-enter");
|
|
||||||
}.on("willDestroyElement"),
|
|
||||||
|
|
||||||
|
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||||
_save() {
|
_save() {
|
||||||
const setting = this.get('buffered'),
|
const setting = this.get('buffered');
|
||||||
action = SiteSetting.update(setting.get('setting'), setting.get('value'));
|
return SiteSetting.update(setting.get('setting'), setting.get('value'));
|
||||||
action.then(() => {
|
|
||||||
this.set('validationMessage', null);
|
|
||||||
this.commitBuffer();
|
|
||||||
}).catch((e) => {
|
|
||||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
|
||||||
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
|
||||||
} else {
|
|
||||||
this.set('validationMessage', I18n.t('generic_error'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
save() {
|
|
||||||
this._save();
|
|
||||||
},
|
|
||||||
|
|
||||||
resetDefault() {
|
|
||||||
this.set('buffered.value', this.get('setting.default'));
|
|
||||||
this._save();
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.rollbackBuffer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,7 @@ export default Ember.Component.extend({
|
|||||||
enabled: {
|
enabled: {
|
||||||
get(value) {
|
get(value) {
|
||||||
if (Ember.isEmpty(value)) { return false; }
|
if (Ember.isEmpty(value)) { return false; }
|
||||||
return value === "true";
|
return value.toString() === "true";
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.set("value", value ? "true" : "false");
|
this.set("value", value ? "true" : "false");
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||||
|
import SettingComponent from 'admin/mixins/setting-component';
|
||||||
|
|
||||||
|
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||||
|
layoutName: 'admin/templates/components/site-setting',
|
||||||
|
_save() {
|
||||||
|
return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value'));
|
||||||
|
}
|
||||||
|
});
|
@ -6,11 +6,22 @@ export default Ember.Controller.extend({
|
|||||||
section: null,
|
section: null,
|
||||||
|
|
||||||
targets: [
|
targets: [
|
||||||
{id: 0, name: I18n.t('admin.customize.theme.common')},
|
{ id: 0, name: 'common' },
|
||||||
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
|
{ id: 1, name: 'desktop' },
|
||||||
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
|
{ id: 2, name: 'mobile' },
|
||||||
|
{ id: 3, name: 'settings' }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
fieldsForTarget: function (target) {
|
||||||
|
const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"];
|
||||||
|
switch(target) {
|
||||||
|
case "common": return [...common, "embedded_scss"];
|
||||||
|
case "desktop": return common;
|
||||||
|
case "mobile": return common;
|
||||||
|
case "settings": return ["yaml"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
@computed('onlyOverridden')
|
@computed('onlyOverridden')
|
||||||
showCommon() {
|
showCommon() {
|
||||||
return this.shouldShow('common');
|
return this.shouldShow('common');
|
||||||
@ -26,6 +37,11 @@ export default Ember.Controller.extend({
|
|||||||
return this.shouldShow('mobile');
|
return this.shouldShow('mobile');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed('onlyOverridden')
|
||||||
|
showSettings() {
|
||||||
|
return this.shouldShow('settings');
|
||||||
|
},
|
||||||
|
|
||||||
@observes('onlyOverridden')
|
@observes('onlyOverridden')
|
||||||
onlyOverriddenChanged() {
|
onlyOverriddenChanged() {
|
||||||
if (this.get('onlyOverridden')) {
|
if (this.get('onlyOverridden')) {
|
||||||
@ -51,27 +67,19 @@ export default Ember.Controller.extend({
|
|||||||
currentTarget: 0,
|
currentTarget: 0,
|
||||||
|
|
||||||
setTargetName: function(name) {
|
setTargetName: function(name) {
|
||||||
let target;
|
const target = this.get('targets').find(t => t.name === name);
|
||||||
switch(name) {
|
this.set("currentTarget", target && target.id);
|
||||||
case "common": target = 0; break;
|
|
||||||
case "desktop": target = 1; break;
|
|
||||||
case "mobile": target = 2; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("currentTarget", target);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("currentTarget")
|
@computed("currentTarget")
|
||||||
currentTargetName(target) {
|
currentTargetName(id) {
|
||||||
switch(parseInt(target)) {
|
const target = this.get('targets').find(t => t.id === parseInt(id, 10));
|
||||||
case 0: return "common";
|
return target && target.name;
|
||||||
case 1: return "desktop";
|
|
||||||
case 2: return "mobile";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("fieldName")
|
@computed("fieldName")
|
||||||
activeSectionMode(fieldName) {
|
activeSectionMode(fieldName) {
|
||||||
|
if (fieldName === "yaml") return "yaml";
|
||||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -96,15 +104,9 @@ export default Ember.Controller.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed("currentTarget", "onlyOverridden")
|
@computed("currentTargetName", "onlyOverridden")
|
||||||
fields(target, onlyOverridden) {
|
fields(target, onlyOverridden) {
|
||||||
let fields = [
|
let fields = this.fieldsForTarget(target);
|
||||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
|
||||||
];
|
|
||||||
|
|
||||||
if (parseInt(target) === 0) {
|
|
||||||
fields.push("embedded_scss");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onlyOverridden) {
|
if (onlyOverridden) {
|
||||||
const model = this.get("model");
|
const model = this.get("model");
|
||||||
@ -155,5 +157,4 @@ export default Ember.Controller.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
|
|||||||
import { url } from 'discourse/lib/computed';
|
import { url } from 'discourse/lib/computed';
|
||||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
import showModal from 'discourse/lib/show-modal';
|
import showModal from 'discourse/lib/show-modal';
|
||||||
|
import ThemeSettings from 'admin/models/theme-settings';
|
||||||
|
|
||||||
const THEME_UPLOAD_VAR = 2;
|
const THEME_UPLOAD_VAR = 2;
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ export default Ember.Controller.extend({
|
|||||||
return text + ": " + localized.join(" , ");
|
return text + ": " + localized.join(" , ");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
['common','desktop','mobile'].forEach(target=> {
|
['common', 'desktop', 'mobile', 'settings'].forEach(target => {
|
||||||
descriptions.push(description(target));
|
descriptions.push(description(target));
|
||||||
});
|
});
|
||||||
return descriptions.reject(d=>Em.isBlank(d));
|
return descriptions.reject(d=>Em.isBlank(d));
|
||||||
@ -77,6 +78,16 @@ export default Ember.Controller.extend({
|
|||||||
return themes;
|
return themes;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@computed("model.settings")
|
||||||
|
settings(settings) {
|
||||||
|
return settings.map(setting => ThemeSettings.create(setting));
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("settings")
|
||||||
|
hasSettings(settings) {
|
||||||
|
return settings.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
downloadUrl: url('model.id', '/admin/themes/%@'),
|
downloadUrl: url('model.id', '/admin/themes/%@'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||||
import { ajax } from 'discourse/lib/ajax';
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
// import computed from 'ember-addons/ember-computed-decorators';
|
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||||
|
|
||||||
export default Ember.Controller.extend(ModalFunctionality, {
|
export default Ember.Controller.extend(ModalFunctionality, {
|
||||||
local: Ember.computed.equal('selection', 'local'),
|
local: Ember.computed.equal('selection', 'local'),
|
||||||
remote: Ember.computed.equal('selection', 'remote'),
|
remote: Ember.computed.equal('selection', 'remote'),
|
||||||
selection: 'local',
|
selection: 'local',
|
||||||
adminCustomizeThemes: Ember.inject.controller(),
|
adminCustomizeThemes: Ember.inject.controller(),
|
||||||
|
loading: false,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
importTheme() {
|
importTheme() {
|
||||||
@ -24,11 +25,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||||||
options.data = {remote: this.get('uploadUrl')};
|
options.data = {remote: this.get('uploadUrl')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.set('loading', true);
|
||||||
ajax('/admin/themes/import', options).then(result=>{
|
ajax('/admin/themes/import', options).then(result=>{
|
||||||
const theme = this.store.createRecord('theme',result.theme);
|
const theme = this.store.createRecord('theme',result.theme);
|
||||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||||
this.send('closeModal');
|
this.send('closeModal');
|
||||||
});
|
}).catch(popupAjaxError).finally(() => this.set('loading', false));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
98
app/assets/javascripts/admin/mixins/setting-component.js.es6
Normal file
98
app/assets/javascripts/admin/mixins/setting-component.js.es6
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
||||||
|
|
||||||
|
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
|
||||||
|
|
||||||
|
export default Ember.Mixin.create({
|
||||||
|
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
||||||
|
content: Ember.computed.alias('setting'),
|
||||||
|
validationMessage: null,
|
||||||
|
|
||||||
|
@computed("buffered.value", "setting.value")
|
||||||
|
dirty(bufferVal, settingVal) {
|
||||||
|
if (bufferVal === null || bufferVal === undefined) bufferVal = '';
|
||||||
|
if (settingVal === null || settingVal === undefined) settingVal = '';
|
||||||
|
|
||||||
|
return bufferVal.toString() !== settingVal.toString();
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("setting", "buffered.value")
|
||||||
|
preview(setting, value) {
|
||||||
|
// A bit hacky, but allows us to use helpers
|
||||||
|
if (setting.get('setting') === 'category_style') {
|
||||||
|
let category = this.site.get('categories.firstObject');
|
||||||
|
if (category) {
|
||||||
|
return categoryLinkHTML(category, {
|
||||||
|
categoryStyle: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview = setting.get('preview');
|
||||||
|
if (preview) {
|
||||||
|
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed('componentType')
|
||||||
|
typeClass(componentType) {
|
||||||
|
return componentType.replace(/\_/g, '-');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("setting.setting")
|
||||||
|
settingName(setting) {
|
||||||
|
return setting.replace(/\_/g, ' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("setting.type")
|
||||||
|
componentType(type) {
|
||||||
|
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
|
||||||
|
},
|
||||||
|
|
||||||
|
@computed("typeClass")
|
||||||
|
componentName(typeClass) {
|
||||||
|
return "site-settings/" + typeClass;
|
||||||
|
},
|
||||||
|
|
||||||
|
_watchEnterKey: function() {
|
||||||
|
const self = this;
|
||||||
|
this.$().on("keydown.setting-enter", ".input-setting-string", function (e) {
|
||||||
|
if (e.keyCode === 13) { // enter key
|
||||||
|
self._save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}.on('didInsertElement'),
|
||||||
|
|
||||||
|
_removeBindings: function() {
|
||||||
|
this.$().off("keydown.setting-enter");
|
||||||
|
}.on("willDestroyElement"),
|
||||||
|
|
||||||
|
_save() {
|
||||||
|
Em.warn("You should define a `_save` method", { id: "admin.mixins.setting-component" });
|
||||||
|
return Ember.RSVP.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this._save().then(() => {
|
||||||
|
this.set('validationMessage', null);
|
||||||
|
this.commitBuffer();
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||||
|
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
||||||
|
} else {
|
||||||
|
this.set('validationMessage', I18n.t('generic_error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetDefault() {
|
||||||
|
this.set('buffered.value', this.get('setting.default'));
|
||||||
|
this._save();
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.rollbackBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
29
app/assets/javascripts/admin/mixins/setting-object.js.es6
Normal file
29
app/assets/javascripts/admin/mixins/setting-object.js.es6
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export default Ember.Mixin.create({
|
||||||
|
overridden: function() {
|
||||||
|
let val = this.get('value'),
|
||||||
|
defaultVal = this.get('default');
|
||||||
|
|
||||||
|
if (val === null) val = '';
|
||||||
|
if (defaultVal === null) defaultVal = '';
|
||||||
|
|
||||||
|
return val.toString() !== defaultVal.toString();
|
||||||
|
}.property('value', 'default'),
|
||||||
|
|
||||||
|
validValues: function() {
|
||||||
|
const vals = [],
|
||||||
|
translateNames = this.get('translate_names');
|
||||||
|
|
||||||
|
this.get('valid_values').forEach(v => {
|
||||||
|
if (v.name && v.name.length > 0 && translateNames) {
|
||||||
|
vals.addObject({ name: I18n.t(v.name), value: v.value });
|
||||||
|
} else {
|
||||||
|
vals.addObject(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return vals;
|
||||||
|
}.property('valid_values'),
|
||||||
|
|
||||||
|
allowsNone: function() {
|
||||||
|
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.settings.none';
|
||||||
|
}.property('valid_values')
|
||||||
|
});
|
@ -1,31 +1,7 @@
|
|||||||
import { ajax } from 'discourse/lib/ajax';
|
import { ajax } from 'discourse/lib/ajax';
|
||||||
const SiteSetting = Discourse.Model.extend({
|
import Setting from 'admin/mixins/setting-object';
|
||||||
overridden: function() {
|
|
||||||
let val = this.get('value'),
|
|
||||||
defaultVal = this.get('default');
|
|
||||||
|
|
||||||
if (val === null) val = '';
|
const SiteSetting = Discourse.Model.extend(Setting, {});
|
||||||
if (defaultVal === null) defaultVal = '';
|
|
||||||
|
|
||||||
return val.toString() !== defaultVal.toString();
|
|
||||||
}.property('value', 'default'),
|
|
||||||
|
|
||||||
validValues: function() {
|
|
||||||
const vals = [],
|
|
||||||
translateNames = this.get('translate_names');
|
|
||||||
|
|
||||||
this.get('valid_values').forEach(function(v) {
|
|
||||||
if (v.name && v.name.length > 0) {
|
|
||||||
vals.addObject(translateNames ? {name: I18n.t(v.name), value: v.value} : v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return vals;
|
|
||||||
}.property('valid_values'),
|
|
||||||
|
|
||||||
allowsNone: function() {
|
|
||||||
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.site_settings.none';
|
|
||||||
}.property('valid_values')
|
|
||||||
});
|
|
||||||
|
|
||||||
SiteSetting.reopenClass({
|
SiteSetting.reopenClass({
|
||||||
findAll() {
|
findAll() {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import Setting from 'admin/mixins/setting-object';
|
||||||
|
|
||||||
|
export default Discourse.Model.extend(Setting, {});
|
@ -2,6 +2,7 @@ import RestModel from 'discourse/models/rest';
|
|||||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
const THEME_UPLOAD_VAR = 2;
|
const THEME_UPLOAD_VAR = 2;
|
||||||
|
const FIELDS_IDS = [0, 1, 5];
|
||||||
|
|
||||||
const Theme = RestModel.extend({
|
const Theme = RestModel.extend({
|
||||||
|
|
||||||
@ -14,13 +15,11 @@ const Theme = RestModel.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hash = {};
|
let hash = {};
|
||||||
if (fields) {
|
fields.forEach(field => {
|
||||||
fields.forEach(field=>{
|
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
||||||
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
|
hash[this.getKey(field)] = field;
|
||||||
hash[this.getKey(field)] = field;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
return hash;
|
return hash;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -29,11 +28,11 @@ const Theme = RestModel.extend({
|
|||||||
if (!fields) {
|
if (!fields) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
return fields.filter(f => f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||||
},
|
},
|
||||||
|
|
||||||
getKey(field){
|
getKey(field){
|
||||||
return field.target + " " + field.name;
|
return `${field.target} ${field.name}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
hasEdited(target, name){
|
hasEdited(target, name){
|
||||||
@ -151,6 +150,11 @@ const Theme = RestModel.extend({
|
|||||||
.then(() => this.set("changed", false));
|
.then(() => this.set("changed", false));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveSettings(name, value) {
|
||||||
|
const settings = {};
|
||||||
|
settings[name] = value;
|
||||||
|
return this.save({ settings });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Theme;
|
export default Theme;
|
||||||
|
@ -18,6 +18,11 @@ export default Ember.Route.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, wrapper) {
|
setupController(controller, wrapper) {
|
||||||
|
const fields = controller.fieldsForTarget(wrapper.target);
|
||||||
|
if (!fields.includes(wrapper.field_name)) {
|
||||||
|
this.transitionTo('adminCustomizeThemes.edit', wrapper.model.id, wrapper.target, fields[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
controller.set("model", wrapper.model);
|
controller.set("model", wrapper.model);
|
||||||
controller.setTargetName(wrapper.target || "common");
|
controller.setTargetName(wrapper.target || "common");
|
||||||
controller.set("fieldName", wrapper.field_name || "scss");
|
controller.set("fieldName", wrapper.field_name || "scss");
|
||||||
|
@ -10,5 +10,5 @@
|
|||||||
{{d-button class="cancel" action="cancel" icon="times"}}
|
{{d-button class="cancel" action="cancel" icon="times"}}
|
||||||
</div>
|
</div>
|
||||||
{{else if setting.overridden}}
|
{{else if setting.overridden}}
|
||||||
{{d-button action="resetDefault" icon="undo" label="admin.site_settings.reset"}}
|
{{d-button action="resetDefault" icon="undo" label="admin.settings.reset"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<div class='search controls'>
|
<div class='search controls'>
|
||||||
<label>
|
<label>
|
||||||
{{input type="checkbox" checked=onlyOverridden}}
|
{{input type="checkbox" checked=onlyOverridden}}
|
||||||
{{i18n 'admin.site_settings.show_overriden'}}
|
{{i18n 'admin.settings.show_overriden'}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,39 +3,47 @@
|
|||||||
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
|
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
|
||||||
|
|
||||||
{{#if error}}
|
{{#if error}}
|
||||||
<pre class='field-error'>{{error}}</pre>
|
<pre class='field-error'>{{error}}</pre>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class='edit-main-nav'>
|
<div class='edit-main-nav'>
|
||||||
<ul class='nav nav-pills target'>
|
<ul class='nav nav-pills target'>
|
||||||
{{#if showCommon}}
|
{{#if showCommon}}
|
||||||
<li>
|
<li>
|
||||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true title=field.title}}
|
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true}}
|
||||||
{{i18n 'admin.customize.theme.common'}}
|
{{i18n 'admin.customize.theme.common'}}
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if showDesktop}}
|
{{#if showDesktop}}
|
||||||
<li>
|
<li>
|
||||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}}
|
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true}}
|
||||||
{{i18n 'admin.customize.theme.desktop'}}
|
{{i18n 'admin.customize.theme.desktop'}}
|
||||||
{{d-icon 'desktop'}}
|
{{d-icon 'desktop'}}
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if showMobile}}
|
{{#if showMobile}}
|
||||||
<li class='mobile'>
|
<li class='mobile'>
|
||||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
|
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true}}
|
||||||
{{i18n 'admin.customize.theme.mobile'}}
|
{{i18n 'admin.customize.theme.mobile'}}
|
||||||
{{d-icon 'mobile'}}
|
{{d-icon 'mobile'}}
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</li>
|
</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}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
<div class='show-overidden'>
|
<div class='show-overidden'>
|
||||||
<label>
|
<label>
|
||||||
{{input type="checkbox" checked=onlyOverridden}}
|
{{input type="checkbox" checked=onlyOverridden}}
|
||||||
{{i18n 'admin.site_settings.show_overriden'}}
|
{{i18n 'admin.settings.show_overriden'}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class='clearfix'></div>
|
<div class='clearfix'></div>
|
||||||
|
@ -50,16 +50,16 @@
|
|||||||
|
|
||||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||||
{{#if hasEditedFields}}
|
{{#if hasEditedFields}}
|
||||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{{#each editedDescriptions as |desc|}}
|
{{#each editedDescriptions as |desc|}}
|
||||||
<li>{{desc}}</li>
|
<li>{{desc}}</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>
|
<p>
|
||||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<p>
|
<p>
|
||||||
{{#if model.remote_theme}}
|
{{#if model.remote_theme}}
|
||||||
@ -71,17 +71,17 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||||
{{#if model.remote_theme}}
|
{{#if model.remote_theme}}
|
||||||
<span class='status-message'>
|
<span class='status-message'>
|
||||||
{{#if updatingRemote}}
|
{{#if updatingRemote}}
|
||||||
{{i18n 'admin.customize.theme.updating'}}
|
{{i18n 'admin.customize.theme.updating'}}
|
||||||
{{else}}
|
|
||||||
{{#if model.remote_theme.commits_behind}}
|
|
||||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
{{#if model.remote_theme.commits_behind}}
|
||||||
|
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
</span>
|
||||||
</span>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -105,6 +105,17 @@
|
|||||||
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h3>{{i18n "admin.customize.theme.theme_settings"}}</h3>
|
||||||
|
{{#d-section class="form-horizontal theme settings"}}
|
||||||
|
{{#if hasSettings}}
|
||||||
|
{{#each settings as |setting|}}
|
||||||
|
{{theme-setting setting=setting model=model class="theme-setting"}}
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
{{i18n "admin.customize.theme.no_settings"}}
|
||||||
|
{{/if}}
|
||||||
|
{{/d-section}}
|
||||||
|
|
||||||
{{#if availableChildThemes}}
|
{{#if availableChildThemes}}
|
||||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||||
{{#unless model.childThemes.length}}
|
{{#unless model.childThemes.length}}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{#if filteredContent}}
|
{{#if filteredContent}}
|
||||||
{{#d-section class="form-horizontal settings"}}
|
{{#d-section class="form-horizontal settings"}}
|
||||||
{{#each filteredContent as |setting|}}
|
{{#each filteredContent as |setting|}}
|
||||||
{{site-setting setting=setting saveAction="saveSetting"}}
|
{{site-setting setting=setting}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/d-section}}
|
{{/d-section}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class='search controls'>
|
<div class='search controls'>
|
||||||
<label>
|
<label>
|
||||||
{{input type="checkbox" checked=onlyOverridden}}
|
{{input type="checkbox" checked=onlyOverridden}}
|
||||||
{{i18n 'admin.site_settings.show_overriden'}}
|
{{i18n 'admin.settings.show_overriden'}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
|
@ -55,6 +55,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme.settings {
|
||||||
|
.theme-setting {
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 18px;
|
||||||
|
min-height: 35px;
|
||||||
|
}
|
||||||
|
.setting-label {
|
||||||
|
width: 25%;
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.current-style.maximized {
|
.current-style.maximized {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -138,6 +138,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
end
|
end
|
||||||
|
|
||||||
set_fields
|
set_fields
|
||||||
|
update_settings
|
||||||
|
|
||||||
save_remote = false
|
save_remote = false
|
||||||
if params[:theme][:remote_check]
|
if params[:theme][:remote_check]
|
||||||
@ -158,7 +159,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
update_default_theme
|
update_default_theme
|
||||||
|
|
||||||
log_theme_change(original_json, @theme)
|
log_theme_change(original_json, @theme)
|
||||||
format.json { render json: @theme, status: :created }
|
format.json { render json: @theme, status: :ok }
|
||||||
else
|
else
|
||||||
format.json {
|
format.json {
|
||||||
|
|
||||||
@ -193,7 +194,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
|
|
||||||
response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json"
|
response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json"
|
||||||
response.sending_file = true
|
response.sending_file = true
|
||||||
render json: ThemeWithEmbeddedUploadsSerializer.new(@theme, root: 'theme')
|
render json: ::ThemeWithEmbeddedUploadsSerializer.new(@theme, root: 'theme')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -223,6 +224,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
:color_scheme_id,
|
:color_scheme_id,
|
||||||
:default,
|
:default,
|
||||||
:user_selectable,
|
:user_selectable,
|
||||||
|
settings: {},
|
||||||
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
||||||
child_theme_ids: []
|
child_theme_ids: []
|
||||||
)
|
)
|
||||||
@ -243,6 +245,14 @@ class Admin::ThemesController < Admin::AdminController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_settings
|
||||||
|
return unless target_settings = theme_params[:settings]
|
||||||
|
|
||||||
|
target_settings.each_pair do |setting_name, new_value|
|
||||||
|
@theme.update_setting(setting_name.to_sym, new_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def log_theme_change(old_record, new_record)
|
def log_theme_change(old_record, new_record)
|
||||||
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
||||||
end
|
end
|
||||||
|
@ -460,6 +460,7 @@ class ApplicationController < ActionController::Base
|
|||||||
def preload_anonymous_data
|
def preload_anonymous_data
|
||||||
store_preloaded("site", Site.json_for(guardian))
|
store_preloaded("site", Site.json_for(guardian))
|
||||||
store_preloaded("siteSettings", SiteSetting.client_settings_json)
|
store_preloaded("siteSettings", SiteSetting.client_settings_json)
|
||||||
|
store_preloaded("themeSettings", Theme.settings_for_client(@theme_key))
|
||||||
store_preloaded("customHTML", custom_html_json)
|
store_preloaded("customHTML", custom_html_json)
|
||||||
store_preloaded("banner", banner_json)
|
store_preloaded("banner", banner_json)
|
||||||
store_preloaded("customEmoji", custom_emoji)
|
store_preloaded("customEmoji", custom_emoji)
|
||||||
|
@ -76,6 +76,8 @@ class RemoteTheme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
Theme.targets.keys.each do |target|
|
Theme.targets.keys.each do |target|
|
||||||
|
next if target == :settings
|
||||||
|
|
||||||
ALLOWED_FIELDS.each do |field|
|
ALLOWED_FIELDS.each do |field|
|
||||||
lookup =
|
lookup =
|
||||||
if field == "scss"
|
if field == "scss"
|
||||||
@ -91,6 +93,9 @@ class RemoteTheme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
settings_yaml = importer["settings.yaml"] || importer["settings.yml"]
|
||||||
|
theme.set_field(target: :settings, name: "yaml", value: settings_yaml)
|
||||||
|
|
||||||
self.license_url ||= theme_info["license_url"]
|
self.license_url ||= theme_info["license_url"]
|
||||||
self.about_url ||= theme_info["about_url"]
|
self.about_url ||= theme_info["about_url"]
|
||||||
self.remote_updated_at = Time.zone.now
|
self.remote_updated_at = Time.zone.now
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
require_dependency 'distributed_cache'
|
require_dependency 'distributed_cache'
|
||||||
require_dependency 'stylesheet/compiler'
|
require_dependency 'stylesheet/compiler'
|
||||||
require_dependency 'stylesheet/manager'
|
require_dependency 'stylesheet/manager'
|
||||||
|
require_dependency 'theme_settings_parser'
|
||||||
|
require_dependency 'theme_settings_manager'
|
||||||
|
|
||||||
class Theme < ActiveRecord::Base
|
class Theme < ActiveRecord::Base
|
||||||
|
|
||||||
@ -8,6 +10,7 @@ class Theme < ActiveRecord::Base
|
|||||||
|
|
||||||
belongs_to :color_scheme
|
belongs_to :color_scheme
|
||||||
has_many :theme_fields, dependent: :destroy
|
has_many :theme_fields, dependent: :destroy
|
||||||
|
has_many :theme_settings, dependent: :destroy
|
||||||
has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy
|
has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy
|
||||||
has_many :child_themes, through: :child_theme_relation, source: :child_theme
|
has_many :child_themes, through: :child_theme_relation, source: :child_theme
|
||||||
has_many :color_schemes
|
has_many :color_schemes
|
||||||
@ -34,11 +37,13 @@ class Theme < ActiveRecord::Base
|
|||||||
@included_themes = nil
|
@included_themes = nil
|
||||||
|
|
||||||
remove_from_cache!
|
remove_from_cache!
|
||||||
|
clear_cached_settings!
|
||||||
notify_scheme_change if saved_change_to_color_scheme_id?
|
notify_scheme_change if saved_change_to_color_scheme_id?
|
||||||
end
|
end
|
||||||
|
|
||||||
after_destroy do
|
after_destroy do
|
||||||
remove_from_cache!
|
remove_from_cache!
|
||||||
|
clear_cached_settings!
|
||||||
if SiteSetting.default_theme_key == self.key
|
if SiteSetting.default_theme_key == self.key
|
||||||
Theme.clear_default!
|
Theme.clear_default!
|
||||||
end
|
end
|
||||||
@ -122,7 +127,11 @@ class Theme < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.targets
|
def self.targets
|
||||||
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2)
|
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.lookup_target(target_id)
|
||||||
|
self.targets.invert[target_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_scheme_change(clear_manager_cache = true)
|
def notify_scheme_change(clear_manager_cache = true)
|
||||||
@ -288,6 +297,56 @@ class Theme < ActiveRecord::Base
|
|||||||
child_themes.reload
|
child_themes.reload
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def settings
|
||||||
|
field = theme_fields.where(target_id: Theme.targets[:settings], name: "yaml").first
|
||||||
|
return [] unless field && field.error.nil?
|
||||||
|
|
||||||
|
settings = []
|
||||||
|
ThemeSettingsParser.new(field).load do |name, default, type, opts|
|
||||||
|
settings << ThemeSettingsManager.create(name, default, type, self, opts)
|
||||||
|
end
|
||||||
|
settings
|
||||||
|
end
|
||||||
|
|
||||||
|
def cached_settings
|
||||||
|
Rails.cache.fetch("settings_for_theme_#{self.key}", expires_in: 30.minutes) do
|
||||||
|
hash = {}
|
||||||
|
self.settings.each do |setting|
|
||||||
|
hash[setting.name] = setting.value
|
||||||
|
end
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_cached_settings!
|
||||||
|
Rails.cache.delete("settings_for_theme_#{self.key}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def included_settings
|
||||||
|
hash = {}
|
||||||
|
|
||||||
|
self.included_themes.each do |theme|
|
||||||
|
hash.merge!(theme.cached_settings)
|
||||||
|
end
|
||||||
|
|
||||||
|
hash.merge!(self.cached_settings)
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.settings_for_client(key)
|
||||||
|
theme = Theme.find_by(key: key)
|
||||||
|
return {}.to_json unless theme
|
||||||
|
|
||||||
|
theme.included_settings.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_setting(setting_name, new_value)
|
||||||
|
target_setting = settings.find { |setting| setting.name == setting_name }
|
||||||
|
raise Discourse::NotFound unless target_setting
|
||||||
|
|
||||||
|
target_setting.value = new_value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
require_dependency 'theme_settings_parser'
|
||||||
|
|
||||||
class ThemeField < ActiveRecord::Base
|
class ThemeField < ActiveRecord::Base
|
||||||
|
|
||||||
belongs_to :upload
|
belongs_to :upload
|
||||||
@ -7,7 +9,8 @@ class ThemeField < ActiveRecord::Base
|
|||||||
scss: 1,
|
scss: 1,
|
||||||
theme_upload_var: 2,
|
theme_upload_var: 2,
|
||||||
theme_color_var: 3,
|
theme_color_var: 3,
|
||||||
theme_var: 4)
|
theme_var: 4,
|
||||||
|
yaml: 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.theme_var_type_ids
|
def self.theme_var_type_ids
|
||||||
@ -77,11 +80,54 @@ COMPILED
|
|||||||
[doc.to_s, errors&.join("\n")]
|
[doc.to_s, errors&.join("\n")]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_yaml!
|
||||||
|
return unless self.name == "yaml"
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
begin
|
||||||
|
ThemeSettingsParser.new(self).load do |name, default, type, opts|
|
||||||
|
setting = ThemeSetting.new(name: name, data_type: type, theme: theme)
|
||||||
|
translation_key = "themes.settings_errors"
|
||||||
|
|
||||||
|
if setting.invalid?
|
||||||
|
setting.errors.details.each_pair do |attribute, _errors|
|
||||||
|
_errors.each do |hash|
|
||||||
|
errors << I18n.t("#{translation_key}.#{attribute}_#{hash[:error]}", name: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if default.nil?
|
||||||
|
errors << I18n.t("#{translation_key}.default_value_missing", name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
if (min = opts[:min]) && (max = opts[:max])
|
||||||
|
unless ThemeSetting.value_in_range?(default, (min..max), type)
|
||||||
|
errors << I18n.t("#{translation_key}.default_out_range", name: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless ThemeSetting.acceptable_value_for_type?(default, type)
|
||||||
|
errors << I18n.t("#{translation_key}.default_not_match_type", name: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue ThemeSettingsParser::InvalidYaml => e
|
||||||
|
errors << e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
self.error = errors.join("\n").presence unless self.destroyed?
|
||||||
|
if will_save_change_to_error?
|
||||||
|
update_columns(error: self.error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.guess_type(name)
|
def self.guess_type(name)
|
||||||
if html_fields.include?(name.to_s)
|
if html_fields.include?(name.to_s)
|
||||||
types[:html]
|
types[:html]
|
||||||
elsif scss_fields.include?(name.to_s)
|
elsif scss_fields.include?(name.to_s)
|
||||||
types[:scss]
|
types[:scss]
|
||||||
|
elsif name.to_s === "yaml"
|
||||||
|
types[:yaml]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -121,7 +167,7 @@ COMPILED
|
|||||||
)
|
)
|
||||||
self.error = nil unless error.nil?
|
self.error = nil unless error.nil?
|
||||||
rescue SassC::SyntaxError => e
|
rescue SassC::SyntaxError => e
|
||||||
self.error = e.message
|
self.error = e.message unless self.destroyed?
|
||||||
end
|
end
|
||||||
|
|
||||||
if will_save_change_to_error?
|
if will_save_change_to_error?
|
||||||
@ -143,6 +189,8 @@ COMPILED
|
|||||||
after_commit do
|
after_commit do
|
||||||
ensure_baked!
|
ensure_baked!
|
||||||
ensure_scss_compiles!
|
ensure_scss_compiles!
|
||||||
|
validate_yaml!
|
||||||
|
theme.clear_cached_settings!
|
||||||
|
|
||||||
Stylesheet::Manager.clear_theme_cache! if self.name.include?("scss")
|
Stylesheet::Manager.clear_theme_cache! if self.name.include?("scss")
|
||||||
|
|
||||||
|
68
app/models/theme_setting.rb
Normal file
68
app/models/theme_setting.rb
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
class ThemeSetting < ActiveRecord::Base
|
||||||
|
belongs_to :theme
|
||||||
|
|
||||||
|
validates_presence_of :name, :theme
|
||||||
|
validates :data_type, numericality: { only_integer: true }
|
||||||
|
validates :name, length: { maximum: 255 }
|
||||||
|
|
||||||
|
after_save do
|
||||||
|
theme.clear_cached_settings!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.types
|
||||||
|
@types ||= Enum.new(integer: 0, float: 1, string: 2, bool: 3, list: 4, enum: 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.acceptable_value_for_type?(value, type)
|
||||||
|
case type
|
||||||
|
when self.types[:integer]
|
||||||
|
value.is_a?(Integer)
|
||||||
|
when self.types[:float]
|
||||||
|
value.is_a?(Integer) || value.is_a?(Float)
|
||||||
|
when self.types[:bool]
|
||||||
|
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
||||||
|
when self.types[:list]
|
||||||
|
value.is_a?(String)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.value_in_range?(value, range, type)
|
||||||
|
if type == self.types[:integer] || type == self.types[:float]
|
||||||
|
range.include? value
|
||||||
|
elsif type == self.types[:string]
|
||||||
|
range.include? value.to_s.length
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.guess_type(value)
|
||||||
|
case value
|
||||||
|
when Integer
|
||||||
|
types[:integer]
|
||||||
|
when Float
|
||||||
|
types[:float]
|
||||||
|
when String
|
||||||
|
types[:string]
|
||||||
|
when TrueClass, FalseClass
|
||||||
|
types[:bool]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: theme_settings
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# name :string(255) not null
|
||||||
|
# data_type :integer not null
|
||||||
|
# value :string
|
||||||
|
# theme_id :integer not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_theme_settings_on_theme_id (theme_id)
|
||||||
|
#
|
@ -24,11 +24,7 @@ class ThemeFieldSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def target
|
def target
|
||||||
case object.target_id
|
Theme.lookup_target(object.target_id)&.to_s
|
||||||
when 0 then "common"
|
|
||||||
when 1 then "desktop"
|
|
||||||
when 2 then "mobile"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_error?
|
def include_error?
|
||||||
@ -60,7 +56,7 @@ class RemoteThemeSerializer < ApplicationSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
class ThemeSerializer < ChildThemeSerializer
|
class ThemeSerializer < ChildThemeSerializer
|
||||||
attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id
|
attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id, :settings
|
||||||
|
|
||||||
has_many :theme_fields, serializer: ThemeFieldSerializer, embed: :objects
|
has_many :theme_fields, serializer: ThemeFieldSerializer, embed: :objects
|
||||||
has_many :child_themes, serializer: ChildThemeSerializer, embed: :objects
|
has_many :child_themes, serializer: ChildThemeSerializer, embed: :objects
|
||||||
@ -69,6 +65,10 @@ class ThemeSerializer < ChildThemeSerializer
|
|||||||
def child_themes
|
def child_themes
|
||||||
object.child_themes.order(:name)
|
object.child_themes.order(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def settings
|
||||||
|
object.settings.map { |setting| ThemeSettingsSerializer.new(setting, root: false) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class ThemeFieldWithEmbeddedUploadsSerializer < ThemeFieldSerializer
|
class ThemeFieldWithEmbeddedUploadsSerializer < ThemeFieldSerializer
|
||||||
@ -94,4 +94,8 @@ end
|
|||||||
|
|
||||||
class ThemeWithEmbeddedUploadsSerializer < ThemeSerializer
|
class ThemeWithEmbeddedUploadsSerializer < ThemeSerializer
|
||||||
has_many :theme_fields, serializer: ThemeFieldWithEmbeddedUploadsSerializer, embed: :objects
|
has_many :theme_fields, serializer: ThemeFieldWithEmbeddedUploadsSerializer, embed: :objects
|
||||||
|
|
||||||
|
def include_settings?
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
35
app/serializers/theme_settings_serializer.rb
Normal file
35
app/serializers/theme_settings_serializer.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
class ThemeSettingsSerializer < ApplicationSerializer
|
||||||
|
attributes :setting, :type, :default, :value, :description, :valid_values
|
||||||
|
|
||||||
|
def setting
|
||||||
|
object.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
object.type_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def default
|
||||||
|
object.default
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
object.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
object.description
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_values
|
||||||
|
object.choices
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_valid_values?
|
||||||
|
object.type == ThemeSetting.types[:enum]
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_description?
|
||||||
|
object.description.present?
|
||||||
|
end
|
||||||
|
end
|
@ -42,6 +42,7 @@
|
|||||||
Discourse.BaseUri = '<%= Discourse::base_uri %>';
|
Discourse.BaseUri = '<%= Discourse::base_uri %>';
|
||||||
Discourse.Environment = '<%= Rails.env %>';
|
Discourse.Environment = '<%= Rails.env %>';
|
||||||
Discourse.SiteSettings = ps.get('siteSettings');
|
Discourse.SiteSettings = ps.get('siteSettings');
|
||||||
|
Discourse.ThemeSettings = ps.get('themeSettings');
|
||||||
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
|
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
|
||||||
Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>';
|
Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>';
|
||||||
Discourse.ServiceWorkerURL = '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>'
|
Discourse.ServiceWorkerURL = '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>'
|
||||||
|
@ -3045,6 +3045,7 @@ en:
|
|||||||
common: "Common"
|
common: "Common"
|
||||||
desktop: "Desktop"
|
desktop: "Desktop"
|
||||||
mobile: "Mobile"
|
mobile: "Mobile"
|
||||||
|
settings: "Settings"
|
||||||
preview: "Preview"
|
preview: "Preview"
|
||||||
is_default: "Theme is enabled by default"
|
is_default: "Theme is enabled by default"
|
||||||
user_selectable: "Theme can be selected by users"
|
user_selectable: "Theme can be selected by users"
|
||||||
@ -3073,6 +3074,8 @@ en:
|
|||||||
updating: "Updating..."
|
updating: "Updating..."
|
||||||
up_to_date: "Theme is up-to-date, last checked:"
|
up_to_date: "Theme is up-to-date, last checked:"
|
||||||
add: "Add"
|
add: "Add"
|
||||||
|
theme_settings: "Theme Settings"
|
||||||
|
no_settings: "This theme has no settings."
|
||||||
commits_behind:
|
commits_behind:
|
||||||
one: "Theme is 1 commit behind!"
|
one: "Theme is 1 commit behind!"
|
||||||
other: "Theme is {{count}} commits behind!"
|
other: "Theme is {{count}} commits behind!"
|
||||||
@ -3097,6 +3100,9 @@ en:
|
|||||||
body_tag:
|
body_tag:
|
||||||
text: "</body>"
|
text: "</body>"
|
||||||
title: "HTML that will be inserted before the </body> tag"
|
title: "HTML that will be inserted before the </body> tag"
|
||||||
|
yaml:
|
||||||
|
text: "YAML"
|
||||||
|
title: "Define theme settings in YAML format"
|
||||||
colors:
|
colors:
|
||||||
select_base:
|
select_base:
|
||||||
title: "Select base color scheme"
|
title: "Select base color scheme"
|
||||||
@ -3633,11 +3639,12 @@ en:
|
|||||||
recommended: "We recommend customizing the following text to suit your needs:"
|
recommended: "We recommend customizing the following text to suit your needs:"
|
||||||
show_overriden: 'Only show overridden'
|
show_overriden: 'Only show overridden'
|
||||||
|
|
||||||
site_settings:
|
settings: # used by theme and site settings
|
||||||
show_overriden: 'Only show overridden'
|
show_overriden: 'Only show overridden'
|
||||||
title: 'Settings'
|
|
||||||
reset: 'reset'
|
reset: 'reset'
|
||||||
none: 'none'
|
none: 'none'
|
||||||
|
site_settings:
|
||||||
|
title: 'Settings'
|
||||||
no_results: "No results found."
|
no_results: "No results found."
|
||||||
clear_filter: "Clear"
|
clear_filter: "Clear"
|
||||||
add_url: "add URL"
|
add_url: "add URL"
|
||||||
|
@ -59,6 +59,22 @@ en:
|
|||||||
themes:
|
themes:
|
||||||
bad_color_scheme: "Can not update theme, invalid color scheme"
|
bad_color_scheme: "Can not update theme, invalid color scheme"
|
||||||
other_error: "Something went wrong updating theme"
|
other_error: "Something went wrong updating theme"
|
||||||
|
settings_errors:
|
||||||
|
invalid_yaml: "Provided YAML is invalid."
|
||||||
|
data_type_not_a_number: "Setting `%{name}` type is unsupported. Supported types are `integer`, `bool`, `list` and `enum`"
|
||||||
|
name_too_long: "There is a setting with a too long name. Maximum length is 255"
|
||||||
|
default_value_missing: "Setting `%{name}` has no default value"
|
||||||
|
default_not_match_type: "Setting `%{name}` default value's type doesn't match with the setting type."
|
||||||
|
default_out_range: "Setting `%{name}` default value isn't in the specified range."
|
||||||
|
enum_value_not_valid: "Selected value isn't one of the enum choices."
|
||||||
|
number_value_not_valid: "New value isn't within the allowed range."
|
||||||
|
number_value_not_valid_min_max: "It must be between %{min} and %{max}."
|
||||||
|
number_value_not_valid_min: "It must be larger than or equal to %{min}."
|
||||||
|
number_value_not_valid_max: "It must be smaller than or equal to %{max}."
|
||||||
|
string_value_not_valid: "New value length isn't within the allowed range."
|
||||||
|
string_value_not_valid_min_max: "It must be between %{min} and %{max} character long."
|
||||||
|
string_value_not_valid_min: "It must be at least %{min} characters long."
|
||||||
|
string_value_not_valid_max: "It must be at most %{max} characters long."
|
||||||
emails:
|
emails:
|
||||||
incoming:
|
incoming:
|
||||||
default_subject: "This topic needs a title"
|
default_subject: "This topic needs a title"
|
||||||
|
12
db/migrate/20180118215249_create_theme_settings.rb
Normal file
12
db/migrate/20180118215249_create_theme_settings.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class CreateThemeSettings < ActiveRecord::Migration[5.1]
|
||||||
|
def change
|
||||||
|
create_table :theme_settings do |t|
|
||||||
|
t.string :name, limit: 255, null: false
|
||||||
|
t.integer :data_type, null: false
|
||||||
|
t.text :value
|
||||||
|
t.integer :theme_id, null: false
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -39,6 +39,7 @@ module Stylesheet
|
|||||||
colors.each do |n, hex|
|
colors.each do |n, hex|
|
||||||
contents << "$#{n}: ##{hex} !default;\n"
|
contents << "$#{n}: ##{hex} !default;\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
theme&.all_theme_variables&.each do |field|
|
theme&.all_theme_variables&.each do |field|
|
||||||
if field.type_id == ThemeField.types[:theme_upload_var]
|
if field.type_id == ThemeField.types[:theme_upload_var]
|
||||||
if upload = field.upload
|
if upload = field.upload
|
||||||
@ -46,11 +47,14 @@ module Stylesheet
|
|||||||
contents << "$#{field.name}: unquote(\"#{url}\");\n"
|
contents << "$#{field.name}: unquote(\"#{url}\");\n"
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
escaped = field.value.gsub('"', "\\22")
|
contents << to_scss_variable(field.name, field.value)
|
||||||
escaped.gsub!("\n", "\\A")
|
|
||||||
contents << "$#{field.name}: unquote(\"#{escaped}\");\n"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
theme&.included_settings&.each do |name, value|
|
||||||
|
contents << to_scss_variable(name, value)
|
||||||
|
end
|
||||||
|
|
||||||
Import.new("theme_variable.scss", source: contents)
|
Import.new("theme_variable.scss", source: contents)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -132,6 +136,12 @@ COMMENT
|
|||||||
"body.category-#{category.full_slug} { background-image: url(#{upload_cdn_path(category.uploaded_background.url)}) }\n"
|
"body.category-#{category.full_slug} { background-image: url(#{upload_cdn_path(category.uploaded_background.url)}) }\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_scss_variable(name, value)
|
||||||
|
escaped = value.to_s.gsub('"', "\\22")
|
||||||
|
escaped.gsub!("\n", "\\A")
|
||||||
|
"$#{name}: unquote(\"#{escaped}\");\n"
|
||||||
|
end
|
||||||
|
|
||||||
def imports(asset, parent_path)
|
def imports(asset, parent_path)
|
||||||
if asset[-1] == "*"
|
if asset[-1] == "*"
|
||||||
Dir["#{Stylesheet::ASSET_ROOT}/#{asset}.scss"].map do |path|
|
Dir["#{Stylesheet::ASSET_ROOT}/#{asset}.scss"].map do |path|
|
||||||
|
@ -252,7 +252,11 @@ class Stylesheet::Manager
|
|||||||
raise "attempting to look up theme digest for invalid field"
|
raise "attempting to look up theme digest for invalid field"
|
||||||
end
|
end
|
||||||
|
|
||||||
Digest::SHA1.hexdigest(scss.to_s + color_scheme_digest.to_s)
|
Digest::SHA1.hexdigest(scss.to_s + color_scheme_digest.to_s + settings_digest)
|
||||||
|
end
|
||||||
|
|
||||||
|
def settings_digest
|
||||||
|
Digest::SHA1.hexdigest((theme&.included_settings || {}).to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def color_scheme_digest
|
def color_scheme_digest
|
||||||
|
157
lib/theme_settings_manager.rb
Normal file
157
lib/theme_settings_manager.rb
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
class ThemeSettingsManager
|
||||||
|
attr_reader :name, :theme, :default
|
||||||
|
|
||||||
|
def self.types
|
||||||
|
ThemeSetting.types
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create(name, default, type, theme, opts = {})
|
||||||
|
type_name = self.types.invert[type].downcase.capitalize
|
||||||
|
klass = "ThemeSettingsManager::#{type_name}".constantize
|
||||||
|
klass.new(name, default, theme, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(name, default, theme, opts = {})
|
||||||
|
@name = name.to_sym
|
||||||
|
@default = default
|
||||||
|
@theme = theme
|
||||||
|
@opts = opts
|
||||||
|
@types = self.class.types
|
||||||
|
end
|
||||||
|
|
||||||
|
def value
|
||||||
|
has_record? ? db_record.value : @default
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_name
|
||||||
|
self.class.name.demodulize.downcase.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
def type
|
||||||
|
@types[type_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
@opts[:description]
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(new_value)
|
||||||
|
ensure_is_valid_value!(new_value)
|
||||||
|
|
||||||
|
record = has_record? ? db_record : create_record!
|
||||||
|
record.value = new_value.to_s
|
||||||
|
record.save!
|
||||||
|
record.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def db_record
|
||||||
|
ThemeSetting.where(name: @name, data_type: type, theme: @theme).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_record?
|
||||||
|
db_record.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_record!
|
||||||
|
record = ThemeSetting.new(name: @name, data_type: type, theme: @theme)
|
||||||
|
record.save!
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_valid_value?(new_value)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid_value_error_message
|
||||||
|
name = type == @types[:integer] || type == @types[:float] ? "number" : type_name
|
||||||
|
primary_key = "themes.settings_errors.#{name}_value_not_valid"
|
||||||
|
|
||||||
|
secondary_key = primary_key
|
||||||
|
secondary_key += "_min" if has_min?
|
||||||
|
secondary_key += "_max" if has_max?
|
||||||
|
|
||||||
|
translation = I18n.t(primary_key)
|
||||||
|
return translation if secondary_key == primary_key
|
||||||
|
|
||||||
|
translation += " #{I18n.t(secondary_key, min: @opts[:min], max: @opts[:max])}"
|
||||||
|
translation
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_is_valid_value!(new_value)
|
||||||
|
unless is_valid_value?(new_value)
|
||||||
|
raise Discourse::InvalidParameters.new invalid_value_error_message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_min?
|
||||||
|
min = @opts[:min]
|
||||||
|
(min.is_a?(::Integer) || min.is_a?(::Float)) && min != -::Float::INFINITY
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_max?
|
||||||
|
max = @opts[:max]
|
||||||
|
(max.is_a?(::Integer) || max.is_a?(::Float)) && max != ::Float::INFINITY
|
||||||
|
end
|
||||||
|
|
||||||
|
class List < self; end
|
||||||
|
class String < self
|
||||||
|
def is_valid_value?(new_value)
|
||||||
|
(@opts[:min]..@opts[:max]).include? new_value.to_s.length
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Bool < self
|
||||||
|
def value
|
||||||
|
[true, "true"].include?(super)
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(new_value)
|
||||||
|
new_value = ([true, "true"].include?(new_value)).to_s
|
||||||
|
super(new_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Integer < self
|
||||||
|
def value
|
||||||
|
super.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(new_value)
|
||||||
|
super(new_value.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_valid_value?(new_value)
|
||||||
|
(@opts[:min]..@opts[:max]).include? new_value.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Float < self
|
||||||
|
def value
|
||||||
|
super.to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
def value=(new_value)
|
||||||
|
super(new_value.to_f)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_valid_value?(new_value)
|
||||||
|
(@opts[:min]..@opts[:max]).include? new_value.to_f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Enum < self
|
||||||
|
def value
|
||||||
|
val = super
|
||||||
|
match = choices.find { |choice| choice == val || choice.to_s == val }
|
||||||
|
match || val
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_valid_value?(new_value)
|
||||||
|
choices.include?(new_value) || choices.map(&:to_s).include?(new_value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def choices
|
||||||
|
@opts[:choices]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
66
lib/theme_settings_parser.rb
Normal file
66
lib/theme_settings_parser.rb
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
class ThemeSettingsParser
|
||||||
|
class InvalidYaml < StandardError; end
|
||||||
|
|
||||||
|
def initialize(setting_field)
|
||||||
|
@setting_field = setting_field
|
||||||
|
@types = ThemeSetting.types
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_description(desc)
|
||||||
|
return desc if desc.is_a?(String)
|
||||||
|
|
||||||
|
if desc.is_a?(Hash)
|
||||||
|
default_locale = SiteSetting.default_locale.to_sym
|
||||||
|
fallback_locale = desc.keys.find { |key| I18n.locale_available?(key) }
|
||||||
|
locale = desc[I18n.locale] || desc[default_locale] || desc[:en] || desc[fallback_locale]
|
||||||
|
|
||||||
|
locale if locale.is_a?(String)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_opts(default, type, raw_opts = {})
|
||||||
|
opts = {}
|
||||||
|
opts[:description] = extract_description(raw_opts[:description])
|
||||||
|
|
||||||
|
if type == @types[:enum]
|
||||||
|
choices = raw_opts[:choices]
|
||||||
|
choices = [] unless choices.is_a?(Array)
|
||||||
|
choices << default unless choices.include?(default)
|
||||||
|
opts[:choices] = choices
|
||||||
|
end
|
||||||
|
|
||||||
|
if [@types[:integer], @types[:string], @types[:float]].include?(type)
|
||||||
|
opts[:max] = raw_opts[:max].is_a?(Numeric) ? raw_opts[:max] : Float::INFINITY
|
||||||
|
opts[:min] = raw_opts[:min].is_a?(Numeric) ? raw_opts[:min] : -Float::INFINITY
|
||||||
|
end
|
||||||
|
opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
return if @setting_field.value.blank?
|
||||||
|
|
||||||
|
begin
|
||||||
|
parsed = YAML.safe_load(@setting_field.value)
|
||||||
|
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
||||||
|
raise InvalidYaml.new(e.message)
|
||||||
|
end
|
||||||
|
raise InvalidYaml.new(I18n.t("themes.settings_errors.invalid_yaml")) unless parsed.is_a?(Hash)
|
||||||
|
|
||||||
|
parsed.deep_symbolize_keys!
|
||||||
|
|
||||||
|
parsed.each_pair do |setting, value|
|
||||||
|
if (type = ThemeSetting.guess_type(value)).present?
|
||||||
|
result = [setting, value, type, create_opts(value, type)]
|
||||||
|
elsif (hash = value).is_a?(Hash)
|
||||||
|
default = hash[:default]
|
||||||
|
type = hash.key?(:type) ? @types[hash[:type]&.to_sym] : ThemeSetting.guess_type(default)
|
||||||
|
|
||||||
|
result = [setting, default, type, create_opts(default, type, hash)]
|
||||||
|
else
|
||||||
|
result = [setting, value, nil, {}]
|
||||||
|
end
|
||||||
|
|
||||||
|
yield(*result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
117
spec/components/theme_settings_manager_spec.rb
Normal file
117
spec/components/theme_settings_manager_spec.rb
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require 'theme_settings_manager'
|
||||||
|
|
||||||
|
describe ThemeSettingsManager do
|
||||||
|
|
||||||
|
let(:theme_settings) do
|
||||||
|
theme = Theme.create!(name: "awesome theme", user_id: -1)
|
||||||
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
||||||
|
theme.set_field(target: :settings, name: "yaml", value: yaml)
|
||||||
|
theme.save!
|
||||||
|
theme.settings
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_name(name)
|
||||||
|
theme_settings.find { |setting| setting.name == name }
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Enum" do
|
||||||
|
it "only accepts values from its choices" do
|
||||||
|
enum_setting = find_by_name(:enum_setting)
|
||||||
|
expect { enum_setting.value = "trust level 2" }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
expect { enum_setting.value = "trust level 0" }.not_to raise_error
|
||||||
|
|
||||||
|
enum_setting = find_by_name(:enum_setting_02)
|
||||||
|
expect { enum_setting.value = "10" }.not_to raise_error
|
||||||
|
|
||||||
|
enum_setting = find_by_name(:enum_setting_03)
|
||||||
|
expect { enum_setting.value = "10" }.not_to raise_error
|
||||||
|
expect { enum_setting.value = 1 }.not_to raise_error
|
||||||
|
expect { enum_setting.value = 15 }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Bool" do
|
||||||
|
it "is either true or false" do
|
||||||
|
bool_setting = find_by_name(:boolean_setting)
|
||||||
|
expect(bool_setting.value).to eq(true) # default
|
||||||
|
|
||||||
|
bool_setting.value = "true"
|
||||||
|
expect(bool_setting.value).to eq(true)
|
||||||
|
|
||||||
|
bool_setting.value = "falsse" # intentionally misspelled
|
||||||
|
expect(bool_setting.value).to eq(false)
|
||||||
|
|
||||||
|
bool_setting.value = true
|
||||||
|
expect(bool_setting.value).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Integer" do
|
||||||
|
it "is always an integer" do
|
||||||
|
int_setting = find_by_name(:integer_setting)
|
||||||
|
int_setting.value = 1.6
|
||||||
|
expect(int_setting.value).to eq(1)
|
||||||
|
|
||||||
|
int_setting.value = "4.3"
|
||||||
|
expect(int_setting.value).to eq(4)
|
||||||
|
|
||||||
|
int_setting.value = "10"
|
||||||
|
expect(int_setting.value).to eq(10)
|
||||||
|
|
||||||
|
int_setting.value = "text"
|
||||||
|
expect(int_setting.value).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can have min or max value" do
|
||||||
|
int_setting = find_by_name(:integer_setting_02)
|
||||||
|
expect { int_setting.value = 0 }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
expect { int_setting.value = 61 }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
|
||||||
|
int_setting.value = 60
|
||||||
|
expect(int_setting.value).to eq(60)
|
||||||
|
|
||||||
|
int_setting.value = 1
|
||||||
|
expect(int_setting.value).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Float" do
|
||||||
|
it "is always a float" do
|
||||||
|
float_setting = find_by_name(:float_setting)
|
||||||
|
float_setting.value = 1.615
|
||||||
|
expect(float_setting.value).to eq(1.615)
|
||||||
|
|
||||||
|
float_setting.value = "3.1415"
|
||||||
|
expect(float_setting.value).to eq(3.1415)
|
||||||
|
|
||||||
|
float_setting.value = 10
|
||||||
|
expect(float_setting.value).to eq(10)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can have min or max value" do
|
||||||
|
float_setting = find_by_name(:float_setting)
|
||||||
|
expect { float_setting.value = 1.4 }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
expect { float_setting.value = 10.01 }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
expect { float_setting.value = "text" }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
|
||||||
|
float_setting.value = 9.521
|
||||||
|
expect(float_setting.value).to eq(9.521)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "String" do
|
||||||
|
it "can have min or max length" do
|
||||||
|
string_setting = find_by_name(:string_setting_02)
|
||||||
|
expect { string_setting.value = "a" }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
|
||||||
|
string_setting.value = "ab"
|
||||||
|
expect(string_setting.value).to eq("ab")
|
||||||
|
|
||||||
|
string_setting.value = "ab" * 10
|
||||||
|
expect(string_setting.value).to eq("ab" * 10)
|
||||||
|
|
||||||
|
expect { string_setting.value = ("a" * 21) }.to raise_error(Discourse::InvalidParameters)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
85
spec/components/theme_settings_parser_spec.rb
Normal file
85
spec/components/theme_settings_parser_spec.rb
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require 'theme_settings_parser'
|
||||||
|
|
||||||
|
describe ThemeSettingsParser do
|
||||||
|
after(:all) do
|
||||||
|
ThemeField.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def types
|
||||||
|
ThemeSetting.types
|
||||||
|
end
|
||||||
|
|
||||||
|
class Loader
|
||||||
|
def initialize
|
||||||
|
@settings ||= []
|
||||||
|
load_settings
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_settings
|
||||||
|
yaml = File.read("#{Rails.root}/spec/fixtures/theme_settings/valid_settings.yaml")
|
||||||
|
field = ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: yaml)
|
||||||
|
|
||||||
|
ThemeSettingsParser.new(field).load do |name, default, type, opts|
|
||||||
|
@settings << setting(name, default, type, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def setting(name, default, type, opts = {})
|
||||||
|
{ name: name, default: default, type: type, opts: opts }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by_name(name)
|
||||||
|
@settings.find { |setting| setting[:name] == name }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:loader) { Loader.new }
|
||||||
|
|
||||||
|
it "guesses types correctly" do
|
||||||
|
expect(loader.find_by_name(:boolean_setting)[:type]).to eq(types[:bool])
|
||||||
|
expect(loader.find_by_name(:boolean_setting_02)[:type]).to eq(types[:bool])
|
||||||
|
expect(loader.find_by_name(:string_setting)[:type]).to eq(types[:string])
|
||||||
|
expect(loader.find_by_name(:integer_setting)[:type]).to eq(types[:integer])
|
||||||
|
expect(loader.find_by_name(:integer_setting_03)[:type]).to eq(types[:integer])
|
||||||
|
expect(loader.find_by_name(:float_setting)[:type]).to eq(types[:float])
|
||||||
|
expect(loader.find_by_name(:list_setting)[:type]).to eq(types[:list])
|
||||||
|
expect(loader.find_by_name(:enum_setting)[:type]).to eq(types[:enum])
|
||||||
|
end
|
||||||
|
|
||||||
|
context "description locale" do
|
||||||
|
it "favors I18n.locale" do
|
||||||
|
I18n.locale = :ar
|
||||||
|
SiteSetting.default_locale = "en"
|
||||||
|
expect(loader.find_by_name(:enum_setting_02)[:opts][:description]).to eq("Arabic text")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses SiteSetting.default_locale if I18n.locale isn't supported" do
|
||||||
|
I18n.locale = :en
|
||||||
|
SiteSetting.default_locale = "es"
|
||||||
|
expect(loader.find_by_name(:integer_setting_02)[:opts][:description]).to eq("Spanish text")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "finds the first supported locale and uses it as a last resort" do
|
||||||
|
I18n.locale = :de
|
||||||
|
SiteSetting.default_locale = "it"
|
||||||
|
expect(loader.find_by_name(:integer_setting_02)[:opts][:description]).to eq("French text")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't set locale if no supported locale is provided" do
|
||||||
|
expect(loader.find_by_name(:integer_setting_03)[:opts][:description]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "enum setting" do
|
||||||
|
it "should never have less than 1 choices" do
|
||||||
|
choices = loader.find_by_name(:enum_setting)[:opts][:choices]
|
||||||
|
expect(choices.class).to eq(Array)
|
||||||
|
expect(choices.length).to eq(3)
|
||||||
|
|
||||||
|
choices = loader.find_by_name(:enum_setting_02)[:opts][:choices]
|
||||||
|
expect(choices.class).to eq(Array)
|
||||||
|
expect(choices.length).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
19
spec/fixtures/theme_settings/invalid_settings.yaml
vendored
Normal file
19
spec/fixtures/theme_settings/invalid_settings.yaml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
no_match_setting:
|
||||||
|
type: bool
|
||||||
|
default: "string value"
|
||||||
|
|
||||||
|
no_default_setting:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
invalid_type_setting:
|
||||||
|
type: listt
|
||||||
|
default: "name|age|last name"
|
||||||
|
|
||||||
|
default_out_of_range:
|
||||||
|
default: 100
|
||||||
|
min: 1
|
||||||
|
max: 20
|
||||||
|
|
||||||
|
string_default_out_of_range:
|
||||||
|
default: "abcdefg"
|
||||||
|
min: 20
|
60
spec/fixtures/theme_settings/valid_settings.yaml
vendored
Normal file
60
spec/fixtures/theme_settings/valid_settings.yaml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
boolean_setting: true
|
||||||
|
|
||||||
|
boolean_setting_02:
|
||||||
|
default: false
|
||||||
|
|
||||||
|
string_setting: "string value"
|
||||||
|
|
||||||
|
string_setting_02:
|
||||||
|
default: "string value"
|
||||||
|
min: 2
|
||||||
|
max: 20
|
||||||
|
|
||||||
|
integer_setting: 51
|
||||||
|
|
||||||
|
integer_setting_02:
|
||||||
|
type: integer
|
||||||
|
default: 51
|
||||||
|
min: 1
|
||||||
|
max: 60
|
||||||
|
description:
|
||||||
|
fr: "French text"
|
||||||
|
es: "Spanish text"
|
||||||
|
|
||||||
|
integer_setting_03:
|
||||||
|
default: 15
|
||||||
|
max: 60
|
||||||
|
description:
|
||||||
|
xyz: "invalid language"
|
||||||
|
|
||||||
|
float_setting:
|
||||||
|
default: 2.5
|
||||||
|
min: 1.5
|
||||||
|
max: 10
|
||||||
|
|
||||||
|
list_setting:
|
||||||
|
type: list
|
||||||
|
description: "help text"
|
||||||
|
default: "name|age|last name"
|
||||||
|
|
||||||
|
enum_setting:
|
||||||
|
default: "trust level 4"
|
||||||
|
type: enum
|
||||||
|
choices:
|
||||||
|
- "trust level 0"
|
||||||
|
- "trust level 1"
|
||||||
|
|
||||||
|
enum_setting_02:
|
||||||
|
type: enum
|
||||||
|
default: 10
|
||||||
|
description:
|
||||||
|
en: "English text"
|
||||||
|
ar: "Arabic text"
|
||||||
|
|
||||||
|
enum_setting_03:
|
||||||
|
type: enum
|
||||||
|
default: 1
|
||||||
|
choices:
|
||||||
|
- 10
|
||||||
|
- 100
|
||||||
|
- 1000
|
@ -58,6 +58,7 @@ JSON
|
|||||||
"common/random.html" => "I AM SILLY",
|
"common/random.html" => "I AM SILLY",
|
||||||
"common/embedded.scss" => "EMBED",
|
"common/embedded.scss" => "EMBED",
|
||||||
"assets/awesome.woff2" => "FAKE FONT",
|
"assets/awesome.woff2" => "FAKE FONT",
|
||||||
|
"settings.yaml" => "boolean_setting: true"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -81,7 +82,7 @@ JSON
|
|||||||
expect(remote.about_url).to eq("https://www.site.com/about")
|
expect(remote.about_url).to eq("https://www.site.com/about")
|
||||||
expect(remote.license_url).to eq("https://www.site.com/license")
|
expect(remote.license_url).to eq("https://www.site.com/license")
|
||||||
|
|
||||||
expect(@theme.theme_fields.length).to eq(6)
|
expect(@theme.theme_fields.length).to eq(7)
|
||||||
|
|
||||||
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
|
mapped = Hash[*@theme.theme_fields.map { |f| ["#{f.target_id}-#{f.name}", f.value] }.flatten]
|
||||||
|
|
||||||
@ -93,7 +94,12 @@ JSON
|
|||||||
expect(mapped["0-font"]).to eq("")
|
expect(mapped["0-font"]).to eq("")
|
||||||
expect(mapped["0-name"]).to eq("sam")
|
expect(mapped["0-name"]).to eq("sam")
|
||||||
|
|
||||||
expect(mapped.length).to eq(6)
|
expect(mapped["3-yaml"]).to eq("boolean_setting: true")
|
||||||
|
|
||||||
|
expect(mapped.length).to eq(7)
|
||||||
|
|
||||||
|
expect(@theme.settings.length).to eq(1)
|
||||||
|
expect(@theme.settings.first.value).to eq(true)
|
||||||
|
|
||||||
expect(remote.remote_updated_at).to eq(time)
|
expect(remote.remote_updated_at).to eq(time)
|
||||||
|
|
||||||
@ -104,6 +110,10 @@ JSON
|
|||||||
File.write("#{initial_repo}/common/header.html", "I AM UPDATED")
|
File.write("#{initial_repo}/common/header.html", "I AM UPDATED")
|
||||||
File.write("#{initial_repo}/about.json", about_json(love: "EAEAEA"))
|
File.write("#{initial_repo}/about.json", about_json(love: "EAEAEA"))
|
||||||
|
|
||||||
|
File.write("#{initial_repo}/settings.yml", "integer_setting: 32")
|
||||||
|
`cd #{initial_repo} && git add settings.yml`
|
||||||
|
|
||||||
|
File.delete("#{initial_repo}/settings.yaml")
|
||||||
`cd #{initial_repo} && git commit -am "update"`
|
`cd #{initial_repo} && git commit -am "update"`
|
||||||
|
|
||||||
time = Time.new('2001')
|
time = Time.new('2001')
|
||||||
@ -125,8 +135,11 @@ JSON
|
|||||||
|
|
||||||
expect(mapped["0-header"]).to eq("I AM UPDATED")
|
expect(mapped["0-header"]).to eq("I AM UPDATED")
|
||||||
expect(mapped["1-scss"]).to eq(scss_data)
|
expect(mapped["1-scss"]).to eq(scss_data)
|
||||||
expect(remote.remote_updated_at).to eq(time)
|
|
||||||
|
|
||||||
|
expect(@theme.settings.length).to eq(1)
|
||||||
|
expect(@theme.settings.first.value).to eq(32)
|
||||||
|
|
||||||
|
expect(remote.remote_updated_at).to eq(time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe ThemeField do
|
describe ThemeField do
|
||||||
|
after(:all) do
|
||||||
|
ThemeField.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
it "correctly generates errors for transpiled js" do
|
it "correctly generates errors for transpiled js" do
|
||||||
html = <<HTML
|
html = <<HTML
|
||||||
<script type="text/discourse-plugin" version="0.8">
|
<script type="text/discourse-plugin" version="0.8">
|
||||||
@ -44,4 +48,52 @@ HTML
|
|||||||
expect { create_upload_theme_field!("a42") }.not_to raise_error
|
expect { create_upload_theme_field!("a42") }.not_to raise_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_fixture(type)
|
||||||
|
File.read("#{Rails.root}/spec/fixtures/theme_settings/#{type}_settings.yaml")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_yaml_field(value)
|
||||||
|
field = ThemeField.create!(theme_id: 1, target_id: Theme.targets[:settings], name: "yaml", value: value)
|
||||||
|
field.reload
|
||||||
|
field
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:key) { "themes.settings_errors" }
|
||||||
|
|
||||||
|
it "generates errors for bad YAML" do
|
||||||
|
yaml = "invalid_setting 5"
|
||||||
|
field = create_yaml_field(yaml)
|
||||||
|
expect(field.error).to eq(I18n.t("#{key}.invalid_yaml"))
|
||||||
|
|
||||||
|
field.value = "valid_setting: true"
|
||||||
|
field.save!
|
||||||
|
field.reload
|
||||||
|
expect(field.error).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "generates errors when default value's type doesn't match setting type" do
|
||||||
|
field = create_yaml_field(get_fixture("invalid"))
|
||||||
|
expect(field.error).to include(I18n.t("#{key}.default_not_match_type", name: "no_match_setting"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "generates errors when no default value is passed" do
|
||||||
|
field = create_yaml_field(get_fixture("invalid"))
|
||||||
|
expect(field.error).to include(I18n.t("#{key}.default_value_missing", name: "no_default_setting"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "generates errors when invalid type is passed" do
|
||||||
|
field = create_yaml_field(get_fixture("invalid"))
|
||||||
|
expect(field.error).to include(I18n.t("#{key}.data_type_not_a_number", name: "invalid_type_setting"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "generates errors when default value is not within allowed range" do
|
||||||
|
field = create_yaml_field(get_fixture("invalid"))
|
||||||
|
expect(field.error).to include(I18n.t("#{key}.default_out_range", name: "default_out_of_range"))
|
||||||
|
expect(field.error).to include(I18n.t("#{key}.default_out_range", name: "string_default_out_of_range"))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works correctly when valid yaml is provided" do
|
||||||
|
field = create_yaml_field(get_fixture("valid"))
|
||||||
|
expect(field.error).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -123,7 +123,7 @@ HTML
|
|||||||
|
|
||||||
context "plugin api" do
|
context "plugin api" do
|
||||||
def transpile(html)
|
def transpile(html)
|
||||||
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: -1, name: "after_header", value: html)
|
f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html)
|
||||||
f.value_baked
|
f.value_baked
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -213,6 +213,19 @@ HTML
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "theme settings" do
|
||||||
|
it "values can be used in scss" do
|
||||||
|
theme = Theme.new(name: "awesome theme", user_id: -1)
|
||||||
|
theme.set_field(target: :settings, name: :yaml, value: "background_color: red\nfont_size: 25px")
|
||||||
|
theme.set_field(target: :common, name: :scss, value: 'body {background-color: $background_color; font-size: $font-size}')
|
||||||
|
theme.save!
|
||||||
|
|
||||||
|
scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id)
|
||||||
|
expect(scss).to include("background-color:red")
|
||||||
|
expect(scss).to include("font-size:25px")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'correctly caches theme keys' do
|
it 'correctly caches theme keys' do
|
||||||
Theme.destroy_all
|
Theme.destroy_all
|
||||||
|
|
||||||
@ -266,4 +279,41 @@ HTML
|
|||||||
expect(user_themes).to eq([])
|
expect(user_themes).to eq([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cached_settings(key)
|
||||||
|
Theme.settings_for_client(key) # returns json
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles settings cache correctly' do
|
||||||
|
Theme.destroy_all
|
||||||
|
expect(cached_settings(nil)).to eq("{}")
|
||||||
|
|
||||||
|
theme = Theme.create!(name: "awesome theme", user_id: -1)
|
||||||
|
theme.save!
|
||||||
|
expect(cached_settings(theme.key)).to eq("{}")
|
||||||
|
|
||||||
|
theme.set_field(target: :settings, name: "yaml", value: "boolean_setting: true")
|
||||||
|
theme.save!
|
||||||
|
expect(cached_settings(theme.key)).to match(/\"boolean_setting\":true/)
|
||||||
|
|
||||||
|
theme.settings.first.value = "false"
|
||||||
|
expect(cached_settings(theme.key)).to match(/\"boolean_setting\":false/)
|
||||||
|
|
||||||
|
child = Theme.create!(name: "child theme", user_id: -1)
|
||||||
|
child.set_field(target: :settings, name: "yaml", value: "integer_setting: 54")
|
||||||
|
|
||||||
|
child.save!
|
||||||
|
theme.add_child_theme!(child)
|
||||||
|
|
||||||
|
json = cached_settings(theme.key)
|
||||||
|
expect(json).to match(/\"boolean_setting\":false/)
|
||||||
|
expect(json).to match(/\"integer_setting\":54/)
|
||||||
|
|
||||||
|
expect(cached_settings(child.key)).to eq("{\"integer_setting\":54}")
|
||||||
|
|
||||||
|
child.destroy!
|
||||||
|
json = cached_settings(theme.key)
|
||||||
|
expect(json).not_to match(/\"integer_setting\":54/)
|
||||||
|
expect(json).to match(/\"boolean_setting\":false/)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user