Theming: color scheme editing. Unfinished! Doesn't have any effect on css files yet.
This commit is contained in:
parent
0f4014eef1
commit
feaaf55a0c
|
@ -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.
|
||||
|
||||
@class AdminCustomizeController
|
||||
@class AdminCustomizeCssHtmlController
|
||||
@extends Ember.Controller
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminCustomizeController = Ember.ArrayController.extend({
|
||||
Discourse.AdminCustomizeCssHtmlController = Ember.ArrayController.extend({
|
||||
|
||||
actions: {
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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
|
||||
|
||||
@class AdminCustomizeRoute
|
||||
@class AdminCustomizeIndexRoute
|
||||
@extends Discourse.Route
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminCustomizeRoute = Discourse.Route.extend({
|
||||
|
||||
model: function() {
|
||||
return Discourse.SiteCustomization.findAll();
|
||||
Discourse.AdminCustomizeIndexRoute = Discourse.Route.extend({
|
||||
redirect: function() {
|
||||
this.transitionTo('adminCustomize.css_html');
|
||||
}
|
||||
|
||||
});
|
||||
});
|
|
@ -22,7 +22,10 @@ Discourse.Route.buildRoutes(function() {
|
|||
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.resource('admin.backups', { path: '/backups' }, function() {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<li>{{#link-to 'adminFlags'}}{{i18n admin.flags.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminLogs'}}{{i18n admin.logs.title}}{{/link-to}}</li>
|
||||
{{#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.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,70 +1,12 @@
|
|||
<div class='content-list span6'>
|
||||
<h3>{{i18n admin.customize.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 class='admin-controls'>
|
||||
<div class='span15'>
|
||||
<ul class="nav nav-pills">
|
||||
<li>{{#link-to 'adminCustomize.colors'}}{{i18n admin.customize.colors.title}}{{/link-to}}</li>
|
||||
<li>{{#link-to 'adminCustomize.css_html'}}{{i18n admin.customize.css_html.title}}{{/link-to}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</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 class="admin-container">
|
||||
{{outlet}}
|
||||
</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',
|
||||
classNames: ['customize'],
|
||||
selected: 'stylesheet',
|
||||
headerActive: Em.computed.equal('selected', 'header'),
|
||||
stylesheetActive: Em.computed.equal('selected', 'stylesheet'),
|
||||
headerActive: Em.computed.equal('selected', 'header'),
|
||||
stylesheetActive: Em.computed.equal('selected', 'stylesheet'),
|
||||
mobileHeaderActive: Em.computed.equal('selected', 'mobileHeader'),
|
||||
mobileStylesheetActive: Em.computed.equal('selected', 'mobileStylesheet'),
|
||||
|
||||
actions: {
|
||||
selectHeader: function() { this.set('selected', 'header'); },
|
||||
selectStylesheet: function() { this.set('selected', 'stylesheet'); },
|
||||
|
||||
selectHeader: function() { this.set('selected', 'header'); },
|
||||
selectStylesheet: function() { this.set('selected', 'stylesheet'); },
|
||||
selectMobileHeader: function() { this.set('selected', 'mobileHeader'); },
|
||||
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;
|
||||
font-size: 15px;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li:first-of-type {
|
||||
border-top: 1px solid $primary_border_color;
|
||||
}
|
||||
li {
|
||||
border-bottom: 1px solid $primary_border_color;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
class ColorSchemeColorSerializer < ApplicationSerializer
|
||||
attributes :name, :hex, :opacity
|
||||
|
||||
def hex
|
||||
object.hex # otherwise something crazy is returned
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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_confirm: "Delete this customization?"
|
||||
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:
|
||||
title: "Email"
|
||||
|
|
|
@ -1491,3 +1491,6 @@ en:
|
|||
message_to_blank: "message.to is blank"
|
||||
text_part_body_blank: "text_part.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]
|
||||
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/:filter" => "flags#index"
|
||||
post "flags/agree/:id" => "flags#agree"
|
||||
|
@ -93,6 +95,7 @@ Discourse::Application.routes.draw do
|
|||
resources :site_customizations, constraints: AdminConstraint.new
|
||||
resources :site_contents, constraints: AdminConstraint.new
|
||||
resources :site_content_types, constraints: AdminConstraint.new
|
||||
resources :color_schemes, constraints: AdminConstraint.new
|
||||
|
||||
get "version_check" => "versions#show"
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
Fabricator(:color_scheme_color) do
|
||||
color_scheme
|
||||
name { sequence(:name) {|i| "$color_#{i}" } }
|
||||
hex "333333"
|
||||
opacity 100
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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…
Reference in New Issue