diff --git a/app/assets/stylesheets/alchemy/_variables.scss b/app/assets/stylesheets/alchemy/_variables.scss index 99692f3fd8..8d80fee2fe 100644 --- a/app/assets/stylesheets/alchemy/_variables.scss +++ b/app/assets/stylesheets/alchemy/_variables.scss @@ -145,6 +145,8 @@ $elements-window-min-width: 400px !default; $element-header-bg-color: $medium-gray !default; $element-header-active-bg-color: $dark-blue !default; $element-header-active-color: $white !default; +$element-header-deprecated-bg-color: rgba(253, 213, 175, 0.25) !default; +$element-deprecated-border-color: rgb(253, 213, 175) !default; $top-menu-height: 75px !default; $tabs-height: 31px !default; diff --git a/app/assets/stylesheets/alchemy/elements.scss b/app/assets/stylesheets/alchemy/elements.scss index 83cb5114e1..33930f4f76 100644 --- a/app/assets/stylesheets/alchemy/elements.scss +++ b/app/assets/stylesheets/alchemy/elements.scss @@ -170,13 +170,32 @@ } } + &.deprecated { + border-color: $element-deprecated-border-color; + + > .element-header { + background-color: $element-header-deprecated-bg-color; + background-image: linear-gradient( + 45deg, + $element-header-deprecated-bg-color 25%, + $element-header-bg-color 25%, + $element-header-bg-color 50%, + $element-header-deprecated-bg-color 50%, + $element-header-deprecated-bg-color 75%, + $element-header-bg-color 75%, + $element-header-bg-color 100% + ); + background-size: 28.28px 28.28px; + } + } + &.selected:not(.is-fixed), &:hover { &:not(.hidden) { box-shadow: 0 2px 8px rgba(#9b9b9b, 0.75); } } - &.selected:not(.is-fixed):not(.folded):not(.dirty):not(.hidden) { + &.selected:not(.is-fixed):not(.folded):not(.dirty):not(.hidden):not(.deprecated) { > .element-header { background-color: $element-header-active-bg-color; color: $element-header-active-color; @@ -670,6 +689,24 @@ select.long { } } + &.deprecated { + border-radius: $default-border-radius; + background-color: $element-header-deprecated-bg-color; + background-image: linear-gradient( + 45deg, + $element-header-deprecated-bg-color 25%, + $element-header-bg-color 25%, + $element-header-bg-color 50%, + $element-header-deprecated-bg-color 50%, + $element-header-deprecated-bg-color 75%, + $element-header-bg-color 75%, + $element-header-bg-color 100% + ); + background-size: 28.28px 28.28px; + padding-left: 2px; + padding-right: 2px; + } + label { display: block; margin: $default-margin 0; @@ -802,10 +839,6 @@ textarea.has_tinymce { } } -.element-handle .hint-with-icon { - top: -1px; -} - .is-fixed { &.with-contents { >.element-footer { diff --git a/app/decorators/alchemy/content_editor.rb b/app/decorators/alchemy/content_editor.rb index 4d7bcf6317..aef2b92d55 100644 --- a/app/decorators/alchemy/content_editor.rb +++ b/app/decorators/alchemy/content_editor.rb @@ -12,6 +12,7 @@ def css_classes [ "content_editor", essence_partial_name, + deprecated? ? "deprecated" : nil, ].compact end @@ -51,5 +52,68 @@ def respond_to?(method_name) super end + + def has_warnings? + definition.blank? || deprecated? + end + + def warnings + return unless has_warnings? + + if definition.blank? + Logger.warn("Content #{name} is missing its definition", caller(1..1)) + Alchemy.t(:content_definition_missing) + else + deprecation_notice + end + end + + # Returns a deprecation notice for contents marked deprecated + # + # You can either use localizations or pass a String as notice + # in the content definition. + # + # == Custom deprecation notices + # + # Use general content deprecation notice + # + # - name: element_name + # contents: + # - name: old_content + # type: EssenceText + # deprecated: true + # + # Add a translation to your locale file for a per content notice. + # + # en: + # alchemy: + # content_deprecation_notices: + # element_name: + # old_content: Foo baz widget is deprecated + # + # or use the global translation that apply to all deprecated contents. + # + # en: + # alchemy: + # content_deprecation_notice: Foo baz widget is deprecated + # + # or pass string as deprecation notice. + # + # - name: element_name + # contents: + # - name: old_content + # type: EssenceText + # deprecated: This content will be removed soon. + # + def deprecation_notice + case definition["deprecated"] + when String + definition["deprecated"] + when TrueClass + Alchemy.t(name, + scope: [:content_deprecation_notices, element.name], + default: Alchemy.t(:content_deprecated)) + end + end end end diff --git a/app/decorators/alchemy/element_editor.rb b/app/decorators/alchemy/element_editor.rb index f8eb4de4a6..0b7512c4a5 100644 --- a/app/decorators/alchemy/element_editor.rb +++ b/app/decorators/alchemy/element_editor.rb @@ -17,6 +17,7 @@ def css_classes taggable? ? "taggable" : "not-taggable", folded ? "folded" : "expanded", compact? ? "compact" : nil, + deprecated? ? "deprecated" : nil, fixed? ? "is-fixed" : "not-fixed", public? ? "visible" : "hidden", ].join(" ") @@ -36,5 +37,46 @@ def respond_to?(method_name) super end + + # Returns a deprecation notice for elements marked deprecated + # + # You can either use localizations or pass a String as notice + # in the element definition. + # + # == Custom deprecation notices + # + # Use general element deprecation notice + # + # - name: old_element + # deprecated: true + # + # Add a translation to your locale file for a per element notice. + # + # en: + # alchemy: + # element_deprecation_notices: + # old_element: Foo baz widget is deprecated + # + # or use the global translation that apply to all deprecated elements. + # + # en: + # alchemy: + # element_deprecation_notice: Foo baz widget is deprecated + # + # or pass string as deprecation notice. + # + # - name: old_element + # deprecated: This element will be removed soon. + # + def deprecation_notice + case definition["deprecated"] + when String + definition["deprecated"] + when TrueClass + Alchemy.t(name, + scope: :element_deprecation_notices, + default: Alchemy.t(:element_deprecated)) + end + end end end diff --git a/app/helpers/alchemy/admin/contents_helper.rb b/app/helpers/alchemy/admin/contents_helper.rb index 18b4fcf214..b8faa066e1 100644 --- a/app/helpers/alchemy/admin/contents_helper.rb +++ b/app/helpers/alchemy/admin/contents_helper.rb @@ -19,13 +19,8 @@ def render_content_name(content) content_name = content.name_for_label - if content.definition.blank? - warning("Content #{content.name} is missing its definition") - - icon = hint_with_tooltip( - Alchemy.t(:content_definition_missing), - ) - + if content.has_warnings? + icon = hint_with_tooltip(content.warnings) content_name = "#{icon} #{content_name}".html_safe end @@ -39,7 +34,7 @@ def render_content_name(content) # Renders the label and a remove link for a content. def content_label(content) content_tag :label, for: content.form_field_id do - [render_hint_for(content), render_content_name(content)].compact.join(" ").html_safe + [render_content_name(content), render_hint_for(content)].compact.join(" ").html_safe end end end diff --git a/app/models/alchemy/content.rb b/app/models/alchemy/content.rb index 20fc053681..98ec8029e2 100644 --- a/app/models/alchemy/content.rb +++ b/app/models/alchemy/content.rb @@ -192,6 +192,10 @@ def linked? essence && !essence.link.blank? end + def deprecated? + !!definition["deprecated"] + end + # Returns true if this content should be taken for element preview. def preview_content? !!definition["as_element_title"] diff --git a/app/models/alchemy/element.rb b/app/models/alchemy/element.rb index 1d6a8a0282..b852c836a2 100644 --- a/app/models/alchemy/element.rb +++ b/app/models/alchemy/element.rb @@ -37,6 +37,7 @@ class Element < BaseRecord "taggable", "compact", "message", + "deprecated", ].freeze SKIPPED_ATTRIBUTES_ON_COPY = [ @@ -252,6 +253,38 @@ def compact? definition["compact"] == true end + # Defined as deprecated element? + # + # You can either set true or a String on your elements definition. + # + # == Passing true + # + # - name: old_element + # deprecated: true + # + # The deprecation notice can be translated. Either as global notice for all deprecated elements. + # + # en: + # alchemy: + # element_deprecation_notice: Foo baz widget is deprecated + # + # Or add a translation to your locale file for a per element notice. + # + # en: + # alchemy: + # element_deprecation_notices: + # old_element: Foo baz widget is deprecated + # + # == Pass a String + # + # - name: old_element + # deprecated: This element will be removed soon. + # + # @return Boolean + def deprecated? + !!definition["deprecated"] + end + # The element's view partial is dependent from its name # # == Define elements diff --git a/app/views/alchemy/admin/elements/_element_header.html.erb b/app/views/alchemy/admin/elements/_element_header.html.erb index e8f4a0f61b..8a0d37a37d 100644 --- a/app/views/alchemy/admin/elements/_element_header.html.erb +++ b/app/views/alchemy/admin/elements/_element_header.html.erb @@ -2,6 +2,8 @@ <% if element.definition.blank? %> <%= hint_with_tooltip Alchemy.t(:element_definition_missing) %> + <% elsif element.deprecated? %> + <%= hint_with_tooltip element.deprecation_notice %> <% else %> <% if element.public? %> <%= render_icon('window-maximize', style: 'regular', class: 'element') %> diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml index 8e813f05ef..84040899fb 100644 --- a/config/locales/alchemy.en.yml +++ b/config/locales/alchemy.en.yml @@ -295,7 +295,9 @@ en: "Visit page": "Visit page" "Warning!": "Warning!" content_definition_missing: "Warning: Content is missing its definition. Please check the elements.yml" + content_deprecated: "WARNING! This content is deprecated and will be removed soon. Please do not use it anymore." element_definition_missing: "WARNING! Missing element definition. Please check your elements.yml file." + element_deprecated: "WARNING! This element is deprecated and will be removed soon. Please do not use it anymore." page_definition_missing: "WARNING! Missing page layout definition. Please check your page_layouts.yml file." "Welcome to Alchemy": "Welcome to Alchemy" "Who else is online": "Who else is online" diff --git a/spec/decorators/alchemy/content_editor_spec.rb b/spec/decorators/alchemy/content_editor_spec.rb index 69cf742917..c177fd9429 100644 --- a/spec/decorators/alchemy/content_editor_spec.rb +++ b/spec/decorators/alchemy/content_editor_spec.rb @@ -14,12 +14,24 @@ end describe "#css_classes" do + subject { content_editor.css_classes } + it "includes content_editor class" do - expect(content_editor.css_classes).to include("content_editor") + is_expected.to include("content_editor") end it "includes essence partial class" do - expect(content_editor.css_classes).to include(content_editor.essence_partial_name) + is_expected.to include(content_editor.essence_partial_name) + end + + context "when deprecated" do + before do + expect(content).to receive(:deprecated?) { true } + end + + it "includes deprecated" do + is_expected.to include("deprecated") + end end end @@ -70,4 +82,118 @@ it { is_expected.to be(false) } end + + describe "#has_warnings?" do + subject { content_editor.has_warnings? } + + context "when content is not deprecated" do + let(:content) { build(:alchemy_content) } + + it { is_expected.to be(false) } + end + + context "when content is deprecated" do + let(:content) do + mock_model("Content", definition: { deprecated: true }, deprecated?: true) + end + + it { is_expected.to be(true) } + end + + context "when content is missing its definition" do + let(:content) do + mock_model("Content", definition: {}) + end + + it { is_expected.to be(true) } + end + end + + describe "#warnings" do + subject { content_editor.warnings } + + context "when content has no warnings" do + let(:content) { build(:alchemy_content) } + + it { is_expected.to be_nil } + end + + context "when content is missing its definition" do + let(:content) do + mock_model("Content", name: "foo", definition: {}) + end + + it { is_expected.to eq Alchemy.t(:content_definition_missing) } + + it "logs a warning" do + expect(Alchemy::Logger).to receive(:warn) + subject + end + end + + context "when content is deprecated" do + let(:content) do + mock_model("Content", + name: "foo", + definition: { "name" => "foo", "deprecated" => "Deprecated" }, + deprecated?: true) + end + + it "returns a deprecation notice" do + is_expected.to eq("Deprecated") + end + end + end + + describe "#deprecation_notice" do + subject { content_editor.deprecation_notice } + + context "when content is not deprecated" do + let(:content) { build(:alchemy_content) } + + it { is_expected.to be_nil } + end + + context "when content is deprecated" do + let(:element) { build(:alchemy_element, name: "all_you_can_eat") } + let(:content) { build(:alchemy_content, name: "essence_html", element: element) } + + context "with custom content translation" do + it { is_expected.to eq("Old content is deprecated") } + end + + context "without custom content translation" do + let(:content) { build(:alchemy_content, name: "old_too", element: element) } + + before do + allow(content).to receive(:definition) do + { + "name" => "old_too", + "deprecated" => true, + } + end + end + + it do + is_expected.to eq( + "WARNING! This content is deprecated and will be removed soon. " \ + "Please do not use it anymore." + ) + end + end + + context "with String as deprecation" do + before do + allow(content).to receive(:definition) do + { + "name" => "old", + "deprecated" => "Foo baz widget", + } + end + end + + it { is_expected.to eq("Foo baz widget") } + end + end + end end diff --git a/spec/decorators/alchemy/element_editor_spec.rb b/spec/decorators/alchemy/element_editor_spec.rb index 7411d4800b..e9cfc3e7d5 100644 --- a/spec/decorators/alchemy/element_editor_spec.rb +++ b/spec/decorators/alchemy/element_editor_spec.rb @@ -98,6 +98,14 @@ it { is_expected.to include("not-nestable") } end + + context "with element being deprecated" do + before do + allow(element).to receive(:deprecated?) { true } + end + + it { is_expected.to include("deprecated") } + end end describe "#editable?" do @@ -141,4 +149,55 @@ it { is_expected.to be(false) } end + + describe "deprecation_notice" do + subject { element_editor.deprecation_notice } + + context "when element is not deprecated" do + let(:element) { build(:alchemy_element, name: "article") } + + it { is_expected.to be_nil } + end + + context "when element is deprecated" do + let(:element) { build(:alchemy_element, name: "old") } + + context "with custom element translation" do + it { is_expected.to eq("Old element is deprecated") } + end + + context "without custom element translation" do + let(:element) { build(:alchemy_element, name: "old_too") } + + before do + allow(element).to receive(:definition) do + { + "name" => "old_too", + "deprecated" => true, + } + end + end + + it do + is_expected.to eq( + "WARNING! This element is deprecated and will be removed soon. " \ + "Please do not use it anymore." + ) + end + end + + context "with String as deprecation" do + before do + allow(element).to receive(:definition) do + { + "name" => "old", + "deprecated" => "Foo baz widget", + } + end + end + + it { is_expected.to eq("Foo baz widget") } + end + end + end end diff --git a/spec/dummy/config/alchemy/elements.yml b/spec/dummy/config/alchemy/elements.yml index 36c9316a97..f80cc2b97b 100644 --- a/spec/dummy/config/alchemy/elements.yml +++ b/spec/dummy/config/alchemy/elements.yml @@ -110,6 +110,7 @@ - name: essence_html type: EssenceHtml hint: true + deprecated: true - name: essence_link type: EssenceLink hint: true @@ -169,3 +170,11 @@ contents: - name: menu type: EssenceNode + +- name: old + deprecated: true + contents: + - name: title + type: EssenceText + - name: text + type: EssenceRichtext diff --git a/spec/dummy/config/alchemy/page_layouts.yml b/spec/dummy/config/alchemy/page_layouts.yml index c1fd6b89e8..7355b7922d 100644 --- a/spec/dummy/config/alchemy/page_layouts.yml +++ b/spec/dummy/config/alchemy/page_layouts.yml @@ -20,7 +20,7 @@ autogenerate: [header, article, download] - name: everything - elements: [text, all_you_can_eat, gallery, right_column, left_column] + elements: [text, all_you_can_eat, gallery, right_column, left_column, old] autogenerate: [all_you_can_eat, right_column, left_column] - name: news diff --git a/spec/dummy/config/locales/alchemy.en.yml b/spec/dummy/config/locales/alchemy.en.yml index 3dc7b0f045..6cc35f9b66 100644 --- a/spec/dummy/config/locales/alchemy.en.yml +++ b/spec/dummy/config/locales/alchemy.en.yml @@ -29,6 +29,11 @@ en: resource_help_texts: party: name: Party + content_deprecation_notices: + all_you_can_eat: + essence_html: Old content is deprecated + element_deprecation_notices: + old: Old element is deprecated activemodel: models: diff --git a/spec/helpers/alchemy/admin/contents_helper_spec.rb b/spec/helpers/alchemy/admin/contents_helper_spec.rb index 00f0b6399a..68914bdfb1 100644 --- a/spec/helpers/alchemy/admin/contents_helper_spec.rb +++ b/spec/helpers/alchemy/admin/contents_helper_spec.rb @@ -22,10 +22,13 @@ let(:content) do mock_model "Content", name: "intro", - definition: {name: "intro", type: "EssenceText"}, + definition: { name: "intro", type: "EssenceText" }, name_for_label: "Intro", - has_validations?: false + has_validations?: false, + deprecated?: false, + has_warnings?: false end + subject { helper.render_content_name(content) } it "returns the content name" do @@ -41,11 +44,38 @@ end context "with missing definition" do - before { expect(content).to receive(:definition).and_return({}) } + let(:content) do + mock_model "Content", + name: "intro", + definition: { }, + name_for_label: "Intro", + has_validations?: false, + deprecated?: false, + has_warnings?: true, + warnings: Alchemy.t(:content_definition_missing) + end it "renders a warning with tooltip" do is_expected.to have_selector(".hint-with-icon .hint-bubble") - is_expected.to have_content("Intro") + is_expected.to have_content Alchemy.t(:content_definition_missing) + end + end + + context "when deprecated" do + let(:content) do + mock_model "Content", + name: "intro", + definition: { name: "intro", type: "EssenceText" }, + name_for_label: "Intro", + has_validations?: false, + deprecated?: true, + has_warnings?: true, + warnings: Alchemy.t(:content_deprecated) + end + + it "renders a deprecation notice with tooltip" do + is_expected.to have_selector(".hint-with-icon .hint-bubble") + is_expected.to have_content Alchemy.t(:content_deprecated) end end diff --git a/spec/models/alchemy/content_spec.rb b/spec/models/alchemy/content_spec.rb index f1649caec8..70f5f66f7b 100644 --- a/spec/models/alchemy/content_spec.rb +++ b/spec/models/alchemy/content_spec.rb @@ -263,6 +263,42 @@ module Alchemy end end + describe "#deprecated?" do + let(:content) { build_stubbed(:alchemy_content) } + + subject { content.deprecated? } + + context "not defined as deprecated" do + it "returns false" do + expect(content.deprecated?).to be false + end + end + + context "defined as deprecated" do + before do + expect(content).to receive(:definition).at_least(:once).and_return({ + "deprecated" => true, + }) + end + + it "returns true" do + expect(content.deprecated?).to be true + end + end + + context "defined as deprecated per String" do + before do + expect(content).to receive(:definition).at_least(:once).and_return({ + "deprecated" => "This content is deprecated", + }) + end + + it "returns true" do + expect(content.deprecated?).to be true + end + end + end + describe "#preview_content?" do let(:content) { build_stubbed(:alchemy_content) } diff --git a/spec/models/alchemy/element_spec.rb b/spec/models/alchemy/element_spec.rb index 2be5529197..006de3e28d 100644 --- a/spec/models/alchemy/element_spec.rb +++ b/spec/models/alchemy/element_spec.rb @@ -771,6 +771,31 @@ module Alchemy end end + describe "#deprecated?" do + subject { element.deprecated? } + + let(:element) { build(:alchemy_element) } + + before do + expect(element).to receive(:definition) { definition } + end + + context "definition has 'deprecated' key with true value" do + let(:definition) { { "deprecated" => true } } + it { is_expected.to be(true) } + end + + context "definition has 'deprecated' key with foo value" do + let(:definition) { { "deprecated" => "This is deprecated" } } + it { is_expected.to be(true) } + end + + context "definition has no 'deprecated' key" do + let(:definition) { { "name" => "article" } } + it { is_expected.to be(false) } + end + end + describe "#trash!" do let(:element) { create(:alchemy_element) } diff --git a/spec/models/alchemy/site_spec.rb b/spec/models/alchemy/site_spec.rb index 33577de080..12522f916c 100644 --- a/spec/models/alchemy/site_spec.rb +++ b/spec/models/alchemy/site_spec.rb @@ -301,6 +301,7 @@ module Alchemy "gallery", "right_column", "left_column", + "old", ], }, {