mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 06:49:14 +08:00
FEATURE: allow themes to share color schemes
This commit is contained in:
parent
1872a1714f
commit
5e3a0846f7
|
@ -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) {
|
||||||
|
|
|
@ -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")];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -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}); })
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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..."
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddThemeIdToColorScheme < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :color_schemes, :theme_id, :int
|
||||||
|
end
|
||||||
|
end
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user