mirror of
https://github.com/discourse/discourse.git
synced 2025-09-06 09:10:25 +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:
parent
322618fc34
commit
282f53f0cd
42 changed files with 1202 additions and 217 deletions
|
@ -1,96 +1,10 @@
|
|||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import SiteSetting from 'admin/models/site-setting';
|
||||
import { propertyNotEqual } from 'discourse/lib/computed';
|
||||
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"),
|
||||
import SettingComponent from 'admin/mixins/setting-component';
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
_save() {
|
||||
const setting = this.get('buffered'),
|
||||
action = 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();
|
||||
}
|
||||
const setting = this.get('buffered');
|
||||
return SiteSetting.update(setting.get('setting'), setting.get('value'));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ export default Ember.Component.extend({
|
|||
enabled: {
|
||||
get(value) {
|
||||
if (Ember.isEmpty(value)) { return false; }
|
||||
return value === "true";
|
||||
return value.toString() === "true";
|
||||
},
|
||||
set(value) {
|
||||
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,
|
||||
|
||||
targets: [
|
||||
{id: 0, name: I18n.t('admin.customize.theme.common')},
|
||||
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
|
||||
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
|
||||
{ id: 0, name: 'common' },
|
||||
{ id: 1, name: 'desktop' },
|
||||
{ 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')
|
||||
showCommon() {
|
||||
return this.shouldShow('common');
|
||||
|
@ -26,6 +37,11 @@ export default Ember.Controller.extend({
|
|||
return this.shouldShow('mobile');
|
||||
},
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showSettings() {
|
||||
return this.shouldShow('settings');
|
||||
},
|
||||
|
||||
@observes('onlyOverridden')
|
||||
onlyOverriddenChanged() {
|
||||
if (this.get('onlyOverridden')) {
|
||||
|
@ -51,27 +67,19 @@ export default Ember.Controller.extend({
|
|||
currentTarget: 0,
|
||||
|
||||
setTargetName: function(name) {
|
||||
let target;
|
||||
switch(name) {
|
||||
case "common": target = 0; break;
|
||||
case "desktop": target = 1; break;
|
||||
case "mobile": target = 2; break;
|
||||
}
|
||||
|
||||
this.set("currentTarget", target);
|
||||
const target = this.get('targets').find(t => t.name === name);
|
||||
this.set("currentTarget", target && target.id);
|
||||
},
|
||||
|
||||
@computed("currentTarget")
|
||||
currentTargetName(target) {
|
||||
switch(parseInt(target)) {
|
||||
case 0: return "common";
|
||||
case 1: return "desktop";
|
||||
case 2: return "mobile";
|
||||
}
|
||||
currentTargetName(id) {
|
||||
const target = this.get('targets').find(t => t.id === parseInt(id, 10));
|
||||
return target && target.name;
|
||||
},
|
||||
|
||||
@computed("fieldName")
|
||||
activeSectionMode(fieldName) {
|
||||
if (fieldName === "yaml") return "yaml";
|
||||
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) {
|
||||
let fields = [
|
||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
||||
];
|
||||
|
||||
if (parseInt(target) === 0) {
|
||||
fields.push("embedded_scss");
|
||||
}
|
||||
let fields = this.fieldsForTarget(target);
|
||||
|
||||
if (onlyOverridden) {
|
||||
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 { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import ThemeSettings from 'admin/models/theme-settings';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
|
@ -30,7 +31,7 @@ export default Ember.Controller.extend({
|
|||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
['common','desktop','mobile'].forEach(target=> {
|
||||
['common', 'desktop', 'mobile', 'settings'].forEach(target => {
|
||||
descriptions.push(description(target));
|
||||
});
|
||||
return descriptions.reject(d=>Em.isBlank(d));
|
||||
|
@ -77,6 +78,16 @@ export default Ember.Controller.extend({
|
|||
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/%@'),
|
||||
|
||||
actions: {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
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, {
|
||||
local: Ember.computed.equal('selection', 'local'),
|
||||
remote: Ember.computed.equal('selection', 'remote'),
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
loading: false,
|
||||
|
||||
actions: {
|
||||
importTheme() {
|
||||
|
@ -24,11 +25,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
|||
options.data = {remote: this.get('uploadUrl')};
|
||||
}
|
||||
|
||||
this.set('loading', true);
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
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';
|
||||
const SiteSetting = Discourse.Model.extend({
|
||||
overridden: function() {
|
||||
let val = this.get('value'),
|
||||
defaultVal = this.get('default');
|
||||
import Setting from 'admin/mixins/setting-object';
|
||||
|
||||
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(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')
|
||||
});
|
||||
const SiteSetting = Discourse.Model.extend(Setting, {});
|
||||
|
||||
SiteSetting.reopenClass({
|
||||
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';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const FIELDS_IDS = [0, 1, 5];
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
|
@ -14,13 +15,11 @@ const Theme = RestModel.extend({
|
|||
}
|
||||
|
||||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
}
|
||||
fields.forEach(field => {
|
||||
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
return hash;
|
||||
},
|
||||
|
||||
|
@ -29,11 +28,11 @@ const Theme = RestModel.extend({
|
|||
if (!fields) {
|
||||
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){
|
||||
return field.target + " " + field.name;
|
||||
return `${field.target} ${field.name}`;
|
||||
},
|
||||
|
||||
hasEdited(target, name){
|
||||
|
@ -151,6 +150,11 @@ const Theme = RestModel.extend({
|
|||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
saveSettings(name, value) {
|
||||
const settings = {};
|
||||
settings[name] = value;
|
||||
return this.save({ settings });
|
||||
}
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
|
|
|
@ -18,6 +18,11 @@ export default Ember.Route.extend({
|
|||
},
|
||||
|
||||
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.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
|
|
|
@ -10,5 +10,5 @@
|
|||
{{d-button class="cancel" action="cancel" icon="times"}}
|
||||
</div>
|
||||
{{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}}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</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>
|
||||
|
||||
{{#if error}}
|
||||
<pre class='field-error'>{{error}}</pre>
|
||||
<pre class='field-error'>{{error}}</pre>
|
||||
{{/if}}
|
||||
|
||||
<div class='edit-main-nav'>
|
||||
<ul class='nav nav-pills target'>
|
||||
{{#if showCommon}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.common'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.common'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showDesktop}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{d-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{d-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showMobile}}
|
||||
<li class='mobile'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{d-icon 'mobile'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class='mobile'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{d-icon 'mobile'}}
|
||||
{{/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>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
|
|
|
@ -50,16 +50,16 @@
|
|||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
|
@ -71,17 +71,17 @@
|
|||
{{/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}}
|
||||
{{i18n 'admin.customize.theme.updating'}}
|
||||
{{else}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
{{i18n 'admin.customize.theme.updating'}}
|
||||
{{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}}
|
||||
</span>
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
|
@ -105,6 +105,17 @@
|
|||
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</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}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#if filteredContent}}
|
||||
{{#d-section class="form-horizontal settings"}}
|
||||
{{#each filteredContent as |setting|}}
|
||||
{{site-setting setting=setting saveAction="saveSetting"}}
|
||||
{{site-setting setting=setting}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
{{else}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
<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 {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
|
@ -138,6 +138,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
end
|
||||
|
||||
set_fields
|
||||
update_settings
|
||||
|
||||
save_remote = false
|
||||
if params[:theme][:remote_check]
|
||||
|
@ -158,7 +159,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
update_default_theme
|
||||
|
||||
log_theme_change(original_json, @theme)
|
||||
format.json { render json: @theme, status: :created }
|
||||
format.json { render json: @theme, status: :ok }
|
||||
else
|
||||
format.json {
|
||||
|
||||
|
@ -193,7 +194,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
|
||||
response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json"
|
||||
response.sending_file = true
|
||||
render json: ThemeWithEmbeddedUploadsSerializer.new(@theme, root: 'theme')
|
||||
render json: ::ThemeWithEmbeddedUploadsSerializer.new(@theme, root: 'theme')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -223,6 +224,7 @@ class Admin::ThemesController < Admin::AdminController
|
|||
:color_scheme_id,
|
||||
:default,
|
||||
:user_selectable,
|
||||
settings: {},
|
||||
theme_fields: [:name, :target, :value, :upload_id, :type_id],
|
||||
child_theme_ids: []
|
||||
)
|
||||
|
@ -243,6 +245,14 @@ class Admin::ThemesController < Admin::AdminController
|
|||
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)
|
||||
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
||||
end
|
||||
|
|
|
@ -460,6 +460,7 @@ class ApplicationController < ActionController::Base
|
|||
def preload_anonymous_data
|
||||
store_preloaded("site", Site.json_for(guardian))
|
||||
store_preloaded("siteSettings", SiteSetting.client_settings_json)
|
||||
store_preloaded("themeSettings", Theme.settings_for_client(@theme_key))
|
||||
store_preloaded("customHTML", custom_html_json)
|
||||
store_preloaded("banner", banner_json)
|
||||
store_preloaded("customEmoji", custom_emoji)
|
||||
|
|
|
@ -76,6 +76,8 @@ class RemoteTheme < ActiveRecord::Base
|
|||
end
|
||||
|
||||
Theme.targets.keys.each do |target|
|
||||
next if target == :settings
|
||||
|
||||
ALLOWED_FIELDS.each do |field|
|
||||
lookup =
|
||||
if field == "scss"
|
||||
|
@ -91,6 +93,9 @@ class RemoteTheme < ActiveRecord::Base
|
|||
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.about_url ||= theme_info["about_url"]
|
||||
self.remote_updated_at = Time.zone.now
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require_dependency 'distributed_cache'
|
||||
require_dependency 'stylesheet/compiler'
|
||||
require_dependency 'stylesheet/manager'
|
||||
require_dependency 'theme_settings_parser'
|
||||
require_dependency 'theme_settings_manager'
|
||||
|
||||
class Theme < ActiveRecord::Base
|
||||
|
||||
|
@ -8,6 +10,7 @@ class Theme < ActiveRecord::Base
|
|||
|
||||
belongs_to :color_scheme
|
||||
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_themes, through: :child_theme_relation, source: :child_theme
|
||||
has_many :color_schemes
|
||||
|
@ -34,11 +37,13 @@ class Theme < ActiveRecord::Base
|
|||
@included_themes = nil
|
||||
|
||||
remove_from_cache!
|
||||
clear_cached_settings!
|
||||
notify_scheme_change if saved_change_to_color_scheme_id?
|
||||
end
|
||||
|
||||
after_destroy do
|
||||
remove_from_cache!
|
||||
clear_cached_settings!
|
||||
if SiteSetting.default_theme_key == self.key
|
||||
Theme.clear_default!
|
||||
end
|
||||
|
@ -122,7 +127,11 @@ class Theme < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def notify_scheme_change(clear_manager_cache = true)
|
||||
|
@ -288,6 +297,56 @@ class Theme < ActiveRecord::Base
|
|||
child_themes.reload
|
||||
save!
|
||||
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
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require_dependency 'theme_settings_parser'
|
||||
|
||||
class ThemeField < ActiveRecord::Base
|
||||
|
||||
belongs_to :upload
|
||||
|
@ -7,7 +9,8 @@ class ThemeField < ActiveRecord::Base
|
|||
scss: 1,
|
||||
theme_upload_var: 2,
|
||||
theme_color_var: 3,
|
||||
theme_var: 4)
|
||||
theme_var: 4,
|
||||
yaml: 5)
|
||||
end
|
||||
|
||||
def self.theme_var_type_ids
|
||||
|
@ -77,11 +80,54 @@ COMPILED
|
|||
[doc.to_s, errors&.join("\n")]
|
||||
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)
|
||||
if html_fields.include?(name.to_s)
|
||||
types[:html]
|
||||
elsif scss_fields.include?(name.to_s)
|
||||
types[:scss]
|
||||
elsif name.to_s === "yaml"
|
||||
types[:yaml]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -121,7 +167,7 @@ COMPILED
|
|||
)
|
||||
self.error = nil unless error.nil?
|
||||
rescue SassC::SyntaxError => e
|
||||
self.error = e.message
|
||||
self.error = e.message unless self.destroyed?
|
||||
end
|
||||
|
||||
if will_save_change_to_error?
|
||||
|
@ -143,6 +189,8 @@ COMPILED
|
|||
after_commit do
|
||||
ensure_baked!
|
||||
ensure_scss_compiles!
|
||||
validate_yaml!
|
||||
theme.clear_cached_settings!
|
||||
|
||||
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
|
||||
|
||||
def target
|
||||
case object.target_id
|
||||
when 0 then "common"
|
||||
when 1 then "desktop"
|
||||
when 2 then "mobile"
|
||||
end
|
||||
Theme.lookup_target(object.target_id)&.to_s
|
||||
end
|
||||
|
||||
def include_error?
|
||||
|
@ -60,7 +56,7 @@ class RemoteThemeSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
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 :child_themes, serializer: ChildThemeSerializer, embed: :objects
|
||||
|
@ -69,6 +65,10 @@ class ThemeSerializer < ChildThemeSerializer
|
|||
def child_themes
|
||||
object.child_themes.order(:name)
|
||||
end
|
||||
|
||||
def settings
|
||||
object.settings.map { |setting| ThemeSettingsSerializer.new(setting, root: false) }
|
||||
end
|
||||
end
|
||||
|
||||
class ThemeFieldWithEmbeddedUploadsSerializer < ThemeFieldSerializer
|
||||
|
@ -94,4 +94,8 @@ end
|
|||
|
||||
class ThemeWithEmbeddedUploadsSerializer < ThemeSerializer
|
||||
has_many :theme_fields, serializer: ThemeFieldWithEmbeddedUploadsSerializer, embed: :objects
|
||||
|
||||
def include_settings?
|
||||
false
|
||||
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.Environment = '<%= Rails.env %>';
|
||||
Discourse.SiteSettings = ps.get('siteSettings');
|
||||
Discourse.ThemeSettings = ps.get('themeSettings');
|
||||
Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>';
|
||||
Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>';
|
||||
Discourse.ServiceWorkerURL = '<%= Rails.application.assets_manifest.assets['service-worker.js'] %>'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue