From b6800ea36247161af3a97dd047cf7da121db496b Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Thu, 21 Apr 2022 07:52:05 +0200 Subject: [PATCH] Merge pull request #2292 from dbwinger/paste-single-element Allow pasting into parent element with only one nested element type --- .../alchemy/admin/elements_controller.rb | 29 ++++++---- app/models/alchemy/element.rb | 15 +++-- .../alchemy/admin/clipboard/insert.js.erb | 13 +++++ .../_add_nested_element_form.html.erb | 27 +++++++++ .../alchemy/admin/elements/_element.html.erb | 24 +------- .../alchemy/admin/elements/_form.html.erb | 6 +- .../alchemy/admin/elements_controller_spec.rb | 2 +- .../admin/edit_elements_feature_spec.rb | 51 ++++++++++++++--- spec/models/alchemy/element_spec.rb | 55 +++++++++++++++---- 9 files changed, 163 insertions(+), 59 deletions(-) create mode 100644 app/views/alchemy/admin/elements/_add_nested_element_form.html.erb diff --git a/app/controllers/alchemy/admin/elements_controller.rb b/app/controllers/alchemy/admin/elements_controller.rb index c9eeac749d..001433a5b9 100644 --- a/app/controllers/alchemy/admin/elements_controller.rb +++ b/app/controllers/alchemy/admin/elements_controller.rb @@ -12,6 +12,7 @@ def index elements = @page_version.elements.order(:position).includes(*element_includes) @elements = elements.not_nested.unfixed @fixed_elements = elements.not_nested.fixed + load_clipboard_items end def new @@ -20,8 +21,7 @@ def new @parent_element = Element.find_by(id: params[:parent_element_id]) @elements = @page.available_elements_within_current_scope(@parent_element) @element = @page_version.elements.build - @clipboard = get_clipboard("elements") - @clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page) + load_clipboard_items end # Creates a element as discribed in config/alchemy/elements.yml on page via AJAX. @@ -44,8 +44,7 @@ def create else @element.page_version = @page_version @elements = @page.available_element_definitions - @clipboard = get_clipboard("elements") - @clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page) + load_clipboard_items render :new end end @@ -126,19 +125,27 @@ def load_element @element = Element.find(params[:id]) end + def load_clipboard_items + @clipboard = get_clipboard("elements") + @clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page) + end + def element_from_clipboard @element_from_clipboard ||= begin - @clipboard = get_clipboard("elements") - @clipboard.detect { |item| item["id"].to_i == params[:paste_from_clipboard].to_i } - end + @clipboard = get_clipboard("elements") + @clipboard.detect { |item| item["id"].to_i == params[:paste_from_clipboard].to_i } + end end def paste_element_from_clipboard @source_element = Element.find(element_from_clipboard["id"]) - element = Element.copy(@source_element, { - parent_element_id: create_element_params[:parent_element_id], - page_version_id: @page_version.id, - }) + element = Element.copy( + @source_element, + { + parent_element_id: create_element_params[:parent_element_id], + page_version_id: @page_version.id, + } + ) if element_from_clipboard["action"] == "cut" @cut_element_id = @source_element.id @clipboard.delete_if { |item| item["id"] == @source_element.id.to_s } diff --git a/app/models/alchemy/element.rb b/app/models/alchemy/element.rb index 3a7183273a..23893fc5c2 100644 --- a/app/models/alchemy/element.rb +++ b/app/models/alchemy/element.rb @@ -159,7 +159,7 @@ def copy(source_element, differences = {}) end def all_from_clipboard(clipboard) - return [] if clipboard.nil? + return none if clipboard.nil? where(id: clipboard.collect { |e| e["id"] }) end @@ -167,11 +167,16 @@ def all_from_clipboard(clipboard) # All elements in clipboard that could be placed on page # def all_from_clipboard_for_page(clipboard, page) - return [] if clipboard.nil? || page.nil? + return none if clipboard.nil? || page.nil? - all_from_clipboard(clipboard).select { |ce| - page.available_element_names.include?(ce.name) - } + all_from_clipboard(clipboard).where(name: page.available_element_names) + end + + # All elements in clipboard that could be placed as a child of `parent_element` + def all_from_clipboard_for_parent_element(clipboard, parent_element) + return none if clipboard.nil? || parent_element.nil? + + all_from_clipboard(clipboard).where(name: parent_element.definition["nestable_elements"]) end end diff --git a/app/views/alchemy/admin/clipboard/insert.js.erb b/app/views/alchemy/admin/clipboard/insert.js.erb index 900d1b5427..d68d8c98fc 100644 --- a/app/views/alchemy/admin/clipboard/insert.js.erb +++ b/app/views/alchemy/admin/clipboard/insert.js.erb @@ -14,3 +14,16 @@ Alchemy.growl('<%= j Alchemy.t("item copied to clipboard", name: @item.class.name == "Alchemy::Element" ? @item.display_name_with_preview_text : @item.name) %>') <% end -%> $('#clipboard_button .icon').removeClass('fa-clipboard').addClass('fa-paste'); + +<%# Update add nested element forms for any elements that accept ONLY this as a nested element %> +<% if @item.class == Alchemy::Element %> + if (window.location.pathname == "<%= edit_admin_page_path(@item.page.id) %>") { + <% + @item.page.draft_version.elements.expanded.select do |element| + element.definition["nestable_elements"] == [@item.name] + end.each do |element| + %> + $(".add-nested-element[data-element-id='<%= element.id %>']").replaceWith('<%= j render "alchemy/admin/elements/add_nested_element_form", element: element %>') + <% end %> + } +<% end %> diff --git a/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb b/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb new file mode 100644 index 0000000000..6f49ce4b79 --- /dev/null +++ b/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb @@ -0,0 +1,27 @@ +<%= content_tag :div, class: 'add-nested-element', data: { element_id: element.id } do %> + <% if element.expanded? || element.fixed? %> + <% if element.nestable_elements.length == 1 && + (nestable_element = element.nestable_elements.first) && + Alchemy::Element.all_from_clipboard_for_parent_element(get_clipboard("elements"), element).none? + %> + <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)], + remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %> + <%= f.hidden_field :name %> + <%= f.hidden_field :page_version_id, value: element.page_version_id %> + <%= f.hidden_field :parent_element_id, value: element.id %> + + <% end %> + <% else %> + <%= link_to_dialog (nestable_element ? Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element, scope: 'element_names')) : Alchemy.t("New Element")), + alchemy.new_admin_element_path( + parent_element_id: element.id, + page_version_id: element.page_version_id + ), { + size: "320x125", + title: Alchemy.t("New Element") + }, class: "button add-nestable-element-button" %> + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/alchemy/admin/elements/_element.html.erb b/app/views/alchemy/admin/elements/_element.html.erb index e3bd427b39..e865e74b72 100644 --- a/app/views/alchemy/admin/elements/_element.html.erb +++ b/app/views/alchemy/admin/elements/_element.html.erb @@ -55,29 +55,7 @@ } %> <% end %> - <% if element.expanded? || element.fixed? %> - <% if element.nestable_elements.length == 1 %> - <% nestable_element = element.nestable_elements.first %> - <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)], - remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %> - <%= f.hidden_field :name %> - <%= f.hidden_field :page_version_id, value: element.page_version_id %> - <%= f.hidden_field :parent_element_id, value: element.id %> - - <% end %> - <% else %> - <%= link_to_dialog Alchemy.t("New Element"), - alchemy.new_admin_element_path( - parent_element_id: element.id, - page_version_id: element.page_version_id - ), { - size: "320x125", - title: Alchemy.t("New Element") - }, class: "button add-nestable-element-button" %> - <% end %> - <% end %> + <%= render "alchemy/admin/elements/add_nested_element_form", element: element %> <% end %> <% end %> diff --git a/app/views/alchemy/admin/elements/_form.html.erb b/app/views/alchemy/admin/elements/_form.html.erb index 886d189aed..3579183734 100644 --- a/app/views/alchemy/admin/elements/_form.html.erb +++ b/app/views/alchemy/admin/elements/_form.html.erb @@ -9,7 +9,11 @@ label: Alchemy.t(:element_of_type), collection: elements_for_select(@elements), prompt: Alchemy.t(:select_element), - input_html: {class: 'alchemy_selectbox', autofocus: true} %> + selected: (@elements.first if @elements.count == 1), + input_html: {class: 'alchemy_selectbox', autofocus: true, disabled: @elements.count == 1} %> + <% if @elements.count == 1 %> + <%= form.hidden_field :name, value: @elements.first[:name] %> + <% end %> <%= form.hidden_field :parent_element_id, value: @parent_element.try(:id) %> <%= form.submit Alchemy.t(:add) %> <%- end -%> diff --git a/spec/controllers/alchemy/admin/elements_controller_spec.rb b/spec/controllers/alchemy/admin/elements_controller_spec.rb index cfdcff05ff..677d153142 100644 --- a/spec/controllers/alchemy/admin/elements_controller_spec.rb +++ b/spec/controllers/alchemy/admin/elements_controller_spec.rb @@ -91,7 +91,7 @@ module Alchemy let(:page_version) { create(:alchemy_page_version) } it "assign variable for all available element definitions" do - expect_any_instance_of(Alchemy::Page).to receive(:available_element_definitions) + expect_any_instance_of(Alchemy::Page).to receive(:available_element_definitions).twice { [] } get :new, params: { page_version_id: page_version.id } end diff --git a/spec/features/admin/edit_elements_feature_spec.rb b/spec/features/admin/edit_elements_feature_spec.rb index cb42a0aba9..a76ea9a8ec 100644 --- a/spec/features/admin/edit_elements_feature_spec.rb +++ b/spec/features/admin/edit_elements_feature_spec.rb @@ -53,16 +53,53 @@ create(:alchemy_element, :with_nestable_elements, page_version: a_page.draft_version) end - scenario "the add element button immediately creates the nested element.", :js do - visit alchemy.admin_elements_path(page_version_id: element.page_version_id) - button = page.find(".add-nestable-element-button") - expect(button).to have_content "Add slide" - button.click - expect(page).to have_selector(".element-editor[data-element-name='slide']") + context "when clipboard has a nestable element" do + before do + allow_any_instance_of(Alchemy::Admin::ElementsController).to receive(:get_clipboard) do + [ + { "id" => create(:alchemy_element, name: element.definition["nestable_elements"].first).id, "action" => "copy" }, + ] + end + end + + scenario "the add button opens add element form with the clipboard tab" do + visit alchemy.admin_elements_path(page_version_id: element.page_version_id) + button = page.find(".add-nestable-element-button") + expect(button).to have_content "Add slide" + button.click + expect(page).to have_select("Element") + expect(page).to have_link("Paste from clipboard") + end + end + + context "when clipboard does not have a nestable element", :js do + scenario "the add element button immediately creates the nested element." do + visit alchemy.admin_elements_path(page_version_id: element.page_version_id) + button = page.find("button.add-nestable-element-button") + expect(button).to have_content "Add slide" + button.click + expect(page).to have_selector(".element-editor[data-element-name='slide']") + end + + context "when a nested element is copied to clipboard" do + before do + visit alchemy.edit_admin_page_path(element.page) + page.find(".add-nestable-element-button").click + new_element = Alchemy::Element.last + page.find("#element_#{new_element.id} .element-header").hover + page.first("a[href^='/admin/clipboard/insert?remarkable_id=#{new_element.id}&remarkable_type=elements']").click + end + + scenario "the add button now opens add element form with the clipboard tab" do + find("a.add-nestable-element-button").click + expect(page).to have_select("Element") + expect(page).to have_link("Paste from clipboard") + end + end end end - context "With an element having multiple nestable element defined" do + context "With an element having multiple nestable elements defined" do let!(:element) do create(:alchemy_element, :with_nestable_elements, diff --git a/spec/models/alchemy/element_spec.rb b/spec/models/alchemy/element_spec.rb index f5a061c4e0..777b993e44 100644 --- a/spec/models/alchemy/element_spec.rb +++ b/spec/models/alchemy/element_spec.rb @@ -389,7 +389,9 @@ module Alchemy let(:clipboard) { [{ "id" => element_1.id.to_s }, { "id" => element_2.id.to_s }] } before do - allow(Element).to receive(:all_from_clipboard).and_return([element_1, element_2]) + allow(Element).to receive(:all_from_clipboard).and_return( + Element.where(id: [element_1, element_2].map(&:id)) + ) end it "return all elements from clipboard that could be placed on page" do @@ -411,6 +413,37 @@ module Alchemy end end + describe ".all_from_clipboard_for_parent_element" do + subject { Element.all_from_clipboard_for_parent_element(clipboard, parent_element) } + + let(:element_1) { create(:alchemy_element) } + let(:element_2) { create(:alchemy_element, name: "slide") } + let(:clipboard) { [{ "id" => element_1.id.to_s }, { "id" => element_2.id.to_s }] } + let(:parent_element) { create :alchemy_element, name: "slider" } + + before do + allow(Element).to receive(:all_from_clipboard).and_return( + Element.where(id: [element_1, element_2].map(&:id)) + ) + end + + it "returns all elements from clipboard that can be nested in the parent element" do + expect(subject).to match_array [element_2] + end + + context "when clipboard nil" do + let(:clipboard) { nil } + + it { is_expected.to be_empty } + end + + context "when parent_element nil" do + let(:parent_element) { nil } + + it { is_expected.to be_empty } + end + end + # InstanceMethods describe "#all_contents_by_type" do @@ -480,19 +513,19 @@ module Alchemy it "should return the translation with the translated content label" do expect(Alchemy).to receive(:t) - .with("content_names.content", default: "Content") - .and_return("Content") + .with("content_names.content", default: "Content") + .and_return("Content") expect(Alchemy).to receive(:t) - .with("content", scope: "content_names.article", default: "Content") - .and_return("Contenido") + .with("content", scope: "content_names.article", default: "Content") + .and_return("Contenido") expect(Alchemy).to receive(:t) - .with("article.content.invalid", { - scope: "content_validations", - default: [:"fields.content.invalid", :"errors.invalid"], - field: "Contenido", - }) + .with("article.content.invalid", { + scope: "content_validations", + default: [:"fields.content.invalid", :"errors.invalid"], + field: "Contenido", + }) expect(element).to receive(:essence_errors) - .and_return({ "content" => [:invalid] }) + .and_return({ "content" => [:invalid] }) element.essence_error_messages end