FEATURE: Multiple SCSS file support for themes (#7351)

Theme developers can include any number of scss files within the /scss/ directory of a theme. These can then be imported from the main common/desktop/mobile scss.
This commit is contained in:
David Taylor
2019-04-12 11:36:08 +01:00
committed by GitHub
parent 0e9a0a31f5
commit 268d4d4c82
19 changed files with 302 additions and 125 deletions

View File

@ -124,13 +124,14 @@ class RemoteTheme < ActiveRecord::Base
end
theme_info = RemoteTheme.extract_theme_info(importer)
updated_fields = []
theme_info["assets"]&.each do |name, relative_path|
if path = importer.real_path(relative_path)
new_path = "#{File.dirname(path)}/#{SecureRandom.hex}#{File.extname(path)}"
File.rename(path, new_path) # OptimizedImage has strict file name restrictions, so rename temporarily
upload = UploadCreator.new(File.open(new_path), File.basename(relative_path), for_theme: true).create_for(theme.user_id)
theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
updated_fields << theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
end
end
@ -144,9 +145,13 @@ class RemoteTheme < ActiveRecord::Base
importer.all_files.each do |filename|
next unless opts = ThemeField.opts_from_file_path(filename)
value = importer[filename]
theme.set_field(opts.merge(value: value))
updated_fields << theme.set_field(opts.merge(value: value))
end
# Destroy fields that no longer exist in the remote theme
field_ids_to_destroy = theme.theme_fields.pluck(:id) - updated_fields.map(&:id)
ThemeField.where(id: field_ids_to_destroy).destroy_all
if !skip_update
self.remote_updated_at = Time.zone.now
self.remote_version = importer.version

View File

@ -51,6 +51,10 @@ class Theme < ActiveRecord::Base
Theme.expire_site_cache! if saved_change_to_user_selectable? || saved_change_to_name?
reload
settings_field&.ensure_baked! # Other fields require setting to be **baked**
theme_fields.each(&:ensure_baked!)
remove_from_cache!
clear_cached_settings!
ColorScheme.hex_cache.clear
@ -76,6 +80,8 @@ class Theme < ActiveRecord::Base
Theme.expire_site_cache!
ColorScheme.hex_cache.clear
CSP::Extension.clear_theme_extensions_cache!
SvgSprite.expire_cache
end
after_commit ->(theme) do
@ -224,7 +230,7 @@ class Theme < ActiveRecord::Base
end
def self.targets
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3, translations: 4)
@targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3, translations: 4, extra_scss: 5)
end
def self.lookup_target(target_id)
@ -267,15 +273,15 @@ class Theme < ActiveRecord::Base
def self.list_baked_fields(theme_ids, target, name)
target = target.to_sym
name = name.to_sym
name = name&.to_sym
if target == :translations
fields = ThemeField.find_first_locale_fields(theme_ids, I18n.fallbacks[name])
else
fields = ThemeField.find_by_theme_ids(theme_ids)
.where(target_id: [Theme.targets[target], Theme.targets[:common]])
.where(name: name.to_s)
.order(:target_id)
fields = fields.where(name: name.to_s) unless name.nil?
fields = fields.order(:target_id)
end
fields.each(&:ensure_baked!)
@ -325,6 +331,7 @@ class Theme < ActiveRecord::Base
changed_fields << field
end
end
field
else
theme_fields.build(target_id: target_id, value: value, name: name, type_id: type_id, upload_id: upload_id) if value.present? || upload_id.present?
end

View File

@ -7,11 +7,6 @@ class ThemeField < ActiveRecord::Base
belongs_to :upload
has_one :javascript_cache, dependent: :destroy
after_commit do |field|
SvgSprite.expire_cache if field.target_id == Theme.targets[:settings]
SvgSprite.expire_cache if field.name == SvgSprite.theme_sprite_variable_name
end
scope :find_by_theme_ids, ->(theme_ids) {
return none unless theme_ids.present?
@ -221,18 +216,16 @@ class ThemeField < ActiveRecord::Base
end
self.error = errors.join("\n").presence
if !self.error && self.target_id == Theme.targets[:settings]
# when settings YAML changes, we need to re-transpile theme JS and CSS
theme.theme_fields.where.not(id: self.id).update_all(value_baked: nil)
end
end
def self.guess_type(name:, target:)
if html_fields.include?(name.to_s)
if basic_targets.include?(target.to_s) && html_fields.include?(name.to_s)
types[:html]
elsif scss_fields.include?(name.to_s)
elsif basic_targets.include?(target.to_s) && scss_fields.include?(name.to_s)
types[:scss]
elsif name.to_s == "yaml" || target.to_s == "translations"
elsif target.to_s == "extra_scss"
types[:scss]
elsif target.to_s == "settings" || target.to_s == "translations"
types[:yaml]
end
end
@ -245,46 +238,93 @@ class ThemeField < ActiveRecord::Base
@scss_fields ||= %w(scss embedded_scss)
end
def self.basic_targets
@basic_targets ||= %w(common desktop mobile)
end
def basic_html_field?
ThemeField.basic_targets.include?(Theme.targets[self.target_id].to_s) &&
ThemeField.html_fields.include?(self.name)
end
def basic_scss_field?
ThemeField.basic_targets.include?(Theme.targets[self.target_id].to_s) &&
ThemeField.scss_fields.include?(self.name)
end
def extra_scss_field?
Theme.targets[self.target_id] == :extra_scss
end
def settings_field?
Theme.targets[:settings] == self.target_id
end
def translation_field?
Theme.targets[:translations] == self.target_id
end
def svg_sprite_field?
ThemeField.theme_var_type_ids.include?(self.type_id) && self.name == SvgSprite.theme_sprite_variable_name
end
def ensure_baked!
if ThemeField.html_fields.include?(self.name) || translation = Theme.targets[:translations] == self.target_id
if !self.value_baked || compiler_version != COMPILER_VERSION
self.value_baked, self.error = translation ? process_translation : process_html(self.value)
self.error = nil unless self.error.present?
self.compiler_version = COMPILER_VERSION
needs_baking = !self.value_baked || compiler_version != COMPILER_VERSION
return unless needs_baking
if self.will_save_change_to_value_baked? ||
self.will_save_change_to_compiler_version? ||
self.will_save_change_to_error?
self.update_columns(value_baked: value_baked,
compiler_version: compiler_version,
error: error)
end
end
if basic_html_field? || translation_field?
self.value_baked, self.error = translation_field? ? process_translation : process_html(self.value)
self.error = nil unless self.error.present?
self.compiler_version = COMPILER_VERSION
elsif basic_scss_field?
ensure_scss_compiles!
Stylesheet::Manager.clear_theme_cache!
elsif settings_field?
validate_yaml!
theme.clear_cached_settings!
CSP::Extension.clear_theme_extensions_cache!
SvgSprite.expire_cache
self.value_baked = "baked"
self.compiler_version = COMPILER_VERSION
elsif svg_sprite_field?
SvgSprite.expire_cache
self.error = nil
self.value_baked = "baked"
self.compiler_version = COMPILER_VERSION
end
if self.will_save_change_to_value_baked? ||
self.will_save_change_to_compiler_version? ||
self.will_save_change_to_error?
self.update_columns(value_baked: value_baked,
compiler_version: compiler_version,
error: error)
end
end
def compile_scss
Stylesheet::Compiler.compile("@import \"common/foundation/variables\"; @import \"theme_variables\"; @import \"theme_field\";",
"theme.scss",
theme_field: self.value.dup,
theme: self.theme
)
end
def ensure_scss_compiles!
if ThemeField.scss_fields.include?(self.name)
begin
Stylesheet::Compiler.compile("@import \"common/foundation/variables\"; @import \"theme_variables\"; @import \"theme_field\";",
"theme.scss",
theme_field: self.value.dup,
theme: self.theme
)
self.error = nil unless error.nil?
rescue SassC::SyntaxError => e
self.error = e.message unless self.destroyed?
end
if will_save_change_to_error?
update_columns(error: self.error)
end
result = ["failed"]
begin
result = compile_scss
self.error = nil unless error.nil?
rescue SassC::SyntaxError => e
self.error = e.message unless self.destroyed?
end
self.compiler_version = COMPILER_VERSION
self.value_baked = Digest::SHA1.hexdigest(result.join(",")) # We don't use the compiled CSS here, we just use it to invalidate the stylesheet cache
end
def target_name
Theme.targets.invert[target_id].to_s
Theme.targets[target_id].to_s
end
class ThemeFileMatcher
@ -311,7 +351,7 @@ class ThemeField < ActiveRecord::Base
hash = {}
OPTIONS.each do |option|
plural = :"#{option}s"
hash[option] = @allowed_values[plural][0] if @allowed_values[plural].length == 1
hash[option] = @allowed_values[plural][0] if @allowed_values[plural] && @allowed_values[plural].length == 1
hash[option] = match[option] if hash[option].nil?
end
hash
@ -337,6 +377,9 @@ class ThemeField < ActiveRecord::Base
ThemeFileMatcher.new(regex: /^common\/embedded\.scss$/,
targets: :common, names: "embedded_scss", types: :scss,
canonical: -> (h) { "common/embedded.scss" }),
ThemeFileMatcher.new(regex: /^scss\/(?<name>.+)\.scss$/,
targets: :extra_scss, names: nil, types: :scss,
canonical: -> (h) { "scss/#{h[:name]}.scss" }),
ThemeFileMatcher.new(regex: /^settings\.ya?ml$/,
names: "yaml", types: :yaml, targets: :settings,
canonical: -> (h) { "settings.yml" }),
@ -370,24 +413,33 @@ class ThemeField < ActiveRecord::Base
nil
end
before_save do
validate_yaml!
def dependent_fields
if extra_scss_field?
return theme.theme_fields.where(target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] },
name: ThemeField.scss_fields)
elsif settings_field?
return theme.theme_fields.where(target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] },
name: ThemeField.scss_fields + ThemeField.html_fields)
end
ThemeField.none
end
def invalidate_baked!
update_column(:value_baked, nil)
dependent_fields.update_all(value_baked: nil)
end
before_save do
if will_save_change_to_value? && !will_save_change_to_value_baked?
self.value_baked = nil
end
end
after_save do
dependent_fields.each(&:invalidate_baked!)
end
after_commit do
unless destroyed?
ensure_baked!
ensure_scss_compiles!
theme.clear_cached_settings!
end
Stylesheet::Manager.clear_theme_cache! if self.name.include?("scss")
CSP::Extension.clear_theme_extensions_cache! if name == 'yaml'
# TODO message for mobile vs desktop
MessageBus.publish "/header-change/#{theme.id}", self.value if theme && self.name == "header"
MessageBus.publish "/footer-change/#{theme.id}", self.value if theme && self.name == "footer"

View File

@ -5,13 +5,12 @@ class ThemeSetting < ActiveRecord::Base
validates :data_type, numericality: { only_integer: true }
validates :name, length: { maximum: 255 }
after_save do
theme.clear_cached_settings!
theme.remove_from_cache!
theme.theme_fields.update_all(value_baked: nil)
theme.theme_settings.reload
SvgSprite.expire_cache if self.name.to_s.include?("_icon")
CSP::Extension.clear_theme_extensions_cache! if name.to_s == CSP::Extension::THEME_SETTING
after_save :clear_settings_cache
after_destroy :clear_settings_cache
def clear_settings_cache
# All necessary caches will be cleared on next ensure_baked!
theme.settings_field&.invalidate_baked!
end
def self.types