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:
OsamaSayegh
2018-03-05 03:04:23 +03:00
committed by Sam
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));
}
}
@@ -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();
}
}
});
@@ -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'>