mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: Native theme support
This feature introduces the concept of themes. Themes are an evolution of site customizations. Themes introduce two very big conceptual changes: - A theme may include other "child themes", children can include grand children and so on. - A theme may specify a color scheme The change does away with the idea of "enabled" color schemes. It also adds a bunch of big niceties like - You can source a theme from a git repo - History for themes is much improved - You can only have a single enabled theme. Themes can be selected by users, if you opt for it. On a technical level this change comes with a whole bunch of goodies - All CSS is now compiled using a custom pipeline that uses libsass see /lib/stylesheet - There is a single pipeline for css compilation (in the past we used one for customizations and another one for the rest of the app - The stylesheet pipeline is now divorced of sprockets, there is no reliance on sprockets for CSS bundling - CSS is generated with source maps everywhere (including themes) this makes debugging much easier - Our "live reloader" is smarter and avoid a flash of unstyled content we run a file watcher in "puma" in dev so you no longer need to run rake autospec to watch for CSS changes
This commit is contained in:
parent
1a9afa976d
commit
a3e8c3cd7b
4
Gemfile
4
Gemfile
|
@ -93,8 +93,6 @@ gem 'thor', require: false
|
|||
gem 'rest-client'
|
||||
gem 'rinku'
|
||||
gem 'sanitize'
|
||||
gem 'sass'
|
||||
gem 'sass-rails'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-statistic'
|
||||
|
||||
|
@ -181,3 +179,5 @@ gem 'memory_profiler', require: false, platform: :mri
|
|||
gem 'rmmseg-cpp', require: false
|
||||
|
||||
gem 'logster'
|
||||
|
||||
gem 'sassc', require: false
|
||||
|
|
17
Gemfile.lock
17
Gemfile.lock
|
@ -105,7 +105,7 @@ GEM
|
|||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
ffi (1.9.17)
|
||||
ffi (1.9.18)
|
||||
flamegraph (0.9.5)
|
||||
foreman (0.82.0)
|
||||
thor (~> 0.19.1)
|
||||
|
@ -318,13 +318,11 @@ GEM
|
|||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
sass (3.2.19)
|
||||
sass-rails (5.0.4)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
sass (~> 3.1)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
sprockets-rails (>= 2.0, < 4.0)
|
||||
tilt (>= 1.1, < 3)
|
||||
sass (3.4.23)
|
||||
sassc (1.11.2)
|
||||
bundler
|
||||
ffi (~> 1.9.6)
|
||||
sass (>= 3.3.0)
|
||||
seed-fu (2.3.5)
|
||||
activerecord (>= 3.1, < 4.3)
|
||||
activesupport (>= 3.1, < 4.3)
|
||||
|
@ -462,8 +460,7 @@ DEPENDENCIES
|
|||
rtlit
|
||||
ruby-readability
|
||||
sanitize
|
||||
sass
|
||||
sass-rails
|
||||
sassc
|
||||
seed-fu (~> 2.3.5)
|
||||
shoulda
|
||||
sidekiq
|
||||
|
|
20
app/assets/javascripts/admin/adapters/theme.js.es6
Normal file
20
app/assets/javascripts/admin/adapters/theme.js.es6
Normal file
|
@ -0,0 +1,20 @@
|
|||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
basePath() {
|
||||
return "/admin/";
|
||||
},
|
||||
|
||||
afterFindAll(results) {
|
||||
let map = {};
|
||||
results.forEach(theme => {map[theme.id] = theme;});
|
||||
results.forEach(theme => {
|
||||
let mapped = theme.get("child_themes") || [];
|
||||
mapped = mapped.map(t => map[t.id]);
|
||||
theme.set("childThemes", mapped);
|
||||
});
|
||||
return results;
|
||||
},
|
||||
|
||||
jsonMode: true
|
||||
});
|
|
@ -14,6 +14,13 @@ export default Ember.Component.extend({
|
|||
}
|
||||
},
|
||||
|
||||
@observes('mode')
|
||||
modeChanged() {
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setMode("ace/mode/" + this.get('mode'));
|
||||
}
|
||||
},
|
||||
|
||||
_destroyEditor: function() {
|
||||
if (this._editor) {
|
||||
this._editor.destroy();
|
||||
|
@ -41,6 +48,7 @@ export default Ember.Component.extend({
|
|||
|
||||
editor.setTheme("ace/theme/chrome");
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setOptions({fontSize: "14px"});
|
||||
editor.getSession().setMode("ace/mode/" + this.get('mode'));
|
||||
editor.on('change', () => {
|
||||
this._skipContentChangeEvent = true;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {default as loadScript, loadCSS } from 'discourse/lib/load-script';
|
||||
|
||||
/**
|
||||
An input field for a color.
|
||||
|
||||
|
@ -6,19 +8,36 @@
|
|||
@params valid is a boolean indicating if the input field is a valid color.
|
||||
**/
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['color-picker'],
|
||||
hexValueChanged: function() {
|
||||
var hex = this.get('hexValue');
|
||||
let $text = this.$('input.hex-input');
|
||||
|
||||
if (this.get('valid')) {
|
||||
this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||
$text.attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||
|
||||
if (this.get('pickerLoaded')) {
|
||||
this.$('.picker').spectrum({color: "#" + this.get('hexValue')});
|
||||
}
|
||||
} else {
|
||||
this.$('input').attr('style', '');
|
||||
$text.attr('style', '');
|
||||
}
|
||||
}.observes('hexValue', 'brightnessValue', 'valid'),
|
||||
|
||||
_triggerHexChanged: function() {
|
||||
var self = this;
|
||||
Em.run.schedule('afterRender', function() {
|
||||
self.hexValueChanged();
|
||||
didInsertElement() {
|
||||
loadScript('/javascripts/spectrum.js').then(()=>{
|
||||
loadCSS('/javascripts/spectrum.css').then(()=>{
|
||||
Em.run.schedule('afterRender', ()=>{
|
||||
this.$('.picker').spectrum({color: "#" + this.get('hexValue')})
|
||||
.on("change.spectrum", (me, color)=>{
|
||||
this.set('hexValue', color.toHexString().replace("#",""));
|
||||
});
|
||||
this.set('pickerLoaded', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
}.on('didInsertElement')
|
||||
Em.run.schedule('afterRender', ()=>{
|
||||
this.hexValueChanged();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { getOwner } from 'discourse-common/lib/get-owner';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
router: function() {
|
||||
return getOwner(this).lookup('router:main');
|
||||
}.property(),
|
||||
|
||||
active: function() {
|
||||
const id = this.get('customization.id');
|
||||
return this.get('router.url').indexOf(`/customize/css_html/${id}/css`) !== -1;
|
||||
}.property('router.url', 'customization.id')
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import {default as computed, observes} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
init(){
|
||||
this._super();
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
classNames: ['inline-edit'],
|
||||
|
||||
@observes("checked")
|
||||
checkedChanged() {
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
@computed("labelKey")
|
||||
label(key) {
|
||||
return I18n.t(key);
|
||||
},
|
||||
|
||||
@computed("checked", "checkedInternal")
|
||||
changed(checked, checkedInternal) {
|
||||
return (!!checked) !== (!!checkedInternal);
|
||||
},
|
||||
|
||||
actions: {
|
||||
cancelled(){
|
||||
this.set("checkedInternal", this.get("checked"));
|
||||
},
|
||||
|
||||
finished(){
|
||||
this.set("checked", this.get("checkedInternal"));
|
||||
this.sendAction();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed("model.colors","onlyOverridden")
|
||||
colors(allColors, onlyOverridden) {
|
||||
if (onlyOverridden) {
|
||||
return allColors.filter(color => color.get("overridden"));
|
||||
} else {
|
||||
return allColors;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
revert: function(color) {
|
||||
color.revert();
|
||||
},
|
||||
|
||||
undo: function(color) {
|
||||
color.undo();
|
||||
},
|
||||
|
||||
copy() {
|
||||
var newColorScheme = Em.copy(this.get('model'), true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + this.get('model.name'));
|
||||
newColorScheme.save().then(()=>{
|
||||
this.get('allColors').pushObject(newColorScheme);
|
||||
this.replaceRoute('adminCustomize.colors.show', newColorScheme);
|
||||
});
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.get('model').save();
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
|
||||
return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
this.get('model').destroy().then(()=>{
|
||||
this.get('allColors').removeObject(this.get('model'));
|
||||
this.replaceRoute('adminCustomize.colors');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,10 +1,14 @@
|
|||
export default Ember.Controller.extend({
|
||||
onlyOverridden: false,
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
baseColorScheme: function() {
|
||||
return this.get('model').findBy('is_base', true);
|
||||
}.property('model.@each.id'),
|
||||
|
||||
baseColorSchemes: function() {
|
||||
return this.get('model').filterBy('is_base', true);
|
||||
}.property('model.@each.id'),
|
||||
|
||||
baseColors: function() {
|
||||
var baseColorsHash = Em.Object.create({});
|
||||
_.each(this.get('baseColorScheme.colors'), function(color){
|
||||
|
@ -13,99 +17,25 @@ export default Ember.Controller.extend({
|
|||
return baseColorsHash;
|
||||
}.property('baseColorScheme'),
|
||||
|
||||
removeSelected() {
|
||||
this.get('model').removeObject(this.get('selectedItem'));
|
||||
this.set('selectedItem', null);
|
||||
},
|
||||
|
||||
filterContent: function() {
|
||||
if (!this.get('selectedItem')) { return; }
|
||||
|
||||
if (!this.get('onlyOverridden')) {
|
||||
this.set('colors', this.get('selectedItem.colors'));
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
_.each(this.get('selectedItem.colors'), function(color){
|
||||
if (color.get('overridden')) matches.pushObject(color);
|
||||
});
|
||||
|
||||
this.set('colors', matches);
|
||||
}.observes('onlyOverridden'),
|
||||
|
||||
updateEnabled: function() {
|
||||
var selectedItem = this.get('selectedItem');
|
||||
if (selectedItem.get('enabled')) {
|
||||
this.get('model').forEach(function(c) {
|
||||
if (c !== selectedItem) {
|
||||
c.set('enabled', false);
|
||||
c.startTrackingChanges();
|
||||
c.notifyPropertyChange('description');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectColorScheme: function(colorScheme) {
|
||||
if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); }
|
||||
this.set('selectedItem', colorScheme);
|
||||
this.set('colors', colorScheme.get('colors'));
|
||||
colorScheme.set('savingStatus', null);
|
||||
colorScheme.set('selected', true);
|
||||
this.filterContent();
|
||||
|
||||
newColorSchemeWithBase(baseKey) {
|
||||
const base = this.get('baseColorSchemes').findBy('base_scheme_id', baseKey);
|
||||
const newColorScheme = Em.copy(base, true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||
newColorScheme.set('base_scheme_id', base.get('base_scheme_id'));
|
||||
newColorScheme.save().then(()=>{
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
newColorScheme.set('savingStatus', null);
|
||||
this.replaceRoute('adminCustomize.colors.show', newColorScheme);
|
||||
});
|
||||
},
|
||||
|
||||
newColorScheme() {
|
||||
const newColorScheme = Em.copy(this.get('baseColorScheme'), true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
this.send('selectColorScheme', newColorScheme);
|
||||
this.set('onlyOverridden', false);
|
||||
showModal('admin-color-scheme-select-base', { model: this.get('baseColorSchemes'), admin: true});
|
||||
},
|
||||
|
||||
revert: function(color) {
|
||||
color.revert();
|
||||
},
|
||||
|
||||
undo: function(color) {
|
||||
color.undo();
|
||||
},
|
||||
|
||||
toggleEnabled: function() {
|
||||
var selectedItem = this.get('selectedItem');
|
||||
selectedItem.toggleProperty('enabled');
|
||||
selectedItem.save({enabledOnly: true});
|
||||
this.updateEnabled();
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.get('selectedItem').save();
|
||||
this.updateEnabled();
|
||||
},
|
||||
|
||||
copy(colorScheme) {
|
||||
var newColorScheme = Em.copy(colorScheme, true);
|
||||
newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + colorScheme.get('name'));
|
||||
this.get('model').pushObject(newColorScheme);
|
||||
this.send('selectColorScheme', newColorScheme);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
var self = this,
|
||||
item = self.get('selectedItem');
|
||||
|
||||
return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||
if (result) {
|
||||
if (item.get('newRecord')) {
|
||||
self.removeSelected();
|
||||
} else {
|
||||
item.destroy().then(function(){ self.removeSelected(); });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import { url } from 'discourse/lib/computed';
|
||||
|
||||
const sections = ['css', 'header', 'top', 'footer', 'head-tag', 'body-tag',
|
||||
'mobile-css', 'mobile-header', 'mobile-top', 'mobile-footer',
|
||||
'embedded-css'];
|
||||
|
||||
const activeSections = {};
|
||||
sections.forEach(function(s) {
|
||||
activeSections[Ember.String.camelize(s) + "Active"] = Ember.computed.equal('section', s);
|
||||
});
|
||||
|
||||
|
||||
export default Ember.Controller.extend(activeSections, {
|
||||
maximized: false,
|
||||
section: null,
|
||||
|
||||
previewUrl: url("model.key", "/?preview-style=%@"),
|
||||
downloadUrl: url('model.id', '/admin/site_customizations/%@'),
|
||||
|
||||
mobile: function() {
|
||||
return this.get('section').indexOf('mobile-') === 0;
|
||||
}.property('section'),
|
||||
|
||||
maximizeIcon: function() {
|
||||
return this.get('maximized') ? 'compress' : 'expand';
|
||||
}.property('maximized'),
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save');
|
||||
}.property('model.isSaving'),
|
||||
|
||||
saveDisabled: function() {
|
||||
return !this.get('model.changed') || this.get('model.isSaving');
|
||||
}.property('model.changed', 'model.isSaving'),
|
||||
|
||||
adminCustomizeCssHtml: Ember.inject.controller(),
|
||||
|
||||
undoPreviewUrl: url('/?preview-style='),
|
||||
defaultStyleUrl: url('/?preview-style=default'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('model').saveChanges();
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
const model = this.get('model');
|
||||
model.destroyRecord().then(() => {
|
||||
this.get('adminCustomizeCssHtml').get('model').removeObject(model);
|
||||
this.transitionToRoute('adminCustomizeCssHtml');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleMaximize: function() {
|
||||
this.toggleProperty('maximized');
|
||||
},
|
||||
|
||||
toggleMobile: function() {
|
||||
const section = this.get('section');
|
||||
|
||||
// Try to send to the same tab as before
|
||||
let dest;
|
||||
if (this.get('mobile')) {
|
||||
dest = section.replace('mobile-', '');
|
||||
if (sections.indexOf(dest) === -1) { dest = 'css'; }
|
||||
} else {
|
||||
dest = 'mobile-' + section;
|
||||
if (sections.indexOf(dest) === -1) { dest = 'mobile-css'; }
|
||||
}
|
||||
this.replaceRoute('adminCustomizeCssHtml.show', this.get('model.id'), dest);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,106 @@
|
|||
import { url } from 'discourse/lib/computed';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
maximized: false,
|
||||
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')}
|
||||
],
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
@computed("currentTarget")
|
||||
currentTargetName(target) {
|
||||
switch(parseInt(target)) {
|
||||
case 0: return "common";
|
||||
case 1: return "desktop";
|
||||
case 2: return "mobile";
|
||||
}
|
||||
},
|
||||
|
||||
@computed("fieldName")
|
||||
activeSectionMode(fieldName) {
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "css" : "html";
|
||||
},
|
||||
|
||||
@computed("fieldName", "currentTargetName", "model")
|
||||
activeSection: {
|
||||
get(fieldName, target, model) {
|
||||
return model.getField(target, fieldName);
|
||||
},
|
||||
set(value, fieldName, target, model) {
|
||||
model.setField(target, fieldName, value);
|
||||
return value;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@computed("currentTarget")
|
||||
fields(target) {
|
||||
let fields = [
|
||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
||||
];
|
||||
|
||||
if (parseInt(target) === 0) {
|
||||
fields.push("embedded_scss");
|
||||
}
|
||||
|
||||
return fields.map(name=>{
|
||||
let hash = {
|
||||
key: (`admin.customize.theme.${name}.text`),
|
||||
name: name
|
||||
};
|
||||
|
||||
if (name.indexOf("_tag") > 0) {
|
||||
hash.icon = "file-text-o";
|
||||
}
|
||||
|
||||
hash.title = I18n.t(`admin.customize.theme.${name}.title`);
|
||||
|
||||
return hash;
|
||||
});
|
||||
},
|
||||
|
||||
previewUrl: url('model.key', '/?preview-style=%@'),
|
||||
|
||||
maximizeIcon: function() {
|
||||
return this.get('maximized') ? 'compress' : 'expand';
|
||||
}.property('maximized'),
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save');
|
||||
}.property('model.isSaving'),
|
||||
|
||||
saveDisabled: function() {
|
||||
return !this.get('model.changed') || this.get('model.isSaving');
|
||||
}.property('model.changed', 'model.isSaving'),
|
||||
|
||||
undoPreviewUrl: url('/?preview-style='),
|
||||
defaultStyleUrl: url('/?preview-style=default'),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.get('model').saveChanges("theme_fields");
|
||||
},
|
||||
|
||||
toggleMaximize: function() {
|
||||
this.toggleProperty('maximized');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,163 @@
|
|||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
@computed("model.theme_fields.@each")
|
||||
hasEditedFields(fields) {
|
||||
return fields.any(f=>!Em.isBlank(f.value));
|
||||
},
|
||||
|
||||
@computed('model.theme_fields.@each')
|
||||
editedDescriptions(fields) {
|
||||
let descriptions = [];
|
||||
let description = target => {
|
||||
let current = fields.filter(field => field.target === target && !Em.isBlank(field.value));
|
||||
if (current.length > 0) {
|
||||
let text = I18n.t('admin.customize.theme.'+target);
|
||||
let localized = current.map(f=>I18n.t('admin.customize.theme.'+f.name + '.text'));
|
||||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
['common','desktop','mobile'].forEach(target=> {
|
||||
descriptions.push(description(target));
|
||||
});
|
||||
return descriptions.reject(d=>Em.isBlank(d));
|
||||
},
|
||||
|
||||
@computed("colorSchemeId", "model.color_scheme_id")
|
||||
colorSchemeChanged(colorSchemeId, existingId) {
|
||||
colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId);
|
||||
return colorSchemeId !== existingId;
|
||||
},
|
||||
|
||||
@computed("availableChildThemes", "model.childThemes.@each", "model", "allowChildThemes")
|
||||
selectableChildThemes(available, childThemes, model, allowChildThemes) {
|
||||
if (!allowChildThemes && (!childThemes || childThemes.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let themes = [];
|
||||
available.forEach(t=> {
|
||||
if (!childThemes || (childThemes.indexOf(t) === -1)) {
|
||||
themes.push(t);
|
||||
};
|
||||
});
|
||||
return themes.length === 0 ? null : themes;
|
||||
},
|
||||
|
||||
showSchemes: Em.computed.or("model.default", "model.user_selectable"),
|
||||
|
||||
@computed("allThemes", "allThemes.length", "model")
|
||||
availableChildThemes(allThemes, count) {
|
||||
if (count === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let excludeIds = [this.get("model.id")];
|
||||
|
||||
let themes = [];
|
||||
allThemes.forEach(theme => {
|
||||
if (excludeIds.indexOf(theme.get("id")) === -1) {
|
||||
themes.push(theme);
|
||||
}
|
||||
});
|
||||
|
||||
return themes;
|
||||
},
|
||||
|
||||
downloadUrl: url('model.id', '/admin/themes/%@'),
|
||||
|
||||
actions: {
|
||||
|
||||
updateToLatest() {
|
||||
this.set("updatingRemote", true);
|
||||
this.get("model").updateToLatest().finally(()=>{
|
||||
this.set("updatingRemote", false);
|
||||
});
|
||||
},
|
||||
|
||||
checkForThemeUpdates() {
|
||||
this.set("updatingRemote", true);
|
||||
this.get("model").checkForUpdates().finally(()=>{
|
||||
this.set("updatingRemote", false);
|
||||
});
|
||||
},
|
||||
|
||||
cancelChangeScheme() {
|
||||
this.set("colorSchemeId", this.get("model.color_scheme_id"));
|
||||
},
|
||||
changeScheme(){
|
||||
let schemeId = this.get("colorSchemeId");
|
||||
this.set("model.color_scheme_id", schemeId === null ? null : parseInt(schemeId));
|
||||
this.get("model").saveChanges("color_scheme_id");
|
||||
},
|
||||
startEditingName() {
|
||||
this.set("oldName", this.get("model.name"));
|
||||
this.set("editingName", true);
|
||||
},
|
||||
cancelEditingName() {
|
||||
this.set("model.name", this.get("oldName"));
|
||||
this.set("editingName", false);
|
||||
},
|
||||
finishedEditingName() {
|
||||
this.get("model").saveChanges("name");
|
||||
this.set("editingName", false);
|
||||
},
|
||||
|
||||
editTheme() {
|
||||
let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', {model: this.get('model')});
|
||||
|
||||
if (this.get("model.remote_theme")) {
|
||||
bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => {
|
||||
if (result) {
|
||||
edit();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
edit();
|
||||
}
|
||||
},
|
||||
|
||||
applyDefault() {
|
||||
const model = this.get("model");
|
||||
model.saveChanges("default").then(()=>{
|
||||
if (model.get("default")) {
|
||||
this.get("allThemes").forEach(theme=>{
|
||||
if (theme !== model && theme.get('default')) {
|
||||
theme.set("default", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
applyUserSelectable() {
|
||||
this.get("model").saveChanges("user_selectable");
|
||||
},
|
||||
|
||||
addChildTheme() {
|
||||
let themeId = parseInt(this.get("selectedChildThemeId"));
|
||||
let theme = this.get("allThemes").findBy("id", themeId);
|
||||
this.get("model").addChildTheme(theme);
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
this.get("model").removeChildTheme(theme);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => {
|
||||
if (result) {
|
||||
const model = this.get('model');
|
||||
model.destroyRecord().then(() => {
|
||||
this.get('allThemes').removeObject(model);
|
||||
this.transitionToRoute('adminCustomizeThemes');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
adminCustomizeColors: Ember.inject.controller(),
|
||||
|
||||
actions: {
|
||||
selectBase() {
|
||||
this.get('adminCustomizeColors')
|
||||
.send('newColorSchemeWithBase', this.get('selectedBaseThemeId'));
|
||||
this.send('closeModal');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
// import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
local: Ember.computed.equal('selection', 'local'),
|
||||
remote: Ember.computed.equal('selection', 'remote'),
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
|
||||
actions: {
|
||||
importTheme() {
|
||||
|
||||
let options = {
|
||||
type: 'POST'
|
||||
};
|
||||
|
||||
if (this.get('local')) {
|
||||
options.processData = false;
|
||||
options.contentType = false;
|
||||
options.data = new FormData();
|
||||
options.data.append('theme', $('#file-input')[0].files[0]);
|
||||
} else {
|
||||
options.data = {remote: this.get('uploadUrl')};
|
||||
}
|
||||
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
this.send('closeModal');
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loadDiff() {
|
||||
this.set('loading', true);
|
||||
ajax('/admin/logs/staff_action_logs/' + this.get('model.id') + '/diff')
|
||||
.then(diff=>{
|
||||
this.set('loading', false);
|
||||
this.set('diff', diff.side_by_side);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
previousSelected: Ember.computed.equal('selectedTab', 'previous'),
|
||||
newSelected: Ember.computed.equal('selectedTab', 'new'),
|
||||
|
||||
onShow: function() {
|
||||
this.send("selectNew");
|
||||
},
|
||||
|
||||
actions: {
|
||||
selectNew: function() {
|
||||
this.set('selectedTab', 'new');
|
||||
},
|
||||
|
||||
selectPrevious: function() {
|
||||
this.set('selectedTab', 'previous');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
import ChangeSiteCustomizationDetailsController from "admin/controllers/modals/change-site-customization-details";
|
||||
|
||||
export default ChangeSiteCustomizationDetailsController.extend({
|
||||
onShow() {
|
||||
this.send("selectPrevious");
|
||||
}
|
||||
});
|
|
@ -9,18 +9,17 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
|||
},
|
||||
|
||||
description: function() {
|
||||
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||
return "" + this.name;
|
||||
}.property(),
|
||||
|
||||
startTrackingChanges: function() {
|
||||
this.set('originals', {
|
||||
name: this.get('name'),
|
||||
enabled: this.get('enabled')
|
||||
name: this.get('name')
|
||||
});
|
||||
},
|
||||
|
||||
copy: function() {
|
||||
var newScheme = ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()});
|
||||
var newScheme = ColorScheme.create({name: this.get('name'), can_edit: true, colors: Em.A()});
|
||||
_.each(this.get('colors'), function(c){
|
||||
newScheme.colors.pushObject(ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex'), default_hex: c.get('default_hex')}));
|
||||
});
|
||||
|
@ -29,19 +28,15 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
|||
|
||||
changed: function() {
|
||||
if (!this.originals) return false;
|
||||
if (this.originals['name'] !== this.get('name') || this.originals['enabled'] !== this.get('enabled')) return true;
|
||||
if (this.originals['name'] !== this.get('name')) return true;
|
||||
if (_.any(this.get('colors'), function(c){ return c.get('changed'); })) return true;
|
||||
return false;
|
||||
}.property('name', 'enabled', 'colors.@each.changed', 'saving'),
|
||||
}.property('name', 'colors.@each.changed', 'saving'),
|
||||
|
||||
disableSave: function() {
|
||||
return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); });
|
||||
}.property('changed'),
|
||||
|
||||
disableEnable: function() {
|
||||
return !this.get('id') || this.get('saving');
|
||||
}.property('id', 'saving'),
|
||||
|
||||
newRecord: function() {
|
||||
return (!this.get('id'));
|
||||
}.property('id'),
|
||||
|
@ -53,11 +48,11 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
|||
this.set('savingStatus', I18n.t('saving'));
|
||||
this.set('saving',true);
|
||||
|
||||
var data = { enabled: this.enabled };
|
||||
var data = {};
|
||||
|
||||
if (!opts || !opts.enabledOnly) {
|
||||
data.name = this.name;
|
||||
|
||||
data.base_scheme_id = this.get('base_scheme_id');
|
||||
data.colors = [];
|
||||
_.each(this.get('colors'), function(c) {
|
||||
if (!self.id || c.get('changed')) {
|
||||
|
@ -78,8 +73,6 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
|||
_.each(self.get('colors'), function(c) {
|
||||
c.startTrackingChanges();
|
||||
});
|
||||
} else {
|
||||
self.set('originals.enabled', data.enabled);
|
||||
}
|
||||
self.set('savingStatus', I18n.t('saved'));
|
||||
self.set('saving', false);
|
||||
|
@ -96,30 +89,23 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
|||
});
|
||||
|
||||
var ColorSchemes = Ember.ArrayProxy.extend({
|
||||
selectedItemChanged: function() {
|
||||
var selected = this.get('selectedItem');
|
||||
_.each(this.get('content'),function(i) {
|
||||
return i.set('selected', selected === i);
|
||||
});
|
||||
}.observes('selectedItem')
|
||||
});
|
||||
|
||||
ColorScheme.reopenClass({
|
||||
findAll: function() {
|
||||
var colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
||||
ajax('/admin/color_schemes').then(function(all) {
|
||||
return ajax('/admin/color_schemes').then(function(all) {
|
||||
_.each(all, function(colorScheme){
|
||||
colorSchemes.pushObject(ColorScheme.create({
|
||||
id: colorScheme.id,
|
||||
name: colorScheme.name,
|
||||
enabled: colorScheme.enabled,
|
||||
is_base: colorScheme.is_base,
|
||||
base_scheme_id: colorScheme.base_scheme_id,
|
||||
colors: colorScheme.colors.map(function(c) { return ColorSchemeColor.create({name: c.name, hex: c.hex, default_hex: c.default_hex}); })
|
||||
}));
|
||||
});
|
||||
colorSchemes.set('loading', false);
|
||||
return colorSchemes;
|
||||
});
|
||||
return colorSchemes;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
|
||||
const trackedProperties = [
|
||||
'enabled', 'name', 'stylesheet', 'header', 'top', 'footer', 'mobile_stylesheet',
|
||||
'mobile_header', 'mobile_top', 'mobile_footer', 'head_tag', 'body_tag', 'embedded_css'
|
||||
];
|
||||
|
||||
function changed() {
|
||||
const originals = this.get('originals');
|
||||
if (!originals) { return false; }
|
||||
return _.some(trackedProperties, (p) => originals[p] !== this.get(p));
|
||||
}
|
||||
|
||||
const SiteCustomization = RestModel.extend({
|
||||
description: function() {
|
||||
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||
}.property('selected', 'name', 'enabled'),
|
||||
|
||||
changed: changed.property.apply(changed, trackedProperties.concat('originals')),
|
||||
|
||||
startTrackingChanges: function() {
|
||||
this.set('originals', this.getProperties(trackedProperties));
|
||||
}.on('init'),
|
||||
|
||||
saveChanges() {
|
||||
return this.save(this.getProperties(trackedProperties)).then(() => this.startTrackingChanges());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default SiteCustomization;
|
|
@ -39,7 +39,7 @@ const StaffActionLog = Discourse.Model.extend({
|
|||
}.property('action_name'),
|
||||
|
||||
useCustomModalForDetails: function() {
|
||||
return _.contains(['change_site_customization', 'delete_site_customization'], this.get('action_name'));
|
||||
return _.contains(['change_theme', 'delete_theme'], this.get('action_name'));
|
||||
}.property('action_name')
|
||||
});
|
||||
|
||||
|
|
94
app/assets/javascripts/admin/models/theme.js.es6
Normal file
94
app/assets/javascripts/admin/models/theme.js.es6
Normal file
|
@ -0,0 +1,94 @@
|
|||
import RestModel from 'discourse/models/rest';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
@computed('theme_fields')
|
||||
themeFields(fields) {
|
||||
|
||||
if (!fields) {
|
||||
this.set('theme_fields', []);
|
||||
return {};
|
||||
}
|
||||
|
||||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
hash[field.target + " " + field.name] = field;
|
||||
});
|
||||
}
|
||||
return hash;
|
||||
},
|
||||
|
||||
getField(target, name) {
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
return field ? field.value : "";
|
||||
},
|
||||
|
||||
setField(target, name, value) {
|
||||
this.set("changed", true);
|
||||
|
||||
let themeFields = this.get("themeFields");
|
||||
let key = target + " " + name;
|
||||
let field = themeFields[key];
|
||||
if (!field) {
|
||||
field = {name, target, value};
|
||||
this.theme_fields.push(field);
|
||||
themeFields[key] = field;
|
||||
} else {
|
||||
field.value = value;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("childThemes.@each")
|
||||
child_theme_ids(childThemes) {
|
||||
if (childThemes) {
|
||||
return childThemes.map(theme => Ember.get(theme, "id"));
|
||||
}
|
||||
},
|
||||
|
||||
removeChildTheme(theme) {
|
||||
const childThemes = this.get("childThemes");
|
||||
childThemes.removeObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
addChildTheme(theme){
|
||||
let childThemes = this.get("childThemes");
|
||||
childThemes.removeObject(theme);
|
||||
childThemes.pushObject(theme);
|
||||
return this.saveChanges("child_theme_ids");
|
||||
},
|
||||
|
||||
@computed('name', 'default')
|
||||
description: function(name, isDefault) {
|
||||
if (isDefault) {
|
||||
return I18n.t('admin.customize.theme.default_name', {name: name});
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
},
|
||||
|
||||
checkForUpdates() {
|
||||
return this.save({remote_check: true})
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
updateToLatest() {
|
||||
return this.save({remote_update: true})
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
changed: false,
|
||||
|
||||
saveChanges() {
|
||||
const hash = this.getProperties.apply(this, arguments);
|
||||
return this.save(hash)
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default Theme;
|
|
@ -0,0 +1,18 @@
|
|||
export default Ember.Route.extend({
|
||||
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomize.colors');
|
||||
const model = all.findBy('id', parseInt(params.scheme_id));
|
||||
return model ? model : this.replaceWith('adminCustomize.colors.index');
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
return {scheme_id: model.get('id')};
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set('model', model);
|
||||
controller.set('allColors', this.modelFor('adminCustomize.colors'));
|
||||
}
|
||||
});
|
||||
|
|
@ -6,9 +6,7 @@ export default Ember.Route.extend({
|
|||
return ColorScheme.findAll();
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super();
|
||||
this.controllerFor('adminCustomizeColors').set('selectedItem', null);
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeCssHtml');
|
||||
const model = all.findBy('id', parseInt(params.site_customization_id));
|
||||
return model ? { model, section: params.section } : this.replaceWith('adminCustomizeCssHtml.index');
|
||||
},
|
||||
|
||||
setupController(controller, hash) {
|
||||
controller.setProperties(hash);
|
||||
}
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import showModal from 'discourse/lib/show-modal';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('site-customization');
|
||||
},
|
||||
|
||||
actions: {
|
||||
importModal() {
|
||||
showModal('upload-customization');
|
||||
},
|
||||
|
||||
newCustomization(obj) {
|
||||
obj = obj || {name: I18n.t("admin.customize.new_style")};
|
||||
const item = this.store.createRecord('site-customization');
|
||||
|
||||
const all = this.modelFor('adminCustomizeCssHtml');
|
||||
const self = this;
|
||||
item.save(obj).then(function() {
|
||||
all.pushObject(item);
|
||||
self.transitionTo('adminCustomizeCssHtml.show', item.get('id'), 'css');
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
export default Ember.Route.extend({
|
||||
beforeModel() {
|
||||
this.transitionTo('adminCustomize.colors');
|
||||
this.transitionTo('adminCustomizeThemes');
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
export default Ember.Route.extend({
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
const model = all.findBy('id', parseInt(params.theme_id));
|
||||
return model ? { model, target: params.target, field_name: params.field_name} : this.replaceWith('adminCustomizeThemes.index');
|
||||
},
|
||||
|
||||
serialize(wrapper) {
|
||||
return {
|
||||
model: wrapper.model,
|
||||
target: wrapper.target || "common",
|
||||
field_name: wrapper.field_name || "scss",
|
||||
theme_id: wrapper.model.get("id")
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
setupController(controller, wrapper) {
|
||||
controller.set("model", wrapper.model);
|
||||
controller.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
this.controllerFor("adminCustomizeThemes").set("editingTheme", true);
|
||||
},
|
||||
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
export default Ember.Route.extend({
|
||||
|
||||
serialize(model) {
|
||||
return {theme_id: model.get('id')};
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
const model = all.findBy('id', parseInt(params.theme_id));
|
||||
return model ? model : this.replaceWith('adminCustomizeTheme.index');
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
const parentController = this.controllerFor("adminCustomizeThemes");
|
||||
parentController.set("editingTheme", false);
|
||||
controller.set("allThemes", parentController.get("model"));
|
||||
controller.set("colorSchemes", parentController.get("model.extras.color_schemes"));
|
||||
controller.set("colorSchemeId", model.get("color_scheme_id"));
|
||||
}
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import showModal from 'discourse/lib/show-modal';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return this.store.findAll('theme');
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super(controller, model);
|
||||
// TODO ColorScheme to model
|
||||
controller.set("editingTheme", false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
importModal() {
|
||||
showModal('admin-import-theme', {admin: true});
|
||||
},
|
||||
|
||||
addTheme(theme) {
|
||||
const all = this.modelFor('adminCustomizeThemes');
|
||||
all.pushObject(theme);
|
||||
this.transitionTo('adminCustomizeThemes.show', theme.get('id'));
|
||||
},
|
||||
|
||||
|
||||
newTheme(obj) {
|
||||
obj = obj || {name: I18n.t("admin.customize.new_style")};
|
||||
const item = this.store.createRecord('theme');
|
||||
|
||||
item.save(obj).then(() => {
|
||||
this.send('addTheme', item);
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -13,14 +13,9 @@ export default Discourse.Route.extend({
|
|||
},
|
||||
|
||||
showCustomDetailsModal(model) {
|
||||
const modalName = (model.action_name + '_details').replace(/\_/g, "-");
|
||||
|
||||
showModal(modalName, {
|
||||
model,
|
||||
admin: true,
|
||||
templateName: 'site-customization-change'
|
||||
});
|
||||
this.controllerFor('modal').set('modalClass', 'tabbed-modal log-details-modal');
|
||||
let modal = showModal('admin-theme-change', { model, admin: true});
|
||||
this.controllerFor('modal').set('modalClass', 'history-modal');
|
||||
modal.loadDiff();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,10 +15,14 @@ export default function() {
|
|||
});
|
||||
|
||||
this.route('adminCustomize', { path: '/customize', resetNamespace: true } ,function() {
|
||||
this.route('colors');
|
||||
|
||||
this.route('adminCustomizeCssHtml', { path: 'css_html', resetNamespace: true }, function() {
|
||||
this.route('show', {path: '/:site_customization_id/:section'});
|
||||
this.route('colors', function() {
|
||||
this.route('show', {path: '/:scheme_id'});
|
||||
});
|
||||
|
||||
this.route('adminCustomizeThemes', { path: 'themes', resetNamespace: true }, function() {
|
||||
this.route('show', {path: '/:theme_id'});
|
||||
this.route('edit', {path: '/:theme_id/:target/:field_name/edit'});
|
||||
});
|
||||
|
||||
this.route('adminSiteText', { path: '/site_texts', resetNamespace: true }, function() {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<li>
|
||||
<a href="/admin/customize/css_html/{{customization.id}}/css" class="{{if active 'active'}}">
|
||||
{{customization.description}}
|
||||
</a>
|
||||
</li>
|
|
@ -0,0 +1,8 @@
|
|||
<label class='checkbox-label'>
|
||||
{{input type="checkbox" disabled=disabled checked=checkedInternal}}
|
||||
{{label}}
|
||||
</label>
|
||||
{{#if changed}}
|
||||
{{d-button action="finished" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelled" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
|
@ -0,0 +1 @@
|
|||
<p class="about">{{i18n 'admin.customize.colors.about'}}</p>
|
|
@ -0,0 +1,53 @@
|
|||
<div class="color-scheme show-current-style">
|
||||
<div class="admin-container">
|
||||
<h1>{{text-field class="style-name" value=model.name}}</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button>
|
||||
<button {{action "copy" model}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button>
|
||||
<button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button>
|
||||
<span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
<table class="table colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="hex">{{i18n 'admin.customize.color'}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each colors as |c|}}
|
||||
<tr class="{{if c.changed 'changed'}} {{if c.valid 'valid' 'invalid'}}">
|
||||
<td class="name" title={{c.name}}>
|
||||
<b>{{c.translatedName}}</b>
|
||||
<br/>
|
||||
<span class="description">{{c.description}}</span>
|
||||
</td>
|
||||
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
|
||||
<td class="actions">
|
||||
<button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button>
|
||||
<button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -3,76 +3,16 @@
|
|||
<ul>
|
||||
{{#each model as |scheme|}}
|
||||
{{#unless scheme.is_base}}
|
||||
<li><a {{action "selectColorScheme" scheme}} class="{{if scheme.selected 'active'}}">{{scheme.description}}</a></li>
|
||||
|
||||
<li>
|
||||
{{#link-to 'adminCustomize.colors.show' scheme replace=true}}{{scheme.description}}{{/link-to}}
|
||||
</li>
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
<button {{action "newColorScheme"}} class='btn'><i class="fa fa-plus"></i>{{i18n 'admin.customize.new'}}</button>
|
||||
<button {{action "newColorScheme"}} class='btn'>{{fa-icon 'plus'}}{{i18n 'admin.customize.new'}}</button>
|
||||
</div>
|
||||
|
||||
{{#if selectedItem}}
|
||||
<div class="current-style color-scheme">
|
||||
<div class="admin-container">
|
||||
<h1>{{text-field class="style-name" value=selectedItem.name}}</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button {{action "save"}} disabled={{selectedItem.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button>
|
||||
<button {{action "toggleEnabled"}} disabled={{selectedItem.disableEnable}} class="btn">
|
||||
{{#if selectedItem.enabled}}
|
||||
{{i18n 'disable'}}
|
||||
{{else}}
|
||||
{{i18n 'enable'}}
|
||||
{{/if}}
|
||||
</button>
|
||||
<button {{action "copy" selectedItem}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button>
|
||||
<button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button>
|
||||
<span class="saving {{unless selectedItem.savingStatus 'hidden'}}">{{selectedItem.savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if colors.length}}
|
||||
<table class="table colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="hex">{{i18n 'admin.customize.color'}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each colors as |c|}}
|
||||
<tr class="{{if c.changed 'changed'}} {{if c.valid 'valid' 'invalid'}}">
|
||||
<td class="name" title={{c.name}}>
|
||||
<b>{{c.translatedName}}</b>
|
||||
<br/>
|
||||
<span class="description">{{c.description}}</span>
|
||||
</td>
|
||||
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
|
||||
<td class="actions">
|
||||
<button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button>
|
||||
<button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="about">{{i18n 'admin.customize.colors.about'}}</p>
|
||||
{{/if}}
|
||||
{{outlet}}
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
<div class="current-style {{if maximized 'maximized'}}">
|
||||
<div class='wrapper'>
|
||||
{{text-field class="style-name" value=model.name}}
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<ul class="nav nav-pills">
|
||||
{{#if mobile}}
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-css' replace=true}}{{i18n "admin.customize.css"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-header' replace=true}}{{i18n "admin.customize.header"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-top' replace=true}}{{i18n "admin.customize.top"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'mobile-footer' replace=true}}{{i18n "admin.customize.footer"}}{{/link-to}}</li>
|
||||
{{else}}
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'css' replace=true}}{{i18n "admin.customize.css"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'header' replace=true}}{{i18n "admin.customize.header"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'top' replace=true}}{{i18n "admin.customize.top"}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'footer' replace=true}}{{i18n "admin.customize.footer"}}{{/link-to}}</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeCssHtml.show' model.id 'head-tag'}}
|
||||
{{fa-icon "file-text-o"}} {{i18n 'admin.customize.head_tag.text'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeCssHtml.show' model.id 'body-tag'}}
|
||||
{{fa-icon "file-text-o"}} {{i18n 'admin.customize.body_tag.text'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>{{#link-to 'adminCustomizeCssHtml.show' model.id 'embedded-css' replace=true}}{{i18n "admin.customize.embedded_css"}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
<li class='toggle-mobile'>
|
||||
<a class="{{if mobile 'active'}}" {{action "toggleMobile"}}>{{fa-icon "mobile"}}</a>
|
||||
</li>
|
||||
<li class='toggle-maximize'>
|
||||
<a {{action "toggleMaximize"}}>
|
||||
<i class="fa fa-{{maximizeIcon}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admin-container">
|
||||
{{#if cssActive}}{{ace-editor content=model.stylesheet mode="scss"}}{{/if}}
|
||||
{{#if headerActive}}{{ace-editor content=model.header mode="html"}}{{/if}}
|
||||
{{#if topActive}}{{ace-editor content=model.top mode="html"}}{{/if}}
|
||||
{{#if footerActive}}{{ace-editor content=model.footer mode="html"}}{{/if}}
|
||||
{{#if headTagActive}}{{ace-editor content=model.head_tag mode="html"}}{{/if}}
|
||||
{{#if bodyTagActive}}{{ace-editor content=model.body_tag mode="html"}}{{/if}}
|
||||
{{#if embeddedCssActive}}{{ace-editor content=model.embedded_css mode="css"}}{{/if}}
|
||||
{{#if mobileCssActive}}{{ace-editor content=model.mobile_stylesheet mode="scss"}}{{/if}}
|
||||
{{#if mobileHeaderActive}}{{ace-editor content=model.mobile_header mode="html"}}{{/if}}
|
||||
{{#if mobileTopActive}}{{ace-editor content=model.mobile_top mode="html"}}{{/if}}
|
||||
{{#if mobileFooterActive}}{{ace-editor content=model.mobile_footer mode="html"}}{{/if}}
|
||||
</div>
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='status-actions'>
|
||||
<span>{{i18n 'admin.customize.enabled'}} {{input type="checkbox" checked=model.enabled}}</span>
|
||||
{{#unless model.changed}}
|
||||
<a class='preview-link' href={{previewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_preview'}}">{{i18n 'admin.customize.preview'}}</a>
|
||||
|
|
||||
<a href={{undoPreviewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_undo_preview'}}">{{i18n 'admin.customize.undo_preview'}}</a>
|
||||
|
|
||||
<a href={{defaultStyleUrl}} target='_blank' title="{{i18n 'admin.customize.explain_rescue_preview'}}">{{i18n 'admin.customize.rescue_preview'}}</a><br>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class='buttons'>
|
||||
{{#d-button action="save" disabled=saveDisabled class='btn-primary'}}
|
||||
{{saveButtonText}}
|
||||
{{/d-button}}
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.customize.css_html.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each model as |c|}}
|
||||
{{customize-link customization=c}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button label="admin.customize.new" icon="plus" action="newCustomization" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
|
||||
{{outlet}}
|
|
@ -0,0 +1,62 @@
|
|||
<div class="current-style {{if maximized 'maximized'}}">
|
||||
<div class='wrapper'>
|
||||
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
|
||||
|
||||
<ul class='nav nav-pills target'>
|
||||
<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 'desktop' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{fa-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{fa-icon 'mobile'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class='admin-controls'>
|
||||
<ul class='nav nav-pills fields'>
|
||||
{{#each fields as |field|}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id currentTargetName field.name replace=true title=field.title}}
|
||||
{{#if field.icon}}{{fa-icon field.icon}} {{/if}}
|
||||
{{i18n field.key}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
<li class='toggle-maximize'>
|
||||
<a {{action "toggleMaximize"}}>
|
||||
<i class="fa fa-{{maximizeIcon}}"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class='custom-ace-gutter'></div>
|
||||
{{ace-editor content=activeSection mode=activeSectionMode}}
|
||||
</div>
|
||||
|
||||
<div class='admin-footer'>
|
||||
<div class='status-actions'>
|
||||
{{#unless model.changed}}
|
||||
<a class='preview-link' href={{previewUrl}} target='_blank' title="{{i18n 'admin.customize.explain_preview'}}">{{i18n 'admin.customize.preview'}}</a>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div class='buttons'>
|
||||
{{#d-button action="save" disabled=saveDisabled class='btn-primary'}}
|
||||
{{saveButtonText}}
|
||||
{{/d-button}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
112
app/assets/javascripts/admin/templates/customize-themes-show.hbs
Normal file
112
app/assets/javascripts/admin/templates/customize-themes-show.hbs
Normal file
|
@ -0,0 +1,112 @@
|
|||
<div class="show-current-style">
|
||||
<h2>
|
||||
{{#if editingName}}
|
||||
{{text-field value=model.name autofocus="true"}}
|
||||
{{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelEditingName" class="btn-small cancel-edit" icon="times"}}
|
||||
{{else}}
|
||||
{{model.name}} <a {{action "startEditingName"}}>{{fa-icon "pencil"}}</a>
|
||||
{{/if}}
|
||||
</h2>
|
||||
|
||||
{{#if model.remote_theme}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.about_url}}">{{i18n "admin.customize.theme.about_theme"}}</a>
|
||||
</p>
|
||||
{{#if model.remote_theme.license_url}}
|
||||
<p>
|
||||
<a href="{{model.remote_theme.license_url}}">{{i18n "admin.customize.theme.license"}} {{fa-icon "copyright"}}</a>
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
<p>
|
||||
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
|
||||
</p>
|
||||
|
||||
{{#if showSchemes}}
|
||||
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
|
||||
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
|
||||
<p>{{combo-box content=colorSchemes
|
||||
nameProperty="name"
|
||||
value=colorSchemeId
|
||||
valueAttribute="id"}}
|
||||
{{#if colorSchemeChanged}}
|
||||
{{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}}
|
||||
{{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
{{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}}
|
||||
{{/if}}
|
||||
|
||||
<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>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{#d-button action="updateToLatest" icon="download"}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}}
|
||||
{{else}}
|
||||
{{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||
{{#if model.remote_theme}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
{{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}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
{{#if availableChildThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.included_themes"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
<p>
|
||||
<label class='checkbox-label'>
|
||||
{{input type="checkbox" checked=allowChildThemes}}
|
||||
{{i18n "admin.customize.theme.child_themes_check"}}
|
||||
</label>
|
||||
</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{child.name}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/unless}}
|
||||
{{#if selectableChildThemes}}
|
||||
<p>{{combo-box content=selectableChildThemes
|
||||
nameProperty="name"
|
||||
value=selectedChildThemeId
|
||||
valueAttribute="id"}}
|
||||
|
||||
{{#d-button action="addChildTheme" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<a class="btn export" target="_blank" href={{downloadUrl}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
{{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}}
|
||||
</div>
|
24
app/assets/javascripts/admin/templates/customize-themes.hbs
Normal file
24
app/assets/javascripts/admin/templates/customize-themes.hbs
Normal file
|
@ -0,0 +1,24 @@
|
|||
{{#unless editingTheme}}
|
||||
<div class='content-list span6'>
|
||||
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
|
||||
<ul>
|
||||
{{#each model as |theme|}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.show' theme replace=true}}
|
||||
{{theme.name}}
|
||||
{{#if theme.user_selectable}}
|
||||
{{fa-icon "user"}}
|
||||
{{/if}}
|
||||
{{#if theme.default}}
|
||||
{{fa-icon "asterisk"}}
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
{{d-button label="admin.customize.new" icon="plus" action="newTheme" class="btn-primary"}}
|
||||
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{outlet}}
|
|
@ -1,7 +1,7 @@
|
|||
<div class='customize'>
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}}
|
||||
{{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}}
|
||||
{{nav-item route='adminCustomizeCssHtml' label='admin.customize.css_html.title'}}
|
||||
{{nav-item route='adminSiteText' label='admin.site_text.title'}}
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
|
||||
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<div>
|
||||
{{#d-modal-body title="admin.customize.colors.select_base.title"}}
|
||||
{{i18n "admin.customize.colors.select_base.description"}}
|
||||
{{combo-box content=model
|
||||
nameProperty="name"
|
||||
value=selectedBaseThemeId
|
||||
valueAttribute="base_scheme_id"}}
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "selectBase"}}>{{fa-icon 'plus'}}{{i18n 'admin.customize.new'}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,27 @@
|
|||
{{#d-modal-body class='upload-selector' title="admin.customize.theme.import_theme"}}
|
||||
<div class="radios">
|
||||
{{radio-button name="upload" id="local" value="local" selection=selection}}
|
||||
<label class="radio" for="local">{{i18n 'upload_selector.from_my_computer'}}</label>
|
||||
{{#if local}}
|
||||
<div class="inputs">
|
||||
<input type="file" id="file-input" accept='.dcstyle.json'><br>
|
||||
<span class="description">{{i18n 'admin.customize.theme.import_file_tip'}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="radios">
|
||||
{{radio-button name="upload" id="remote" value="remote" selection=selection}}
|
||||
<label class="radio" for="remote">{{i18n 'upload_selector.from_the_web'}}</label>
|
||||
{{#if remote}}
|
||||
<div class="inputs">
|
||||
{{input value=uploadUrl placeholder="https://github.com/discourse/discourse/sample_theme"}}
|
||||
<span class="description">{{i18n 'admin.customize.theme.import_web_tip'}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="importTheme" disabled=loading class='btn btn-primary' icon='upload' label='admin.customize.import'}}
|
||||
<a href {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<div>
|
||||
{{#d-modal-body title="admin.logs.staff_actions.modal_title"}}
|
||||
{{{diff}}}
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,29 +0,0 @@
|
|||
<div>
|
||||
<ul class="nav nav-pills">
|
||||
<li class="{{if newSelected 'active'}}">
|
||||
<a href {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a>
|
||||
</li>
|
||||
<li class="{{if previousSelected 'active'}}">
|
||||
<a href {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{#d-modal-body title="admin.logs.staff_actions.modal_title"}}
|
||||
<div class="modal-tab new-tab {{unless newSelected 'invisible'}}">
|
||||
{{#if model.new_value}}
|
||||
{{site-customization-change-details change=model.new_value}}
|
||||
{{else}}
|
||||
{{i18n 'admin.logs.staff_actions.deleted'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-tab previous-tab {{unless previousSelected 'invisible'}}">
|
||||
{{#if model.previous_value}}
|
||||
{{site-customization-change-details change=model.previous_value}}
|
||||
{{else}}
|
||||
{{i18n 'admin.logs.staff_actions.no_previous'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/d-modal-body}}
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
</div>
|
||||
</div>
|
|
@ -22,17 +22,40 @@ export default Ember.Component.extend(bufferedRender({
|
|||
let selected = this.get('value');
|
||||
if (!Em.isNone(selected)) { selected = selected.toString(); }
|
||||
|
||||
if (this.get('content')) {
|
||||
this.get('content').forEach(o => {
|
||||
let selectedFound = false;
|
||||
let firstVal = undefined;
|
||||
const content = this.get('content');
|
||||
|
||||
if (content) {
|
||||
let first = true;
|
||||
content.forEach(o => {
|
||||
let val = o[this.get('valueAttribute')];
|
||||
if (typeof val === "undefined") { val = o; }
|
||||
if (!Em.isNone(val)) { val = val.toString(); }
|
||||
|
||||
const selectedText = (val === selected) ? "selected" : "";
|
||||
const name = Handlebars.Utils.escapeExpression(Ember.get(o, nameProperty) || o);
|
||||
|
||||
if (val === selected) {
|
||||
selectedFound = true;
|
||||
}
|
||||
if (first) {
|
||||
firstVal = val;
|
||||
first = false;
|
||||
}
|
||||
buffer.push(`<option ${selectedText} value="${val}">${name}</option>`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedFound) {
|
||||
if (none) {
|
||||
this.set('value', null);
|
||||
} else {
|
||||
this.set('value', firstVal);
|
||||
}
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._updateSelect2);
|
||||
},
|
||||
|
||||
@observes('value')
|
||||
|
@ -81,9 +104,14 @@ export default Ember.Component.extend(bufferedRender({
|
|||
}
|
||||
this.set('value', val);
|
||||
});
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._triggerChange);
|
||||
},
|
||||
|
||||
_updateSelect2() {
|
||||
this.$().trigger('change.select2');
|
||||
},
|
||||
|
||||
_triggerChange() {
|
||||
this.$().trigger('change');
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host', 'web-hook', 'web-hook-event'];
|
||||
const ADMIN_MODELS = ['plugin', 'theme', 'embeddable-host', 'web-hook', 'web-hook-event'];
|
||||
|
||||
export function Result(payload, responseJson) {
|
||||
this.payload = payload;
|
||||
|
@ -76,22 +76,38 @@ export default Ember.Object.extend({
|
|||
this.cached[this.storageKey(type,findArgs,opts)] = hydrated;
|
||||
},
|
||||
|
||||
jsonMode: false,
|
||||
|
||||
getPayload(method, data) {
|
||||
let payload = {method, data};
|
||||
|
||||
if (this.jsonMode) {
|
||||
payload.contentType = "application/json";
|
||||
payload.data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
update(store, type, id, attrs) {
|
||||
const data = {};
|
||||
const typeField = Ember.String.underscore(type);
|
||||
data[typeField] = attrs;
|
||||
return ajax(this.pathFor(store, type, id), { method: 'PUT', data }).then(function(json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
|
||||
return ajax(this.pathFor(store, type, id), this.getPayload('PUT', data))
|
||||
.then(function(json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
},
|
||||
|
||||
createRecord(store, type, attrs) {
|
||||
const data = {};
|
||||
const typeField = Ember.String.underscore(type);
|
||||
data[typeField] = attrs;
|
||||
return ajax(this.pathFor(store, type), { method: 'POST', data }).then(function (json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
return ajax(this.pathFor(store, type), this.getPayload('POST', data))
|
||||
.then(function (json) {
|
||||
return new Result(json[typeField], json);
|
||||
});
|
||||
},
|
||||
|
||||
destroyRecord(store, type, record) {
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
|
||||
export default Em.Component.extend({
|
||||
fileInput: null,
|
||||
loading: false,
|
||||
expectedRootObjectName: null,
|
||||
hover: 0,
|
||||
|
||||
classNames: ['json-uploader'],
|
||||
|
||||
_initialize: function() {
|
||||
const $this = this.$();
|
||||
const self = this;
|
||||
|
||||
const $fileInput = $this.find('#js-file-input');
|
||||
this.set('fileInput', $fileInput[0]);
|
||||
|
||||
$fileInput.on('change', function() {
|
||||
self.fileSelected(this.files);
|
||||
});
|
||||
|
||||
$this.on('dragover', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$this.on('dragenter', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
self.set('hover', self.get('hover') + 1);
|
||||
return false;
|
||||
});
|
||||
$this.on('dragleave', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
self.set('hover', self.get('hover') - 1);
|
||||
return false;
|
||||
});
|
||||
$this.on('drop', function(e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
|
||||
self.set('hover', 0);
|
||||
self.fileSelected(e.dataTransfer.files);
|
||||
return false;
|
||||
});
|
||||
|
||||
}.on('didInsertElement'),
|
||||
|
||||
accept: function() {
|
||||
return ".json,application/json,application/x-javascript,text/json" + (this.get('extension') ? "," + this.get('extension') : "");
|
||||
}.property('extension'),
|
||||
|
||||
setReady: function() {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(this.get('value'));
|
||||
} catch (e) {
|
||||
this.set('ready', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootObject = parsed[this.get('expectedRootObjectName')];
|
||||
|
||||
if (rootObject !== null && rootObject !== undefined) {
|
||||
this.set('ready', true);
|
||||
} else {
|
||||
this.set('ready', false);
|
||||
}
|
||||
}.observes('destination', 'expectedRootObjectName'),
|
||||
|
||||
actions: {
|
||||
selectFile: function() {
|
||||
const $fileInput = $(this.get('fileInput'));
|
||||
$fileInput.click();
|
||||
}
|
||||
},
|
||||
|
||||
fileSelected(fileList) {
|
||||
const self = this;
|
||||
let files = [];
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
files[i] = fileList[i];
|
||||
}
|
||||
const fileNameRegex = /\.(json|txt)$/;
|
||||
files = files.filter(function(file) {
|
||||
if (fileNameRegex.test(file.name)) {
|
||||
return true;
|
||||
}
|
||||
if (file.type === "text/plain") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const firstFile = fileList[0];
|
||||
|
||||
this.set('loading', true);
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(evt) {
|
||||
self.set('value', evt.target.result);
|
||||
self.set('loading', false);
|
||||
};
|
||||
|
||||
reader.readAsText(firstFile);
|
||||
}
|
||||
|
||||
});
|
|
@ -143,12 +143,14 @@ export default Em.Component.extend({
|
|||
matchRegEx = matchRegEx || replaceRegEx;
|
||||
const match = this.filterBlocks(matchRegEx);
|
||||
|
||||
let val = this.get(key);
|
||||
|
||||
if (match.length !== 0) {
|
||||
const userInput = match[0].replace(replaceRegEx, '');
|
||||
if (this.get(key) !== userInput) {
|
||||
if (val !== userInput) {
|
||||
this.set(key, userInput);
|
||||
}
|
||||
} else if(this.get(key).length !== 0) {
|
||||
} else if(val && val.length !== 0) {
|
||||
this.set(key, '');
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
import { setting } from 'discourse/lib/computed';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import { cook } from 'discourse/lib/text';
|
||||
import { NotificationLevels } from 'discourse/lib/notification-levels';
|
||||
import { listThemes, selectDefaultTheme, previewTheme } from 'discourse/lib/theme-selector';
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
userSelectableThemes: function(){
|
||||
return listThemes(this.site);
|
||||
}.property(),
|
||||
|
||||
@observes("selectedTheme")
|
||||
themeKeyChanged() {
|
||||
let key = this.get("selectedTheme");
|
||||
previewTheme(key);
|
||||
},
|
||||
|
||||
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
|
||||
selectedCategories(watched, tracked, muted) {
|
||||
return [].concat(watched, tracked, muted);
|
||||
|
@ -162,6 +173,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
|||
Discourse.User.currentProp('name', model.get('name'));
|
||||
}
|
||||
model.set('bio_cooked', cook(model.get('bio_raw')));
|
||||
selectDefaultTheme(this.get('selectedTheme'));
|
||||
this.set('saved', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
notReady: Em.computed.not('ready'),
|
||||
adminCustomizeCssHtml: Ember.inject.controller(),
|
||||
|
||||
ready: function() {
|
||||
try {
|
||||
const parsed = JSON.parse(this.get('customizationFile'));
|
||||
return !!parsed["site_customization"];
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}.property('customizationFile'),
|
||||
|
||||
actions: {
|
||||
createCustomization: function() {
|
||||
const object = JSON.parse(this.get('customizationFile')).site_customization;
|
||||
|
||||
// Slight fixup before creating object
|
||||
object.enabled = false;
|
||||
delete object.id;
|
||||
delete object.key;
|
||||
|
||||
const controller = this.get('adminCustomizeCssHtml');
|
||||
controller.send('newCustomization', object);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -1,5 +1,45 @@
|
|||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export function refreshCSS(node, hash, newHref, options) {
|
||||
|
||||
let $orig = $(node);
|
||||
|
||||
if ($orig.data('reloading')) {
|
||||
|
||||
if (options && options.force) {
|
||||
clearTimeout($orig.data('timeout'));
|
||||
$orig.data("copy").remove();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$orig.data('orig')) {
|
||||
$orig.data('orig', node.href);
|
||||
}
|
||||
|
||||
$orig.data('reloading', true);
|
||||
|
||||
const orig = $(node).data('orig');
|
||||
|
||||
let reloaded = $orig.clone(true);
|
||||
if (hash) {
|
||||
reloaded[0].href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + hash;
|
||||
} else {
|
||||
reloaded[0].href = newHref;
|
||||
}
|
||||
|
||||
$orig.after(reloaded);
|
||||
|
||||
let timeout = setTimeout(()=>{
|
||||
$orig.remove();
|
||||
reloaded.data('reloading', false);
|
||||
}, 2000);
|
||||
|
||||
$orig.data("timeout", timeout);
|
||||
$orig.data("copy", reloaded);
|
||||
}
|
||||
|
||||
// Use the message bus for live reloading of components for faster development.
|
||||
export default {
|
||||
name: "live-development",
|
||||
|
@ -48,17 +88,8 @@ export default {
|
|||
document.location.reload(true);
|
||||
} else {
|
||||
$('link').each(function() {
|
||||
// TODO: stop bundling css in DEV please
|
||||
if (true || (this.href.match(me.name) && me.hash)) {
|
||||
if (!$(this).data('orig')) {
|
||||
$(this).data('orig', this.href);
|
||||
}
|
||||
const orig = $(this).data('orig');
|
||||
if (!me.hash) {
|
||||
window.__uniq = window.__uniq || 1;
|
||||
me.hash = window.__uniq++;
|
||||
}
|
||||
this.href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + me.hash;
|
||||
if (this.href.match(me.name) && (me.hash || me.new_href)) {
|
||||
refreshCSS(this, me.hash, me.new_href);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ function loadWithTag(path, cb) {
|
|||
};
|
||||
}
|
||||
|
||||
export function loadCSS(url) {
|
||||
return loadScript(url, { css: true });
|
||||
}
|
||||
|
||||
export default function loadScript(url, opts) {
|
||||
|
||||
// TODO: Remove this once plugins have been updated not to use it:
|
||||
|
@ -47,8 +51,11 @@ export default function loadScript(url, opts) {
|
|||
delete _loading[url];
|
||||
});
|
||||
|
||||
const cb = function() {
|
||||
const cb = function(data) {
|
||||
_loaded[url] = true;
|
||||
if (opts && opts.css) {
|
||||
$("head").append("<style>" + data + "</style>");
|
||||
}
|
||||
done();
|
||||
resolve();
|
||||
};
|
||||
|
@ -66,7 +73,7 @@ export default function loadScript(url, opts) {
|
|||
if (opts.scriptTag) {
|
||||
loadWithTag(cdnUrl, cb);
|
||||
} else {
|
||||
ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb);
|
||||
ajax({url: cdnUrl, dataType: opts.css ? "text": "script", cache: true}).then(cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
62
app/assets/javascripts/discourse/lib/theme-selector.js.es6
Normal file
62
app/assets/javascripts/discourse/lib/theme-selector.js.es6
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { refreshCSS } from 'discourse/initializers/live-development';
|
||||
const keySelector = 'meta[name=discourse_theme_key]';
|
||||
|
||||
function currentThemeKey() {
|
||||
let themeKey = null;
|
||||
let elem = _.first($(keySelector));
|
||||
if (elem) {
|
||||
themeKey = elem.content;
|
||||
}
|
||||
return themeKey;
|
||||
}
|
||||
|
||||
export function selectDefaultTheme(key) {
|
||||
if (key) {
|
||||
$.cookie('preview_style', key);
|
||||
} else {
|
||||
$.cookie('preview_style', null);
|
||||
}
|
||||
}
|
||||
|
||||
export function previewTheme(key) {
|
||||
if (currentThemeKey() !== key) {
|
||||
|
||||
Discourse.set("assetVersion", "forceRefresh");
|
||||
|
||||
ajax(`/themes/assets/${key ? key : 'default'}`).then(results => {
|
||||
let elem = _.first($(keySelector));
|
||||
if (elem) {
|
||||
elem.content = key;
|
||||
}
|
||||
|
||||
results.themes.forEach(theme => {
|
||||
let node = $(`link[rel=stylesheet][data-target=${theme.target}]`)[0];
|
||||
if (node) {
|
||||
refreshCSS(node, null, theme.url, {force: true});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function listThemes(site) {
|
||||
let themes = site.get('user_themes');
|
||||
|
||||
if (!themes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let hasDefault = !!themes.findBy('default', true);
|
||||
|
||||
let results = [];
|
||||
if (!hasDefault) {
|
||||
results.push({name: I18n.t('themes.default_description'), id: null});
|
||||
}
|
||||
|
||||
themes.forEach(t=>{
|
||||
results.push({name: t.name, id: t.theme_key});
|
||||
});
|
||||
|
||||
return results.length === 0 ? null : results;
|
||||
}
|
|
@ -56,9 +56,13 @@ export default Ember.Object.extend({
|
|||
},
|
||||
|
||||
findAll(type, findArgs) {
|
||||
const self = this;
|
||||
return this.adapterFor(type).findAll(this, type, findArgs).then(function(result) {
|
||||
return self._resultSet(type, result);
|
||||
const adapter = this.adapterFor(type);
|
||||
return adapter.findAll(this, type, findArgs).then((result) => {
|
||||
let results = this._resultSet(type, result);
|
||||
if (adapter.afterFindAll) {
|
||||
results = adapter.afterFindAll(results);
|
||||
}
|
||||
return results;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ export default RestrictedUserRoute.extend({
|
|||
controller.reset();
|
||||
controller.setProperties({
|
||||
model: user,
|
||||
newNameInput: user.get('name')
|
||||
newNameInput: user.get('name'),
|
||||
selectedTheme: $.cookie('preview_style')
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
{{text-field class="hex-input" value=hexValue maxlength="6"}}
|
||||
{{text-field class="hex-input" value=hexValue maxlength="6"}} <input class="picker" type="input"/>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<div class="jsfu-shade-container">
|
||||
<div class="jsfu-file">
|
||||
<input id="js-file-input" type="file" style="display:none;" accept={{accept}}>
|
||||
{{d-button class="fileSelect" action="selectFile" class="" icon="upload" label="upload_selector.select_file"}}
|
||||
{{conditional-loading-spinner condition=loading size="small"}}
|
||||
</div>
|
||||
<div class="jsfu-separator">{{i18n "alternation"}}</div>
|
||||
<div class="jsfu-paste">
|
||||
{{textarea value=value}}
|
||||
</div>
|
||||
<div class="jsfu-shade {{if hover '' 'hidden'}}"><span class="text">{{fa-icon "upload"}}</span></div>
|
||||
</div>
|
|
@ -354,6 +354,15 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if userSelectableThemes}}
|
||||
<div class="control-group theme">
|
||||
<label class="control-label">{{i18n 'user.theme'}}</label>
|
||||
<div class="controls">
|
||||
{{combo-box content=userSelectableThemes value=selectedTheme}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
|
||||
|
||||
<div class="control-group save-button">
|
||||
|
|
|
@ -11,7 +11,7 @@ export default createPreviewComponent(659, 320, {
|
|||
logo: null,
|
||||
avatar: null,
|
||||
|
||||
@observes('step.fieldsById.theme_id.value')
|
||||
@observes('step.fieldsById.base_scheme_id.value')
|
||||
themeChanged() {
|
||||
this.triggerRepaint();
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@ const Wizard = Ember.Object.extend({
|
|||
const colorStep = this.get('steps').findBy('id', 'colors');
|
||||
if (!colorStep) { return; }
|
||||
|
||||
const themeChoice = colorStep.get('fieldsById.theme_id');
|
||||
const themeChoice = colorStep.get('fieldsById.base_scheme_id');
|
||||
if (!themeChoice) { return; }
|
||||
|
||||
const themeId = themeChoice.get('value');
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
@import "common/foundation/mixins";
|
||||
@import "common/foundation/helpers";
|
||||
|
||||
@import "common/admin/customize";
|
||||
|
||||
$mobile-breakpoint: 700px;
|
||||
|
||||
// Change the box model for .admin-content
|
||||
|
@ -724,138 +726,6 @@ section.details {
|
|||
}
|
||||
}
|
||||
|
||||
// Customise area
|
||||
.customize {
|
||||
.admin-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.current-style.maximized {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100000;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 10px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
.nav.nav-pills {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.content-list, .current-style {
|
||||
float: left;
|
||||
}
|
||||
.content-list ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.current-style {
|
||||
.nav.nav-pills{
|
||||
position: relative;
|
||||
}
|
||||
.toggle-mobile {
|
||||
position: absolute;
|
||||
right: 35px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.toggle-maximize {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
}
|
||||
.delete-link {
|
||||
margin-left: 15px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.preview-link {
|
||||
margin-left: 15px;
|
||||
}
|
||||
.export {
|
||||
float: right;
|
||||
}
|
||||
padding-left: 10px;
|
||||
width: 70%;
|
||||
.style-name {
|
||||
width: 350px;
|
||||
height: 25px;
|
||||
// Remove height to for `box-sizing: border-box`
|
||||
height: auto;
|
||||
}
|
||||
.ace-wrapper {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
&.maximized {
|
||||
.admin-container {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
top: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
.admin-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
.ace-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.ace_editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.status-actions {
|
||||
float: right;
|
||||
margin-top: 7px;
|
||||
span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.buttons {
|
||||
float: left;
|
||||
width: 200px;
|
||||
.saving {
|
||||
padding: 5px 0 0 0;
|
||||
margin-left: 10px;
|
||||
width: 80px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color-scheme {
|
||||
.controls {
|
||||
span, button, a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.colors {
|
||||
thead th { border: none; }
|
||||
td.hex { width: 100px; }
|
||||
td.actions { width: 200px; }
|
||||
.hex-input { width: 80px; margin-bottom: 0; }
|
||||
.hex { text-align: center; }
|
||||
.description { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); }
|
||||
|
||||
.invalid .hex input {
|
||||
background-color: white;
|
||||
color: black;
|
||||
border-color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-flags {
|
||||
.hidden-post td.excerpt,
|
||||
.hidden-post td.user {
|
||||
|
@ -1970,6 +1840,12 @@ table#user-badges {
|
|||
background: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-edit label {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.cbox0 { background: blend-primary-secondary(0%); }
|
||||
.cbox10 { background: blend-primary-secondary(10%); }
|
||||
.cbox20 { background: blend-primary-secondary(20%); }
|
||||
|
|
157
app/assets/stylesheets/common/admin/customize.scss
Normal file
157
app/assets/stylesheets/common/admin/customize.scss
Normal file
|
@ -0,0 +1,157 @@
|
|||
// Customise area
|
||||
.customize {
|
||||
.admin-container {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.admin-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.show-current-style {
|
||||
margin-left: 20px;
|
||||
float: left;
|
||||
width: 70%;
|
||||
h2 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.current-style.maximized {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100000;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 10px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav.nav-pills.fields {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.content-list, .current-style {
|
||||
float: left;
|
||||
}
|
||||
.content-list ul {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.current-style {
|
||||
width: 100%;
|
||||
|
||||
.admin-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav.target {
|
||||
margin-top: 15px;
|
||||
.fa {
|
||||
margin-left: 3px;
|
||||
}
|
||||
.fa-mobile {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-maximize {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
}
|
||||
|
||||
.ace-wrapper {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.maximized {
|
||||
.admin-container {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
top: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
.admin-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
.ace-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-ace-gutter {
|
||||
width: 41px;
|
||||
background-color: #ebebeb;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
float: right;
|
||||
margin-top: 7px;
|
||||
span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
float: left;
|
||||
width: 200px;
|
||||
.saving {
|
||||
padding: 5px 0 0 0;
|
||||
margin-left: 10px;
|
||||
width: 80px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
.color-scheme {
|
||||
.controls {
|
||||
span, button, a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.colors {
|
||||
thead th { border: none; }
|
||||
td.hex { width: 160px; }
|
||||
td.actions { width: 200px; }
|
||||
.hex-input { width: 80px; margin-bottom: 0; }
|
||||
.hex { text-align: center; }
|
||||
.description { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); }
|
||||
|
||||
.invalid .hex input {
|
||||
background-color: white;
|
||||
color: black;
|
||||
border-color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
.status-message {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
@import "common/foundation/variables";
|
||||
@import "common/foundation/mixins";
|
||||
@import "./variables";
|
||||
@import "./mixins";
|
||||
|
||||
// --------------------------------------------------
|
||||
// Base styles for HTML elements
|
||||
|
|
|
@ -28,7 +28,7 @@ $base-font-size: 14px !default;
|
|||
$base-line-height: 19px !default;
|
||||
$base-font-family: Helvetica, Arial, sans-serif !default;
|
||||
|
||||
/* These files don't actually exist. They're injected by DiscourseSassImporter. */
|
||||
/* These files don't actually exist. They're injected by Stylesheet::Compiler. */
|
||||
@import "theme_variables";
|
||||
@import "plugins_variables";
|
||||
@import "common/foundation/math";
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
@import "desktop/menu-panel";
|
||||
@import "desktop/group";
|
||||
|
||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||
/* These files doesn't actually exist, they are injected by Stylesheet::Compiler. */
|
||||
|
||||
@import "plugins";
|
||||
@import "plugins_desktop";
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
//= require ./vendor/normalize
|
||||
//= require ./common/foundation/base
|
||||
|
||||
@import "./vendor/normalize";
|
||||
@import "./common/foundation/base";
|
||||
@import "./common/foundation/variables";
|
||||
@import "./common/foundation/colors";
|
||||
@import "./common/foundation/mixins";
|
|
@ -24,7 +24,7 @@
|
|||
@import "mobile/ring";
|
||||
@import "mobile/group";
|
||||
|
||||
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */
|
||||
/* These files doesn't actually exist, they are injected by Stylesheet::Compiler. */
|
||||
|
||||
@import "plugins";
|
||||
@import "plugins_mobile";
|
||||
|
|
0
app/assets/stylesheets/vendor/sweetalert.css → app/assets/stylesheets/vendor/sweetalert.scss
vendored
Executable file → Normal file
0
app/assets/stylesheets/vendor/sweetalert.css → app/assets/stylesheets/vendor/sweetalert.scss
vendored
Executable file → Normal file
|
@ -3,7 +3,7 @@ class Admin::ColorSchemesController < Admin::AdminController
|
|||
before_filter :fetch_color_scheme, only: [:update, :destroy]
|
||||
|
||||
def index
|
||||
render_serialized([ColorScheme.base] + ColorScheme.current_version.order('id ASC').all.to_a, ColorSchemeSerializer)
|
||||
render_serialized(ColorScheme.base_color_schemes + ColorScheme.order('id ASC').all.to_a, ColorSchemeSerializer)
|
||||
end
|
||||
|
||||
def create
|
||||
|
@ -37,6 +37,6 @@ class Admin::ColorSchemesController < Admin::AdminController
|
|||
end
|
||||
|
||||
def color_scheme_params
|
||||
params.permit(color_scheme: [:enabled, :name, colors: [:name, :hex]])[:color_scheme]
|
||||
params.permit(color_scheme: [:base_scheme_id, :name, colors: [:name, :hex]])[:color_scheme]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
class Admin::SiteCustomizationsController < Admin::AdminController
|
||||
|
||||
before_filter :enable_customization
|
||||
|
||||
skip_before_filter :check_xhr, only: [:show]
|
||||
|
||||
def index
|
||||
@site_customizations = SiteCustomization.order(:name)
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: @site_customizations }
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@site_customization = SiteCustomization.new(site_customization_params)
|
||||
@site_customization.user_id = current_user.id
|
||||
|
||||
respond_to do |format|
|
||||
if @site_customization.save
|
||||
log_site_customization_change(nil, site_customization_params)
|
||||
format.json { render json: @site_customization, status: :created}
|
||||
else
|
||||
format.json { render json: @site_customization.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@site_customization = SiteCustomization.find(params[:id])
|
||||
log_record = log_site_customization_change(@site_customization, site_customization_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @site_customization.update_attributes(site_customization_params)
|
||||
format.json { render json: @site_customization, status: :created}
|
||||
else
|
||||
log_record.destroy if log_record
|
||||
format.json { render json: @site_customization.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@site_customization = SiteCustomization.find(params[:id])
|
||||
StaffActionLogger.new(current_user).log_site_customization_destroy(@site_customization)
|
||||
@site_customization.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@site_customization = SiteCustomization.find(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
check_xhr
|
||||
render json: SiteCustomizationSerializer.new(@site_customization)
|
||||
end
|
||||
|
||||
format.any(:html, :text) do
|
||||
raise RenderEmpty.new if request.xhr?
|
||||
|
||||
response.headers['Content-Disposition'] = "attachment; filename=#{@site_customization.name.parameterize}.dcstyle.json"
|
||||
response.sending_file = true
|
||||
render json: SiteCustomizationSerializer.new(@site_customization)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def site_customization_params
|
||||
params.require(:site_customization)
|
||||
.permit(:name, :stylesheet, :header, :top, :footer,
|
||||
:mobile_stylesheet, :mobile_header, :mobile_top, :mobile_footer,
|
||||
:head_tag, :body_tag,
|
||||
:position, :enabled, :key,
|
||||
:stylesheet_baked, :embedded_css)
|
||||
end
|
||||
|
||||
def log_site_customization_change(old_record, new_params)
|
||||
StaffActionLogger.new(current_user).log_site_customization_change(old_record, new_params)
|
||||
end
|
||||
|
||||
def enable_customization
|
||||
session[:disable_customization] = false
|
||||
end
|
||||
|
||||
end
|
|
@ -6,4 +6,73 @@ class Admin::StaffActionLogsController < Admin::AdminController
|
|||
render_serialized(staff_action_logs, UserHistorySerializer)
|
||||
end
|
||||
|
||||
def diff
|
||||
require_dependency "discourse_diff"
|
||||
|
||||
@history = UserHistory.find(params[:id])
|
||||
prev = @history.previous_value
|
||||
cur = @history.new_value
|
||||
|
||||
prev = JSON.parse(prev) if prev
|
||||
cur = JSON.parse(cur) if cur
|
||||
|
||||
diff_fields = {}
|
||||
|
||||
output = "<h2>#{CGI.escapeHTML(cur["name"].to_s)}</h2><p></p>"
|
||||
|
||||
diff_fields["name"] = {
|
||||
prev: prev["name"].to_s,
|
||||
cur: cur["name"].to_s,
|
||||
}
|
||||
|
||||
["default", "user_selectable"].each do |f|
|
||||
diff_fields[f] = {
|
||||
prev: (!!prev[f]).to_s,
|
||||
cur: (!!cur[f]).to_s
|
||||
}
|
||||
end
|
||||
|
||||
diff_fields["color scheme"] = {
|
||||
prev: prev["color_scheme"]&.fetch("name").to_s,
|
||||
cur: cur["color_scheme"]&.fetch("name").to_s,
|
||||
}
|
||||
|
||||
diff_fields["included themes"] = {
|
||||
prev: child_themes(prev),
|
||||
cur: child_themes(cur)
|
||||
}
|
||||
|
||||
|
||||
load_diff(diff_fields, :cur, cur)
|
||||
load_diff(diff_fields, :prev, prev)
|
||||
|
||||
diff_fields.delete_if{|k,v| v[:cur] == v[:prev]}
|
||||
|
||||
|
||||
diff_fields.each do |k,v|
|
||||
output << "<h3>#{k}</h3><p></p>"
|
||||
diff = DiscourseDiff.new(v[:prev] || "", v[:cur] || "")
|
||||
output << diff.side_by_side_markdown
|
||||
end
|
||||
|
||||
render json: {side_by_side: output}
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def child_themes(theme)
|
||||
return "" unless children = theme["child_themes"]
|
||||
|
||||
children.map{|row| row["name"]}.join(" ").to_s
|
||||
end
|
||||
|
||||
def load_diff(hash, key, val)
|
||||
if f=val["theme_fields"]
|
||||
f.each do |row|
|
||||
entry = hash[row["target"] + " " + row["name"]] ||= {}
|
||||
entry[key] = row["value"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
192
app/controllers/admin/themes_controller.rb
Normal file
192
app/controllers/admin/themes_controller.rb
Normal file
|
@ -0,0 +1,192 @@
|
|||
class Admin::ThemesController < Admin::AdminController
|
||||
|
||||
skip_before_filter :check_xhr, only: [:show]
|
||||
|
||||
def import
|
||||
|
||||
@theme = nil
|
||||
if params[:theme]
|
||||
json = JSON::parse(params[:theme].read)
|
||||
theme = json['theme']
|
||||
|
||||
@theme = Theme.new(name: theme["name"], user_id: current_user.id)
|
||||
theme["theme_fields"]&.each do |field|
|
||||
@theme.set_field(field["target"], field["name"], field["value"])
|
||||
end
|
||||
|
||||
if @theme.save
|
||||
log_theme_change(nil, @theme)
|
||||
render json: @theme, status: :created
|
||||
else
|
||||
render json: @theme.errors, status: :unprocessable_entity
|
||||
end
|
||||
elsif params[:remote]
|
||||
@theme = RemoteTheme.import_theme(params[:remote])
|
||||
render json: @theme, status: :created
|
||||
else
|
||||
render json: @theme.errors, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def index
|
||||
@theme = Theme.order(:name).includes(:theme_fields, :remote_theme)
|
||||
@color_schemes = ColorScheme.all.to_a
|
||||
light = ColorScheme.new(name: I18n.t("color_schemes.default"))
|
||||
@color_schemes.unshift(light)
|
||||
|
||||
payload = {
|
||||
themes: ActiveModel::ArraySerializer.new(@theme, each_serializer: ThemeSerializer),
|
||||
extras: {
|
||||
color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer)
|
||||
}
|
||||
}
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: payload}
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@theme = Theme.new(name: theme_params[:name],
|
||||
user_id: current_user.id,
|
||||
user_selectable: theme_params[:user_selectable] || false,
|
||||
color_scheme_id: theme_params[:color_scheme_id])
|
||||
set_fields
|
||||
|
||||
respond_to do |format|
|
||||
if @theme.save
|
||||
update_default_theme
|
||||
log_theme_change(nil, @theme)
|
||||
format.json { render json: @theme, status: :created}
|
||||
else
|
||||
format.json { render json: @theme.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@theme = Theme.find(params[:id])
|
||||
|
||||
original_json = ThemeSerializer.new(@theme, root: false).to_json
|
||||
|
||||
[:name, :color_scheme_id, :user_selectable].each do |field|
|
||||
if theme_params.key?(field)
|
||||
@theme.send("#{field}=", theme_params[field])
|
||||
end
|
||||
end
|
||||
|
||||
if theme_params.key?(:child_theme_ids)
|
||||
expected = theme_params[:child_theme_ids].map(&:to_i)
|
||||
|
||||
@theme.child_theme_relation.to_a.each do |child|
|
||||
if expected.include?(child.child_theme_id)
|
||||
expected.reject!{|id| id == child.child_theme_id}
|
||||
else
|
||||
child.destroy
|
||||
end
|
||||
end
|
||||
|
||||
Theme.where(id: expected).each do |theme|
|
||||
@theme.add_child_theme!(theme)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
set_fields
|
||||
|
||||
if params[:theme][:remote_check]
|
||||
@theme.remote_theme.update_remote_version
|
||||
@theme.remote_theme.save!
|
||||
end
|
||||
|
||||
if params[:theme][:remote_update]
|
||||
@theme.remote_theme.update_from_remote
|
||||
@theme.remote_theme.save!
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
if @theme.save
|
||||
|
||||
update_default_theme
|
||||
|
||||
log_theme_change(original_json, @theme)
|
||||
format.json { render json: @theme, status: :created}
|
||||
else
|
||||
format.json { render json: @theme.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@theme = Theme.find(params[:id])
|
||||
StaffActionLogger.new(current_user).log_theme_destroy(@theme)
|
||||
@theme.destroy
|
||||
|
||||
respond_to do |format|
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@theme = Theme.find(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
check_xhr
|
||||
render json: ThemeSerializer.new(@theme)
|
||||
end
|
||||
|
||||
format.any(:html, :text) do
|
||||
raise RenderEmpty.new if request.xhr?
|
||||
|
||||
response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json"
|
||||
response.sending_file = true
|
||||
render json: ThemeSerializer.new(@theme)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_default_theme
|
||||
if theme_params.key?(:default)
|
||||
is_default = theme_params[:default]
|
||||
if @theme.key == SiteSetting.default_theme_key && !is_default
|
||||
Theme.clear_default!
|
||||
elsif is_default
|
||||
@theme.set_default!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def theme_params
|
||||
@theme_params ||=
|
||||
begin
|
||||
# deep munge is a train wreck, work around it for now
|
||||
params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids)
|
||||
params.require(:theme)
|
||||
.permit(:name,
|
||||
:color_scheme_id,
|
||||
:default,
|
||||
:user_selectable,
|
||||
theme_fields: [:name, :target, :value],
|
||||
child_theme_ids: [])
|
||||
end
|
||||
end
|
||||
|
||||
def set_fields
|
||||
|
||||
return unless fields = theme_params[:theme_fields]
|
||||
|
||||
fields.each do |field|
|
||||
@theme.set_field(field[:target], field[:name], field[:value])
|
||||
end
|
||||
end
|
||||
|
||||
def log_theme_change(old_record, new_record)
|
||||
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
|
||||
end
|
||||
|
||||
end
|
|
@ -411,8 +411,8 @@ class ApplicationController < ActionController::Base
|
|||
def custom_html_json
|
||||
target = view_context.mobile_view? ? :mobile : :desktop
|
||||
data = {
|
||||
top: SiteCustomization.custom_top(session[:preview_style], target),
|
||||
footer: SiteCustomization.custom_footer(session[:preview_style], target)
|
||||
top: Theme.lookup_field(session[:preview_style], target, "after_header"),
|
||||
footer: Theme.lookup_field(session[:preview_style], target, "footer")
|
||||
}
|
||||
|
||||
if DiscoursePluginRegistry.custom_html
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
class SiteCustomizationsController < ApplicationController
|
||||
skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required
|
||||
|
||||
def show
|
||||
no_cookies
|
||||
|
||||
cache_time = request.env["HTTP_IF_MODIFIED_SINCE"]
|
||||
cache_time = Time.rfc2822(cache_time) rescue nil if cache_time
|
||||
stylesheet_time =
|
||||
begin
|
||||
if params[:key].to_s == SiteCustomization::ENABLED_KEY
|
||||
SiteCustomization.where(enabled: true)
|
||||
.order('created_at desc')
|
||||
.limit(1)
|
||||
.pluck(:created_at)
|
||||
.first
|
||||
else
|
||||
SiteCustomization.where(key: params[:key].to_s).pluck(:created_at).first
|
||||
end
|
||||
end
|
||||
|
||||
if !stylesheet_time
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
|
||||
if cache_time && stylesheet_time <= cache_time
|
||||
return render nothing: true, status: 304
|
||||
end
|
||||
|
||||
response.headers["Last-Modified"] = stylesheet_time.httpdate
|
||||
expires_in 1.year, public: true
|
||||
render text: SiteCustomization.stylesheet_contents(params[:key], params[:target]),
|
||||
content_type: "text/css"
|
||||
end
|
||||
end
|
|
@ -1,12 +1,40 @@
|
|||
class StylesheetsController < ApplicationController
|
||||
skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show]
|
||||
skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map]
|
||||
|
||||
def show_source_map
|
||||
show_resource(source_map: true)
|
||||
end
|
||||
|
||||
def show
|
||||
show_resource
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def show_resource(source_map: false)
|
||||
|
||||
extension = source_map ? ".css.map" : ".css"
|
||||
|
||||
params[:name]
|
||||
|
||||
no_cookies
|
||||
|
||||
target,digest = params[:name].split(/_([a-f0-9]{40})/)
|
||||
|
||||
if Rails.env == "development"
|
||||
# TODO add theme
|
||||
# calling this method ensures we have a cache for said target
|
||||
# we hold of re-compilation till someone asks for asset
|
||||
if target.include?("theme")
|
||||
split_target,theme_id = target.split(/_(-?[0-9]+)/)
|
||||
theme = Theme.find(theme_id) if theme_id
|
||||
else
|
||||
split_target,color_scheme_id = target.split(/_(-?[0-9]+)/)
|
||||
theme = Theme.find_by(color_scheme_id: color_scheme_id)
|
||||
end
|
||||
Stylesheet::Manager.stylesheet_link_tag(split_target, nil, theme&.key)
|
||||
end
|
||||
|
||||
cache_time = request.env["HTTP_IF_MODIFIED_SINCE"]
|
||||
cache_time = Time.rfc2822(cache_time) rescue nil if cache_time
|
||||
|
||||
|
@ -19,7 +47,7 @@ class StylesheetsController < ApplicationController
|
|||
|
||||
# Security note, safe due to route constraint
|
||||
underscore_digest = digest ? "_" + digest : ""
|
||||
location = "#{Rails.root}/#{DiscourseStylesheets::CACHE_PATH}/#{target}#{underscore_digest}.css"
|
||||
location = "#{Rails.root}/#{Stylesheet::Manager::CACHE_PATH}/#{target}#{underscore_digest}#{extension}"
|
||||
|
||||
stylesheet_time = query.pluck(:created_at).first
|
||||
|
||||
|
@ -33,24 +61,31 @@ class StylesheetsController < ApplicationController
|
|||
|
||||
|
||||
unless File.exist?(location)
|
||||
if current = query.first
|
||||
File.write(location, current.content)
|
||||
if current = query.limit(1).pluck(source_map ? :source_map : :content).first
|
||||
File.write(location, current)
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
end
|
||||
|
||||
response.headers['Last-Modified'] = stylesheet_time.httpdate if stylesheet_time
|
||||
immutable_for(1.year) unless Rails.env == "development"
|
||||
if Rails.env == "development"
|
||||
response.headers['Last-Modified'] = Time.zone.now.httpdate
|
||||
immutable_for(1.second)
|
||||
else
|
||||
response.headers['Last-Modified'] = stylesheet_time.httpdate if stylesheet_time
|
||||
immutable_for(1.year)
|
||||
end
|
||||
send_file(location, disposition: :inline)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def handle_missing_cache(location, name, digest)
|
||||
location = location.sub(".css.map", ".css")
|
||||
source_map_location = location + ".map"
|
||||
|
||||
existing = File.read(location) rescue nil
|
||||
if existing && digest
|
||||
StylesheetCache.add(name, digest, existing)
|
||||
source_map = File.read(source_map_location) rescue nil
|
||||
StylesheetCache.add(name, digest, existing, source_map)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
28
app/controllers/themes_controller.rb
Normal file
28
app/controllers/themes_controller.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
class ThemesController < ::ApplicationController
|
||||
def assets
|
||||
theme_key = params[:key].to_s
|
||||
|
||||
if theme_key == "default"
|
||||
theme_key = nil
|
||||
else
|
||||
raise Discourse::NotFound unless Theme.where(key: theme_key).exists?
|
||||
end
|
||||
|
||||
object = [:mobile, :desktop, :desktop_theme, :mobile_theme].map do |target|
|
||||
link = Stylesheet::Manager.stylesheet_link_tag(target, 'all', params[:key])
|
||||
if link
|
||||
href = link.split(/["']/)[1]
|
||||
if Rails.env.development?
|
||||
href << (href.include?("?") ? "&" : "?")
|
||||
href << SecureRandom.hex
|
||||
end
|
||||
{
|
||||
target: target,
|
||||
url: href
|
||||
}
|
||||
end
|
||||
end.compact
|
||||
|
||||
render json: object.as_json
|
||||
end
|
||||
end
|
|
@ -316,4 +316,27 @@ module ApplicationHelper
|
|||
''
|
||||
end
|
||||
end
|
||||
|
||||
def theme_key
|
||||
if customization_disabled?
|
||||
nil
|
||||
else
|
||||
session[:preview_style] || SiteSetting.default_theme_key
|
||||
end
|
||||
end
|
||||
|
||||
def theme_lookup(name)
|
||||
lookup = Theme.lookup_field(theme_key, mobile_view? ? :mobile : :desktop, name)
|
||||
lookup.html_safe if lookup
|
||||
end
|
||||
|
||||
def discourse_stylesheet_link_tag(name, opts={})
|
||||
if opts.key?(:theme_key)
|
||||
key = opts[:theme_key] unless customization_disabled?
|
||||
else
|
||||
key = theme_key
|
||||
end
|
||||
|
||||
Stylesheet::Manager.stylesheet_link_tag(name, 'all', key)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
require_dependency 'distributed_cache'
|
||||
require_dependency 'sass/discourse_stylesheets'
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
|
||||
|
@ -492,7 +491,7 @@ SQL
|
|||
end
|
||||
|
||||
def publish_discourse_stylesheet
|
||||
DiscourseStylesheets.cache.clear
|
||||
Stylesheet::Manager.cache.clear
|
||||
end
|
||||
|
||||
def index_search
|
||||
|
|
20
app/models/child_theme.rb
Normal file
20
app/models/child_theme.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
class ChildTheme < ActiveRecord::Base
|
||||
belongs_to :parent_theme, class_name: 'Theme'
|
||||
belongs_to :child_theme, class_name: 'Theme'
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: child_themes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# parent_theme_id :integer
|
||||
# child_theme_id :integer
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_child_themes_on_child_theme_id_and_parent_theme_id (child_theme_id,parent_theme_id) UNIQUE
|
||||
# index_child_themes_on_parent_theme_id_and_child_theme_id (parent_theme_id,child_theme_id) UNIQUE
|
||||
#
|
|
@ -1,32 +1,36 @@
|
|||
require_dependency 'sass/discourse_stylesheets'
|
||||
require_dependency 'distributed_cache'
|
||||
|
||||
class ColorScheme < ActiveRecord::Base
|
||||
|
||||
def self.themes
|
||||
CUSTOM_SCHEMES = {
|
||||
dark: {
|
||||
"primary" => 'dddddd',
|
||||
"secondary" => '222222',
|
||||
"tertiary" => '0f82af',
|
||||
"quaternary" => 'c14924',
|
||||
"header_background" => '111111',
|
||||
"header_primary" => '333333',
|
||||
"highlight" => 'a87137',
|
||||
"danger" => 'e45735',
|
||||
"success" => '1ca551',
|
||||
"love" => 'fa6c8d'
|
||||
}
|
||||
}
|
||||
|
||||
def self.base_color_scheme_colors
|
||||
base_with_hash = {}
|
||||
base_colors.each do |name, color|
|
||||
base_with_hash[name] = "##{color}"
|
||||
base_with_hash[name] = "#{color}"
|
||||
end
|
||||
|
||||
[
|
||||
{ id: 'default', colors: base_with_hash },
|
||||
{
|
||||
id: 'dark',
|
||||
colors: {
|
||||
"primary" => '#dddddd',
|
||||
"secondary" => '#222222',
|
||||
"tertiary" => '#0f82af',
|
||||
"quaternary" => '#c14924',
|
||||
"header_background" => '#111111',
|
||||
"header_primary" => '#333333',
|
||||
"highlight" => '#a87137',
|
||||
"danger" => '#e45735',
|
||||
"success" => '#1ca551',
|
||||
"love" => '#fa6c8d'
|
||||
}
|
||||
}
|
||||
list = [
|
||||
{ id: 'default', colors: base_with_hash }
|
||||
]
|
||||
|
||||
CUSTOM_SCHEMES.each do |k,v|
|
||||
list.push({id: k.to_s, colors: v})
|
||||
end
|
||||
list
|
||||
end
|
||||
|
||||
def self.hex_cache
|
||||
|
@ -39,9 +43,12 @@ class ColorScheme < ActiveRecord::Base
|
|||
|
||||
alias_method :colors, :color_scheme_colors
|
||||
|
||||
scope :current_version, ->{ where(versioned_id: nil) }
|
||||
before_save do
|
||||
if self.id
|
||||
self.version += 1
|
||||
end
|
||||
end
|
||||
|
||||
after_destroy :destroy_versions
|
||||
after_save :publish_discourse_stylesheet
|
||||
after_save :dump_hex_cache
|
||||
after_destroy :dump_hex_cache
|
||||
|
@ -64,13 +71,18 @@ class ColorScheme < ActiveRecord::Base
|
|||
@base_colors
|
||||
end
|
||||
|
||||
def self.enabled
|
||||
current_version.find_by(enabled: true)
|
||||
def self.base_color_schemes
|
||||
base_color_scheme_colors.map do |hash|
|
||||
scheme = new(name: I18n.t("color_schemes.#{hash[:id]}"), base_scheme_id: hash[:id])
|
||||
scheme.colors = hash[:colors].map{|k,v| {name: k.to_s, hex: v.sub("#","")}}
|
||||
scheme.is_base = true
|
||||
scheme
|
||||
end
|
||||
end
|
||||
|
||||
def self.base
|
||||
return @base_color_scheme if @base_color_scheme
|
||||
@base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name'), enabled: false)
|
||||
@base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name'))
|
||||
@base_color_scheme.colors = base_colors.map { |name, hex| {name: name, hex: hex} }
|
||||
@base_color_scheme.is_base = true
|
||||
@base_color_scheme
|
||||
|
@ -101,7 +113,7 @@ class ColorScheme < ActiveRecord::Base
|
|||
end
|
||||
|
||||
# Can't use `where` here because base doesn't allow it
|
||||
(enabled || base).colors.find {|c| c.name == name }.try(:hex) || :nil
|
||||
(base).colors.find {|c| c.name == name }.try(:hex) || :nil
|
||||
end
|
||||
|
||||
def self.hex_for_name(name)
|
||||
|
@ -129,17 +141,39 @@ class ColorScheme < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def previous_version
|
||||
ColorScheme.where(versioned_id: self.id).where('version < ?', self.version).order('version DESC').first
|
||||
def base_colors
|
||||
colors = nil
|
||||
if base_scheme_id && base_scheme_id != "default"
|
||||
colors = CUSTOM_SCHEMES[base_scheme_id.to_sym]
|
||||
end
|
||||
colors || ColorScheme.base_colors
|
||||
end
|
||||
|
||||
def destroy_versions
|
||||
ColorScheme.where(versioned_id: self.id).destroy_all
|
||||
def resolved_colors
|
||||
resolved = ColorScheme.base_colors.dup
|
||||
if base_scheme_id && base_scheme_id != "default"
|
||||
if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym]
|
||||
scheme.each do |name, value|
|
||||
resolved[name] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
colors.each do |c|
|
||||
resolved[c.name] = c.hex
|
||||
end
|
||||
resolved
|
||||
end
|
||||
|
||||
def publish_discourse_stylesheet
|
||||
MessageBus.publish("/discourse_stylesheet", self.name)
|
||||
DiscourseStylesheets.cache.clear
|
||||
if self.id
|
||||
themes = Theme.where(color_scheme_id: self.id).to_a
|
||||
if themes.present?
|
||||
Stylesheet::Manager.cache.clear
|
||||
themes.each do |theme|
|
||||
theme.notify_scheme_change(_clear_manager_cache = false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def dump_hex_cache
|
||||
|
@ -152,13 +186,11 @@ end
|
|||
#
|
||||
# Table name: color_schemes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# enabled :boolean default(FALSE), not null
|
||||
# versioned_id :integer
|
||||
# version :integer default(1), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# via_wizard :boolean default(FALSE), not null
|
||||
# theme_id :string
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# version :integer default(1), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# via_wizard :boolean default(FALSE), not null
|
||||
# base_scheme_id :string
|
||||
#
|
||||
|
|
85
app/models/remote_theme.rb
Normal file
85
app/models/remote_theme.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
require_dependency 'git_importer'
|
||||
|
||||
class RemoteTheme < ActiveRecord::Base
|
||||
has_one :theme
|
||||
|
||||
def self.import_theme(url, user=Discourse.system_user)
|
||||
importer = GitImporter.new(url)
|
||||
importer.import!
|
||||
|
||||
theme_info = JSON.parse(importer["about.json"])
|
||||
theme = Theme.new(user_id: user&.id || -1, name: theme_info["name"])
|
||||
|
||||
remote_theme = new
|
||||
theme.remote_theme = remote_theme
|
||||
|
||||
remote_theme.remote_url = importer.url
|
||||
remote_theme.update_from_remote(importer)
|
||||
|
||||
theme.save!
|
||||
theme
|
||||
ensure
|
||||
begin
|
||||
importer.cleanup!
|
||||
rescue => e
|
||||
Rails.logger.warn("Failed cleanup remote git #{e}")
|
||||
end
|
||||
end
|
||||
|
||||
def update_remote_version
|
||||
importer = GitImporter.new(remote_url)
|
||||
importer.import!
|
||||
self.updated_at = Time.zone.now
|
||||
self.remote_version, self.commits_behind = importer.commits_since(remote_version)
|
||||
end
|
||||
|
||||
def update_from_remote(importer=nil)
|
||||
return unless remote_url
|
||||
cleanup = false
|
||||
unless importer
|
||||
cleanup = true
|
||||
importer = GitImporter.new(remote_url)
|
||||
importer.import!
|
||||
end
|
||||
|
||||
Theme.targets.keys.each do |target|
|
||||
Theme::ALLOWED_FIELDS.each do |field|
|
||||
value = importer["#{target}/#{field=="scss"?"#{target}.scss":"#{field}.html"}"]
|
||||
theme.set_field(target.to_sym, field, value)
|
||||
end
|
||||
end
|
||||
|
||||
theme_info = JSON.parse(importer["about.json"])
|
||||
self.license_url ||= theme_info["license_url"]
|
||||
self.about_url ||= theme_info["about_url"]
|
||||
|
||||
self.remote_updated_at = Time.zone.now
|
||||
self.remote_version = importer.version
|
||||
self.local_version = importer.version
|
||||
self.commits_behind = 0
|
||||
|
||||
self
|
||||
ensure
|
||||
begin
|
||||
importer.cleanup! if cleanup
|
||||
rescue => e
|
||||
Rails.logger.warn("Failed cleanup remote git #{e}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: remote_themes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# remote_url :string not null
|
||||
# remote_version :string
|
||||
# local_version :string
|
||||
# about_url :string
|
||||
# license_url :string
|
||||
# commits_behind :integer
|
||||
# remote_updated_at :datetime
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
#
|
|
@ -1,299 +0,0 @@
|
|||
require_dependency 'sass/discourse_sass_compiler'
|
||||
require_dependency 'sass/discourse_stylesheets'
|
||||
require_dependency 'distributed_cache'
|
||||
|
||||
class SiteCustomization < ActiveRecord::Base
|
||||
ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd'
|
||||
|
||||
COMPILER_VERSION = 4
|
||||
|
||||
@cache = DistributedCache.new('site_customization')
|
||||
|
||||
def self.css_fields
|
||||
%w(stylesheet mobile_stylesheet embedded_css)
|
||||
end
|
||||
|
||||
def self.html_fields
|
||||
%w(body_tag head_tag header mobile_header footer mobile_footer)
|
||||
end
|
||||
|
||||
before_create do
|
||||
self.enabled ||= false
|
||||
self.key ||= SecureRandom.uuid
|
||||
true
|
||||
end
|
||||
|
||||
def compile_stylesheet(scss)
|
||||
DiscourseSassCompiler.compile("@import \"theme_variables\";\n" << scss, 'custom')
|
||||
rescue => e
|
||||
puts e.backtrace.join("\n") unless Sass::SyntaxError === e
|
||||
raise e
|
||||
end
|
||||
|
||||
def transpile(es6_source, version)
|
||||
template = Tilt::ES6ModuleTranspilerTemplate.new {}
|
||||
wrapped = <<PLUGIN_API_JS
|
||||
Discourse._registerPluginCode('#{version}', api => {
|
||||
#{es6_source}
|
||||
});
|
||||
PLUGIN_API_JS
|
||||
|
||||
template.babel_transpile(wrapped)
|
||||
end
|
||||
|
||||
def process_html(html)
|
||||
doc = Nokogiri::HTML.fragment(html)
|
||||
doc.css('script[type="text/x-handlebars"]').each do |node|
|
||||
name = node["name"] || node["data-template-name"] || "broken"
|
||||
is_raw = name =~ /\.raw$/
|
||||
if is_raw
|
||||
template = "require('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})"
|
||||
node.replace <<COMPILED
|
||||
<script>
|
||||
(function() {
|
||||
Discourse.RAW_TEMPLATES[#{name.sub(/\.raw$/, '').inspect}] = #{template};
|
||||
})();
|
||||
</script>
|
||||
COMPILED
|
||||
else
|
||||
template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})"
|
||||
node.replace <<COMPILED
|
||||
<script>
|
||||
(function() {
|
||||
Ember.TEMPLATES[#{name.inspect}] = #{template};
|
||||
})();
|
||||
</script>
|
||||
COMPILED
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
doc.css('script[type="text/discourse-plugin"]').each do |node|
|
||||
if node['version'].present?
|
||||
begin
|
||||
code = transpile(node.inner_html, node['version'])
|
||||
node.replace("<script>#{code}</script>")
|
||||
rescue MiniRacer::RuntimeError => ex
|
||||
node.replace("<script type='text/discourse-js-error'>#{ex.message}</script>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc.to_s
|
||||
end
|
||||
|
||||
before_save do
|
||||
SiteCustomization.html_fields.each do |html_attr|
|
||||
if self.send("#{html_attr}_changed?")
|
||||
self.send("#{html_attr}_baked=", process_html(self.send(html_attr)))
|
||||
end
|
||||
end
|
||||
|
||||
SiteCustomization.css_fields.each do |stylesheet_attr|
|
||||
if self.send("#{stylesheet_attr}_changed?")
|
||||
begin
|
||||
self.send("#{stylesheet_attr}_baked=", compile_stylesheet(self.send(stylesheet_attr)))
|
||||
rescue Sass::SyntaxError => e
|
||||
self.send("#{stylesheet_attr}_baked=", DiscourseSassCompiler.error_as_css(e, "custom stylesheet"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def any_stylesheet_changed?
|
||||
SiteCustomization.css_fields.each do |fieldname|
|
||||
return true if self.send("#{fieldname}_changed?")
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
after_save do
|
||||
remove_from_cache!
|
||||
if any_stylesheet_changed?
|
||||
MessageBus.publish "/file-change/#{key}", SecureRandom.hex
|
||||
MessageBus.publish "/file-change/#{SiteCustomization::ENABLED_KEY}", SecureRandom.hex
|
||||
end
|
||||
MessageBus.publish "/header-change/#{key}", header if header_changed?
|
||||
MessageBus.publish "/footer-change/#{key}", footer if footer_changed?
|
||||
DiscourseStylesheets.cache.clear
|
||||
end
|
||||
|
||||
after_destroy do
|
||||
remove_from_cache!
|
||||
end
|
||||
|
||||
def self.enabled_key
|
||||
ENABLED_KEY.dup << RailsMultisite::ConnectionManagement.current_db
|
||||
end
|
||||
|
||||
def self.field_for_target(target=nil)
|
||||
target ||= :desktop
|
||||
|
||||
case target.to_sym
|
||||
when :mobile then :mobile_stylesheet
|
||||
when :desktop then :stylesheet
|
||||
when :embedded then :embedded_css
|
||||
end
|
||||
end
|
||||
|
||||
def self.baked_for_target(target=nil)
|
||||
"#{field_for_target(target)}_baked".to_sym
|
||||
end
|
||||
|
||||
def self.enabled_stylesheet_contents(target=:desktop)
|
||||
@cache["enabled_stylesheet_#{target}:#{COMPILER_VERSION}"] ||= where(enabled: true)
|
||||
.order(:name)
|
||||
.pluck(baked_for_target(target))
|
||||
.compact
|
||||
.join("\n")
|
||||
end
|
||||
|
||||
def self.stylesheet_contents(key, target)
|
||||
if key == ENABLED_KEY
|
||||
enabled_stylesheet_contents(target)
|
||||
else
|
||||
where(key: key)
|
||||
.pluck(baked_for_target(target))
|
||||
.first
|
||||
end
|
||||
end
|
||||
|
||||
def self.custom_stylesheet(preview_style=nil, target=:desktop)
|
||||
preview_style ||= ENABLED_KEY
|
||||
if preview_style == ENABLED_KEY
|
||||
stylesheet_link_tag(ENABLED_KEY, target, enabled_stylesheet_contents(target))
|
||||
else
|
||||
lookup_field(preview_style, target, :stylesheet_link_tag)
|
||||
end
|
||||
end
|
||||
|
||||
%i{header top footer head_tag body_tag}.each do |name|
|
||||
define_singleton_method("custom_#{name}") do |preview_style=nil, target=:desktop|
|
||||
preview_style ||= ENABLED_KEY
|
||||
lookup_field(preview_style, target, name)
|
||||
end
|
||||
end
|
||||
|
||||
def self.lookup_field(key, target, field)
|
||||
return if key.blank?
|
||||
|
||||
cache_key = "#{key}:#{target}:#{field}:#{COMPILER_VERSION}"
|
||||
|
||||
lookup = @cache[cache_key]
|
||||
return lookup.html_safe if lookup
|
||||
|
||||
styles = if key == ENABLED_KEY
|
||||
order(:name).where(enabled:true).to_a
|
||||
else
|
||||
[find_by(key: key)].compact
|
||||
end
|
||||
|
||||
val = if styles.present?
|
||||
styles.map do |style|
|
||||
lookup = target == :mobile ? "mobile_#{field}" : field
|
||||
if html_fields.include?(lookup.to_s)
|
||||
style.ensure_baked!(lookup)
|
||||
style.send("#{lookup}_baked")
|
||||
else
|
||||
style.send(lookup)
|
||||
end
|
||||
end.compact.join("\n")
|
||||
end
|
||||
|
||||
(@cache[cache_key] = val || "").html_safe
|
||||
end
|
||||
|
||||
def self.remove_from_cache!(key, broadcast = true)
|
||||
MessageBus.publish('/site_customization', key: key) if broadcast
|
||||
clear_cache!
|
||||
end
|
||||
|
||||
def self.clear_cache!
|
||||
@cache.clear
|
||||
end
|
||||
|
||||
def ensure_baked!(field)
|
||||
|
||||
# If the version number changes, clear out all the baked fields
|
||||
if compiler_version != COMPILER_VERSION
|
||||
updates = { compiler_version: COMPILER_VERSION }
|
||||
SiteCustomization.html_fields.each do |f|
|
||||
updates["#{f}_baked".to_sym] = nil
|
||||
end
|
||||
|
||||
update_columns(updates)
|
||||
end
|
||||
|
||||
baked = send("#{field}_baked")
|
||||
if baked.blank?
|
||||
if val = self.send(field)
|
||||
val = process_html(val) rescue ""
|
||||
self.update_columns("#{field}_baked" => val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_cache!
|
||||
self.class.remove_from_cache!(self.class.enabled_key)
|
||||
self.class.remove_from_cache!(key)
|
||||
end
|
||||
|
||||
def mobile_stylesheet_link_tag
|
||||
stylesheet_link_tag(:mobile)
|
||||
end
|
||||
|
||||
def stylesheet_link_tag(target=:desktop)
|
||||
content = self.send(SiteCustomization.field_for_target(target))
|
||||
SiteCustomization.stylesheet_link_tag(key, target, content)
|
||||
end
|
||||
|
||||
def self.stylesheet_link_tag(key, target, content)
|
||||
return "" unless content.present?
|
||||
|
||||
hash = Digest::MD5.hexdigest(content)
|
||||
link_css_tag "/site_customizations/#{key}.css?target=#{target}&v=#{hash}"
|
||||
end
|
||||
|
||||
def self.link_css_tag(href)
|
||||
href = (GlobalSetting.cdn_url || "") + "#{GlobalSetting.relative_url_root}#{href}&__ws=#{Discourse.current_hostname}"
|
||||
%Q{<link class="custom-css" rel="stylesheet" href="#{href}" type="text/css" media="all">}.html_safe
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: site_customizations
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# stylesheet :text
|
||||
# header :text
|
||||
# user_id :integer not null
|
||||
# enabled :boolean not null
|
||||
# key :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# stylesheet_baked :text default(""), not null
|
||||
# mobile_stylesheet :text
|
||||
# mobile_header :text
|
||||
# mobile_stylesheet_baked :text
|
||||
# footer :text
|
||||
# mobile_footer :text
|
||||
# head_tag :text
|
||||
# body_tag :text
|
||||
# top :text
|
||||
# mobile_top :text
|
||||
# embedded_css :text
|
||||
# embedded_css_baked :text
|
||||
# head_tag_baked :text
|
||||
# body_tag_baked :text
|
||||
# header_baked :text
|
||||
# mobile_header_baked :text
|
||||
# footer_baked :text
|
||||
# mobile_footer_baked :text
|
||||
# compiler_version :integer default(0), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_site_customizations_on_key (key)
|
||||
#
|
|
@ -3,11 +3,11 @@ class StylesheetCache < ActiveRecord::Base
|
|||
|
||||
MAX_TO_KEEP = 50
|
||||
|
||||
def self.add(target,digest,content)
|
||||
def self.add(target,digest,content,source_map)
|
||||
|
||||
return false if where(target: target, digest: digest).exists?
|
||||
|
||||
success = create(target: target, digest: digest, content: content)
|
||||
success = create(target: target, digest: digest, content: content, source_map: source_map)
|
||||
|
||||
count = StylesheetCache.count
|
||||
if count > MAX_TO_KEEP
|
||||
|
@ -39,6 +39,8 @@ end
|
|||
# content :text not null
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# theme_id :integer default(-1), not null
|
||||
# source_map :text
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
256
app/models/theme.rb
Normal file
256
app/models/theme.rb
Normal file
|
@ -0,0 +1,256 @@
|
|||
require_dependency 'distributed_cache'
|
||||
require_dependency 'stylesheet/compiler'
|
||||
require_dependency 'stylesheet/manager'
|
||||
|
||||
class Theme < ActiveRecord::Base
|
||||
|
||||
ALLOWED_FIELDS = %w{scss head_tag header after_header body_tag footer}
|
||||
|
||||
@cache = DistributedCache.new('theme')
|
||||
|
||||
belongs_to :color_scheme
|
||||
has_many :theme_fields, 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
|
||||
belongs_to :remote_theme
|
||||
|
||||
before_create do
|
||||
self.key ||= SecureRandom.uuid
|
||||
true
|
||||
end
|
||||
|
||||
after_save do
|
||||
changed_fields.each(&:save!)
|
||||
changed_fields.clear
|
||||
|
||||
Theme.expire_site_cache! if user_selectable_changed?
|
||||
|
||||
@dependant_themes = nil
|
||||
@included_themes = nil
|
||||
end
|
||||
|
||||
after_save do
|
||||
remove_from_cache!
|
||||
notify_scheme_change if color_scheme_id_changed?
|
||||
end
|
||||
|
||||
after_destroy do
|
||||
remove_from_cache!
|
||||
if SiteSetting.default_theme_key == self.key
|
||||
Theme.clear_default!
|
||||
end
|
||||
end
|
||||
|
||||
after_commit ->(theme) do
|
||||
theme.notify_theme_change
|
||||
end, on: :update
|
||||
|
||||
def self.expire_site_cache!
|
||||
Site.clear_anon_cache!
|
||||
ApplicationSerializer.expire_cache_fragment!("user_themes")
|
||||
end
|
||||
|
||||
def self.clear_default!
|
||||
SiteSetting.default_theme_key = ""
|
||||
expire_site_cache!
|
||||
end
|
||||
|
||||
def set_default!
|
||||
SiteSetting.default_theme_key = key
|
||||
Theme.expire_site_cache!
|
||||
end
|
||||
|
||||
def self.lookup_field(key, target, field)
|
||||
return if key.blank?
|
||||
|
||||
cache_key = "#{key}:#{target}:#{field}:#{ThemeField::COMPILER_VERSION}"
|
||||
lookup = @cache[cache_key]
|
||||
return lookup.html_safe if lookup
|
||||
|
||||
target = target.to_sym
|
||||
theme = find_by(key: key)
|
||||
|
||||
val = theme.resolve_baked_field(target, field) if theme
|
||||
|
||||
(@cache[cache_key] = val || "").html_safe
|
||||
end
|
||||
|
||||
def self.remove_from_cache!(themes=nil)
|
||||
clear_cache!
|
||||
end
|
||||
|
||||
def self.clear_cache!
|
||||
@cache.clear
|
||||
end
|
||||
|
||||
|
||||
def self.targets
|
||||
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2)
|
||||
end
|
||||
|
||||
|
||||
def notify_scheme_change(clear_manager_cache=true)
|
||||
Stylesheet::Manager.cache.clear if clear_manager_cache
|
||||
message = refresh_message_for_targets(["desktop", "mobile", "admin"], self.color_scheme_id, self, Rails.env.development?)
|
||||
MessageBus.publish('/file-change', message)
|
||||
end
|
||||
|
||||
def notify_theme_change
|
||||
Stylesheet::Manager.clear_theme_cache!
|
||||
|
||||
themes = [self] + dependant_themes
|
||||
|
||||
message = themes.map do |theme|
|
||||
refresh_message_for_targets([:mobile_theme,:desktop_theme], theme.id, theme)
|
||||
end.compact.flatten
|
||||
MessageBus.publish('/file-change', message)
|
||||
end
|
||||
|
||||
def refresh_message_for_targets(targets, id, theme, add_cache_breaker=false)
|
||||
targets.map do |target|
|
||||
link = Stylesheet::Manager.stylesheet_link_tag(target.to_sym, 'all', theme.key)
|
||||
if link
|
||||
href = link.split(/["']/)[1]
|
||||
if add_cache_breaker
|
||||
href << (href.include?("?") ? "&" : "?")
|
||||
href << SecureRandom.hex
|
||||
end
|
||||
{
|
||||
name: "/stylesheets/#{target}#{id ? "_#{id}": ""}",
|
||||
new_href: href
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def dependant_themes
|
||||
@dependant_themes ||= resolve_dependant_themes(:up)
|
||||
end
|
||||
|
||||
def included_themes
|
||||
@included_themes ||= resolve_dependant_themes(:down)
|
||||
end
|
||||
|
||||
def resolve_dependant_themes(direction)
|
||||
|
||||
select_field,where_field=nil
|
||||
|
||||
if direction == :up
|
||||
select_field = "parent_theme_id"
|
||||
where_field = "child_theme_id"
|
||||
elsif direction == :down
|
||||
select_field = "child_theme_id"
|
||||
where_field = "parent_theme_id"
|
||||
else
|
||||
raise "Unknown direction"
|
||||
end
|
||||
|
||||
themes = []
|
||||
return [] unless id
|
||||
|
||||
uniq = Set.new
|
||||
uniq << id
|
||||
|
||||
iterations = 0
|
||||
added = [id]
|
||||
|
||||
while added.length > 0 && iterations < 5
|
||||
|
||||
iterations += 1
|
||||
|
||||
new_themes = Theme.where("id in (SELECT #{select_field}
|
||||
FROM child_themes
|
||||
WHERE #{where_field} in (?))", added).to_a
|
||||
|
||||
added = []
|
||||
new_themes.each do |theme|
|
||||
unless uniq.include?(theme.id)
|
||||
added << theme.id
|
||||
uniq << theme.id
|
||||
themes << theme
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
themes
|
||||
end
|
||||
|
||||
def resolve_baked_field(target, name)
|
||||
list_baked_fields(target,name).map{|f| f.value_baked || f.value}.join("\n")
|
||||
end
|
||||
|
||||
def list_baked_fields(target, name)
|
||||
|
||||
target = target.to_sym
|
||||
|
||||
theme_ids = [self.id] + (included_themes.map(&:id) || [])
|
||||
fields = ThemeField.where(target: [Theme.targets[target], Theme.targets[:common]])
|
||||
.where(name: name.to_s)
|
||||
.includes(:theme)
|
||||
.joins("JOIN (
|
||||
SELECT #{theme_ids.map.with_index{|id,idx| "#{id} AS theme_id, #{idx} AS sort_column"}.join(" UNION ALL SELECT ")}
|
||||
) as X ON X.theme_id = theme_fields.theme_id")
|
||||
.order('sort_column, target')
|
||||
fields.each(&:ensure_baked!)
|
||||
fields
|
||||
end
|
||||
|
||||
def remove_from_cache!
|
||||
self.class.remove_from_cache!
|
||||
end
|
||||
|
||||
def changed_fields
|
||||
@changed_fields ||= []
|
||||
end
|
||||
|
||||
def set_field(target, name, value)
|
||||
name = name.to_s
|
||||
|
||||
target_id = Theme.targets[target.to_sym]
|
||||
raise "Unknown target #{target} passed to set field" unless target_id
|
||||
|
||||
field = theme_fields.find{|f| f.name==name && f.target == target_id}
|
||||
if field
|
||||
if value.blank?
|
||||
field.destroy
|
||||
else
|
||||
if field.value != value
|
||||
field.value = value
|
||||
changed_fields << field
|
||||
end
|
||||
end
|
||||
else
|
||||
theme_fields.build(target: target_id, value: value, name: name) if value.present?
|
||||
end
|
||||
end
|
||||
|
||||
def add_child_theme!(theme)
|
||||
child_theme_relation.create!(child_theme_id: theme.id)
|
||||
@included_themes = nil
|
||||
child_themes.reload
|
||||
save!
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: themes
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# user_id :integer not null
|
||||
# key :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# compiler_version :integer default(0), not null
|
||||
# user_selectable :boolean default(FALSE), not null
|
||||
# hidden :boolean default(FALSE), not null
|
||||
# color_scheme_id :integer
|
||||
# remote_theme_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_themes_on_key (key)
|
||||
# index_themes_on_remote_theme_id (remote_theme_id) UNIQUE
|
||||
#
|
117
app/models/theme_field.rb
Normal file
117
app/models/theme_field.rb
Normal file
|
@ -0,0 +1,117 @@
|
|||
class ThemeField < ActiveRecord::Base
|
||||
|
||||
COMPILER_VERSION = 5
|
||||
|
||||
belongs_to :theme
|
||||
|
||||
def transpile(es6_source, version)
|
||||
template = Tilt::ES6ModuleTranspilerTemplate.new {}
|
||||
wrapped = <<PLUGIN_API_JS
|
||||
Discourse._registerPluginCode('#{version}', api => {
|
||||
#{es6_source}
|
||||
});
|
||||
PLUGIN_API_JS
|
||||
|
||||
template.babel_transpile(wrapped)
|
||||
end
|
||||
|
||||
def process_html(html)
|
||||
doc = Nokogiri::HTML.fragment(html)
|
||||
doc.css('script[type="text/x-handlebars"]').each do |node|
|
||||
name = node["name"] || node["data-template-name"] || "broken"
|
||||
is_raw = name =~ /\.raw$/
|
||||
if is_raw
|
||||
template = "require('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})"
|
||||
node.replace <<COMPILED
|
||||
<script>
|
||||
(function() {
|
||||
Discourse.RAW_TEMPLATES[#{name.sub(/\.raw$/, '').inspect}] = #{template};
|
||||
})();
|
||||
</script>
|
||||
COMPILED
|
||||
else
|
||||
template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})"
|
||||
node.replace <<COMPILED
|
||||
<script>
|
||||
(function() {
|
||||
Ember.TEMPLATES[#{name.inspect}] = #{template};
|
||||
})();
|
||||
</script>
|
||||
COMPILED
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
doc.css('script[type="text/discourse-plugin"]').each do |node|
|
||||
if node['version'].present?
|
||||
begin
|
||||
code = transpile(node.inner_html, node['version'])
|
||||
node.replace("<script>#{code}</script>")
|
||||
rescue MiniRacer::RuntimeError => ex
|
||||
node.replace("<script type='text/discourse-js-error'>#{ex.message}</script>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
doc.to_s
|
||||
end
|
||||
|
||||
|
||||
def self.html_fields
|
||||
%w(body_tag head_tag header footer after_header)
|
||||
end
|
||||
|
||||
|
||||
def ensure_baked!
|
||||
if ThemeField.html_fields.include?(self.name)
|
||||
if !self.value_baked || compiler_version != COMPILER_VERSION
|
||||
|
||||
self.value_baked = process_html(self.value)
|
||||
self.compiler_version = COMPILER_VERSION
|
||||
|
||||
if self.value_baked_changed? || compiler_version.changed?
|
||||
self.update_columns(value_baked: value_baked, compiler_version: compiler_version)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def target_name
|
||||
Theme.targets.invert[target].to_s
|
||||
end
|
||||
|
||||
before_save do
|
||||
if value_changed? && !value_baked_changed?
|
||||
self.value_baked = nil
|
||||
end
|
||||
end
|
||||
|
||||
after_commit do
|
||||
ensure_baked!
|
||||
|
||||
Stylesheet::Manager.clear_theme_cache! if self.name.include?("scss")
|
||||
|
||||
# TODO message for mobile vs desktop
|
||||
MessageBus.publish "/header-change/#{theme.key}", self.value if self.name == "header"
|
||||
MessageBus.publish "/footer-change/#{theme.key}", self.value if self.name == "footer"
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: theme_fields
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# theme_id :integer not null
|
||||
# target :integer not null
|
||||
# name :string not null
|
||||
# value :text not null
|
||||
# value_baked :text
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# compiler_version :integer default(0), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_theme_fields_on_theme_id_and_target_and_name (theme_id,target,name) UNIQUE
|
||||
#
|
|
@ -19,8 +19,8 @@ class UserHistory < ActiveRecord::Base
|
|||
@actions ||= Enum.new(delete_user: 1,
|
||||
change_trust_level: 2,
|
||||
change_site_setting: 3,
|
||||
change_site_customization: 4,
|
||||
delete_site_customization: 5,
|
||||
change_theme: 4,
|
||||
delete_theme: 5,
|
||||
checked_for_custom_avatar: 6, # not used anymore
|
||||
notified_about_avatar: 7,
|
||||
notified_about_sequential_replies: 8,
|
||||
|
@ -71,8 +71,8 @@ class UserHistory < ActiveRecord::Base
|
|||
@staff_actions ||= [:delete_user,
|
||||
:change_trust_level,
|
||||
:change_site_setting,
|
||||
:change_site_customization,
|
||||
:delete_site_customization,
|
||||
:change_theme,
|
||||
:delete_theme,
|
||||
:change_site_text,
|
||||
:suspend_user,
|
||||
:unsuspend_user,
|
||||
|
@ -158,7 +158,7 @@ class UserHistory < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def new_value_is_json?
|
||||
[UserHistory.actions[:change_site_customization], UserHistory.actions[:delete_site_customization]].include?(action)
|
||||
[UserHistory.actions[:change_theme], UserHistory.actions[:delete_theme]].include?(action)
|
||||
end
|
||||
|
||||
def previous_value_is_json?
|
||||
|
|
|
@ -6,6 +6,11 @@ class ColorSchemeColorSerializer < ApplicationSerializer
|
|||
end
|
||||
|
||||
def default_hex
|
||||
ColorScheme.base_colors[object.name]
|
||||
if object.color_scheme
|
||||
object.color_scheme.base_colors[object.name]
|
||||
else
|
||||
# it is a base color so it is already default
|
||||
object.hex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
class ColorSchemeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :enabled, :is_base
|
||||
attributes :id, :name, :is_base, :base_scheme_id
|
||||
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
|
||||
|
||||
def base
|
||||
object.is_base || false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
class SiteCustomizationSerializer < ApplicationSerializer
|
||||
|
||||
attributes :id, :name, :key, :enabled, :created_at, :updated_at,
|
||||
:stylesheet, :header, :footer, :top,
|
||||
:mobile_stylesheet, :mobile_header, :mobile_footer, :mobile_top,
|
||||
:head_tag, :body_tag, :embedded_css
|
||||
end
|
|
@ -24,13 +24,25 @@ class SiteSerializer < ApplicationSerializer
|
|||
:tags_filter_regexp,
|
||||
:top_tags,
|
||||
:wizard_required,
|
||||
:topic_featured_link_allowed_category_ids
|
||||
:topic_featured_link_allowed_category_ids,
|
||||
:user_themes
|
||||
|
||||
has_many :categories, serializer: BasicCategorySerializer, embed: :objects
|
||||
has_many :trust_levels, embed: :objects
|
||||
has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer
|
||||
has_many :user_fields, embed: :objects, serialzer: UserFieldSerializer
|
||||
|
||||
def user_themes
|
||||
cache_fragment("user_themes") do
|
||||
Theme.where('key = :default OR user_selectable',
|
||||
default: SiteSetting.default_theme_key)
|
||||
.order(:name)
|
||||
.pluck(:key, :name)
|
||||
.map{|k,n| {theme_key: k, name: n, default: k == SiteSetting.default_theme_key}}
|
||||
.as_json
|
||||
end
|
||||
end
|
||||
|
||||
def groups
|
||||
cache_fragment("group_names") do
|
||||
Group.order(:name).pluck(:id,:name).map { |id,name| { id: id, name: name } }.as_json
|
||||
|
|
42
app/serializers/theme_serializer.rb
Normal file
42
app/serializers/theme_serializer.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
class ThemeFieldSerializer < ApplicationSerializer
|
||||
attributes :name, :target, :value
|
||||
|
||||
def target
|
||||
case object.target
|
||||
when 0 then "common"
|
||||
when 1 then "desktop"
|
||||
when 2 then "mobile"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ChildThemeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :key, :created_at, :updated_at, :default
|
||||
|
||||
def include_default?
|
||||
object.key == SiteSetting.default_theme_key
|
||||
end
|
||||
|
||||
def default
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class RemoteThemeSerializer < ApplicationSerializer
|
||||
attributes :id, :remote_url, :remote_version, :local_version, :about_url,
|
||||
:license_url, :commits_behind, :remote_updated_at, :updated_at
|
||||
|
||||
# wow, AMS has some pretty nutty logic where it tries to find the path here
|
||||
# from action dispatch, tell it not to
|
||||
def about_url
|
||||
object.about_url
|
||||
end
|
||||
end
|
||||
|
||||
class ThemeSerializer < ChildThemeSerializer
|
||||
attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id
|
||||
|
||||
has_many :theme_fields, serializer: ThemeFieldSerializer, embed: :objects
|
||||
has_many :child_themes, serializer: ChildThemeSerializer, embed: :objects
|
||||
has_one :remote_theme, serializer: RemoteThemeSerializer, embed: :objects
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user