From f749d39a61e3e20cbec9b6cc98592c3ccfd05ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A0xim=20Colls?= Date: Thu, 5 Dec 2024 19:28:11 +0100 Subject: [PATCH] Implement nested assemblies navigation (#13662) --- Gemfile.lock | 3 + .../decidim/admin/_table-list.scss | 4 + .../assemblies/admin/assemblies_controller.rb | 5 +- .../admin/assembly_copies_controller.rb | 2 +- .../assemblies/admin/assemblies_helper.rb | 30 +++- .../decidim_assemblies_admin_list.js | 1 + .../assemblies/admin/assemblies_list.js | 72 +++++++++ .../admin/assemblies/_assembly_row.html.erb | 42 ++++-- .../admin/assemblies/_form.html.erb | 7 +- .../admin/assemblies/index.html.erb | 2 + .../assemblies/admin/assemblies/index.js.erb | 10 ++ decidim-assemblies/config/assets.rb | 3 +- decidim-assemblies/config/locales/en.yml | 2 +- .../spec/shared/manage_assemblies_examples.rb | 4 - .../admin/admin_manages_assemblies_spec.rb | 32 ++-- ...es_assemblies_with_parent_selector_spec.rb | 139 ------------------ ...mbly_admin_accesses_admin_sections_spec.rb | 7 +- 17 files changed, 168 insertions(+), 197 deletions(-) create mode 100644 decidim-assemblies/app/packs/entrypoints/decidim_assemblies_admin_list.js create mode 100644 decidim-assemblies/app/packs/src/decidim/assemblies/admin/assemblies_list.js create mode 100644 decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb delete mode 100644 decidim-assemblies/spec/system/admin/admin_manages_assemblies_with_parent_selector_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index f1dbdf773ee78..92f3b806f7c8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -509,6 +509,8 @@ GEM net-smtp (0.3.4) net-protocol nio4r (2.7.3) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) oauth (1.1.0) @@ -798,6 +800,7 @@ GEM zeitwerk (2.6.18) PLATFORMS + arm64-darwin-23 x86_64-linux DEPENDENCIES diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss index 9685ca30ebcb9..35a01b1fbeb69 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss @@ -167,3 +167,7 @@ @apply inline-block; } } + +.table-list__title-ellipsis { + @apply whitespace-nowrap overflow-hidden text-ellipsis max-w-[200px] sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-2xl inline-block align-middle; +} diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/assemblies_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/assemblies_controller.rb index d142ead2a9ea5..25647dd429829 100644 --- a/decidim-assemblies/app/controllers/decidim/assemblies/admin/assemblies_controller.rb +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/assemblies_controller.rb @@ -9,7 +9,8 @@ class AssembliesController < Decidim::Assemblies::Admin::ApplicationController include Decidim::Assemblies::Admin::Filterable include Decidim::Admin::ParticipatorySpaceAdminContext include Decidim::Admin::HasTrashableResources - helper_method :current_assembly, :parent_assembly, :current_participatory_space + + helper_method :current_assembly, :parent_assembly, :parent_assembly_id, :current_participatory_space layout "decidim/admin/assemblies" @@ -31,7 +32,7 @@ def create CreateAssembly.call(@form) do on(:ok) do |assembly| flash[:notice] = I18n.t("assemblies.create.success", scope: "decidim.admin") - redirect_to assemblies_path(q: { parent_id_eq: assembly.parent_id }) + redirect_to components_path(assembly) end on(:invalid) do diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/assembly_copies_controller.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/assembly_copies_controller.rb index 7e93c4aefb749..943905dd1f3e1 100644 --- a/decidim-assemblies/app/controllers/decidim/assemblies/admin/assembly_copies_controller.rb +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/assembly_copies_controller.rb @@ -20,7 +20,7 @@ def create CopyAssembly.call(@form, current_assembly, current_user) do on(:ok) do flash[:notice] = I18n.t("assemblies_copies.create.success", scope: "decidim.admin") - redirect_to assemblies_path(parent_id: current_assembly.parent_id) + redirect_to assemblies_path end on(:invalid) do diff --git a/decidim-assemblies/app/helpers/decidim/assemblies/admin/assemblies_helper.rb b/decidim-assemblies/app/helpers/decidim/assemblies/admin/assemblies_helper.rb index 4879173bfc46b..13f0580aed5a9 100644 --- a/decidim-assemblies/app/helpers/decidim/assemblies/admin/assemblies_helper.rb +++ b/decidim-assemblies/app/helpers/decidim/assemblies/admin/assemblies_helper.rb @@ -15,10 +15,32 @@ def processes_selected end end - # Public: A collection of Assemblies that can be selected as parent - # assemblies for another assembly; to be used in forms. - def parent_assemblies_for_select - @parent_assemblies_for_select ||= ParentAssembliesForSelect.for(current_organization, current_assembly) + # Public: select options representing a collection of Assemblies that + # can be selected as parent assemblies for another assembly; to be used in forms. + def parent_assemblies_options + options = [] + root_assemblies = ParentAssembliesForSelect.for(current_organization, current_assembly).where(parent_id: nil).sort_by(&:weight) + + root_assemblies.each do |assembly| + build_assembly_options(assembly, options) + end + + options + end + + private + + # Recursively build the options for the assembly tree + def build_assembly_options(assembly, options, level = 0) + name = sanitize("#{" " * 4 * level} #{assembly.translated_title}") + options << [name, assembly.id] + + # Skip the current assembly to avoid selecting a child as parent + return if assembly == current_assembly + + assembly.children.each do |child| + build_assembly_options(child, options, level + 1) + end end end end diff --git a/decidim-assemblies/app/packs/entrypoints/decidim_assemblies_admin_list.js b/decidim-assemblies/app/packs/entrypoints/decidim_assemblies_admin_list.js new file mode 100644 index 0000000000000..f4bf48fe7f8b8 --- /dev/null +++ b/decidim-assemblies/app/packs/entrypoints/decidim_assemblies_admin_list.js @@ -0,0 +1 @@ +import "src/decidim/assemblies/admin/assemblies_list" diff --git a/decidim-assemblies/app/packs/src/decidim/assemblies/admin/assemblies_list.js b/decidim-assemblies/app/packs/src/decidim/assemblies/admin/assemblies_list.js new file mode 100644 index 0000000000000..321749a34d5f3 --- /dev/null +++ b/decidim-assemblies/app/packs/src/decidim/assemblies/admin/assemblies_list.js @@ -0,0 +1,72 @@ +/* eslint-disable require-jsdoc */ + +class AdminAssembliesListComponent { + run() { + this.rebindArrows(); + } + + rebindArrows() { + this.unbindArrows(); + this.bindArrows(); + } + + bindArrows() { + document.querySelectorAll("[data-arrow-up]").forEach((element) => { + element.addEventListener("click", this._onClickUpArrow); + }); + document.querySelectorAll("[data-arrow-down]").forEach((element) => { + element.addEventListener("click", this._onClickDownArrow); + }); + } + + unbindArrows() { + document.querySelectorAll("[data-arrow-up]").forEach((element) => { + element.removeEventListener("click", this._onClickUpArrow); + }); + document.querySelectorAll("[data-arrow-down]").forEach((element) => { + element.removeEventListener("click", this._onClickDownArrow); + }); + } + + _onClickDownArrow(event) { + event.preventDefault(); + + const target = event.currentTarget; + const assembly = target.closest("[data-assembly-id]"); + const upArrow = assembly.querySelector("[data-arrow-up]"); + + target.classList.toggle("hidden"); + upArrow.classList.toggle("hidden"); + } + + _onClickUpArrow(event) { + event.preventDefault(); + + const target = event.currentTarget; + const assembly = target.closest("[data-assembly-id]"); + const parentLevel = assembly.dataset.level; + const downArrow = assembly.querySelector("[data-arrow-down]"); + + target.classList.toggle("hidden"); + downArrow.classList.toggle("hidden"); + + // Get all following tr elements + let nextElement = assembly.nextElementSibling; + while (nextElement) { + const currentLevel = nextElement.dataset.level; + const nextSibling = nextElement.nextElementSibling; + + if (currentLevel > parentLevel) { + nextElement.remove(); + } else { + break; + } + nextElement = nextSibling; + } + } +} + +window.Decidim.AdminAssembliesListComponent = AdminAssembliesListComponent; +const component = new AdminAssembliesListComponent(); + +component.run(); diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_assembly_row.html.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_assembly_row.html.erb index 90cf9f275f13a..70ed82442a5c6 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_assembly_row.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_assembly_row.html.erb @@ -1,15 +1,33 @@ - - + + + <% if parent_assembly_id %> + <% [assembly.ancestors.count - 1, 0].max.times do |index| %> + + <% end %> + | + <% end %> + <% if assembly.promoted? %> <%= icon_with_tooltip "star-s-fill", t("models.assembly.fields.promoted", scope: "decidim.admin") %> <% end %> - <% if allowed_to? :update, :assembly, assembly: assembly %> - <%= link_to translated_attribute(assembly.title), edit_assembly_path(assembly) %> - <% elsif allowed_to? :read, :component, assembly: assembly %> - <%= link_to translated_attribute(assembly.title), components_path(assembly) %>
- <% else %> - <%= translated_attribute(assembly.title) %> + + <% if allowed_to? :update, :assembly, assembly: assembly %> + <%= link_to translated_attribute(assembly.title), edit_assembly_path(assembly) %> + <% elsif allowed_to? :read, :component, assembly: assembly %> + <%= link_to translated_attribute(assembly.title), components_path(assembly) %>
+ <% else %> + <%= translated_attribute(assembly.title) %> + <% end %> +
+ + <% if assembly.children.count.positive? %> + <%= link_to url_for(query_params_with(parent_id_eq: assembly.id)), remote: true, data: { arrow_down: true } do %> + <%= icon "arrow-down-s-line", class: "w-4 h-4 ml-2" %> + <% end %> + <%= link_to "#", class: "hidden", data: { arrow_up: true } do %> + <%= icon "arrow-up-s-line", class: "w-4 h-4 ml-2" %> + <% end %> <% end %> @@ -47,14 +65,6 @@ <% else %> <% end %> - <% if assembly.children.count.positive? || allowed_to?(:read, :assembly, assembly:) %> - <%= icon_link_to "government-line", - url_for(query_params_with(parent_id_eq: assembly.id)), - t("decidim.admin.titles.assemblies"), - class: "action-icon--dial #{"highlighted" if assembly.children.count.positive?}" %> - <% else %> - - <% end %> <% if allowed_to? :copy, :assembly, assembly: assembly, assembly: parent_assembly %> <%= icon_link_to "file-copy-line", new_assembly_copy_path(assembly), t("actions.duplicate", scope: "decidim.admin"), class: "action-icon--copy" %> <% else %> diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_form.html.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_form.html.erb index 30650f3b0e6c0..23af062e0e2b7 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_form.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/_form.html.erb @@ -224,12 +224,7 @@ <% else %>
<%= form.select :parent_id, - options_from_collection_for_select( - parent_assemblies_for_select, - :id, - :translated_title, - selected: current_assembly.try(:parent_id) - ), + parent_assemblies_options, include_blank: t(".select_parent_assembly") %>
<% end %> diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.html.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.html.erb index 3717ad8515e62..de5edb0f3f9a0 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.html.erb @@ -40,3 +40,5 @@ <%= decidim_paginate @assemblies %> + +<%= append_javascript_pack_tag "decidim_assemblies_admin_list" %> diff --git a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb new file mode 100644 index 0000000000000..fce1b92c9be03 --- /dev/null +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb @@ -0,0 +1,10 @@ +$('[data-assembly-id="<%= parent_assembly_id %>"]').after( + '<%= j(render partial: "decidim/assemblies/admin/assemblies/assembly_row", + collection: @assemblies, + as: :assembly, + locals: { view: :index }).strip.html_safe %>' +); + +var component = new window.Decidim.AdminAssembliesListComponent(); + +component.run(); diff --git a/decidim-assemblies/config/assets.rb b/decidim-assemblies/config/assets.rb index 2a4c08f703996..3f0d945b05c8c 100644 --- a/decidim-assemblies/config/assets.rb +++ b/decidim-assemblies/config/assets.rb @@ -5,5 +5,6 @@ Decidim::Webpacker.register_path("#{base_path}/app/packs") Decidim::Webpacker.register_entrypoints( decidim_assemblies: "#{base_path}/app/packs/entrypoints/decidim_assemblies.js", - decidim_assemblies_admin: "#{base_path}/app/packs/entrypoints/decidim_assemblies_admin.js" + decidim_assemblies_admin: "#{base_path}/app/packs/entrypoints/decidim_assemblies_admin.js", + decidim_assemblies_admin_list: "#{base_path}/app/packs/entrypoints/decidim_assemblies_admin_list.js" ) diff --git a/decidim-assemblies/config/locales/en.yml b/decidim-assemblies/config/locales/en.yml index 4fea63424d5d8..8fc58d0964bc0 100644 --- a/decidim-assemblies/config/locales/en.yml +++ b/decidim-assemblies/config/locales/en.yml @@ -104,7 +104,7 @@ en: assemblies: create: error: There was a problem creating a new assembly. - success: Assembly created successfully. + success: Assembly created successfully. You can now add components and configure it. edit: update: Update index: diff --git a/decidim-assemblies/spec/shared/manage_assemblies_examples.rb b/decidim-assemblies/spec/shared/manage_assemblies_examples.rb index c68893ab06886..16cf616c161e8 100644 --- a/decidim-assemblies/spec/shared/manage_assemblies_examples.rb +++ b/decidim-assemblies/spec/shared/manage_assemblies_examples.rb @@ -204,8 +204,4 @@ expect(page).to have_admin_callout("successfully") end end - - it "shows the Assemblies link to manage nested assemblies" do - expect(page).to have_link("Assemblies", href: decidim_admin_assemblies.assemblies_path(q: { parent_id_eq: assembly.id })) - end end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb index 2ce717000a6be..2949307828454 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -98,8 +98,7 @@ expect(last_assembly.taxonomies).to contain_exactly(taxonomy) within "[data-content]" do - expect(page).to have_current_path decidim_admin_assemblies.assemblies_path(q: { parent_id_eq: parent_assembly&.id }) - expect(page).to have_content(translated(attributes[:title])) + expect(page).to have_current_path decidim_admin_assemblies.components_path(last_assembly) end visit decidim_admin.root_path @@ -192,7 +191,7 @@ def assembly_without_type(type) end end - context "when managing child assemblies" do + context "when navigating child assemblies" do let!(:parent_assembly) { create(:assembly, organization:) } let!(:child_assembly) { create(:assembly, :with_content_blocks, organization:, parent: parent_assembly, blocks_manifests: [:announcement]) } let(:assembly) { child_assembly } @@ -201,24 +200,23 @@ def assembly_without_type(type) switch_to_host(organization.host) login_as user, scope: :user visit decidim_admin_assemblies.assemblies_path - within "tr", text: translated(parent_assembly.title) do - click_on "Assemblies" - end end - it_behaves_like "manage assemblies" - it_behaves_like "creating an assembly" - it_behaves_like "manage assemblies announcements" - describe "listing child assemblies" do - it_behaves_like "filtering collection by published/unpublished" do - let!(:published_space) { child_assembly } - let!(:unpublished_space) { create(:assembly, :unpublished, parent: parent_assembly, organization:) } - end + it "expands the parent assembly" do + expect(page).to have_no_content(translated(child_assembly.title)) + + within "tr", text: translated(parent_assembly.title) do + find("a[data-arrow-down]").click + end + + expect(page).to have_content(translated(child_assembly.title)) + + within "tr", text: translated(parent_assembly.title) do + find("a[data-arrow-up]").click + end - it_behaves_like "filtering collection by private/public" do - let!(:public_space) { child_assembly } - let!(:private_space) { create(:assembly, :private, parent: parent_assembly, organization:) } + expect(page).to have_no_content(translated(child_assembly.title)) end end end diff --git a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_with_parent_selector_spec.rb b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_with_parent_selector_spec.rb deleted file mode 100644 index a4b3991bd8223..0000000000000 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_with_parent_selector_spec.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin manages assemblies with parent selector" do - include_context "when admin administrating an assembly" - - let(:image1_filename) { "city.jpeg" } - let(:image1_path) { Decidim::Dev.asset(image1_filename) } - let(:image2_filename) { "city2.jpeg" } - let(:image2_path) { Decidim::Dev.asset(image2_filename) } - - before do - switch_to_host(organization.host) - login_as user, scope: :user - visit decidim_admin_assemblies.assemblies_path - end - - context "when selecting a parent_assembly in the form" do - before do - click_on "New assembly" - end - - it "creates a new assembly" do - within ".new_assembly" do - fill_in_i18n( - :assembly_title, - "#assembly-title-tabs", - en: "My assembly", - es: "Mi proceso participativo", - ca: "El meu procés participatiu" - ) - fill_in_i18n( - :assembly_subtitle, - "#assembly-subtitle-tabs", - en: "Subtitle", - es: "Subtítulo", - ca: "Subtítol" - ) - fill_in_i18n_editor( - :assembly_short_description, - "#assembly-short_description-tabs", - en: "Short description", - es: "Descripción corta", - ca: "Descripció curta" - ) - fill_in_i18n_editor( - :assembly_description, - "#assembly-description-tabs", - en: "A longer description", - es: "Descripción más larga", - ca: "Descripció més llarga" - ) - - select assembly.title["en"], from: :assembly_parent_id - - fill_in :assembly_slug, with: "slug" - fill_in :assembly_hashtag, with: "#hashtag" - fill_in :assembly_weight, with: 1 - end - - dynamically_attach_file(:assembly_hero_image, image1_path) - dynamically_attach_file(:assembly_banner_image, image2_path) - - within ".new_assembly" do - find("*[type=submit]").click - end - - expect(page).to have_admin_callout("successfully") - - within "[data-content]" do - expect(page).to have_current_path decidim_admin_assemblies.assemblies_path(q: { parent_id_eq: assembly.id }) - expect(page).to have_content("My assembly") - end - end - end - - context "when managing child assemblies" do - let!(:child_assembly) { create(:assembly, organization:, parent: assembly) } - - before do - within "tr", text: translated(assembly.title) do - click_on "Assemblies" - end - click_on "New assembly" - end - - it "creates a new assembly" do - within ".new_assembly" do - fill_in_i18n( - :assembly_title, - "#assembly-title-tabs", - en: "My assembly", - es: "Mi proceso participativo", - ca: "El meu procés participatiu" - ) - fill_in_i18n( - :assembly_subtitle, - "#assembly-subtitle-tabs", - en: "Subtitle", - es: "Subtítulo", - ca: "Subtítol" - ) - fill_in_i18n_editor( - :assembly_short_description, - "#assembly-short_description-tabs", - en: "Short description", - es: "Descripción corta", - ca: "Descripció curta" - ) - fill_in_i18n_editor( - :assembly_description, - "#assembly-description-tabs", - en: "A longer description", - es: "Descripción más larga", - ca: "Descripció més llarga" - ) - - fill_in :assembly_slug, with: "slug" - fill_in :assembly_hashtag, with: "#hashtag" - fill_in :assembly_weight, with: 1 - end - - dynamically_attach_file(:assembly_hero_image, image1_path) - dynamically_attach_file(:assembly_banner_image, image2_path) - - within ".new_assembly" do - find("*[type=submit]").click - end - - expect(page).to have_admin_callout("successfully") - - within "[data-content]" do - expect(page).to have_current_path decidim_admin_assemblies.assemblies_path(q: { parent_id_eq: assembly.id }) - expect(page).to have_content("My assembly") - end - end - end -end diff --git a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb index 6bf30b336c8ff..8e3bae38024cd 100644 --- a/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_admin_accesses_admin_sections_spec.rb @@ -57,12 +57,7 @@ let!(:child_assembly) { create(:assembly, parent: assembly, organization:, hashtag: "child") } before do - visit decidim_admin_assemblies.assemblies_path - within "tr", text: translated(assembly.title) do - click_on "Assemblies" - end - - click_on "Configure" + visit decidim_admin_assemblies.edit_assembly_path(child_assembly) end context "when is a public assembly" do