From bab584bdc30bbe00bbebafbd4cbc21e67d789f0f Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Fri, 2 Feb 2024 09:50:01 +0100 Subject: [PATCH 1/2] Add alchemy-growl custom component --- app/javascript/alchemy_admin.js | 1 + app/javascript/alchemy_admin/components/growl.js | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 app/javascript/alchemy_admin/components/growl.js diff --git a/app/javascript/alchemy_admin.js b/app/javascript/alchemy_admin.js index 8c2b755834..268ca46c3b 100644 --- a/app/javascript/alchemy_admin.js +++ b/app/javascript/alchemy_admin.js @@ -25,6 +25,7 @@ import "alchemy_admin/components/clipboard_button" import "alchemy_admin/components/datepicker" import "alchemy_admin/components/dialog_link" import "alchemy_admin/components/element_editor" +import "alchemy_admin/components/growl" import "alchemy_admin/components/ingredient_group" import "alchemy_admin/components/link_buttons" import "alchemy_admin/components/node_select" diff --git a/app/javascript/alchemy_admin/components/growl.js b/app/javascript/alchemy_admin/components/growl.js new file mode 100644 index 0000000000..9959dd2422 --- /dev/null +++ b/app/javascript/alchemy_admin/components/growl.js @@ -0,0 +1,10 @@ +class Growl extends HTMLElement { + connectedCallback() { + Alchemy.growl( + this.getAttribute("message"), + this.getAttribute("type") || "notice" + ) + } +} + +customElements.define("alchemy-growl", Growl) From 061afe63619923e36fec8ed26ea1b95e150fd9c8 Mon Sep 17 00:00:00 2001 From: Sascha Karnatz <122262394+sascha-karnatz@users.noreply.github.com> Date: Fri, 19 Jan 2024 19:00:08 +0100 Subject: [PATCH 2/2] Add nodes to page dialog Add a new menu tab to page configuration dialog. This is a shortcut to create a menu entry for the current page. It is also the first try to provide Turbo Frames inside of Alchemy, which is coming with a few challenges (flash messages, and updated tab headline). --- app/assets/stylesheets/alchemy/forms.scss | 5 +- .../alchemy/admin/base_controller.rb | 2 +- .../alchemy/admin/nodes_controller.rb | 26 +++++++ app/views/alchemy/admin/nodes/_label.html.erb | 1 + .../alchemy/admin/nodes/_page_nodes.html.erb | 48 +++++++++++++ .../admin/nodes/_update.turbo_stream.erb | 9 +++ .../admin/nodes/create.turbo_stream.erb | 1 + .../admin/nodes/destroy.turbo_stream.erb | 1 + .../alchemy/admin/pages/configure.html.erb | 6 ++ config/locales/alchemy.en.yml | 3 + .../alchemy/admin/node_select_spec.rb | 2 +- .../alchemy/admin/nodes_controller_spec.rb | 24 +++++++ spec/features/admin/nodes_management_spec.rb | 70 +++++++++++++++++++ .../admin/page_editing_feature_spec.rb | 4 +- 14 files changed, 195 insertions(+), 7 deletions(-) create mode 100644 app/views/alchemy/admin/nodes/_label.html.erb create mode 100644 app/views/alchemy/admin/nodes/_page_nodes.html.erb create mode 100644 app/views/alchemy/admin/nodes/_update.turbo_stream.erb create mode 100644 app/views/alchemy/admin/nodes/create.turbo_stream.erb create mode 100644 app/views/alchemy/admin/nodes/destroy.turbo_stream.erb create mode 100644 spec/features/admin/nodes_management_spec.rb diff --git a/app/assets/stylesheets/alchemy/forms.scss b/app/assets/stylesheets/alchemy/forms.scss index 219529a5f0..ae294f1661 100644 --- a/app/assets/stylesheets/alchemy/forms.scss +++ b/app/assets/stylesheets/alchemy/forms.scss @@ -169,7 +169,8 @@ form { } .inline-input { - @include clearfix; + align-items: center; + display: flex; margin: 0 -1 * $default-margin; .left-column, @@ -179,12 +180,10 @@ form { .left-column { width: $form-right-width; - float: left; } .right-column { width: $form-left-width; - float: right; } button, diff --git a/app/controllers/alchemy/admin/base_controller.rb b/app/controllers/alchemy/admin/base_controller.rb index e36f133e67..d752cbddb1 100644 --- a/app/controllers/alchemy/admin/base_controller.rb +++ b/app/controllers/alchemy/admin/base_controller.rb @@ -33,7 +33,7 @@ def leave # Disable layout rendering for xhr requests. def set_layout - request.xhr? ? false : "alchemy/admin" + (request.xhr? || turbo_frame_request?) ? false : "alchemy/admin" end # Handles exceptions diff --git a/app/controllers/alchemy/admin/nodes_controller.rb b/app/controllers/alchemy/admin/nodes_controller.rb index 9695f26048..d9ad9a0d10 100644 --- a/app/controllers/alchemy/admin/nodes_controller.rb +++ b/app/controllers/alchemy/admin/nodes_controller.rb @@ -16,6 +16,32 @@ def new ) end + def create + if turbo_frame_request? + @page = Alchemy::Page.find(resource_params[:page_id]) + @node = @page.nodes.build(resource_params) + if @node.valid? + @node.save + flash_notice_for_resource_action(:create) + else + flash[:error] = @node.errors.full_messages.join(", ") + end + else + super + end + end + + def destroy + if turbo_frame_request? + @node = Alchemy::Node.find(params[:id]) + @page = @node.page + @page.nodes.destroy(@node) + flash_notice_for_resource_action(:destroy) + else + super + end + end + private def resource_params diff --git a/app/views/alchemy/admin/nodes/_label.html.erb b/app/views/alchemy/admin/nodes/_label.html.erb new file mode 100644 index 0000000000..1951643d34 --- /dev/null +++ b/app/views/alchemy/admin/nodes/_label.html.erb @@ -0,0 +1 @@ +(<%= count %>) <%= Alchemy::Node.model_name.human(count: count) %> diff --git a/app/views/alchemy/admin/nodes/_page_nodes.html.erb b/app/views/alchemy/admin/nodes/_page_nodes.html.erb new file mode 100644 index 0000000000..678274cc5b --- /dev/null +++ b/app/views/alchemy/admin/nodes/_page_nodes.html.erb @@ -0,0 +1,48 @@ +<%= turbo_frame_tag("page_nodes") do %> + + + + + + <% nodes = @page.nodes.select(&:persisted?) %> + <% if nodes.length > 0 %> + <% nodes.each do |node| %> + + + + + <% end %> + <% else %> + + + + + <% end %> +
+ <%= Alchemy::Node.model_name.human %> +
<%= "#{node.ancestors.map(&:name).join(" / ")} / #{node.name}" %> + "> + <%= link_to render_icon(:minus), + admin_node_path(node), + class: "icon_button", + data: { turbo_method: :delete, turbo_confirm: Alchemy.t('confirm_to_delete_node') } %> + +
<%= Alchemy.t('No menu node for this page found') %>
+ +
+ <%= Alchemy.t('Create node on parent:') %> + + <%= alchemy_form_for([:admin, @page.nodes.build], id: "new_node_form") do |f| %> + <%= f.hidden_field :page_id, value: @page.id %> + <%= f.hidden_field :language_id, value: @page.language_id %> + + <%= render Alchemy::Admin::NodeSelect.new(nil, url: alchemy.api_nodes_path(language_id: @page.language_id, include: :ancestors)) do %> + <%= f.text_field :parent_id, class: 'alchemy_selectbox full_width' %> + <% end %> + +
+ +
+ <% end %> +
+<% end %> diff --git a/app/views/alchemy/admin/nodes/_update.turbo_stream.erb b/app/views/alchemy/admin/nodes/_update.turbo_stream.erb new file mode 100644 index 0000000000..a2682f8804 --- /dev/null +++ b/app/views/alchemy/admin/nodes/_update.turbo_stream.erb @@ -0,0 +1,9 @@ +<% key = flash.keys.first %> + + +<%= turbo_stream.replace "page_nodes" do %> + <%= render "page_nodes" %> +<% end %> +<%= turbo_stream.update_all '[panel="nodes"]' do %> + <%= render('alchemy/admin/nodes/label', count: @page.nodes.select(&:persisted?).length) %> +<% end %> diff --git a/app/views/alchemy/admin/nodes/create.turbo_stream.erb b/app/views/alchemy/admin/nodes/create.turbo_stream.erb new file mode 100644 index 0000000000..69bab1ba1e --- /dev/null +++ b/app/views/alchemy/admin/nodes/create.turbo_stream.erb @@ -0,0 +1 @@ +<%= render "update" %> diff --git a/app/views/alchemy/admin/nodes/destroy.turbo_stream.erb b/app/views/alchemy/admin/nodes/destroy.turbo_stream.erb new file mode 100644 index 0000000000..69bab1ba1e --- /dev/null +++ b/app/views/alchemy/admin/nodes/destroy.turbo_stream.erb @@ -0,0 +1 @@ +<%= render "update" %> diff --git a/app/views/alchemy/admin/pages/configure.html.erb b/app/views/alchemy/admin/pages/configure.html.erb index e051a06769..648eda6cae 100644 --- a/app/views/alchemy/admin/pages/configure.html.erb +++ b/app/views/alchemy/admin/pages/configure.html.erb @@ -2,12 +2,18 @@ <%= Alchemy.t('Properties') %> + + <%= render 'alchemy/admin/nodes/label', count: @page.nodes.size %> + <%= render 'alchemy/admin/legacy_page_urls/label', count: @page.legacy_urls.size %> <%= render 'form' %> + + <%= render 'alchemy/admin/nodes/page_nodes' %> + <%= render 'legacy_urls' %> diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml index a5a51f2f11..888ca58819 100644 --- a/config/locales/alchemy.en.yml +++ b/config/locales/alchemy.en.yml @@ -250,6 +250,7 @@ en: "Confirm new password": "Confirm new password" "Copy": "Copy" "Could not load Adobe Flash® Plugin!": "Could not load Adobe Flash® Plugin!" + "Create node on parent:": "Create node on parent:" "Currently locked pages": "Currently locked pages" "Default language has to be public": "Default language has to be public" "Delete image": "Delete image" @@ -286,6 +287,7 @@ en: "New": "New" "New Element": "New element" "New page": "New page" + "No menu node for this page found": "No menu node for this page found" "No page links for this page found": "No page links for this page found" "New password": "New password" "New Tag": "New tag" @@ -411,6 +413,7 @@ en: delete_node: "Delete this menu node" delete_page: "Delete this page" delete_tag: "Delete tag" + search_node: "Search menu node" document: "File" download_csv: "Download CSV" download_file: "Download file '%{filename}'" diff --git a/spec/components/alchemy/admin/node_select_spec.rb b/spec/components/alchemy/admin/node_select_spec.rb index 40d4dba979..b9033d7ba4 100644 --- a/spec/components/alchemy/admin/node_select_spec.rb +++ b/spec/components/alchemy/admin/node_select_spec.rb @@ -16,7 +16,7 @@ end it "should have the default placeholder" do - expect(page).to have_selector("alchemy-node-select[placeholder='Search node']") + expect(page).to have_selector("alchemy-node-select[placeholder='Search menu node']") end it "should have the default node api - url" do diff --git a/spec/controllers/alchemy/admin/nodes_controller_spec.rb b/spec/controllers/alchemy/admin/nodes_controller_spec.rb index f162f8f168..530a604610 100644 --- a/spec/controllers/alchemy/admin/nodes_controller_spec.rb +++ b/spec/controllers/alchemy/admin/nodes_controller_spec.rb @@ -78,5 +78,29 @@ module Alchemy end end end + + describe "#destroy" do + let(:node) { create(:alchemy_node) } + + context "as default call" do + it "removes node and redirects to index" do + expect { + delete :destroy, params: {id: node.id} + }.to change { Alchemy::Node.count }.by(0) + expect(response).to redirect_to(admin_nodes_path) + end + end + + context "as turbo stream call", type: :request do + let!(:page) { create(:alchemy_page, nodes: [node]) } + + it "removes node and returns the new frame" do + expect(Alchemy::Node.count).to eq(1) + delete admin_node_path(node), as: :turbo_stream + expect(Alchemy::Node.count).to eq(0) + expect(response.code).to eq("302") + end + end + end end end diff --git a/spec/features/admin/nodes_management_spec.rb b/spec/features/admin/nodes_management_spec.rb new file mode 100644 index 0000000000..1ff5a94b10 --- /dev/null +++ b/spec/features/admin/nodes_management_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Nodes management", type: :system, js: true do + before do + authorize_user(:as_admin) + end + + let!(:a_page) { create(:alchemy_page) } + let!(:a_menu) { create(:alchemy_node, name: "Menu") } + + def open_page_properties + visit admin_pages_path + expect(page.find("alchemy-overlay").style("display")["display"]).to have_content("none") + page.find("a[href='#{configure_admin_page_path(a_page)}']", wait: 10).click + find("[panel='nodes']").click + end + + def add_menu_item + find("#new_node_form .select2-choice").click + find(".select2-result:first-child").click + + click_button "Add a menu node" + end + + it "lets a user add a menu node" do + open_page_properties + add_menu_item + + within "#page_nodes table" do + expect(page).to have_content("Menu node Menu / A Page 1") + end + within "[panel='nodes']" do + expect(page).to have_content("(1) Menu node") + end + end + + context "without parent id" do + it "displays error message" do + open_page_properties + + click_button "Add a menu node" + within ".flash.error" do + expect(page).to have_content("Menu Type can't be blank") + end + end + end + + context "with menu node present" do + before do + open_page_properties + add_menu_item + end + + it "lets a user remove a menu node" do + page.accept_alert "Do you really want to delete this menu node?" do + click_link_with_tooltip("Delete this menu node") + end + + within "#page_nodes table" do + expect(page).to_not have_content("Menu node Menu / A Page 1") + expect(page).to have_content(Alchemy.t("No menu node for this page found")) + end + within "[panel='nodes']" do + expect(page).to have_content("(0) Menu nodes") + end + end + end +end diff --git a/spec/features/admin/page_editing_feature_spec.rb b/spec/features/admin/page_editing_feature_spec.rb index 1a5df80b8b..0d72de45e7 100644 --- a/spec/features/admin/page_editing_feature_spec.rb +++ b/spec/features/admin/page_editing_feature_spec.rb @@ -226,7 +226,7 @@ it "saves the name" do within(".alchemy-dialog.modal") do find("input#page_name").set("name with some %!x^)'([@!{}]|/?:# characters") - find(".submit button").click + find(".edit_page .submit button").click end expect(page).to_not have_selector(".alchemy-dialog-overlay.open") expect(page).to have_selector("#sitemap a.sitemap_pagename_link", text: "name with some %!x^)'([@!{}]|/?:# characters") @@ -240,7 +240,7 @@ within(".alchemy-dialog.modal") do expect(page).to have_css("#s2id_page_parent_id") select2_search(new_parent.name, from: "Parent") - find(".submit button").click + find(".edit_page .submit button").click end expect(page).to_not have_selector(".alchemy-dialog-overlay.open") expect(page).to have_selector("#sitemap .sitemap_url", text: "/#{new_parent.urlname}/#{a_page.urlname}")