mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-04-28 15:03:58 -04:00
Theming: color scheme editing. Unfinished! Doesn't have any effect on css files yet.
This commit is contained in:
parent
0f4014eef1
commit
feaaf55a0c
34 changed files with 1086 additions and 84 deletions
app
assets
javascripts
admin
components
controllers
models
routes
admin_customize_colors_route.jsadmin_customize_css_html_route.jsadmin_customize_route.jsadmin_routes.js
templates
admin.js.handlebarscustomize.js.handlebarscustomize_colors.js.handlebarscustomize_css_html.js.handlebars
views
discourse/templates/components
stylesheets/common/admin
controllers/admin
models
serializers
services
config
db
fixtures
migrate
spec
controllers/admin
fabricators
models
services
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
An input field for a color.
|
||||||
|
|
||||||
|
@param hexValue is a reference to the color's hex value.
|
||||||
|
|
||||||
|
@class Discourse.ColorInputComponent
|
||||||
|
@extends Ember.Component
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.ColorInputComponent = Ember.Component.extend({
|
||||||
|
layoutName: 'components/color-input',
|
||||||
|
|
||||||
|
hexValueChanged: function() {
|
||||||
|
var hex = this.get('hexValue');
|
||||||
|
if (hex && (hex.length === 3 || hex.length === 6) && this.get('brightnessValue')) {
|
||||||
|
this.$('input').attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';');
|
||||||
|
}
|
||||||
|
}.observes('hexValue', 'brightnessValue'),
|
||||||
|
|
||||||
|
didInsertElement: function() {
|
||||||
|
var self = this;
|
||||||
|
this._super();
|
||||||
|
Em.run.schedule('afterRender', function() {
|
||||||
|
self.hexValueChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,76 @@
|
||||||
|
/**
|
||||||
|
This controller supports interface for creating custom CSS skins in Discourse.
|
||||||
|
|
||||||
|
@class AdminCustomizeColorsController
|
||||||
|
@extends Ember.Controller
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminCustomizeColorsController = Ember.ArrayController.extend({
|
||||||
|
|
||||||
|
baseColorScheme: function() {
|
||||||
|
return this.get('model').findBy('id', 1);
|
||||||
|
}.property('model.@each.id'),
|
||||||
|
|
||||||
|
removeSelected: function() {
|
||||||
|
this.removeObject(this.get('selectedItem'));
|
||||||
|
this.set('selectedItem', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
selectColorScheme: function(colorScheme) {
|
||||||
|
if (this.get('selectedItem')) { this.get('selectedItem').set('selected', false); }
|
||||||
|
this.set('selectedItem', colorScheme);
|
||||||
|
colorScheme.set('savingStatus', null);
|
||||||
|
colorScheme.set('selected', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
newColorScheme: function() {
|
||||||
|
var newColorScheme = Em.copy(this.get('baseColorScheme'), true);
|
||||||
|
newColorScheme.set('name', I18n.t('admin.customize.colors.new_name'));
|
||||||
|
this.pushObject(newColorScheme);
|
||||||
|
this.send('selectColorScheme', newColorScheme);
|
||||||
|
},
|
||||||
|
|
||||||
|
undo: function(color) {
|
||||||
|
color.undo();
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
var selectedItem = this.get('selectedItem');
|
||||||
|
selectedItem.save();
|
||||||
|
if (selectedItem.get('enabled')) {
|
||||||
|
this.get('model').forEach(function(c) {
|
||||||
|
if (c !== selectedItem) {
|
||||||
|
c.set('enabled', false);
|
||||||
|
c.startTrackingChanges();
|
||||||
|
c.notifyPropertyChange('description');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
copy: function(colorScheme) {
|
||||||
|
var newColorScheme = Em.copy(colorScheme, true);
|
||||||
|
newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + colorScheme.get('name'));
|
||||||
|
this.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,12 +1,12 @@
|
||||||
/**
|
/**
|
||||||
This controller supports interface for creating custom CSS skins in Discourse.
|
This controller supports interface for creating custom CSS skins in Discourse.
|
||||||
|
|
||||||
@class AdminCustomizeController
|
@class AdminCustomizeCssHtmlController
|
||||||
@extends Ember.Controller
|
@extends Ember.Controller
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.AdminCustomizeController = Ember.ArrayController.extend({
|
Discourse.AdminCustomizeCssHtmlController = Ember.ArrayController.extend({
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
115
app/assets/javascripts/admin/models/color_scheme.js
Normal file
115
app/assets/javascripts/admin/models/color_scheme.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
Our data model for a color scheme.
|
||||||
|
|
||||||
|
@class ColorScheme
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.ColorScheme = Discourse.Model.extend(Ember.Copyable, {
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this._super();
|
||||||
|
this.startTrackingChanges();
|
||||||
|
},
|
||||||
|
|
||||||
|
description: function() {
|
||||||
|
return "" + this.name + (this.enabled ? ' (*)' : '');
|
||||||
|
}.property(),
|
||||||
|
|
||||||
|
startTrackingChanges: function() {
|
||||||
|
this.set('originals', {
|
||||||
|
name: this.get('name'),
|
||||||
|
enabled: this.get('enabled')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
copy: function() {
|
||||||
|
var newScheme = Discourse.ColorScheme.create({name: this.get('name'), enabled: false, can_edit: true, colors: Em.A()});
|
||||||
|
_.each(this.get('colors'), function(c){
|
||||||
|
newScheme.colors.pushObject(Discourse.ColorSchemeColor.create({name: c.get('name'), hex: c.get('hex'), opacity: c.get('opacity')}));
|
||||||
|
});
|
||||||
|
return newScheme;
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: function() {
|
||||||
|
if (!this.originals) return false;
|
||||||
|
if (this.originals['name'] !== this.get('name') || this.originals['enabled'] !== this.get('enabled')) return true;
|
||||||
|
if (_.any(this.get('colors'), function(c){ return c.get('changed'); })) return true;
|
||||||
|
return false;
|
||||||
|
}.property('name', 'enabled', 'colors.@each.changed', 'saving'),
|
||||||
|
|
||||||
|
disableSave: function() {
|
||||||
|
return !this.get('changed') || this.get('saving');
|
||||||
|
}.property('changed'),
|
||||||
|
|
||||||
|
newRecord: function() {
|
||||||
|
return (!this.get('id'));
|
||||||
|
}.property('id'),
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
var self = this;
|
||||||
|
this.set('savingStatus', I18n.t('saving'));
|
||||||
|
this.set('saving',true);
|
||||||
|
|
||||||
|
var data = { name: this.name, enabled: this.enabled };
|
||||||
|
|
||||||
|
data.colors = [];
|
||||||
|
_.each(this.get('colors'), function(c) {
|
||||||
|
if (!self.id || c.get('changed')) {
|
||||||
|
data.colors.pushObject({name: c.get('name'), hex: c.get('hex'), opacity: c.get('opacity')});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Discourse.ajax("/admin/color_schemes" + (this.id ? '/' + this.id : '') + '.json', {
|
||||||
|
data: JSON.stringify({"color_scheme": data}),
|
||||||
|
type: this.id ? 'PUT' : 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'application/json'
|
||||||
|
}).then(function(result) {
|
||||||
|
if(result.id) { self.set('id', result.id); }
|
||||||
|
self.startTrackingChanges();
|
||||||
|
_.each(self.get('colors'), function(c) {
|
||||||
|
c.startTrackingChanges();
|
||||||
|
});
|
||||||
|
self.set('savingStatus', I18n.t('saved'));
|
||||||
|
self.set('saving', false);
|
||||||
|
self.notifyPropertyChange('description');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
if (this.id) {
|
||||||
|
return Discourse.ajax("/admin/color_schemes/" + this.id, { type: 'DELETE' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
||||||
|
});
|
||||||
|
|
||||||
|
Discourse.ColorScheme.reopenClass({
|
||||||
|
findAll: function() {
|
||||||
|
var colorSchemes = ColorSchemes.create({ content: [], loading: true });
|
||||||
|
Discourse.ajax('/admin/color_schemes').then(function(all) {
|
||||||
|
_.each(all, function(colorScheme){
|
||||||
|
colorSchemes.pushObject(Discourse.ColorScheme.create({
|
||||||
|
id: colorScheme.id,
|
||||||
|
name: colorScheme.name,
|
||||||
|
enabled: colorScheme.enabled,
|
||||||
|
can_edit: colorScheme.can_edit,
|
||||||
|
colors: colorScheme.colors.map(function(c) { return Discourse.ColorSchemeColor.create({name: c.name, hex: c.hex, opacity: c.opacity}); })
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
colorSchemes.set('loading', false);
|
||||||
|
});
|
||||||
|
return colorSchemes;
|
||||||
|
}
|
||||||
|
});
|
71
app/assets/javascripts/admin/models/color_scheme_color.js
Normal file
71
app/assets/javascripts/admin/models/color_scheme_color.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
Our data model for a color within a color scheme.
|
||||||
|
(It's a funny name for a class, but Color seemed too generic for what this class is.)
|
||||||
|
|
||||||
|
@class ColorSchemeColor
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.ColorSchemeColor = Discourse.Model.extend({
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this._super();
|
||||||
|
this.startTrackingChanges();
|
||||||
|
},
|
||||||
|
|
||||||
|
startTrackingChanges: function() {
|
||||||
|
this.set('originals', {
|
||||||
|
hex: this.get('hex') || 'FFFFFF',
|
||||||
|
opacity: this.get('opacity') || '100'
|
||||||
|
});
|
||||||
|
this.notifyPropertyChange('hex'); // force changed property to be recalculated
|
||||||
|
},
|
||||||
|
|
||||||
|
changed: function() {
|
||||||
|
if (!this.originals) return false;
|
||||||
|
|
||||||
|
if (this.get('hex') !== this.originals['hex'] || this.get('opacity').toString() !== this.originals['opacity'].toString()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}.property('hex', 'opacity'),
|
||||||
|
|
||||||
|
undo: function() {
|
||||||
|
if (this.originals) {
|
||||||
|
this.set('hex', this.originals['hex']);
|
||||||
|
this.set('opacity', this.originals['opacity']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
brightness returns a number between 0 (darkest) to 255 (brightest).
|
||||||
|
Undefined if hex is not a valid color.
|
||||||
|
|
||||||
|
@property brightness
|
||||||
|
**/
|
||||||
|
brightness: function() {
|
||||||
|
var hex = this.get('hex');
|
||||||
|
if (hex.length === 6 || hex.length === 3) {
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex.substr(0,1) + hex.substr(0,1) + hex.substr(1,1) + hex.substr(1,1) + hex.substr(2,1) + hex.substr(2,1);
|
||||||
|
}
|
||||||
|
return Math.round(((parseInt('0x'+hex.substr(0,2)) * 299) + (parseInt('0x'+hex.substr(2,2)) * 587) + (parseInt('0x'+hex.substr(4,2)) * 114)) /1000);
|
||||||
|
}
|
||||||
|
}.property('hex'),
|
||||||
|
|
||||||
|
hexValueChanged: function() {
|
||||||
|
if (this.get('hex')) {
|
||||||
|
this.set('hex', this.get('hex').toString().replace(/[^0-9a-fA-F]/g, ""));
|
||||||
|
}
|
||||||
|
}.observes('hex'),
|
||||||
|
|
||||||
|
opacityChanged: function() {
|
||||||
|
if (this.get('opacity')) {
|
||||||
|
var o = this.get('opacity').toString().replace(/[^\d.]/g, "");
|
||||||
|
if (parseInt(o,10) > 100) { o = o.substr(0,o.length-1); }
|
||||||
|
this.set('opacity', o);
|
||||||
|
}
|
||||||
|
}.observes('opacity')
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
Handles routes related to colors customization
|
||||||
|
|
||||||
|
@class AdminCustomizeColorsRoute
|
||||||
|
@extends Discourse.Route
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminCustomizeColorsRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
|
model: function() {
|
||||||
|
return Discourse.ColorScheme.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
deactivate: function() {
|
||||||
|
this._super();
|
||||||
|
this.controllerFor('adminCustomizeColors').set('selectedItem', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
Handles routes related to css/html customization
|
||||||
|
|
||||||
|
@class AdminCustomizeCssHtmlRoute
|
||||||
|
@extends Discourse.Route
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminCustomizeCssHtmlRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
|
model: function() {
|
||||||
|
return Discourse.SiteCustomization.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -1,15 +1,13 @@
|
||||||
/**
|
/**
|
||||||
Handles routes related to customization
|
Handles routes related to customization
|
||||||
|
|
||||||
@class AdminCustomizeRoute
|
@class AdminCustomizeIndexRoute
|
||||||
@extends Discourse.Route
|
@extends Discourse.Route
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
|
Discourse.AdminCustomizeIndexRoute = Discourse.Route.extend({
|
||||||
|
redirect: function() {
|
||||||
model: function() {
|
this.transitionTo('adminCustomize.css_html');
|
||||||
return Discourse.SiteCustomization.findAll();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
|
|
@ -22,7 +22,10 @@ Discourse.Route.buildRoutes(function() {
|
||||||
this.route('previewDigest', { path: '/preview-digest' });
|
this.route('previewDigest', { path: '/preview-digest' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('customize');
|
this.resource('adminCustomize', { path: '/customize' } ,function() {
|
||||||
|
this.route('colors');
|
||||||
|
this.route('css_html');
|
||||||
|
});
|
||||||
this.route('api');
|
this.route('api');
|
||||||
|
|
||||||
this.resource('admin.backups', { path: '/backups' }, function() {
|
this.resource('admin.backups', { path: '/backups' }, function() {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<li>{{#link-to 'adminFlags'}}{{i18n admin.flags.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminFlags'}}{{i18n admin.flags.title}}{{/link-to}}</li>
|
||||||
<li>{{#link-to 'adminLogs'}}{{i18n admin.logs.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminLogs'}}{{i18n admin.logs.title}}{{/link-to}}</li>
|
||||||
{{#if currentUser.admin}}
|
{{#if currentUser.admin}}
|
||||||
<li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
<li>{{#link-to 'adminCustomize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
||||||
<li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
<li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
||||||
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,70 +1,12 @@
|
||||||
<div class='content-list span6'>
|
<div class='admin-controls'>
|
||||||
<h3>{{i18n admin.customize.long_title}}</h3>
|
<div class='span15'>
|
||||||
<ul>
|
<ul class="nav nav-pills">
|
||||||
{{#each model}}
|
<li>{{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}</li>
|
||||||
<li><a {{action selectStyle this}} {{bind-attr class="this.selected:active"}}>{{this.description}}</a></li>
|
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}</li>
|
||||||
{{/each}}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
<button {{action newCustomization}} class='btn'>{{i18n admin.customize.new}}</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
{{#if selectedItem}}
|
{{outlet}}
|
||||||
<div class='current-style'>
|
|
||||||
{{#with selectedItem}}
|
|
||||||
{{textField class="style-name" value=name}}
|
|
||||||
|
|
||||||
<div class='admin-controls'>
|
|
||||||
<ul class="nav nav-pills">
|
|
||||||
<li>
|
|
||||||
<a {{bind-attr class="view.stylesheetActive:active"}}{{action selectStylesheet href="true" target="view"}}>{{i18n admin.customize.css}}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a {{bind-attr class="view.headerActive:active"}}{{action selectHeader href="true" target="view"}}>{{i18n admin.customize.header}}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a {{bind-attr class="view.mobileStylesheetActive:active"}}{{action selectMobileStylesheet href="true" target="view"}}>{{i18n admin.customize.mobile_css}}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a {{bind-attr class="view.mobileHeaderActive:active"}}{{action selectMobileHeader href="true" target="view"}}>{{i18n admin.customize.mobile_header}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-container">
|
|
||||||
{{#if view.headerActive}}
|
|
||||||
{{aceEditor content=header mode="html"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if view.stylesheetActive}}
|
|
||||||
{{aceEditor content=stylesheet mode="scss"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if view.mobileHeaderActive}}
|
|
||||||
{{aceEditor content=mobile_header mode="html"}}
|
|
||||||
{{/if}}
|
|
||||||
{{#if view.mobileStylesheetActive}}
|
|
||||||
{{aceEditor content=mobile_stylesheet mode="scss"}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/with}}
|
|
||||||
<br>
|
|
||||||
<div class='status-actions'>
|
|
||||||
<span>{{i18n admin.customize.override_default}} {{view Ember.Checkbox checkedBinding="selectedItem.override_default_style"}}</span>
|
|
||||||
<span>{{i18n admin.customize.enabled}} {{view Ember.Checkbox checkedBinding="selectedItem.enabled"}}</span>
|
|
||||||
{{#unless selectedItem.changed}}
|
|
||||||
<a class='preview-link' {{bind-attr href="selectedItem.previewUrl"}} target='_blank'>{{i18n admin.customize.preview}}</a>
|
|
||||||
|
|
|
||||||
<a href="/?preview-style=" target='_blank'>{{i18n admin.customize.undo_preview}}</a><br>
|
|
||||||
{{/unless}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class='buttons'>
|
|
||||||
<button {{action save}} {{bind-attr disabled="selectedItem.disableSave"}} class='btn'>{{i18n admin.customize.save}}</button>
|
|
||||||
<span class='saving'>{{selectedItem.savingStatus}}</span>
|
|
||||||
<a {{action destroy}} class='delete-link'>{{i18n admin.customize.delete}}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
|
||||||
<p class="about">{{i18n admin.customize.about}}</p>
|
|
||||||
{{/if}}
|
|
||||||
<div class='clearfix'></div>
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<div class='alert alert-error'>{{i18n admin.customize.colors.under_construction}}</div>
|
||||||
|
|
||||||
|
<div class='content-list span6'>
|
||||||
|
<h3>{{i18n admin.customize.colors.long_title}}</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each model}}
|
||||||
|
{{#if can_edit}}
|
||||||
|
<li><a {{action selectColorScheme this}} {{bind-attr class="selected:active"}}>{{description}}</a></li>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<button {{action newColorScheme}} class='btn'>{{i18n admin.customize.new}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if selectedItem}}
|
||||||
|
{{#with selectedItem}}
|
||||||
|
<div class="current-style color-scheme">
|
||||||
|
<div class="admin-container">
|
||||||
|
<h1>{{textField class="style-name" value=name}}</h1>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button {{action save}} {{bind-attr disabled="disableSave"}} class='btn'>{{i18n admin.customize.save}}</button>
|
||||||
|
<span {{bind-attr class=":saving savingStatus::hidden" }}>{{savingStatus}}</span>
|
||||||
|
<button {{action copy this}} class='btn'><i class="fa fa-copy"></i> {{i18n admin.customize.copy}}</button>
|
||||||
|
<span>{{view Ember.Checkbox checkedBinding="enabled"}} {{i18n admin.customize.enabled}}</span>
|
||||||
|
<a {{action destroy}} class='delete-link'>{{i18n admin.customize.delete}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table colors">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="hex">{{i18n admin.customize.color}}</th>
|
||||||
|
<th class="opacity">{{i18n admin.customize.opacity}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each colors}}
|
||||||
|
<tr {{bind-attr class="changed"}}>
|
||||||
|
<td class="name">{{name}}</td>
|
||||||
|
<td class="hex">{{color-input hexValue=hex brightnessValue=brightness}}</td>
|
||||||
|
<td class="opacity">{{textField class="opacity-input" value=opacity maxlength="3"}}</td>
|
||||||
|
<td class="changed">
|
||||||
|
<button {{bind-attr class=":btn :undo changed::invisible"}} {{action undo this}}>undo</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/with}}
|
||||||
|
{{else}}
|
||||||
|
<p class="about">{{i18n admin.customize.colors.about}}</p>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<div class='content-list span6'>
|
||||||
|
<h3>{{i18n admin.customize.css_html.long_title}}</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each model}}
|
||||||
|
<li><a {{action selectStyle this}} {{bind-attr class="this.selected:active"}}>{{this.description}}</a></li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<button {{action newCustomization}} class='btn'>{{i18n admin.customize.new}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{#if selectedItem}}
|
||||||
|
<div class='current-style'>
|
||||||
|
{{#with selectedItem}}
|
||||||
|
{{textField class="style-name" value=name}}
|
||||||
|
|
||||||
|
<div class='admin-controls'>
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
<li>
|
||||||
|
<a {{bind-attr class="view.stylesheetActive:active"}}{{action selectStylesheet href="true" target="view"}}>{{i18n admin.customize.css}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a {{bind-attr class="view.headerActive:active"}}{{action selectHeader href="true" target="view"}}>{{i18n admin.customize.header}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a {{bind-attr class="view.mobileStylesheetActive:active"}}{{action selectMobileStylesheet href="true" target="view"}}>{{i18n admin.customize.mobile_css}}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a {{bind-attr class="view.mobileHeaderActive:active"}}{{action selectMobileHeader href="true" target="view"}}>{{i18n admin.customize.mobile_header}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
{{#if view.headerActive}}
|
||||||
|
{{aceEditor content=header mode="html"}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if view.stylesheetActive}}
|
||||||
|
{{aceEditor content=stylesheet mode="scss"}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if view.mobileHeaderActive}}
|
||||||
|
{{aceEditor content=mobile_header mode="html"}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if view.mobileStylesheetActive}}
|
||||||
|
{{aceEditor content=mobile_stylesheet mode="scss"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/with}}
|
||||||
|
<br>
|
||||||
|
<div class='status-actions'>
|
||||||
|
<span>{{i18n admin.customize.override_default}} {{view Ember.Checkbox checkedBinding="selectedItem.override_default_style"}}</span>
|
||||||
|
<span>{{i18n admin.customize.enabled}} {{view Ember.Checkbox checkedBinding="selectedItem.enabled"}}</span>
|
||||||
|
{{#unless selectedItem.changed}}
|
||||||
|
<a class='preview-link' {{bind-attr href="selectedItem.previewUrl"}} target='_blank'>{{i18n admin.customize.preview}}</a>
|
||||||
|
|
|
||||||
|
<a href="/?preview-style=" target='_blank'>{{i18n admin.customize.undo_preview}}</a><br>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='buttons'>
|
||||||
|
<button {{action save}} {{bind-attr disabled="selectedItem.disableSave"}} class='btn'>{{i18n admin.customize.save}}</button>
|
||||||
|
<span class='saving'>{{selectedItem.savingStatus}}</span>
|
||||||
|
<a {{action destroy}} class='delete-link'>{{i18n admin.customize.delete}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="about">{{i18n admin.customize.about}}</p>
|
||||||
|
{{/if}}
|
||||||
|
<div class='clearfix'></div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
/**
|
||||||
|
A view to handle color selections within a site customization
|
||||||
|
|
||||||
|
@class AdminCustomizeColorsView
|
||||||
|
@extends Discourse.View
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminCustomizeColorsView = Discourse.View.extend({
|
||||||
|
templateName: 'admin/templates/customize_colors'
|
||||||
|
});
|
|
@ -12,15 +12,14 @@ Discourse.AdminCustomizeView = Discourse.View.extend({
|
||||||
templateName: 'admin/templates/customize',
|
templateName: 'admin/templates/customize',
|
||||||
classNames: ['customize'],
|
classNames: ['customize'],
|
||||||
selected: 'stylesheet',
|
selected: 'stylesheet',
|
||||||
headerActive: Em.computed.equal('selected', 'header'),
|
headerActive: Em.computed.equal('selected', 'header'),
|
||||||
stylesheetActive: Em.computed.equal('selected', 'stylesheet'),
|
stylesheetActive: Em.computed.equal('selected', 'stylesheet'),
|
||||||
mobileHeaderActive: Em.computed.equal('selected', 'mobileHeader'),
|
mobileHeaderActive: Em.computed.equal('selected', 'mobileHeader'),
|
||||||
mobileStylesheetActive: Em.computed.equal('selected', 'mobileStylesheet'),
|
mobileStylesheetActive: Em.computed.equal('selected', 'mobileStylesheet'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
selectHeader: function() { this.set('selected', 'header'); },
|
selectHeader: function() { this.set('selected', 'header'); },
|
||||||
selectStylesheet: function() { this.set('selected', 'stylesheet'); },
|
selectStylesheet: function() { this.set('selected', 'stylesheet'); },
|
||||||
|
|
||||||
selectMobileHeader: function() { this.set('selected', 'mobileHeader'); },
|
selectMobileHeader: function() { this.set('selected', 'mobileHeader'); },
|
||||||
selectMobileStylesheet: function() { this.set('selected', 'mobileStylesheet'); }
|
selectMobileStylesheet: function() { this.set('selected', 'mobileStylesheet'); }
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{textField class="hex-input" value=hexValue maxlength="6"}}
|
|
@ -421,6 +421,25 @@ section.details {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.color-scheme {
|
||||||
|
.controls {
|
||||||
|
span, button, a {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.colors {
|
||||||
|
thead th { border: none; }
|
||||||
|
td.hex { width: 100px; }
|
||||||
|
.hex-input { width: 80px; }
|
||||||
|
td.opacity { width: 50px; }
|
||||||
|
.opacity-input { width: 30px; }
|
||||||
|
.hex, .opacity { text-align: center; }
|
||||||
|
|
||||||
|
.changed .name {
|
||||||
|
color: darken($highlight_text_color, 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -790,12 +809,16 @@ table.api-keys {
|
||||||
color: $primary_text_color;
|
color: $primary_text_color;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
li:first-of-type {
|
||||||
|
border-top: 1px solid $primary_border_color;
|
||||||
|
}
|
||||||
li {
|
li {
|
||||||
border-bottom: 1px solid $primary_border_color;
|
border-bottom: 1px solid $primary_border_color;
|
||||||
}
|
}
|
||||||
|
|
33
app/controllers/admin/color_schemes_controller.rb
Normal file
33
app/controllers/admin/color_schemes_controller.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
class Admin::ColorSchemesController < Admin::AdminController
|
||||||
|
|
||||||
|
before_filter :fetch_color_scheme, only: [:update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
render_serialized(ColorScheme.current_version.order('id ASC').all.to_a, ColorSchemeSerializer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
color_scheme = ColorScheme.create(color_scheme_params)
|
||||||
|
render json: color_scheme, root: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
render json: ColorSchemeRevisor.revise(@color_scheme, color_scheme_params), root: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@color_scheme.destroy
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_color_scheme
|
||||||
|
@color_scheme = ColorScheme.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def color_scheme_params
|
||||||
|
params.permit(color_scheme: [:enabled, :name, colors: [:name, :hex, :opacity]])[:color_scheme]
|
||||||
|
end
|
||||||
|
end
|
51
app/models/color_scheme.rb
Normal file
51
app/models/color_scheme.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
class ColorScheme < ActiveRecord::Base
|
||||||
|
|
||||||
|
has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy
|
||||||
|
|
||||||
|
alias_method :colors, :color_scheme_colors
|
||||||
|
|
||||||
|
scope :current_version, ->{ where(versioned_id: nil) }
|
||||||
|
|
||||||
|
after_destroy :destroy_versions
|
||||||
|
|
||||||
|
def self.enabled
|
||||||
|
current_version.where(enabled: true).first || find(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_edit?
|
||||||
|
self.id != 1 # base theme shouldn't be edited, except by seed data
|
||||||
|
end
|
||||||
|
|
||||||
|
def colors=(arr)
|
||||||
|
@colors_by_name = nil
|
||||||
|
arr.each do |c|
|
||||||
|
self.color_scheme_colors << ColorSchemeColor.new(
|
||||||
|
name: c[:name],
|
||||||
|
hex: c[:hex],
|
||||||
|
opacity: c[:opacity].to_i
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def colors_by_name
|
||||||
|
@colors_by_name ||= self.colors.inject({}) { |sum,c| sum[c.name] = c; sum; }
|
||||||
|
end
|
||||||
|
def clear_colors_cache
|
||||||
|
@colors_by_name = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def colors_hashes
|
||||||
|
color_scheme_colors.map do |c|
|
||||||
|
{name: c.name, hex: c.hex, opacity: c.opacity}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def previous_version
|
||||||
|
ColorScheme.where(versioned_id: self.id).where('version < ?', self.version).order('version DESC').first
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_versions
|
||||||
|
ColorScheme.where(versioned_id: self.id).destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
99
app/models/color_scheme_color.rb
Normal file
99
app/models/color_scheme_color.rb
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
class ColorSchemeColor < ActiveRecord::Base
|
||||||
|
belongs_to :color_scheme
|
||||||
|
|
||||||
|
BASE_COLORS = {
|
||||||
|
primary_border_color: "e6e6e6",
|
||||||
|
secondary_border_color: "f5f5f5",
|
||||||
|
tertiary_border_color: "ffffff",
|
||||||
|
highlight_border_color: "ffff4d",
|
||||||
|
emphasis_border_color: "00aaff",
|
||||||
|
warning_border_color: "f0a28f",
|
||||||
|
success_border_color: "009900",
|
||||||
|
primary_background_color: "ffffff",
|
||||||
|
secondary_background_color: "333333",
|
||||||
|
tertiary_background_color: "4d4d4d",
|
||||||
|
moderator_background_color: "ffffe5",
|
||||||
|
emphasis_background_color: "e5f6ff",
|
||||||
|
success_background_color: "99ff99",
|
||||||
|
warning_background_color: "f6c7bc",
|
||||||
|
highlight_background_color: "ffffc2",
|
||||||
|
like_background_color: "fee7ed",
|
||||||
|
composer_background_color: "e6e6e6",
|
||||||
|
notification_badge_background_color: "8c8c8c",
|
||||||
|
primary_text_color: "333333",
|
||||||
|
secondary_text_color: "999999",
|
||||||
|
tertiary_text_color: "ffffff",
|
||||||
|
warning_text_color: "e45735",
|
||||||
|
success_text_color: "009900",
|
||||||
|
emphasis_text_color: "00aaff",
|
||||||
|
highlight_text_color: "ffff4d",
|
||||||
|
like_color: "fa6c8d",
|
||||||
|
primary_shadow_color: "333333",
|
||||||
|
secondary_shadow_color: "ffffff",
|
||||||
|
warning_shadow_color: "f0a28f",
|
||||||
|
success_shadow_color: "009900",
|
||||||
|
highlight: "ffff4d",
|
||||||
|
header_item_highlight: "e5f6ff",
|
||||||
|
link_color: "0088cc",
|
||||||
|
secondary_link_color: "ffffff",
|
||||||
|
"muted-link-color" => "8c8c8c",
|
||||||
|
"muted-important-link-color" => "8c8c8c",
|
||||||
|
"link-color-visited" => "0088cc",
|
||||||
|
"link-color-hover" => "0088cc",
|
||||||
|
"link-color-active" => "0088cc",
|
||||||
|
heatmap_high: "ea7c62",
|
||||||
|
heatmap_med: "e45735",
|
||||||
|
heatmap_low: "cb3d1b",
|
||||||
|
coldmap_high: "33bbff",
|
||||||
|
coldmap_med: "00aaff",
|
||||||
|
coldmap_low: "0088cc",
|
||||||
|
"btn-default-color" => "333333",
|
||||||
|
"btn-default-background-color" => "b3b3b3",
|
||||||
|
"btn-default-background-color-hover" => "f5f5f5",
|
||||||
|
"btn-primary-border-color" => "0088cc",
|
||||||
|
"btn-primary-background-color" => "00aaff",
|
||||||
|
"btn-primary-background-color-dark" => "00aaff",
|
||||||
|
"btn-primary-background-color-light" => "99ddff",
|
||||||
|
"btn-danger-border-color" => "cb3d1b",
|
||||||
|
"btn-danger-background-color" => "e45735",
|
||||||
|
"btn-danger-background-color-dark" => "cb3d1b",
|
||||||
|
"btn-danger-background-color-light" => "e45735",
|
||||||
|
"btn-success-background" => "00b300",
|
||||||
|
"nav-pills-color" => "333333",
|
||||||
|
"nav-pills-color-hover" => "e45735",
|
||||||
|
"nav-pills-border-color-hover" => "f9dad2",
|
||||||
|
"nav-pills-background-color-hover" => "f9dad2",
|
||||||
|
"nav-pills-color-active" => "e45735",
|
||||||
|
"nav-pills-border-color-active" => "cb3d1b",
|
||||||
|
"nav-pills-background-color-active" => "e45735",
|
||||||
|
"nav-stacked-color" => "333333",
|
||||||
|
"nav-stacked-border-color" => "cccccc",
|
||||||
|
"nav-stacked-background-color" => "f5f5f5",
|
||||||
|
"nav-stacked-divider-color" => "cccccc",
|
||||||
|
"nav-stacked-chevron-color" => "b3b3b3",
|
||||||
|
"nav-stacked-border-color-active" => "e45735",
|
||||||
|
"nav-stacked-background-color-active" => "e45735",
|
||||||
|
"nav-button-color-hover" => "333333",
|
||||||
|
"nav-button-background-color-hover" => "cccccc",
|
||||||
|
"nav-button-color-active" => "333333",
|
||||||
|
"nav-button-background-color-active" => "cccccc",
|
||||||
|
"modal-header-color" => "e45735",
|
||||||
|
"modal-header-border-color" => "b3b3b3",
|
||||||
|
"modal-close-button-color" => "b3b3b3",
|
||||||
|
"nav-like-button-color-hover" => "fa6c8d",
|
||||||
|
"nav-like-button-background-color-hover" => "fed9e1",
|
||||||
|
"nav-like-button-color-active" => "f83b67",
|
||||||
|
"nav-like-button-background-color-active" => "fed9e1",
|
||||||
|
"topic-list-border-color" => "b3b3b3",
|
||||||
|
"topic-list-th-color" => "8c8c8c",
|
||||||
|
"topic-list-th-border-color" => "b3b3b3",
|
||||||
|
"topic-list-th-background-color" => "f5f5f5",
|
||||||
|
"topic-list-td-color" => "8c8c8c",
|
||||||
|
"topic-list-td-border-color" => "cccccc",
|
||||||
|
"topic-list-star-color" => "cccccc",
|
||||||
|
"topic-list-starred-color" => "e45735",
|
||||||
|
"quote-background" => "f5f5f5",
|
||||||
|
topicMenuColor: "333333",
|
||||||
|
bookmarkColor: "00aaff"
|
||||||
|
}
|
||||||
|
end
|
7
app/serializers/color_scheme_color_serializer.rb
Normal file
7
app/serializers/color_scheme_color_serializer.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class ColorSchemeColorSerializer < ApplicationSerializer
|
||||||
|
attributes :name, :hex, :opacity
|
||||||
|
|
||||||
|
def hex
|
||||||
|
object.hex # otherwise something crazy is returned
|
||||||
|
end
|
||||||
|
end
|
9
app/serializers/color_scheme_serializer.rb
Normal file
9
app/serializers/color_scheme_serializer.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
class ColorSchemeSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :enabled, :can_edit
|
||||||
|
|
||||||
|
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
|
||||||
|
|
||||||
|
def can_edit
|
||||||
|
object.can_edit?
|
||||||
|
end
|
||||||
|
end
|
71
app/services/color_scheme_revisor.rb
Normal file
71
app/services/color_scheme_revisor.rb
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
class ColorSchemeRevisor
|
||||||
|
|
||||||
|
def initialize(color_scheme, params={})
|
||||||
|
@color_scheme = color_scheme
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.revise(color_scheme, params)
|
||||||
|
self.new(color_scheme, params).revise
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.revert(color_scheme)
|
||||||
|
self.new(color_scheme).revert
|
||||||
|
end
|
||||||
|
|
||||||
|
def revise
|
||||||
|
ColorScheme.transaction do
|
||||||
|
if @params[:enabled]
|
||||||
|
ColorScheme.update_all enabled: false
|
||||||
|
end
|
||||||
|
|
||||||
|
@color_scheme.name = @params[:name]
|
||||||
|
@color_scheme.enabled = @params[:enabled]
|
||||||
|
new_version = false
|
||||||
|
|
||||||
|
if @params[:colors]
|
||||||
|
new_version = @params[:colors].any? do |c|
|
||||||
|
(existing = @color_scheme.colors_by_name[c[:name]]).nil? or existing.hex != c[:hex] or existing.opacity != c[:opacity]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if new_version
|
||||||
|
old_version = ColorScheme.create(
|
||||||
|
name: @color_scheme.name,
|
||||||
|
enabled: false,
|
||||||
|
colors: @color_scheme.colors_hashes,
|
||||||
|
versioned_id: @color_scheme.id,
|
||||||
|
version: @color_scheme.version)
|
||||||
|
@color_scheme.version += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if @params[:colors]
|
||||||
|
@params[:colors].each do |c|
|
||||||
|
if existing = @color_scheme.colors_by_name[c[:name]]
|
||||||
|
existing.update_attributes(c)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@color_scheme.save!
|
||||||
|
@color_scheme.clear_colors_cache
|
||||||
|
end
|
||||||
|
@color_scheme
|
||||||
|
end
|
||||||
|
|
||||||
|
def revert
|
||||||
|
ColorScheme.transaction do
|
||||||
|
if prev = @color_scheme.previous_version
|
||||||
|
@color_scheme.version = prev.version
|
||||||
|
@color_scheme.colors.clear
|
||||||
|
prev.colors.update_all(color_scheme_id: @color_scheme.id)
|
||||||
|
prev.destroy
|
||||||
|
@color_scheme.save!
|
||||||
|
@color_scheme.clear_colors_cache
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@color_scheme
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1445,6 +1445,21 @@ en:
|
||||||
delete: "Delete"
|
delete: "Delete"
|
||||||
delete_confirm: "Delete this customization?"
|
delete_confirm: "Delete this customization?"
|
||||||
about: "Site Customization allow you to modify stylesheets and headers on the site. Choose or add one to start editing."
|
about: "Site Customization allow you to modify stylesheets and headers on the site. Choose or add one to start editing."
|
||||||
|
color: "Color"
|
||||||
|
opacity: "Opacity"
|
||||||
|
copy: "Copy"
|
||||||
|
css_html:
|
||||||
|
title: "CSS, HTML"
|
||||||
|
long_title: "CSS and HTML Customizations"
|
||||||
|
colors:
|
||||||
|
title: "Colors"
|
||||||
|
long_title: "Color Schemes"
|
||||||
|
about: "Color schemes allow you to modify the colors used on the site without writing CSS. Choose or add one to start."
|
||||||
|
new_name: "New Color Scheme"
|
||||||
|
copy_name_prefix: "Copy of"
|
||||||
|
delete_confirm: "Delete this color scheme?"
|
||||||
|
under_construction: "NOTE: Changes made here will do nothing! This feature is under construction!"
|
||||||
|
|
||||||
|
|
||||||
email:
|
email:
|
||||||
title: "Email"
|
title: "Email"
|
||||||
|
|
|
@ -1491,3 +1491,6 @@ en:
|
||||||
message_to_blank: "message.to is blank"
|
message_to_blank: "message.to is blank"
|
||||||
text_part_body_blank: "text_part.body is blank"
|
text_part_body_blank: "text_part.body is blank"
|
||||||
body_blank: "body is blank"
|
body_blank: "body is blank"
|
||||||
|
|
||||||
|
color_schemes:
|
||||||
|
base_theme_name: "Base"
|
||||||
|
|
|
@ -84,7 +84,9 @@ Discourse::Application.routes.draw do
|
||||||
resources :screened_urls, only: [:index]
|
resources :screened_urls, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
||||||
get "customize" => "site_customizations#index", constraints: AdminConstraint.new
|
get "customize" => "color_schemes#index", constraints: AdminConstraint.new
|
||||||
|
get "customize/css_html" => "site_customizations#index", constraints: AdminConstraint.new
|
||||||
|
get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new
|
||||||
get "flags" => "flags#index"
|
get "flags" => "flags#index"
|
||||||
get "flags/:filter" => "flags#index"
|
get "flags/:filter" => "flags#index"
|
||||||
post "flags/agree/:id" => "flags#agree"
|
post "flags/agree/:id" => "flags#agree"
|
||||||
|
@ -93,6 +95,7 @@ Discourse::Application.routes.draw do
|
||||||
resources :site_customizations, constraints: AdminConstraint.new
|
resources :site_customizations, constraints: AdminConstraint.new
|
||||||
resources :site_contents, constraints: AdminConstraint.new
|
resources :site_contents, constraints: AdminConstraint.new
|
||||||
resources :site_content_types, constraints: AdminConstraint.new
|
resources :site_content_types, constraints: AdminConstraint.new
|
||||||
|
resources :color_schemes, constraints: AdminConstraint.new
|
||||||
|
|
||||||
get "version_check" => "versions#show"
|
get "version_check" => "versions#show"
|
||||||
|
|
||||||
|
|
14
db/fixtures/701_color_schemes.rb
Normal file
14
db/fixtures/701_color_schemes.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
ColorScheme.seed do |s|
|
||||||
|
s.id = 1
|
||||||
|
s.name = I18n.t("color_schemes.base_theme_name")
|
||||||
|
s.enabled = false
|
||||||
|
end
|
||||||
|
|
||||||
|
ColorSchemeColor::BASE_COLORS.each_with_index do |color, i|
|
||||||
|
ColorSchemeColor.seed do |c|
|
||||||
|
c.id = i+1
|
||||||
|
c.name = color[0]
|
||||||
|
c.hex = color[1]
|
||||||
|
c.color_scheme_id = 1
|
||||||
|
end
|
||||||
|
end
|
13
db/migrate/20140416202746_create_color_schemes.rb
Normal file
13
db/migrate/20140416202746_create_color_schemes.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class CreateColorSchemes < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :color_schemes do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.boolean :enabled, null: false, default: false
|
||||||
|
|
||||||
|
t.integer :versioned_id
|
||||||
|
t.integer :version, null: false, default: 1
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
db/migrate/20140416202801_create_color_scheme_colors.rb
Normal file
14
db/migrate/20140416202801_create_color_scheme_colors.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
class CreateColorSchemeColors < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :color_scheme_colors do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :hex, null: false
|
||||||
|
t.integer :opacity, null: false, default: 100
|
||||||
|
t.integer :color_scheme_id, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :color_scheme_colors, [:color_scheme_id]
|
||||||
|
end
|
||||||
|
end
|
72
spec/controllers/admin/color_schemes_controller_spec.rb
Normal file
72
spec/controllers/admin/color_schemes_controller_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Admin::ColorSchemesController do
|
||||||
|
it "is a subclass of AdminController" do
|
||||||
|
(described_class < Admin::AdminController).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
context "while logged in as an admin" do
|
||||||
|
let!(:user) { log_in(:admin) }
|
||||||
|
let(:valid_params) { { color_scheme: {
|
||||||
|
name: 'Such Design',
|
||||||
|
enabled: true,
|
||||||
|
colors: [
|
||||||
|
{name: '$primary_background_color', hex: 'FFBB00', opacity: '100'},
|
||||||
|
{name: '$secondary_background_color', hex: '888888', opacity: '70'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
|
||||||
|
describe "index" do
|
||||||
|
it "returns success" do
|
||||||
|
xhr :get, :index
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns JSON" do
|
||||||
|
Fabricate(:color_scheme)
|
||||||
|
xhr :get, :index
|
||||||
|
::JSON.parse(response.body).should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create" do
|
||||||
|
it "returns success" do
|
||||||
|
xhr :post, :create, valid_params
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns JSON" do
|
||||||
|
xhr :post, :create, valid_params
|
||||||
|
::JSON.parse(response.body)['id'].should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update" do
|
||||||
|
let(:existing) { Fabricate(:color_scheme) }
|
||||||
|
|
||||||
|
it "returns success" do
|
||||||
|
ColorSchemeRevisor.expects(:revise).returns(existing)
|
||||||
|
xhr :put, :update, valid_params.merge(id: existing.id)
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns JSON" do
|
||||||
|
ColorSchemeRevisor.expects(:revise).returns(existing)
|
||||||
|
xhr :put, :update, valid_params.merge(id: existing.id)
|
||||||
|
::JSON.parse(response.body)['id'].should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "destroy" do
|
||||||
|
let!(:existing) { Fabricate(:color_scheme) }
|
||||||
|
|
||||||
|
it "returns success" do
|
||||||
|
expect {
|
||||||
|
xhr :delete, :destroy, id: existing.id
|
||||||
|
}.to change { ColorScheme.count }.by(-1)
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
6
spec/fabricators/color_scheme_color_fabricator.rb
Normal file
6
spec/fabricators/color_scheme_color_fabricator.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Fabricator(:color_scheme_color) do
|
||||||
|
color_scheme
|
||||||
|
name { sequence(:name) {|i| "$color_#{i}" } }
|
||||||
|
hex "333333"
|
||||||
|
opacity 100
|
||||||
|
end
|
5
spec/fabricators/color_scheme_fabricator.rb
Normal file
5
spec/fabricators/color_scheme_fabricator.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Fabricator(:color_scheme) do
|
||||||
|
name { sequence(:name) {|i| "Palette #{i}" } }
|
||||||
|
enabled false
|
||||||
|
color_scheme_colors(count: 2) { |attrs, i| Fabricate.build(:color_scheme_color, color_scheme: nil) }
|
||||||
|
end
|
43
spec/models/color_scheme_spec.rb
Normal file
43
spec/models/color_scheme_spec.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ColorScheme do
|
||||||
|
|
||||||
|
let(:valid_params) { {name: "Best Colors Evar", enabled: true, colors: valid_colors} }
|
||||||
|
let(:valid_colors) { [
|
||||||
|
{name: '$primary_background_color', hex: 'FFBB00', opacity: '100'},
|
||||||
|
{name: '$secondary_background_color', hex: '888888', opacity: '70'}
|
||||||
|
]}
|
||||||
|
|
||||||
|
describe "new" do
|
||||||
|
it "can take colors" do
|
||||||
|
c = described_class.new(valid_params)
|
||||||
|
c.colors.should have(valid_colors.size).colors
|
||||||
|
c.colors.first.should be_a(ColorSchemeColor)
|
||||||
|
expect {
|
||||||
|
c.save.should == true
|
||||||
|
}.to change { ColorSchemeColor.count }.by(valid_colors.size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "destroy" do
|
||||||
|
it "also destroys old versions" do
|
||||||
|
c1 = described_class.create(valid_params.merge(version: 2))
|
||||||
|
c2 = described_class.create(valid_params.merge(versioned_id: c1.id, version: 1))
|
||||||
|
other = described_class.create(valid_params)
|
||||||
|
expect {
|
||||||
|
c1.destroy
|
||||||
|
}.to change { described_class.count }.by(-2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#enabled" do
|
||||||
|
it "returns the base color scheme when there is no enabled record" do
|
||||||
|
described_class.enabled.id.should == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the enabled color scheme" do
|
||||||
|
c = described_class.create(valid_params.merge(enabled: true))
|
||||||
|
described_class.enabled.id.should == c.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
116
spec/services/color_scheme_revisor_spec.rb
Normal file
116
spec/services/color_scheme_revisor_spec.rb
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe ColorSchemeRevisor do
|
||||||
|
|
||||||
|
let(:color) { Fabricate.build(:color_scheme_color, hex: 'FFFFFF', color_scheme: nil) }
|
||||||
|
let(:color_scheme) { Fabricate(:color_scheme, enabled: false, created_at: 1.day.ago, updated_at: 1.day.ago, color_scheme_colors: [color]) }
|
||||||
|
let(:valid_params) { { name: color_scheme.name, enabled: color_scheme.enabled, colors: nil } }
|
||||||
|
|
||||||
|
describe "revise" do
|
||||||
|
it "does nothing if there are no changes" do
|
||||||
|
expect {
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(colors: nil))
|
||||||
|
}.to_not change { color_scheme.reload.updated_at }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can change the name" do
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(name: "Changed Name"))
|
||||||
|
color_scheme.reload.name.should == "Changed Name"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can enable and disable" do
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(enabled: true))
|
||||||
|
color_scheme.reload.should be_enabled
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(enabled: false))
|
||||||
|
color_scheme.reload.should_not be_enabled
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can change colors" do
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
|
{name: color.name, hex: 'BEEF99', opacity: 99}
|
||||||
|
]))
|
||||||
|
color_scheme.reload
|
||||||
|
color_scheme.colors.size.should == 1
|
||||||
|
color_scheme.colors.first.hex.should == 'BEEF99'
|
||||||
|
color_scheme.colors.first.opacity.should == 99
|
||||||
|
end
|
||||||
|
|
||||||
|
it "disables other color scheme before enabling" do
|
||||||
|
prev_enabled = Fabricate(:color_scheme, enabled: true)
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(enabled: true))
|
||||||
|
prev_enabled.reload.enabled.should == false
|
||||||
|
color_scheme.reload.enabled.should == true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "versions" do
|
||||||
|
it "doesn't create a new version if colors is not given" do
|
||||||
|
expect {
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(name: "Changed Name"))
|
||||||
|
}.to_not change { color_scheme.reload.version }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a new version if colors have changed" do
|
||||||
|
old_hex, old_opacity = color.hex, color.opacity
|
||||||
|
expect {
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
|
{name: color.name, hex: 'BEEF99', opacity: 99}
|
||||||
|
]))
|
||||||
|
}.to change { color_scheme.reload.version }.by(1)
|
||||||
|
old_version = ColorScheme.where(versioned_id: color_scheme.id, version: color_scheme.version - 1).first
|
||||||
|
old_version.should_not be_nil
|
||||||
|
old_version.colors.count.should == color_scheme.colors.count
|
||||||
|
old_version.colors_by_name[color.name].hex.should == old_hex
|
||||||
|
old_version.colors_by_name[color.name].opacity.should == old_opacity
|
||||||
|
color_scheme.colors_by_name[color.name].hex.should == 'BEEF99'
|
||||||
|
color_scheme.colors_by_name[color.name].opacity.should == 99
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't create a new version if colors have not changed" do
|
||||||
|
expect {
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(colors: [
|
||||||
|
{name: color.name, hex: color.hex, opacity: color.opacity}
|
||||||
|
]))
|
||||||
|
}.to_not change { color_scheme.reload.version }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "revert" do
|
||||||
|
context "when there are no previous versions" do
|
||||||
|
it "does nothing" do
|
||||||
|
expect {
|
||||||
|
described_class.revert(color_scheme).should == color_scheme
|
||||||
|
}.to_not change { color_scheme.reload.version }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are previous versions' do
|
||||||
|
let(:new_color_params) { {name: color.name, hex: 'BEEF99', opacity: 99} }
|
||||||
|
|
||||||
|
before do
|
||||||
|
@prev_hex, @prev_opacity = color.hex, color.opacity
|
||||||
|
described_class.revise(color_scheme, valid_params.merge(colors: [ new_color_params ]))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reverts the colors to the previous version" do
|
||||||
|
color_scheme.colors_by_name[new_color_params[:name]].hex.should == new_color_params[:hex]
|
||||||
|
expect {
|
||||||
|
described_class.revert(color_scheme)
|
||||||
|
}.to change { color_scheme.reload.version }.by(-1)
|
||||||
|
color_scheme.colors.size.should == 1
|
||||||
|
color_scheme.colors.first.hex.should == @prev_hex
|
||||||
|
color_scheme.colors.first.opacity.should == @prev_opacity
|
||||||
|
color_scheme.colors_by_name[new_color_params[:name]].hex.should == @prev_hex
|
||||||
|
color_scheme.colors_by_name[new_color_params[:name]].opacity.should == @prev_opacity
|
||||||
|
end
|
||||||
|
|
||||||
|
it "destroys the old version's record" do
|
||||||
|
expect {
|
||||||
|
described_class.revert(color_scheme)
|
||||||
|
}.to change { ColorScheme.count }.by(-1)
|
||||||
|
color_scheme.reload.previous_version.should be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue