FEATURE: allow themes to share color schemes

This commit is contained in:
Sam 2017-04-17 15:56:13 -04:00
parent 1872a1714f
commit 5e3a0846f7
14 changed files with 137 additions and 16 deletions

View File

@ -3,6 +3,13 @@ import { url } from 'discourse/lib/computed';
export default Ember.Controller.extend({ export default Ember.Controller.extend({
@computed("model", "allThemes")
parentThemes(model, allThemes) {
let parents = allThemes.filter(theme =>
_.contains(theme.get("childThemes"), model));
return parents.length === 0 ? null : parents;
},
@computed("model.theme_fields.@each") @computed("model.theme_fields.@each")
hasEditedFields(fields) { hasEditedFields(fields) {
return fields.any(f=>!Em.isBlank(f.value)); return fields.any(f=>!Em.isBlank(f.value));
@ -48,8 +55,6 @@ export default Ember.Controller.extend({
return themes.length === 0 ? null : themes; return themes.length === 0 ? null : themes;
}, },
showSchemes: Em.computed.or("model.default", "model.user_selectable"),
@computed("allThemes", "allThemes.length", "model") @computed("allThemes", "allThemes.length", "model")
availableChildThemes(allThemes, count) { availableChildThemes(allThemes, count) {
if (count === 1) { if (count === 1) {

View File

@ -0,0 +1,10 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
@computed('model', 'model.@each')
sortedThemes(themes) {
return _.sortBy(themes.content, t => {
return [!t.get("default"), !t.get("user_selectable"), t.get("name")];
});
}
});

View File

@ -34,6 +34,7 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, {
}.property('name', 'colors.@each.changed', 'saving'), }.property('name', 'colors.@each.changed', 'saving'),
disableSave: function() { disableSave: function() {
if (this.get('theme_id')) { return false; }
return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); }); return !this.get('changed') || this.get('saving') || _.any(this.get('colors'), function(c) { return !c.get('valid'); });
}.property('changed'), }.property('changed'),
@ -100,6 +101,8 @@ ColorScheme.reopenClass({
id: colorScheme.id, id: colorScheme.id,
name: colorScheme.name, name: colorScheme.name,
is_base: colorScheme.is_base, is_base: colorScheme.is_base,
theme_id: colorScheme.theme_id,
theme_name: colorScheme.theme_name,
base_scheme_id: colorScheme.base_scheme_id, base_scheme_id: colorScheme.base_scheme_id,
colors: colorScheme.colors.map(function(c) { return ColorSchemeColor.create({name: c.name, hex: c.hex, default_hex: c.default_hex}); }) colors: colorScheme.colors.map(function(c) { return ColorSchemeColor.create({name: c.name, hex: c.hex, default_hex: c.default_hex}); })
})); }));

View File

@ -1,11 +1,17 @@
<div class="color-scheme show-current-style"> <div class="color-scheme show-current-style">
<div class="admin-container"> <div class="admin-container">
<h1>{{text-field class="style-name" value=model.name}}</h1> <h1>{{#if model.theme_id}}{{model.name}}{{else}}{{text-field class="style-name" value=model.name}}{{/if}}</h1>
<div class="controls"> <div class="controls">
{{#unless model.theme_id}}
<button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button> <button {{action "save"}} disabled={{model.disableSave}} class='btn'>{{i18n 'admin.customize.save'}}</button>
{{/unless}}
<button {{action "copy" model}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button> <button {{action "copy" model}} class='btn'><i class="fa fa-copy"></i> {{i18n 'admin.customize.copy'}}</button>
{{#if model.theme_id}}
{{i18n "admin.customize.theme_owner"}}
{{#link-to "adminCustomizeThemes.show" model.theme_id}}{{model.theme_name}}{{/link-to}}
{{else}}
<button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button> <button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i> {{i18n 'admin.customize.delete'}}</button>
{{/if}}
<span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span> <span class="saving {{unless model.savingStatus 'hidden'}}">{{model.savingStatus}}</span>
</div> </div>
@ -39,8 +45,10 @@
</td> </td>
<td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td> <td class="hex">{{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}}</td>
<td class="actions"> <td class="actions">
{{#unless model.theme_id}}
<button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button> <button class="btn revert {{unless c.savedIsOverriden 'invisible'}}" {{action "revert" c}} title="{{i18n 'admin.customize.colors.revert_title'}}">{{i18n 'revert'}}</button>
<button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button> <button class="btn undo {{unless c.changed 'invisible'}}" {{action "undo" c}} title="{{i18n 'admin.customize.colors.undo_title'}}">{{i18n 'undo'}}</button>
{{/unless}}
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View File

@ -21,12 +21,19 @@
{{/if}} {{/if}}
{{#if parentThemes}}
<h3>{{i18n "admin.customize.theme.component_of"}}</h3>
<ul>
{{#each parentThemes as |theme|}}
<li>{{#link-to 'adminCustomizeThemes.show' theme replace=true}}{{theme.name}}{{/link-to}}</li>
{{/each}}
</ul>
{{else}}
<p> <p>
{{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}} {{inline-edit-checkbox action="applyDefault" labelKey="admin.customize.theme.is_default" checked=model.default}}
{{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}} {{inline-edit-checkbox action="applyUserSelectable" labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
</p> </p>
{{#if showSchemes}}
<h3>{{i18n "admin.customize.theme.color_scheme"}}</h3> <h3>{{i18n "admin.customize.theme.color_scheme"}}</h3>
<p>{{i18n "admin.customize.theme.color_scheme_select"}}</p> <p>{{i18n "admin.customize.theme.color_scheme_select"}}</p>
<p>{{combo-box content=colorSchemes <p>{{combo-box content=colorSchemes
@ -80,7 +87,7 @@
</p> </p>
{{#if availableChildThemes}} {{#if availableChildThemes}}
<h3>{{i18n "admin.customize.theme.included_themes"}}</h3> <h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
{{#unless model.childThemes.length}} {{#unless model.childThemes.length}}
<p> <p>
<label class='checkbox-label'> <label class='checkbox-label'>
@ -91,7 +98,7 @@
{{else}} {{else}}
<ul> <ul>
{{#each model.childThemes as |child|}} {{#each model.childThemes as |child|}}
<li>{{child.name}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}</li> <li>{{#link-to 'adminCustomizeThemes.show' child replace=true}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit" icon="times"}}</li>
{{/each}} {{/each}}
</ul> </ul>
{{/unless}} {{/unless}}

View File

@ -2,7 +2,7 @@
<div class='content-list span6'> <div class='content-list span6'>
<h3>{{i18n 'admin.customize.theme.long_title'}}</h3> <h3>{{i18n 'admin.customize.theme.long_title'}}</h3>
<ul> <ul>
{{#each model as |theme|}} {{#each sortedThemes as |theme|}}
<li> <li>
{{#link-to 'adminCustomizeThemes.show' theme replace=true}} {{#link-to 'adminCustomizeThemes.show' theme replace=true}}
{{theme.name}} {{theme.name}}

View File

@ -1,5 +1,11 @@
// Customise area // Customise area
.customize { .customize {
h1 {
margin-bottom: 10px;
input {
margin-bottom: 0;
}
}
.admin-container { .admin-container {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;

View File

@ -52,6 +52,7 @@ class ColorScheme < ActiveRecord::Base
after_save :publish_discourse_stylesheet after_save :publish_discourse_stylesheet
after_save :dump_hex_cache after_save :dump_hex_cache
after_destroy :dump_hex_cache after_destroy :dump_hex_cache
belongs_to :theme
validates_associated :color_scheme_colors validates_associated :color_scheme_colors

View File

@ -36,6 +36,7 @@ class RemoteTheme < ActiveRecord::Base
def update_from_remote(importer=nil) def update_from_remote(importer=nil)
return unless remote_url return unless remote_url
cleanup = false cleanup = false
unless importer unless importer
cleanup = true cleanup = true
importer = GitImporter.new(remote_url) importer = GitImporter.new(remote_url)
@ -61,12 +62,13 @@ class RemoteTheme < ActiveRecord::Base
theme_info = JSON.parse(importer["about.json"]) theme_info = JSON.parse(importer["about.json"])
self.license_url ||= theme_info["license_url"] self.license_url ||= theme_info["license_url"]
self.about_url ||= theme_info["about_url"] self.about_url ||= theme_info["about_url"]
self.remote_updated_at = Time.zone.now self.remote_updated_at = Time.zone.now
self.remote_version = importer.version self.remote_version = importer.version
self.local_version = importer.version self.local_version = importer.version
self.commits_behind = 0 self.commits_behind = 0
update_theme_color_schemes(theme, theme_info["color_schemes"])
self self
ensure ensure
begin begin
@ -75,6 +77,39 @@ class RemoteTheme < ActiveRecord::Base
Rails.logger.warn("Failed cleanup remote git #{e}") Rails.logger.warn("Failed cleanup remote git #{e}")
end end
end end
def normalize_override(hex)
return unless hex
override = hex.downcase
if override !~ /[0-9a-f]{6}/
override = nil
end
override
end
def update_theme_color_schemes(theme, schemes)
return if schemes.blank?
schemes.each do |name, colors|
existing = theme.color_schemes.find_by(name: name)
if existing
existing.colors.each do |c|
override = normalize_override(colors[c.name])
if override && c.hex != override
c.hex = override
theme.notify_color_change(c)
end
end
else
scheme = theme.color_schemes.build(name: name)
ColorScheme.base.colors_hashes.each do |color|
override = normalize_override(colors[color[:name]])
scheme.color_scheme_colors << ColorSchemeColor.new(name: color[:name], hex: override || color[:hex])
end
end
end
end
end end
# == Schema Information # == Schema Information

View File

@ -12,6 +12,7 @@ class Theme < ActiveRecord::Base
has_many :theme_fields, dependent: :destroy has_many :theme_fields, dependent: :destroy
has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy
has_many :child_themes, through: :child_theme_relation, source: :child_theme has_many :child_themes, through: :child_theme_relation, source: :child_theme
has_many :color_schemes
belongs_to :remote_theme belongs_to :remote_theme
before_create do before_create do
@ -19,7 +20,13 @@ class Theme < ActiveRecord::Base
true true
end end
def notify_color_change(color)
changed_colors << color
end
after_save do after_save do
changed_colors.each(&:save!)
changed_colors.clear
changed_fields.each(&:save!) changed_fields.each(&:save!)
changed_fields.clear changed_fields.clear
@ -222,6 +229,10 @@ class Theme < ActiveRecord::Base
@changed_fields ||= [] @changed_fields ||= []
end end
def changed_colors
@changed_colors ||= []
end
def set_field(target, name, value) def set_field(target, name, value)
name = name.to_s name = name.to_s

View File

@ -1,4 +1,8 @@
class ColorSchemeSerializer < ApplicationSerializer class ColorSchemeSerializer < ApplicationSerializer
attributes :id, :name, :is_base, :base_scheme_id attributes :id, :name, :is_base, :base_scheme_id, :theme_id, :theme_name
has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects has_many :colors, serializer: ColorSchemeColorSerializer, embed: :objects
def theme_name
object.theme&.name
end
end end

View File

@ -2796,6 +2796,7 @@ en:
color: "Color" color: "Color"
opacity: "Opacity" opacity: "Opacity"
copy: "Copy" copy: "Copy"
theme_owner: "Not editable, owned by:"
email_templates: email_templates:
title: "Email Templates" title: "Email Templates"
subject: "Subject" subject: "Subject"
@ -2821,7 +2822,7 @@ en:
color_scheme: "Color Scheme" color_scheme: "Color Scheme"
color_scheme_select: "Select colors to be used by theme" color_scheme_select: "Select colors to be used by theme"
custom_sections: "Custom sections:" custom_sections: "Custom sections:"
included_themes: "Included Themes" theme_components: "Theme Components"
child_themes_check: "Theme includes other child themes" child_themes_check: "Theme includes other child themes"
css_html: "Custom CSS/HTML" css_html: "Custom CSS/HTML"
edit_css_html: "Edit CSS/HTML" edit_css_html: "Edit CSS/HTML"
@ -2830,6 +2831,7 @@ en:
import_file_tip: ".dcstyle.json file containing theme" import_file_tip: ".dcstyle.json file containing theme"
about_theme: "About Theme" about_theme: "About Theme"
license: "License" license: "License"
component_of: "Theme is a component of:"
update_to_latest: "Update to Latest" update_to_latest: "Update to Latest"
check_for_updates: "Check for Updates" check_for_updates: "Check for Updates"
updating: "Updating..." updating: "Updating..."

View File

@ -0,0 +1,5 @@
class AddThemeIdToColorScheme < ActiveRecord::Migration
def change
add_column :color_schemes, :theme_id, :int
end
end

View File

@ -18,13 +18,26 @@ describe RemoteTheme do
repo_dir repo_dir
end end
let :initial_repo do def about_json(options = {})
setup_git_repo( options[:love] ||= "FAFAFA"
"about.json" => '{
<<JSON
{
"name": "awesome theme", "name": "awesome theme",
"about_url": "https://www.site.com/about", "about_url": "https://www.site.com/about",
"license_url": "https://www.site.com/license" "license_url": "https://www.site.com/license",
}', "color_schemes": {
"Amazing": {
"love": "#{options[:love]}"
}
}
}
JSON
end
let :initial_repo do
setup_git_repo(
"about.json" => about_json,
"desktop/desktop.scss" => "body {color: red;}", "desktop/desktop.scss" => "body {color: red;}",
"common/header.html" => "I AM HEADER", "common/header.html" => "I AM HEADER",
"common/random.html" => "I AM SILLY", "common/random.html" => "I AM SILLY",
@ -62,9 +75,16 @@ describe RemoteTheme do
expect(remote.remote_updated_at).to eq(time) expect(remote.remote_updated_at).to eq(time)
scheme = ColorScheme.find_by(theme_id: @theme.id)
expect(scheme.name).to eq("Amazing")
expect(scheme.colors.find_by(name: 'love').hex).to eq('fafafa')
File.write("#{initial_repo}/common/header.html", "I AM UPDATED") File.write("#{initial_repo}/common/header.html", "I AM UPDATED")
File.write("#{initial_repo}/about.json", about_json(love: "EAEAEA"))
`cd #{initial_repo} && git commit -am "update"` `cd #{initial_repo} && git commit -am "update"`
time = Time.new('2001') time = Time.new('2001')
freeze_time time freeze_time time
@ -77,6 +97,10 @@ describe RemoteTheme do
@theme.save @theme.save
@theme.reload @theme.reload
scheme = ColorScheme.find_by(theme_id: @theme.id)
expect(scheme.name).to eq("Amazing")
expect(scheme.colors.find_by(name: 'love').hex).to eq('eaeaea')
mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten] mapped = Hash[*@theme.theme_fields.map{|f| ["#{f.target}-#{f.name}", f.value]}.flatten]
expect(mapped["0-header"]).to eq("I AM UPDATED") expect(mapped["0-header"]).to eq("I AM UPDATED")