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:
Sam
2017-04-12 10:52:52 -04:00
parent 1a9afa976d
commit a3e8c3cd7b
163 changed files with 4415 additions and 2424 deletions
@@ -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')
});
@@ -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"}}&nbsp;{{i18n 'admin.customize.head_tag.text'}}
{{/link-to}}
</li>
<li>
{{#link-to 'adminCustomizeCssHtml.show' model.id 'body-tag'}}
{{fa-icon "file-text-o"}}&nbsp;{{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>
@@ -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>
@@ -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>