diff --git a/app/assets/javascripts/admin/components/themes-list.js.es6 b/app/assets/javascripts/admin/components/themes-list.js.es6 index 00b7af61547..56006005886 100644 --- a/app/assets/javascripts/admin/components/themes-list.js.es6 +++ b/app/assets/javascripts/admin/components/themes-list.js.es6 @@ -31,7 +31,7 @@ export default Ember.Component.extend({ ) inactiveThemes(themes) { if (this.get("componentsTabActive")) { - return themes.filter(theme => theme.get("parentThemes.length") <= 0); + return themes.filter(theme => theme.get("parent_themes.length") <= 0); } return themes.filter( theme => !theme.get("user_selectable") && !theme.get("default") @@ -46,7 +46,7 @@ export default Ember.Component.extend({ ) activeThemes(themes) { if (this.get("componentsTabActive")) { - return themes.filter(theme => theme.get("parentThemes.length") > 0); + return themes.filter(theme => theme.get("parent_themes.length") > 0); } else { themes = themes.filter( theme => theme.get("user_selectable") || theme.get("default") diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index 5ca9c901c29..10cfc8fe3ef 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -16,33 +16,91 @@ {{/each}} - {{#if model.remote_theme}} - {{#if model.remote_theme.remote_url}} - {{model.remote_theme.remote_url}} - {{/if}} - {{i18n "admin.customize.theme.about_theme"}} - {{#if model.remote_theme.license_url}} - {{i18n "admin.customize.theme.license"}} {{d-icon "copyright"}} - {{/if}} - {{/if}} - - {{#if model.parentThemes}} -
-
{{i18n "admin.customize.theme.component_of"}}
- + {{#unless model.enabled}} +
+ {{i18n "admin.customize.theme.required_version.error"}} + {{#if model.remote_theme.minimum_discourse_version}} + {{i18n "admin.customize.theme.required_version.minimum" version=model.remote_theme.minimum_discourse_version}} + {{/if}} + {{#if model.remote_theme.maximum_discourse_version}} + {{i18n "admin.customize.theme.required_version.minimum" version=model.remote_theme.maximum_discourse_version}} + {{/if}}
- {{/if}} + {{/unless}} {{#unless model.component}}
{{inline-edit-checkbox action=(action "applyDefault") labelKey="admin.customize.theme.is_default" checked=model.default}} {{inline-edit-checkbox action=(action "applyUserSelectable") labelKey="admin.customize.theme.user_selectable" checked=model.user_selectable}}
+ {{/unless}} + {{#if model.remote_theme}} + + {{#if model.remote_theme.remote_url}} + {{i18n "admin.customize.theme.source_url"}} {{d-icon "link"}} + {{/if}} + {{#if model.remote_theme.about_url}} + {{i18n "admin.customize.theme.about_theme"}} {{d-icon "link"}} + {{/if}} + {{#if model.remote_theme.license_url}} + {{i18n "admin.customize.theme.license"}} {{d-icon "link"}} + {{/if}} + + {{#if model.description}} + {{model.description}} + {{/if}} + + + {{#if model.remote_theme.authors}}{{i18n "admin.customize.theme.authors"}} {{model.remote_theme.authors}}{{/if}} + {{#if model.remote_theme.theme_version}}{{i18n "admin.customize.theme.version"}} {{model.remote_theme.theme_version}}{{/if}} + + +
+ {{#if model.remote_theme.is_git}} + + {{#if showRemoteError}} +
+ {{d-icon "exclamation-triangle"}} {{I18n "admin.customize.theme.repo_unreachable"}} +
+
+ {{model.remoteError}} +
+ {{/if}} + + {{#if model.remote_theme.commits_behind}} + {{#d-button action=(action "updateToLatest") icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} + {{else}} + {{#d-button action=(action "checkForThemeUpdates") icon="refresh" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} + {{/if}} + + + {{#if updatingRemote}} + {{i18n 'admin.customize.theme.updating'}} + {{else}} + {{#if model.remote_theme.commits_behind}} + {{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}} + {{#if model.remote_theme.github_diff_link}} + + {{i18n 'admin.customize.theme.compare_commits'}} + + {{/if}} + {{else}} + {{#unless showRemoteError}} + {{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}} + {{/unless}} + {{/if}} + {{/if}} + + {{else}} + + {{d-icon "info-circle"}} {{i18n "admin.customize.theme.imported_from_archive"}} + + {{/if}} +
+ {{/if}} + + {{#unless model.component}}
{{i18n "admin.customize.theme.color_scheme"}}
{{i18n "admin.customize.theme.color_scheme_select"}}
@@ -60,6 +118,17 @@
{{/unless}} + {{#if parentThemes}} +
+
{{i18n "admin.customize.theme.component_of"}}
+ +
+ {{/if}} +
{{i18n "admin.customize.theme.css_html"}}
{{#if model.hasEditedFields}} @@ -75,47 +144,7 @@
{{/if}} - {{#if model.remote_theme.is_git}} - {{#if model.remote_theme.commits_behind}} - {{#d-button action=(action "updateToLatest") icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} - {{else}} - {{#d-button action=(action "checkForThemeUpdates") icon="refresh" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} - {{/if}} - {{/if}} - {{#d-button action=(action "editTheme") class="btn btn-default edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} - {{#if model.remote_theme.is_git}} - - {{#if updatingRemote}} - {{i18n 'admin.customize.theme.updating'}} - {{else}} - {{#if model.remote_theme.commits_behind}} - {{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}} - {{#if model.remote_theme.github_diff_link}} - - {{i18n 'admin.customize.theme.compare_commits'}} - - {{/if}} - {{else}} - {{#unless showRemoteError}} - {{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}} - {{/unless}} - {{/if}} - {{/if}} - - {{#if showRemoteError}} -
- {{d-icon "exclamation-triangle"}} {{I18n "admin.customize.theme.repo_unreachable"}} -
-
- {{model.remoteError}} -
- {{/if}} - {{else if model.remote_theme}} - - {{d-icon "info-circle"}} {{i18n "admin.customize.theme.imported_from_archive"}} - - {{/if}}
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 0c233ff7810..4f3d151a973 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -137,26 +137,35 @@ display: inline-block; vertical-align: top; - .url { - margin-bottom: 10px; - } - .title { font-size: $font-up-4; font-weight: bold; margin-bottom: 10px; } + + .theme-description { + display: block; + margin: 10px 0; + } + + .metadata { + .authors, + .version { + display: block; + + .heading { + font-weight: bold; + } + } + } + .remote-url, .about-url, .license-url { - display: block; - margin-bottom: 10px; - } - .remote-url { - margin-top: -5px; - font-size: $font-down-1; - font-style: italic; + display: inline-block; + margin-right: 10px; } + .mini-title { font-size: $font-up-1; font-weight: bold; @@ -347,6 +356,7 @@ } .setting-label { width: 25%; + word-wrap: break-word; h3 { margin-top: 0; margin-bottom: 0.5rem; diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 0f1a28af62c..0e89c160dac 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -3,6 +3,14 @@ require_dependency 'theme_store/tgz_importer' require_dependency 'upload_creator' class RemoteTheme < ActiveRecord::Base + METADATA_PROPERTIES = %i{ + license_url + about_url + authors + theme_version + minimum_discourse_version + maximum_discourse_version + } class ImportError < StandardError; end @@ -16,6 +24,8 @@ class RemoteTheme < ActiveRecord::Base joins("JOIN themes ON themes.remote_theme_id = remote_themes.id").where.not(remote_url: "") } + validates_format_of :minimum_discourse_version, :maximum_discourse_version, with: Discourse::VERSION_REGEXP, allow_nil: true + def self.extract_theme_info(importer) JSON.parse(importer["about.json"]) rescue TypeError, JSON::ParserError @@ -123,8 +133,12 @@ class RemoteTheme < ActiveRecord::Base end end - self.license_url = theme_info["license_url"] - self.about_url = theme_info["about_url"] + METADATA_PROPERTIES.each do |property| + self.public_send(:"#{property}=", theme_info[property.to_s]) + end + if !self.valid? + raise ImportError, I18n.t("themes.import_error.about_json_values", errors: self.errors.full_messages.join(",")) + end importer.all_files.each do |filename| next unless opts = ThemeField.opts_from_file_path(filename) diff --git a/app/models/theme.rb b/app/models/theme.rb index 45cbcb6882d..df91e6e8f32 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -7,7 +7,6 @@ require_dependency 'theme_translation_parser' require_dependency 'theme_translation_manager' class Theme < ActiveRecord::Base - # TODO: remove in 2019 self.ignored_columns = ["key"] @@ -23,7 +22,7 @@ class Theme < ActiveRecord::Base has_many :child_themes, -> { order(:name) }, through: :child_theme_relation, source: :child_theme has_many :parent_themes, -> { order(:name) }, through: :parent_theme_relation, source: :parent_theme has_many :color_schemes - belongs_to :remote_theme + belongs_to :remote_theme, autosave: true has_one :settings_field, -> { where(target_id: Theme.targets[:settings], name: "yaml") }, class_name: 'ThemeField' @@ -53,9 +52,6 @@ class Theme < ActiveRecord::Base Theme.expire_site_cache! if saved_change_to_user_selectable? || saved_change_to_name? - @dependant_themes = nil - @included_themes = nil - remove_from_cache! clear_cached_settings! ColorScheme.hex_cache.clear @@ -125,16 +121,24 @@ class Theme < ActiveRecord::Base end def self.transform_ids(ids, extend: true) - return [] if ids.blank? + get_set_cache "#{extend ? "extended_" : ""}transformed_ids_#{ids.join("_")}" do + next [] if ids.blank? - ids.uniq! - parent = ids.first + ids = ids.dup + ids.uniq! + parent = ids.shift - components = ids[1..-1] - components.push(*components_for(parent)) if extend - components.sort!.uniq! + components = ids + components.push(*components_for(parent)) if extend + components.sort!.uniq! - [parent, *components] + all_ids = [parent, *components] + + enabled_ids = Theme.where(id: all_ids).includes(:remote_theme) + .select(&:enabled?).pluck(:id) + + all_ids & enabled_ids # Maintain ordering using intersection + end end def set_default! @@ -151,6 +155,18 @@ class Theme < ActiveRecord::Base SiteSetting.default_theme_id == id end + def enabled? + if minimum_version = remote_theme&.minimum_discourse_version + return false unless Discourse.has_needed_version?(Discourse::VERSION::STRING, minimum_version) + end + + if maximum_version = remote_theme&.maximum_discourse_version + return false unless Discourse.has_needed_version?(maximum_version, Discourse::VERSION::STRING) + end + + true + end + def component_validations return unless component @@ -234,7 +250,7 @@ class Theme < ActiveRecord::Base end def notify_theme_change(with_scheme: false) - theme_ids = (dependant_themes&.pluck(:id) || []).unshift(self.id) + theme_ids = Theme.transform_ids([id]) self.class.notify_theme_change(theme_ids, with_scheme: with_scheme) end @@ -244,30 +260,6 @@ class Theme < ActiveRecord::Base end end - def dependant_themes - @dependant_themes ||= resolve_dependant_themes(:up) - end - - def included_themes - @included_themes ||= resolve_dependant_themes(:down) - end - - def resolve_dependant_themes(direction) - if direction == :up - join_field = "parent_theme_id" - where_field = "child_theme_id" - elsif direction == :down - join_field = "child_theme_id" - where_field = "parent_theme_id" - else - raise "Unknown direction" - end - - return [] unless id - - Theme.joins("JOIN child_themes ON themes.id = child_themes.#{join_field}").where("#{where_field} = ?", id) - end - def self.resolve_baked_field(theme_ids, target, name) list_baked_fields(theme_ids, target, name).map { |f| f.value_baked || f.value }.join("\n") end @@ -293,7 +285,7 @@ class Theme < ActiveRecord::Base end def list_baked_fields(target, name) - theme_ids = (included_themes&.pluck(:id) || []).unshift(self.id) + theme_ids = Theme.transform_ids([id]) self.class.list_baked_fields(theme_ids, target, name) end @@ -338,7 +330,7 @@ class Theme < ActiveRecord::Base def all_theme_variables fields = {} - ids = (included_themes&.pluck(:id) || []).unshift(self.id) + ids = Theme.transform_ids([id]) ThemeField.find_by_theme_ids(ids).where(type_id: ThemeField.theme_var_type_ids).each do |field| next if fields.key?(field.name) fields[field.name] = field @@ -349,18 +341,22 @@ class Theme < ActiveRecord::Base def add_child_theme!(theme) new_relation = child_theme_relation.new(child_theme_id: theme.id) if new_relation.save - @included_themes = nil child_themes.reload save! + Theme.clear_cache! else raise Discourse::InvalidParameters.new(new_relation.errors.full_messages.join(", ")) end end - def translations + def internal_translations + translations(internal: true) + end + + def translations(internal: false) fallbacks = I18n.fallbacks[I18n.locale] begin - data = theme_fields.find_first_locale_fields([id], fallbacks).first&.translation_data(with_overrides: false) + data = theme_fields.find_first_locale_fields([id], fallbacks).first&.translation_data(with_overrides: false, internal: internal) return {} if data.nil? best_translations = {} fallbacks.reverse.each do |locale| @@ -400,7 +396,7 @@ class Theme < ActiveRecord::Base def included_settings hash = {} - self.included_themes.each do |theme| + Theme.where(id: Theme.transform_ids([id])).each do |theme| hash.merge!(theme.cached_settings) end @@ -435,17 +431,21 @@ class Theme < ActiveRecord::Base end def generate_metadata_hash - { - name: name, - about_url: remote_theme&.about_url, - license_url: remote_theme&.license_url, - component: component, - assets: {}.tap do |hash| + {}.tap do |meta| + meta[:name] = name + meta[:component] = component + + RemoteTheme::METADATA_PROPERTIES.each do |property| + meta[property] = remote_theme&.public_send(property) + end + + meta[:assets] = {}.tap do |hash| theme_fields.where(type_id: ThemeField.types[:theme_upload_var]).each do |field| hash[field.name] = "assets/#{field.upload.original_filename}" end - end, - color_schemes: {}.tap do |hash| + end + + meta[:color_schemes] = {}.tap do |hash| schemes = self.color_schemes # The selected color scheme may not belong to the theme, so include it anyway schemes = [self.color_scheme] + schemes if self.color_scheme @@ -453,7 +453,8 @@ class Theme < ActiveRecord::Base hash[scheme.name] = {}.tap { |colors| scheme.colors.each { |color| colors[color.name] = color.hex } } end end - } + + end end end diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index d7cd020b08f..34c24f1f496 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -119,17 +119,17 @@ class ThemeField < ActiveRecord::Base [doc.to_s, errors&.join("\n")] end - def raw_translation_data + def raw_translation_data(internal: false) # Might raise ThemeTranslationParser::InvalidYaml - ThemeTranslationParser.new(self).load + ThemeTranslationParser.new(self, internal: internal).load end - def translation_data(with_overrides: true) + def translation_data(with_overrides: true, internal: false) fallback_fields = theme.theme_fields.find_locale_fields([theme.id], I18n.fallbacks[name]) fallback_data = fallback_fields.each_with_index.map do |field, index| begin - field.raw_translation_data + field.raw_translation_data(internal: internal) rescue ThemeTranslationParser::InvalidYaml # If this is the locale with the error, raise it. # If not, let the other theme_field raise the error when it processes itself diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb index fbd9b8d6ffb..645285de28c 100644 --- a/app/serializers/theme_serializer.rb +++ b/app/serializers/theme_serializer.rb @@ -45,9 +45,9 @@ class BasicThemeSerializer < ApplicationSerializer end class RemoteThemeSerializer < ApplicationSerializer - attributes :id, :remote_url, :remote_version, :local_version, :about_url, - :license_url, :commits_behind, :remote_updated_at, :updated_at, - :github_diff_link, :last_error_text, :is_git? + attributes :id, :remote_url, :remote_version, :local_version, :commits_behind, + :remote_updated_at, :updated_at, :github_diff_link, :last_error_text, :is_git?, + :license_url, :about_url, :authors, :theme_version, :minimum_discourse_version, :maximum_discourse_version # wow, AMS has some pretty nutty logic where it tries to find the path here # from action dispatch, tell it not to @@ -61,7 +61,7 @@ class RemoteThemeSerializer < ApplicationSerializer end class ThemeSerializer < BasicThemeSerializer - attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id, :settings, :errors + attributes :color_scheme, :color_scheme_id, :user_selectable, :remote_theme_id, :settings, :errors, :enabled?, :description has_one :user, serializer: UserNameSerializer, embed: :object @@ -102,4 +102,8 @@ class ThemeSerializer < BasicThemeSerializer def include_errors? @errors.present? end + + def description + object.internal_translations.find { |t| t.key == "theme_metadata.description" } &.value + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3227ab57ccc..faea38838b6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3386,8 +3386,15 @@ en: is_private: "Theme is in a private git repository" remote_branch: "Branch name (optional)" public_key: "Grant the following public key access to the repo:" - about_theme: "About Theme" + about_theme: "About" license: "License" + version: "Version:" + authors: "Authored by:" + source_url: "Source" + required_version: + error: "This theme has been automatically disabled because it is not compatible with this version of Discourse." + minimum: "Requires Discourse version {{version}} or above." + maximum: "Requires Discourse version {{version}} or below." component_of: "Component of:" update_to_latest: "Update to Latest" check_for_updates: "Check for Updates" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ce915400901..e8d7e0420d8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -77,6 +77,7 @@ en: import_error: generic: An error occured while importing that theme about_json: "Import Error: about.json does not exist, or is invalid" + about_json_values: "about.json contains invalid values: %{errors}" git: "Error cloning git repository, access is denied or repository is not found" unpack_failed: "Failed to unpack file" errors: diff --git a/db/migrate/20190122132732_add_fields_to_remote_themes.rb b/db/migrate/20190122132732_add_fields_to_remote_themes.rb new file mode 100644 index 00000000000..8cd22f963a9 --- /dev/null +++ b/db/migrate/20190122132732_add_fields_to_remote_themes.rb @@ -0,0 +1,8 @@ +class AddFieldsToRemoteThemes < ActiveRecord::Migration[5.2] + def change + add_column :remote_themes, :authors, :string + add_column :remote_themes, :theme_version, :string + add_column :remote_themes, :minimum_discourse_version, :string + add_column :remote_themes, :maximum_discourse_version, :string + end +end diff --git a/lib/theme_translation_parser.rb b/lib/theme_translation_parser.rb index 3ce75803331..616c395a928 100644 --- a/lib/theme_translation_parser.rb +++ b/lib/theme_translation_parser.rb @@ -1,8 +1,10 @@ class ThemeTranslationParser + INTERNAL_KEYS = [:theme_metadata] class InvalidYaml < StandardError; end - def initialize(setting_field) + def initialize(setting_field, internal: internal) @setting_field = setting_field + @internal = internal end def self.check_contains_hashes(hash) @@ -22,6 +24,9 @@ class ThemeTranslationParser parsed.deep_symbolize_keys! + parsed[@setting_field.name.to_sym].slice!(*INTERNAL_KEYS) if @internal + parsed[@setting_field.name.to_sym].except!(*INTERNAL_KEYS) if !@internal + parsed end end diff --git a/lib/version.rb b/lib/version.rb index 73c53ea98c7..9238a0388de 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -1,4 +1,6 @@ module Discourse + VERSION_REGEXP = /\A\d+\.\d+\.\d+(\.beta\d+)?\z/ unless defined? ::Discourse::VERSION_REGEXP + # work around reloader unless defined? ::Discourse::VERSION module VERSION #:nodoc: diff --git a/spec/components/theme_store/tgz_exporter_spec.rb b/spec/components/theme_store/tgz_exporter_spec.rb index 46da6defc13..cde028d12dd 100644 --- a/spec/components/theme_store/tgz_exporter_spec.rb +++ b/spec/components/theme_store/tgz_exporter_spec.rb @@ -11,7 +11,9 @@ describe ThemeStore::TgzExporter do image = file_from_fixtures("logo.png") upload = UploadCreator.new(image, "logo.png").create_for(-1) theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var) - theme.build_remote_theme(remote_url: "", about_url: "abouturl", license_url: "licenseurl") + theme.build_remote_theme(remote_url: "", about_url: "abouturl", license_url: "licenseurl", + authors: "David Taylor", theme_version: "1.0", minimum_discourse_version: "1.0.0", + maximum_discourse_version: "3.0.0.beta1") cs1 = Fabricate(:color_scheme, name: 'Orphan Color Scheme', color_scheme_colors: [ Fabricate(:color_scheme_color, name: 'header_primary', hex: 'F0F0F0'), @@ -71,6 +73,10 @@ describe ThemeStore::TgzExporter do "assets": { "logo": "assets/logo.png" }, + "authors": "David Taylor", + "minimum_discourse_version": "1.0.0", + "maximum_discourse_version": "3.0.0.beta1", + "theme_version": "1.0", "color_schemes": { "Orphan Color Scheme": { "header_primary": "F0F0F0", diff --git a/spec/models/remote_theme_spec.rb b/spec/models/remote_theme_spec.rb index 490ac1694ea..1e9f23c98a4 100644 --- a/spec/models/remote_theme_spec.rb +++ b/spec/models/remote_theme_spec.rb @@ -24,6 +24,8 @@ describe RemoteTheme do "name": "awesome theme", "about_url": "#{about_url}", "license_url": "https://www.site.com/license", + "theme_version": "1.0", + "minimum_discourse_version": "1.0.0", "assets": { "font": "assets/awesome.woff2" }, @@ -72,6 +74,8 @@ describe RemoteTheme do expect(remote.about_url).to eq("https://www.site.com/about") expect(remote.license_url).to eq("https://www.site.com/license") + expect(remote.theme_version).to eq("1.0") + expect(remote.minimum_discourse_version).to eq("1.0.0") expect(@theme.theme_fields.length).to eq(6) diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 1a4b758ee0e..7654f9d9f34 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -57,10 +57,12 @@ describe Theme do end - it 'can correctly find parent themes' do - theme.add_child_theme!(child) + it "can automatically disable for mismatching version" do + expect(theme.enabled?).to eq(true) + theme.create_remote_theme!(remote_url: "", minimum_discourse_version: "99.99.99") + expect(theme.enabled?).to eq(false) - expect(child.dependant_themes.length).to eq(1) + expect(Theme.transform_ids([theme.id])).to be_empty end it "doesn't allow multi-level theme components" do @@ -174,30 +176,34 @@ HTML end describe ".transform_ids" do + let!(:orphan1) { Fabricate(:theme, component: true) } let!(:child) { Fabricate(:theme, component: true) } let!(:child2) { Fabricate(:theme, component: true) } + let!(:orphan2) { Fabricate(:theme, component: true) } + let!(:orphan3) { Fabricate(:theme, component: true) } + let!(:orphan4) { Fabricate(:theme, component: true) } before do theme.add_child_theme!(child) theme.add_child_theme!(child2) end + it "returns an empty array if no ids are passed" do + expect(Theme.transform_ids([])).to eq([]) + end + it "adds the child themes of the parent" do sorted = [child.id, child2.id].sort expect(Theme.transform_ids([theme.id])).to eq([theme.id, *sorted]) - fake_id = [child.id, child2.id, theme.id].min - 5 - fake_id2 = [child.id, child2.id, theme.id].max + 5 - - expect(Theme.transform_ids([theme.id, fake_id2, fake_id])) - .to eq([theme.id, fake_id, *sorted, fake_id2]) + expect(Theme.transform_ids([theme.id, orphan1.id, orphan2.id])).to eq([theme.id, orphan1.id, *sorted, orphan2.id]) end it "doesn't insert children when extend is false" do - fake_id = theme.id + 1 - fake_id2 = fake_id + 2 - fake_id3 = fake_id2 + 3 + fake_id = orphan2.id + fake_id2 = orphan3.id + fake_id3 = orphan4.id expect(Theme.transform_ids([theme.id], extend: false)).to eq([theme.id]) expect(Theme.transform_ids([theme.id, fake_id3, fake_id, fake_id2, fake_id2], extend: false)) @@ -466,6 +472,8 @@ HTML it "can list working theme_translation_manager objects" do en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML) en: + theme_metadata: + description: "Description of my theme" group_of_translations: translation1: en test1 translation2: en test2 @@ -510,6 +518,18 @@ HTML ]) end + it "can list internal theme_translation_manager objects" do + en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML) + en: + theme_metadata: + description: "Description of my theme" + another_translation: en test4 + YAML + translations = theme.internal_translations + expect(translations.map(&:key)).to contain_exactly("theme_metadata.description") + expect(translations.map(&:value)).to contain_exactly("Description of my theme") + end + it "can create a hash of overridden values" do en_translation = ThemeField.create!(theme_id: theme.id, name: "en", type_id: ThemeField.types[:yaml], target_id: Theme.targets[:translations], value: <<~YAML) en: diff --git a/test/javascripts/admin/components/themes-list-test.js.es6 b/test/javascripts/admin/components/themes-list-test.js.es6 index 2a1c58849fc..3842e8d8663 100644 --- a/test/javascripts/admin/components/themes-list-test.js.es6 +++ b/test/javascripts/admin/components/themes-list-test.js.es6 @@ -10,7 +10,8 @@ const components = [1, 2, 3, 4, 5].map(num => Theme.create({ name: `Child ${num}`, component: true, - parentThemes: [themes[num - 1]] + parentThemes: [themes[num - 1]], + parent_themes: [1, 2, 3, 4, 5] }) );