From cedbfd317e092bf62f766e5225f71f391b1400bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 25 Feb 2026 10:51:25 +0100 Subject: [PATCH 001/135] Be smart with the 'most commented' sorting option (#16136) --- .../controllers/decidim/debates/orderable.rb | 18 ++- .../app/models/decidim/debates/debate.rb | 9 ++ decidim-debates/config/locales/en.yml | 2 +- .../controllers/concerns/orderable_spec.rb | 117 ++++++++++++++++++ .../models/decidim/debates/debate_spec.rb | 43 +++++++ .../spec/system/explore_debates_spec.rb | 73 +++++++++++ .../concerns/decidim/proposals/orderable.rb | 8 +- .../app/models/decidim/proposals/proposal.rb | 11 ++ .../lib/decidim/proposals/component.rb | 4 +- .../controllers/concerns/orderable_spec.rb | 89 ++++++++++++- .../lib/decidim/proposals/component_spec.rb | 18 +++ .../models/decidim/proposals/proposal_spec.rb | 55 ++++++++ .../spec/system/proposals_spec.rb | 16 +++ 13 files changed, 456 insertions(+), 7 deletions(-) create mode 100644 decidim-debates/spec/controllers/concerns/orderable_spec.rb diff --git a/decidim-debates/app/controllers/decidim/debates/orderable.rb b/decidim-debates/app/controllers/decidim/debates/orderable.rb index 76123790b3bf0..4803b6c3796fa 100644 --- a/decidim-debates/app/controllers/decidim/debates/orderable.rb +++ b/decidim-debates/app/controllers/decidim/debates/orderable.rb @@ -14,18 +14,32 @@ module Orderable private def available_orders - @available_orders ||= %w(random recent commented updated) + @available_orders ||= possible_orders + end + + def possible_orders + @possible_orders ||= begin + possible_orders = %w(random recent updated) + possible_orders << "most_commented" if most_commented_order_available? + possible_orders + end end def default_order "updated" end + def most_commented_order_available? + return @most_commented_order_available if defined?(@most_commented_order_available) + + @most_commented_order_available = Decidim::Debates::Debate.most_commented_available?(current_component) + end + def reorder(debates) case order when "recent" debates.order(created_at: :desc) - when "commented" + when "most_commented" debates.order(comments_count: :desc) when "updated" debates.order(updated_at: :desc) diff --git a/decidim-debates/app/models/decidim/debates/debate.rb b/decidim-debates/app/models/decidim/debates/debate.rb index 9af90d785d71e..8e78791f9982f 100644 --- a/decidim-debates/app/models/decidim/debates/debate.rb +++ b/decidim-debates/app/models/decidim/debates/debate.rb @@ -236,6 +236,15 @@ def update_comments_count # Create the :search_text ransacker alias for searching from both of these. ransacker_i18n_multi :search_text, [:title, :description] + def self.most_commented_available?(component) + return false unless component.settings.comments_enabled? + + where(component:) + .not_hidden + .where("comments_count > 0") + .exists? + end + def self.ransackable_scopes(_auth_object = nil) [:with_any_state, :with_any_origin, :with_any_taxonomies] end diff --git a/decidim-debates/config/locales/en.yml b/decidim-debates/config/locales/en.yml index cc04c98bf018f..5e24e2cc72158 100644 --- a/decidim-debates/config/locales/en.yml +++ b/decidim-debates/config/locales/en.yml @@ -157,8 +157,8 @@ en: create: Create title: Create new debate orders: - commented: Most commented label: Order debates by + most_commented: Most commented random: Random order recent: Most recent updated: Recently updated diff --git a/decidim-debates/spec/controllers/concerns/orderable_spec.rb b/decidim-debates/spec/controllers/concerns/orderable_spec.rb new file mode 100644 index 0000000000000..a5ed2954312b4 --- /dev/null +++ b/decidim-debates/spec/controllers/concerns/orderable_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Debates + class OrderableFakeController < Decidim::ApplicationController + include Orderable + end + + describe OrderableFakeController do + let(:participatory_process) { create(:participatory_process, :with_steps) } + let(:active_step_id) { participatory_process.active_step.id } + let(:component) { create(:component, :with_one_step, participatory_space: participatory_process, manifest_name: "debates") } + let(:component_settings) do + double( + comments_enabled?: comments_enabled + ) + end + let(:current_settings) do + double(:current_settings, + comments_enabled?: comments_enabled) + end + let(:comments_enabled) { nil } + let(:view) { controller.view_context } + + before do + allow(controller).to receive(:component_settings).and_return(component_settings) + allow(controller).to receive(:current_settings).and_return(current_settings) + allow(controller).to receive(:current_participatory_space).and_return(participatory_process) + allow(controller).to receive(:current_component).and_return(component) + end + + describe "#available_orders" do + context "with comments disabled" do + let(:comments_enabled) { false } + + it "does not show most_commented option to sort" do + expect(view.available_orders).not_to include("most_commented") + end + end + + context "with comments enabled" do + let(:comments_enabled) { true } + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + + it "shows most_commented option to sort" do + expect(view.available_orders).to include("most_commented") + end + end + + context "with or without comments and most_commented availability" do + let!(:debate_without_comments) { create(:debate, component:) } + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + let(:comments_enabled) { true } + + context "when there are no debates with comments" do + before do + debate_with_comments.update!(comments_count: 0) + end + + it "does not show most_commented option to sort" do + expect(view.available_orders).not_to include("most_commented") + end + end + + context "when there are debates with comments" do + it "shows most_commented option to sort" do + expect(view.available_orders).to include("most_commented") + end + end + end + end + + describe "#most_commented_order_available?" do + let!(:debate_without_comments) { create(:debate, component:) } + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + + context "when comments are disabled" do + let(:component) { create(:debates_component, :with_comments_disabled) } + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + + context "when comments are enabled" do + context "when there are debates with only zero comments" do + before do + debate_with_comments.update!(comments_count: 0) + end + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + + context "when there are debates with comments" do + it "returns true" do + expect(controller.send(:most_commented_order_available?)).to be true + end + end + + context "when debates are hidden" do + before do + create(:moderation, reportable: debate_with_comments, hidden_at: Time.current) + end + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + end + end + end + end +end diff --git a/decidim-debates/spec/models/decidim/debates/debate_spec.rb b/decidim-debates/spec/models/decidim/debates/debate_spec.rb index 92af0f10bedf3..57eb3d90fe6fe 100644 --- a/decidim-debates/spec/models/decidim/debates/debate_spec.rb +++ b/decidim-debates/spec/models/decidim/debates/debate_spec.rb @@ -134,4 +134,47 @@ it { is_expected.to be_falsey } end end + + describe ".most_commented_available?" do + let(:component) { create(:debates_component) } + + context "when comments are disabled" do + let(:component) { create(:debates_component, :with_comments_disabled) } + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + + context "when comments are enabled" do + context "when there are no debates with comments" do + let!(:debate_without_comments) { create(:debate, component:) } + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + + context "when there are debates with comments" do + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + + it "returns true" do + expect(described_class.most_commented_available?(component)).to be true + end + end + + context "when debates are hidden" do + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + + before do + create(:moderation, reportable: debate_with_comments, hidden_at: Time.current) + end + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + end + end end diff --git a/decidim-debates/spec/system/explore_debates_spec.rb b/decidim-debates/spec/system/explore_debates_spec.rb index c4f6749fe6124..9fedefb3dfa42 100644 --- a/decidim-debates/spec/system/explore_debates_spec.rb +++ b/decidim-debates/spec/system/explore_debates_spec.rb @@ -89,6 +89,79 @@ end end + context "when there are no debates with comments" do + let!(:debates) { create_list(:debate, 3, component:) } + + before do + visit_component + end + + it "does not show 'most_commented' sorting option" do + within ".order-by" do + expect(page).to have_css("div.order-by a", text: "Random") + page.find("a", text: "Random").click + expect(page).to have_no_content("Most commented") + end + end + end + + shared_examples "ordering debates by selected option" do |selected_option| + let(:first_debate_title) { translated(first_debate.title) } + let(:last_debate_title) { translated(last_debate.title) } + before do + visit_component + within ".order-by" do + expect(page).to have_css("div.order-by a", text: "Random") + page.find("a", text: "Random").click + click_on(selected_option) + end + end + + it "lists the debates ordered by selected option" do + expect(page).to have_css("[id^='debate']:first-child", text: first_debate_title) + expect(page).to have_css("[id^='debate']:last-child", text: last_debate_title) + end + end + + context "when ordering by 'recent'" do + let!(:old_debate) { create(:debate, component:, created_at: 1.day.ago) } + let!(:new_debate) { create(:debate, component:, created_at: Time.current) } + let(:first_debate) { new_debate } + let(:last_debate) { old_debate } + + it_behaves_like "ordering debates by selected option", "Most recent" + end + + context "when ordering by 'updated'" do + let!(:old_debate) { create(:debate, component:, updated_at: 1.day.ago) } + let!(:new_debate) { create(:debate, component:, updated_at: Time.current) } + let(:first_debate) { new_debate } + let(:last_debate) { old_debate } + + it_behaves_like "ordering debates by selected option", "Recently updated" + end + + context "when ordering by 'most_commented'" do + let!(:debate_without_comments) { create(:debate, component:, comments_count: 0) } + let!(:debate_with_comments) { create(:debate, component:, comments_count: 5) } + let(:first_debate) { debate_with_comments } + let(:last_debate) { debate_without_comments } + + before do + visit_component + within ".order-by" do + expect(page).to have_css("div.order-by a", text: "Random") + page.find("a", text: "Random").click + click_on("Most commented") + end + end + + it "lists the debates ordered by selected option" do + expect(page).to have_css("[id^='debate']:first-child", text: translated(debate_with_comments.title)) + expect(page).to have_css("[id^='debate']:last-child", text: translated(debate_without_comments.title)) + end + end + context "when there are open debates" do let(:debates) { nil } let!(:open_debate) do diff --git a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb index f5ea177cf03a7..785bf2b0a3ade 100644 --- a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb +++ b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb @@ -23,7 +23,7 @@ def possible_orders possible_orders = %w(random recent) possible_orders << "most_voted" if most_voted_order_available? possible_orders << "most_liked" if current_settings.likes_enabled? - possible_orders << "most_commented" if component_settings.comments_enabled? + possible_orders << "most_commented" if most_commented_order_available? possible_orders << "most_followed" possible_orders << "with_more_authors" if with_more_authors_order_available? possible_orders @@ -59,6 +59,12 @@ def with_more_authors_order_available? @with_more_authors_order_available = Decidim::Proposals::Proposal.with_more_authors_available?(current_component) end + def most_commented_order_available? + return @most_commented_order_available if defined?(@most_commented_order_available) + + @most_commented_order_available = Decidim::Proposals::Proposal.most_commented_available?(current_component) + end + def order_by_votes? most_voted_order_available? && current_settings.votes_blocked? end diff --git a/decidim-proposals/app/models/decidim/proposals/proposal.rb b/decidim-proposals/app/models/decidim/proposals/proposal.rb index c9c1404723cc8..626515d42b113 100644 --- a/decidim-proposals/app/models/decidim/proposals/proposal.rb +++ b/decidim-proposals/app/models/decidim/proposals/proposal.rb @@ -174,6 +174,17 @@ def self.with_more_authors_available?(component) .exists? end + def self.most_commented_available?(component) + return false unless component.settings.comments_enabled? + + where(component:) + .published + .not_hidden + .not_withdrawn + .where("comments_count > 0") + .exists? + end + acts_as_list scope: :decidim_component_id searchable_fields({ diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index 1137858fb8ffa..efae9f51a2998 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -36,12 +36,14 @@ POSSIBLE_SORT_ORDERS = %w(automatic random recent most_liked most_voted most_commented most_followed with_more_authors).freeze WITH_MORE_AUTHORS_ORDER = "with_more_authors" + MOST_COMMENTED_ORDER = "most_commented" sort_order_choices = lambda do |context| component = context[:component] orders = POSSIBLE_SORT_ORDERS.dup - return orders.excluding(WITH_MORE_AUTHORS_ORDER) unless component && Decidim::Proposals::Proposal.with_more_authors_available?(component) + orders = orders.excluding(WITH_MORE_AUTHORS_ORDER) unless component && Decidim::Proposals::Proposal.with_more_authors_available?(component) + orders = orders.excluding(MOST_COMMENTED_ORDER) unless component && Decidim::Proposals::Proposal.most_commented_available?(component) orders end diff --git a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb index 6ca3afd14e201..e1f625480bb9a 100644 --- a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb +++ b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb @@ -56,6 +56,7 @@ class OrderableFakeController < Decidim::ApplicationController context "when step has default_sort_order" do let(:component_default_sort_order) { "random" } let(:step_default_sort_order) { "most_commented" } + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } it "use it instead of component's" do expect(controller.send(:default_order)).to eq("most_commented") @@ -119,8 +120,18 @@ class OrderableFakeController < Decidim::ApplicationController let(:default_sort_order) { "most_commented" } let(:comments_enabled) { true } - it "default_order is most_commented" do - expect(controller.send(:default_order)).to eq(default_sort_order) + context "when there are no proposals with comments" do + it "defaults to random" do + expect(controller.send(:default_order)).to eq("random") + end + end + + context "when there are proposals with comments" do + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + it "default_order is most_commented" do + expect(controller.send(:default_order)).to eq(default_sort_order) + end end end @@ -200,6 +211,7 @@ class OrderableFakeController < Decidim::ApplicationController context "with comments enabled" do let(:comments_enabled) { true } + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } it "shows most_commented option to sort" do expect(view.available_orders).to include("most_commented") @@ -214,6 +226,28 @@ class OrderableFakeController < Decidim::ApplicationController end end + context "with or without comments and most_commented availability" do + let!(:proposal_without_comments) { create(:proposal, component:) } + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + let(:comments_enabled) { true } + + context "when there are no proposals with comments" do + before do + proposal_with_comments.update!(comments_count: 0) + end + + it "does not show most_commented option to sort" do + expect(view.available_orders).not_to include("most_commented") + end + end + + context "when there are proposals with comments" do + it "shows most_commented option to sort" do + expect(view.available_orders).to include("most_commented") + end + end + end + context "with or without coauthors and with_more_authors availability" do let!(:proposal_with_single_author) { create(:proposal, component:) } let!(:proposal_with_coauthors) { create(:proposal, component:) } @@ -278,6 +312,57 @@ class OrderableFakeController < Decidim::ApplicationController end end end + + describe "#most_commented_order_available?" do + let!(:proposal_without_comments) { create(:proposal, component:) } + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + context "when comments are disabled" do + let(:component) { create(:proposal_component, :with_comments_disabled) } + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + + context "when comments are enabled" do + context "when there are proposals with only zero comments" do + before do + proposal_with_comments.update!(comments_count: 0) + end + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + + context "when there are proposals with comments" do + it "returns true" do + expect(controller.send(:most_commented_order_available?)).to be true + end + end + + context "when proposals are not published" do + before do + proposal_with_comments.update!(published_at: nil) + end + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + + context "when proposals are hidden" do + before do + create(:moderation, reportable: proposal_with_comments, hidden_at: Time.current) + end + + it "returns false" do + expect(controller.send(:most_commented_order_available?)).to be false + end + end + end + end end end end diff --git a/decidim-proposals/spec/lib/decidim/proposals/component_spec.rb b/decidim-proposals/spec/lib/decidim/proposals/component_spec.rb index 863ab0ee2e9f9..5fafc6a05cc1d 100644 --- a/decidim-proposals/spec/lib/decidim/proposals/component_spec.rb +++ b/decidim-proposals/spec/lib/decidim/proposals/component_spec.rb @@ -245,6 +245,24 @@ expect(default_sort_order_container).to have_content("With more authors") end end + + context "when there are no proposals with comments" do + it "does not include most_commented" do + expect(default_sort_order_container).to have_no_content("Most commented") + end + end + + context "when there are proposals with comments" do + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + before do + visit edit_component_path + end + + it "includes most_commented" do + expect(default_sort_order_container).to have_content("Most commented") + end + end end end diff --git a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb index 3fa41773f57d8..ce3ba168b0d34 100644 --- a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb +++ b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb @@ -82,6 +82,61 @@ module Proposals end end + describe ".most_commented_available?" do + let(:component) { create(:proposal_component) } + + context "when comments are disabled" do + let(:component) { create(:proposal_component, :with_comments_disabled) } + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + + context "when comments are enabled" do + context "when there are no proposals with comments" do + let!(:proposal_without_comments) { create(:proposal, component:) } + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + + context "when there are proposals with comments" do + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + it "returns true" do + expect(described_class.most_commented_available?(component)).to be true + end + end + + context "when proposals are not published" do + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + before do + proposal_with_comments.update!(published_at: nil) + end + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + + context "when proposals are hidden" do + let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } + + before do + create(:moderation, reportable: proposal_with_comments, hidden_at: Time.current) + end + + it "returns false" do + expect(described_class.most_commented_available?(component)).to be false + end + end + end + end + it "has a votes association returning proposal votes" do expect(subject.votes.count).to eq(0) end diff --git a/decidim-proposals/spec/system/proposals_spec.rb b/decidim-proposals/spec/system/proposals_spec.rb index 5aac74a1b67b9..2b41406dc7962 100644 --- a/decidim-proposals/spec/system/proposals_spec.rb +++ b/decidim-proposals/spec/system/proposals_spec.rb @@ -664,6 +664,22 @@ end end + context "when there are no proposals with comments" do + let!(:proposals) { create_list(:proposal, 3, component:) } + + before do + visit_component + end + + it "does not show 'most_commented' ordering option" do + within ".order-by" do + expect(page).to have_css("div.order-by a", text: "Random") + page.find("a", text: "Random").click + expect(page).to have_no_content("Most commented") + end + end + end + context "when searching proposals" do let!(:proposals) do [ From ff41a04114d777b516752808c47f652e2ab90350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 25 Feb 2026 11:04:58 +0100 Subject: [PATCH 002/135] Fix transparent checkbox for assembly on edit (#16201) --- .../controllers/assembly_admin/controller.js | 4 ---- .../admin/admin_manages_assemblies_spec.rb | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/controller.js b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/controller.js index 8ef195373f662..2dd8a49aa4c97 100644 --- a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/controller.js +++ b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/controller.js @@ -50,10 +50,6 @@ export default class extends Controller { if (isTransparentCheckbox) { isTransparentCheckbox.disabled = (enabledPrivateSpace === false); - - if (isTransparentCheckbox.checked) { - isTransparentCheckbox.checked = false; - } } if (specialFeatures) { 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 9e945e03285ca..b0afda2e383d6 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -111,15 +111,11 @@ visit decidim_admin_assemblies.assemblies_path end - it "update a participatory process without images does not delete them" do + it "update an assembly without images does not delete them" do within "tr", text: translated(assembly3.title) do click_on translated(assembly3.title) end - within_admin_sidebar_menu do - click_on "About this assembly" - end - select(decidim_sanitize_translated(taxonomy.name), from: "taxonomies-#{taxonomy_filter.id}") click_on "Update" @@ -135,6 +131,18 @@ expect(src).to be_blob_url(hero_blob) end end + + describe "when the assembly is transparent" do + let!(:assembly3) { create(:assembly, :private, :transparent, organization:) } + + it "shows the transparent checkbox correctly" do + within "tr", text: translated(assembly3.title) do + click_on translated(assembly3.title) + end + + expect(page).to have_checked_field("assembly_is_transparent") + end + end end context "when managing parent assemblies" do From 6b7c3b738587a2d6336b3f40e2cdf753b8b9f0e6 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Thu, 21 Aug 2025 14:31:03 +0200 Subject: [PATCH 003/135] feat: add sr-only with email example in registration new --- decidim-core/app/views/decidim/devise/registrations/new.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/decidim-core/app/views/decidim/devise/registrations/new.html.erb b/decidim-core/app/views/decidim/devise/registrations/new.html.erb index 512b21edaa5ff..dc1ed200a32d2 100644 --- a/decidim-core/app/views/decidim/devise/registrations/new.html.erb +++ b/decidim-core/app/views/decidim/devise/registrations/new.html.erb @@ -34,6 +34,7 @@ <%= f.text_field :name, help_text: t("decidim.devise.registrations.new.username_help"), autocomplete: "name", placeholder: "John Doe" %> <%= f.email_field :email, autocomplete: "email", placeholder: t("placeholder_email", scope: "decidim.devise.shared") %> + <%= t("placeholder_email", scope: "decidim.devise.shared") %> <%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %> From 455c970ca3b960a91b9cd5c8faa7a577bddb91dc Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Thu, 21 Aug 2025 14:32:05 +0200 Subject: [PATCH 004/135] feat: improve accessibility on terms of service in registration --- .../app/views/decidim/devise/shared/_tos_fields.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb index 81fa0e1823971..fed8b0962b88d 100644 --- a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb +++ b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb @@ -1,13 +1,13 @@

<%= t("decidim.devise.registrations.new.tos_title") %>

+ Required field -
+
<% terms_of_service_summary_content_blocks.each do |content_block| %> <%= cell content_block.manifest.cell, content_block %> <% end %>
- - <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service", locale: current_locale))), label_options: { class: "form__wrapper-checkbox-label" } %> + <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service", locale: current_locale))), label_options: { class: "form__wrapper-checkbox-label" }, "aria-describedby": "terms_of_service_summary", "required": "required" %>
From 8f410a7f37742076b9cfdae73feb66d87e6da7b7 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Fri, 22 Aug 2025 10:14:21 +0200 Subject: [PATCH 005/135] Trigger CI From 7f5e01fa7954b71e449756e63e4180d647562103 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:08:30 +0200 Subject: [PATCH 006/135] Bump to dependencies: Bump rspec-rails from 6.1.5 to 8.0.3 (#16208) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 16 ++++++++-------- .../spec/lib/upgrade/wysiwyg_migrator_spec.rb | 2 +- decidim-dev/decidim-dev.gemspec | 2 +- decidim-generators/Gemfile.lock | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b28081094ae02..99a43b7c05663 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -139,7 +139,7 @@ PATH rspec (~> 3.12) rspec-cells (~> 0.3.7) rspec-html-matchers (~> 0.10) - rspec-rails (~> 6.0) + rspec-rails (>= 6, < 9) rspec-retry (~> 0.6.2) rspec_junit_formatter (~> 0.6.0) rubocop (~> 1.78.0) @@ -746,7 +746,7 @@ GEM rspec-cells (0.3.10) cells (>= 4.0.0, < 6.0.0) rspec-rails (>= 3.0.0) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -754,20 +754,20 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.5) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (8.0.3) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.13.4) + rspec-support (3.13.7) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.78.0) diff --git a/decidim-core/spec/lib/upgrade/wysiwyg_migrator_spec.rb b/decidim-core/spec/lib/upgrade/wysiwyg_migrator_spec.rb index 30168bbc1fe49..210a089ba249a 100644 --- a/decidim-core/spec/lib/upgrade/wysiwyg_migrator_spec.rb +++ b/decidim-core/spec/lib/upgrade/wysiwyg_migrator_spec.rb @@ -270,7 +270,7 @@ module Decidim value_converter, &block ) - end.to yield_successive_args([klass, 1..100], [klass, 101..150]) + end.to yield_successive_args([klass, 1..100], [klass, 101..data.length]) klass.where(component:).each do |record| expect(record.title).to eq("en" => "Foobar", "machine_translations" => { "es" => "Foobar ES" }) diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index a9e855ac903f2..c7cf427b5d4ab 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -51,7 +51,7 @@ Gem::Specification.new do |s| s.add_dependency "rspec-cells", "~> 0.3.7" s.add_dependency "rspec-html-matchers", "~> 0.10" s.add_dependency "rspec_junit_formatter", "~> 0.6.0" - s.add_dependency "rspec-rails", "~> 6.0" + s.add_dependency "rspec-rails", ">= 6", "< 9" s.add_dependency "rspec-retry", "~> 0.6.2" s.add_dependency "rubocop", "~> 1.78.0" s.add_dependency "rubocop-capybara", "~> 2.22.0", ">= 2.22.1" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 690d931b91041..36032a1e58d87 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -139,7 +139,7 @@ PATH rspec (~> 3.12) rspec-cells (~> 0.3.7) rspec-html-matchers (~> 0.10) - rspec-rails (~> 6.0) + rspec-rails (>= 6, < 9) rspec-retry (~> 0.6.2) rspec_junit_formatter (~> 0.6.0) rubocop (~> 1.78.0) @@ -739,7 +739,7 @@ GEM rspec-cells (0.3.10) cells (>= 4.0.0, < 6.0.0) rspec-rails (>= 3.0.0) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -747,20 +747,20 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.5) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (8.0.3) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.13.4) + rspec-support (3.13.7) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.78.0) From bbffaed8231f3a5ba13e17025d7e2b9741f124b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:31:53 +0200 Subject: [PATCH 007/135] Bump to dependencies: Bump rubocop-rspec from 3.6.0 to 3.7.0 (#16220) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 16 ++++++++-------- decidim-generators/Gemfile.lock | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 99a43b7c05663..ef494ee1d21d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -629,7 +629,7 @@ GEM parallel paranoia (3.0.1) activerecord (>= 6, < 8.1) - parser (3.3.9.0) + parser (3.3.10.2) ast (~> 2.4.1) racc pg (1.5.9) @@ -726,7 +726,7 @@ GEM tsort redcarpet (3.6.1) redis (4.8.1) - regexp_parser (2.10.0) + regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) request_store (1.7.0) @@ -781,9 +781,9 @@ GEM rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.45.1) + rubocop-ast (1.49.0) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -807,7 +807,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.6.0) + rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec_rails (2.31.0) @@ -877,9 +877,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) uniform_notifier (1.17.0) uri (1.1.1) useragent (0.16.11) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 36032a1e58d87..6cffbc9d16e75 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -622,7 +622,7 @@ GEM parallel paranoia (3.0.1) activerecord (>= 6, < 8.1) - parser (3.3.9.0) + parser (3.3.10.2) ast (~> 2.4.1) racc pg (1.5.9) @@ -719,7 +719,7 @@ GEM tsort redcarpet (3.6.1) redis (4.8.1) - regexp_parser (2.10.0) + regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) request_store (1.7.0) @@ -774,9 +774,9 @@ GEM rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.45.1) + rubocop-ast (1.49.0) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -800,7 +800,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.6.0) + rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec_rails (2.31.0) @@ -868,9 +868,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) uniform_notifier (1.17.0) uri (1.1.1) useragent (0.16.11) From 1ab9fa43dab49c5bc00122ffb6cb6a3e36d3b87a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 07:27:29 +0200 Subject: [PATCH 008/135] Bump to dependencies: Bump chartkick from 5.1.5 to 5.2.1 (#16221) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ef494ee1d21d3..21240c6ef0b2a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -63,7 +63,7 @@ PATH cells-erb (~> 0.1.0) cells-rails (~> 0.1.3) charlock_holmes (~> 0.7) - chartkick (~> 5.1.2) + chartkick (>= 5.1.2, < 5.3.0) concurrent-ruby (~> 1.3.0) connection_pool (< 3) data_migrate (~> 11.3) @@ -333,7 +333,7 @@ GEM cells (>= 4.1.6, < 5.0.0) cgi (0.5.1) charlock_holmes (0.7.9) - chartkick (5.1.5) + chartkick (5.2.1) childprocess (5.1.0) logger (~> 1.5) chunky_png (1.4.0) diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 8ba823ef40bb9..6cbeec2547cd7 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -37,7 +37,7 @@ Gem::Specification.new do |s| s.add_dependency "cells-erb", "~> 0.1.0" s.add_dependency "cells-rails", "~> 0.1.3" s.add_dependency "charlock_holmes", "~> 0.7" - s.add_dependency "chartkick", "~> 5.1.2" + s.add_dependency "chartkick", ">= 5.1.2", "< 5.3.0" s.add_dependency "connection_pool", "< 3" s.add_dependency "data_migrate", "~> 11.3" s.add_dependency "date_validator", "~> 0.12.0" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 6cffbc9d16e75..6783eda3d43ca 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -63,7 +63,7 @@ PATH cells-erb (~> 0.1.0) cells-rails (~> 0.1.3) charlock_holmes (~> 0.7) - chartkick (~> 5.1.2) + chartkick (>= 5.1.2, < 5.3.0) concurrent-ruby (~> 1.3.0) connection_pool (< 3) data_migrate (~> 11.3) @@ -332,7 +332,7 @@ GEM cells (>= 4.1.6, < 5.0.0) cgi (0.5.1) charlock_holmes (0.7.9) - chartkick (5.1.5) + chartkick (5.2.1) childprocess (5.1.0) logger (~> 1.5) chunky_png (1.4.0) From 761658113416daa435337e5e65beb646633ff5c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:21:30 +0200 Subject: [PATCH 009/135] Bump to dependencies: Bump listen from 3.9.0 to 3.10.0 (#16222) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile | 2 +- Gemfile.lock | 9 +++++---- decidim-generators/Gemfile.lock | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 294d62a2351f5..8f4b191dc6eaa 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,6 @@ end group :development do gem "letter_opener_web", "~> 3.0" - gem "listen", "~> 3.1" + gem "listen", "~> 3.10" gem "web-console", "~> 4.2" end diff --git a/Gemfile.lock b/Gemfile.lock index 21240c6ef0b2a..337fe8cb037a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -425,8 +425,8 @@ GEM faraday-net_http (3.4.2) net-http (~> 0.5) fast-stemmer (1.0.2) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.3-arm64-darwin) + ffi (1.17.3-x86_64-linux-gnu) fiber-storage (1.0.1) file_validators (3.0.0) activemodel (>= 3.2) @@ -535,7 +535,8 @@ GEM railties (>= 6.1) rexml lint_roller (1.1.0) - listen (3.9.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) @@ -946,7 +947,7 @@ DEPENDENCIES decidim-initiatives! decidim-templates! letter_opener_web (~> 3.0) - listen (~> 3.1) + listen (~> 3.10) parallel_tests (~> 4.2) puma (>= 6.3.1) web-console (~> 4.2) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 6783eda3d43ca..9943fc47eda8e 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -424,7 +424,7 @@ GEM faraday-net_http (3.4.2) net-http (~> 0.5) fast-stemmer (1.0.2) - ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.3-x86_64-linux-gnu) fiber-storage (1.0.1) file_validators (3.0.0) activemodel (>= 3.2) @@ -530,7 +530,8 @@ GEM railties (>= 5.2) rexml lint_roller (1.1.0) - listen (3.9.0) + listen (3.10.0) + logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) From c8411cdabe4ae048e8e88b2893ec1d1f49694009 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Thu, 26 Feb 2026 11:07:22 +0100 Subject: [PATCH 010/135] refactor: add key translation for required field --- .../app/views/decidim/devise/shared/_tos_fields.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb index fed8b0962b88d..44781dad2ef08 100644 --- a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb +++ b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb @@ -1,6 +1,6 @@

<%= t("decidim.devise.registrations.new.tos_title") %>

- Required field + <%= t("forms.required") %>
<% terms_of_service_summary_content_blocks.each do |content_block| %> From 347e4a1d53b320b56477fdf3d0efcae6c24e3b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 26 Feb 2026 11:27:22 +0100 Subject: [PATCH 011/135] Fix password help text for admins and users (#16187) --- .../spec/system/admin_invite_spec.rb | 125 ++++++++++++++++++ .../decidim/devise/invitations/edit.html.erb | 2 +- decidim-core/config/locales/en.yml | 4 +- .../helpers/decidim/passwords_helper_spec.rb | 9 +- decidim-core/spec/system/account_spec.rb | 5 +- .../spec/system/authentication_spec.rb | 3 +- 6 files changed, 140 insertions(+), 8 deletions(-) diff --git a/decidim-admin/spec/system/admin_invite_spec.rb b/decidim-admin/spec/system/admin_invite_spec.rb index ef869626cbdb4..ff67be3186ad4 100644 --- a/decidim-admin/spec/system/admin_invite_spec.rb +++ b/decidim-admin/spec/system/admin_invite_spec.rb @@ -52,5 +52,130 @@ expect(page).to have_current_path "/admin/admin_terms/show" end + + it "displays admin password requirements" do + visit last_email_link + + expect(page).to have_content("15 characters minimum") + expect(page).to have_content("must contain at least 5 different characters") + expect(page).to have_content("must not be too common") + expect(page).to have_content("must be different from your name, nickname, email, the organization's host") + expect(page).to have_content("must be different from your old passwords") + end + + it "rejects passwords that are too short for admin" do + visit last_email_link + + fill_in :invitation_user_nickname, with: "caballo_loco" + fill_in :invitation_user_password, with: "short123" + check :invitation_user_tos_agreement + click_on "Save" + + expect(page).to have_content("password is too short") + end + + it "rejects passwords containing the user's name" do + visit last_email_link + + fill_in :invitation_user_nickname, with: "caballo_loco" + fill_in :invitation_user_password, with: "Fiorello123456789!" + check :invitation_user_tos_agreement + click_on "Save" + + expect(page).to have_content("is too similar to your name") + end + + it "rejects passwords with less than 5 unique characters" do + visit last_email_link + + fill_in :invitation_user_nickname, with: "caballo_loco" + fill_in :invitation_user_password, with: "aaaaaaaaaaaaaaa!" + check :invitation_user_tos_agreement + click_on "Save" + + expect(page).to have_content("does not have enough unique characters") + end + end + + context "when inviting a regular user" do + let(:organization) { create(:organization, host: "new-decide.lvh.me") } + let(:inviter) { create(:user, :confirmed, :admin, organization:) } + + let!(:invited_user) do + perform_enqueued_jobs do + Decidim::User.invite!( + { + organization:, + name: "Invited User", + email: "invited_user@example.org" + }, + inviter + ) + end + end + + it "displays regular user password requirements in help text" do + switch_to_host("new-decide.lvh.me") + visit last_email_link + + expect(page).to have_content("10 characters minimum") + expect(page).to have_content("must contain at least 5 different characters") + expect(page).to have_content("must not be too common") + expect(page).to have_content("must be different from your name, nickname, email and the organization's host") + expect(page).to have_no_content("must be different from your old passwords") + end + + it "allows accepting invitation with valid user password" do + switch_to_host("new-decide.lvh.me") + visit last_email_link + + fill_in :invitation_user_nickname, with: "invited_user" + fill_in :invitation_user_password, with: "decidim123" + check :invitation_user_tos_agreement + click_on "Save" + + expect(page).to have_content("Your password was set successfully. You are now signed in.") + expect(Decidim::User.find_by(email: "invited_user@example.org")).not_to be_admin + end + end + + context "when admin_password_strong is disabled" do + let(:organization) { create(:organization, host: "new-decide.lvh.me") } + let(:inviter) { create(:user, :confirmed, :admin, organization:) } + let!(:invited_admin) do + perform_enqueued_jobs do + Decidim::User.invite!( + { + organization:, + name: "Invited Admin", + email: "invited_admin@example.org", + admin: true + }, + inviter + ) + end + end + + before do + allow(Decidim.config).to receive(:admin_password_strong).and_return(false) + end + + it "displays regular password requirements for admins" do + switch_to_host("new-decide.lvh.me") + visit last_email_link + + expect(page).to have_content("10 characters minimum") + expect(page).to have_content("must contain at least 5 different characters") + expect(page).to have_content("must be different from your name, nickname, email and the organization's host") + expect(page).to have_no_content("must be different from your old passwords") + end + end + + context "with invalid invitation token" do + it "shows error for invalid token" do + visit "/users/invitation/accept?invitation_token=invalid_token" + + expect(page).to have_content("The invitation token provided is not valid") + end end end diff --git a/decidim-core/app/views/decidim/devise/invitations/edit.html.erb b/decidim-core/app/views/decidim/devise/invitations/edit.html.erb index 2a28846b24c1c..9ca7f40621bb1 100644 --- a/decidim-core/app/views/decidim/devise/invitations/edit.html.erb +++ b/decidim-core/app/views/decidim/devise/invitations/edit.html.erb @@ -17,7 +17,7 @@ <%= f.text_field :nickname, help_text: t("devise.invitations.edit.nickname_help", organization: current_organization_name), required: "required", autocomplete: "nickname" %> <% if f.object.class.require_password_on_accepting %> - <%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %> + <%= render partial: "decidim/account/password_fields", locals: { form: f, user: resource.admin? ? :admin : :user } %> <% end %>
diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index 34108432361f5..095281676a61b 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -1891,8 +1891,8 @@ en: confirm_new_password: Confirm new password new_password: New password old_password_help: In order to confirm the changes to your account, please provide your current password. - password_help: "%{minimum_characters} characters minimum, must not be too common (e.g. 123456) and must be different from your nickname and your email." - password_help_admin: "%{minimum_characters} characters minimum, must not be too common (e.g. 123456), must be different from your nickname and your email and must be different from your old passwords." + password_help: "%{minimum_characters} characters minimum, must contain at least 5 different characters, must not be too common (e.g. 123456) and must be different from your name, nickname, email and the organization's host." + password_help_admin: "%{minimum_characters} characters minimum, must contain at least 5 different characters, must not be too common (e.g. 123456), must be different from your name, nickname, email, the organization's host and must be different from your old passwords." title: Password change new: forgot_your_password: Forgot your password? diff --git a/decidim-core/spec/helpers/decidim/passwords_helper_spec.rb b/decidim-core/spec/helpers/decidim/passwords_helper_spec.rb index a53346d58e8fb..bcc3f78bf7b85 100644 --- a/decidim-core/spec/helpers/decidim/passwords_helper_spec.rb +++ b/decidim-core/spec/helpers/decidim/passwords_helper_spec.rb @@ -18,8 +18,9 @@ module Decidim expect(subject[:help_text]).to eq( [ "10 characters minimum,", + "must contain at least 5 different characters,", "must not be too common (e.g. 123456)", - "and must be different from your nickname and your email." + "and must be different from your name, nickname, email and the organization's host." ].join(" ") ) end @@ -36,8 +37,9 @@ module Decidim expect(subject[:help_text]).to eq( [ "15 characters minimum,", + "must contain at least 5 different characters,", "must not be too common (e.g. 123456),", - "must be different from your nickname and your email", + "must be different from your name, nickname, email, the organization's host", "and must be different from your old passwords." ].join(" ") ) @@ -56,8 +58,9 @@ module Decidim expect(subject[:help_text]).to eq( [ "10 characters minimum,", + "must contain at least 5 different characters,", "must not be too common (e.g. 123456)", - "and must be different from your nickname and your email." + "and must be different from your name, nickname, email and the organization's host." ].join(" ") ) end diff --git a/decidim-core/spec/system/account_spec.rb b/decidim-core/spec/system/account_spec.rb index 3b45e611377ce..c6b805af81545 100644 --- a/decidim-core/spec/system/account_spec.rb +++ b/decidim-core/spec/system/account_spec.rb @@ -157,7 +157,10 @@ it "toggles old and new password fields" do within "form.edit_user" do - expect(page).to have_content("must not be too common (e.g. 123456) and must be different from your nickname and your email.") + expect(page).to have_content("10 characters minimum") + expect(page).to have_content("must contain at least 5 different characters") + expect(page).to have_content("must not be too common") + expect(page).to have_content("must be different from your name, nickname, email and the organization's host") expect(page).to have_field("user[password]", with: "", type: "password") expect(page).to have_field("user[old_password]", with: "", type: "password") click_on "Change password" diff --git a/decidim-core/spec/system/authentication_spec.rb b/decidim-core/spec/system/authentication_spec.rb index d6c8312a73a34..6ee300ed09b76 100644 --- a/decidim-core/spec/system/authentication_spec.rb +++ b/decidim-core/spec/system/authentication_spec.rb @@ -610,8 +610,9 @@ end expect(page).to have_content("10 characters minimum") - expect(page).to have_content("must be different from your nickname and your email") + expect(page).to have_content("must contain at least 5 different characters") expect(page).to have_content("must not be too common") + expect(page).to have_content("must be different from your name, nickname, email and the organization's host") expect(page).to have_current_path "/users/password" end From d4855120201f26ed6bff1ac871c25d247d8b2ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 26 Feb 2026 11:47:32 +0100 Subject: [PATCH 012/135] Fix details in releases docs: dollar sign, commands, git pull (#16212) Co-authored-by: Alexandru Emil Lupu --- .../develop/pages/maintainers/releases.adoc | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/modules/develop/pages/maintainers/releases.adoc b/docs/modules/develop/pages/maintainers/releases.adoc index 8ddd934c92365..01ac261e4efc4 100644 --- a/docs/modules/develop/pages/maintainers/releases.adoc +++ b/docs/modules/develop/pages/maintainers/releases.adoc @@ -1,14 +1,14 @@ = Releasing new versions -In order to release new version you need to be owner of all the gems at RubyGems, ask one of the owners to add you before releasing. Try `gem owner decidim` to find out the owners of the gem. It is worth making sure you are owner of all gems. +In order to release new version you need to: -In order to release any other version of Decidim, is mandatory to have the `decidim-maintainers_toolbox` installed, and to have it on the latest version. What we can, we aim to automate. -[source,bash] ----- -gem install decidim-maintainers_toolbox ----- +. be owner of all the gems at RubyGems, ask one of the owners to add you before releasing. Try `gem owner decidim` to find out the owners of the gem. It is worth making sure you are owner of all gems. +. be owner of all the NPM packages +. have the `gh` command line installed. You can install by following the https://github.com/cli/cli/blob/trunk/docs/install_linux.md[GH installation instructions]. +. have the `yq` command line installed. You can install it with `snap install yq` in Ubuntu. +. have the `decidim-maintainers_toolbox` gem. You can install it with `gem install decidim-maintainers_toolbox`. -Before you begin the release process, make sure you check if there are any open or pending backports. Currently, is not mandatory to open or merge all the backports, but is something that we usually aim for. Refer to the xref:develop:backports.adoc[backports] page for more information. +Before you begin the release process, make sure you check if there are any open or pending backports. Currently, is not mandatory to open or merge all the backports, but is something that we usually aim for. Refer to the xref:develop:backports.adoc[backports] page for more information. == Release Candidates @@ -23,7 +23,7 @@ If this is a *Release Candidate version* release, the steps to follow are: [source,bash] ---- gem install decidim-maintainers_toolbox -decidim-releaser --github-token=(gh auth token) --version-type=rc +decidim-releaser --github-token=$(gh auth token) --version-type=rc ---- . This will create the stable branch and also create two Pull Requests: .. One for changing the development version on the `develop` branch (with title "Bump develop to next release version (x.y.z)") @@ -54,7 +54,7 @@ Release Candidates will be tested in a production server (usually Metadecidim) d [source,bash] ---- gem install decidim-maintainers_toolbox -decidim-releaser --github-token=(gh auth token) --version-type=minor +decidim-releaser --github-token=$(gh auth token) --version-type=minor ---- . Wait for the tests to finish and check that everything is passing before releasing the version. NOTE: When you bump the version, the generator tests will fail because the gems and NPM packages have not been actually published yet (as in sent to rubygems/npm). You may see errors such as `No matching version found for @decidim/browserslist-config@~0.xx.y` in the CI logs. This should be fine as long as you have ensured that the generators tests passed in the previous commit. @@ -108,14 +108,14 @@ After you commit this change in `develop` branch you will have to wait a couple Releasing new versions from a *_release/x.y-stable_* branch is quite easy. The process is very similar from releasing a new Decidim version: -. Merge all the https://github.com/decidim/decidim/pulls?q=is%3Apr+is%3Aopen+author%3Adecidim-bot+sort%3Aupdated-desc[Crowdin pull requests created by the user `decidim-bot`], specially the one that is going to be marged against the release branch `release/x.y-stable` that should be returned by the provided example search (pick the correct pull request for the release from the results). +. Merge all the https://github.com/decidim/decidim/pulls?q=is%3Apr+is%3Aopen+author%3Adecidim-bot+sort%3Aupdated-desc[Crowdin pull requests created by the user `decidim-bot`], specially the one that is going to be merged against the release branch `release/x.y-stable` that should be returned by the provided example search (pick the correct pull request for the release from the results). . Make sure that there are no more PRs to backport. Learn more about xref:develop:backports.adoc[Backports]. -. Checkout the branch you want to release: `git checkout -b release/x.y-stable` +. Checkout the branch you want to release: `git checkout release/x.y-stable && git pull origin release/x.y-stable` . Install the last version of the `decidim-maintainers_toolbox` gem, and run the releaser command. Mind that for this to work you need locally the gh CLI from GitHub. [source,bash] ---- gem install decidim-maintainers_toolbox -decidim-releaser --github-token=(gh auth token) --version-type=patch +decidim-releaser --github-token=$(gh auth token) --version-type=patch ---- . This will create a Pull Request for the new release with title `Bump to vx.y.z version`. Wait for the tests to finish and check that everything is passing before releasing the version. NOTE: When you bump the version, the generator tests will fail because the gems and NPM packages have not been actually published yet (as in sent to rubygems/npm). You may see errors such as `No matching version found for @decidim/browserslist-config@~0.xx.y` in the CI logs. This should be fine as long as you have ensured that the generators tests passed in the previous commit. From 6c8d398ed7bc549be889816feb218c41d6f01cfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:32:41 +0100 Subject: [PATCH 013/135] Bump to dependencies: Bump doorkeeper-i18n from 4.0.1 to 5.2.8 (#16223) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 5 +++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 337fe8cb037a9..bbf671eddf24e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,7 +72,7 @@ PATH devise-i18n (~> 1.2) diffy (~> 3.3) doorkeeper (~> 5.6, >= 5.6.6) - doorkeeper-i18n (~> 4.0) + doorkeeper-i18n (>= 4, < 6) file_validators (~> 3.0) fog-local (~> 0.6) geocoder (~> 1.8) @@ -383,7 +383,8 @@ GEM docile (1.4.1) doorkeeper (5.8.2) railties (>= 5) - doorkeeper-i18n (4.0.1) + doorkeeper-i18n (5.2.8) + doorkeeper (>= 5.2) drb (2.2.3) dry-auto_inject (1.1.0) dry-core (~> 1.1) diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 6cbeec2547cd7..48af1c03e80ac 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -45,7 +45,7 @@ Gem::Specification.new do |s| s.add_dependency "devise-i18n", "~> 1.2" s.add_dependency "diffy", "~> 3.3" s.add_dependency "doorkeeper", "~> 5.6", ">= 5.6.6" - s.add_dependency "doorkeeper-i18n", "~> 4.0" + s.add_dependency "doorkeeper-i18n", ">= 4", "< 6" s.add_dependency "file_validators", "~> 3.0" s.add_dependency "fog-local", "~> 0.6" s.add_dependency "geocoder", "~> 1.8" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 9943fc47eda8e..9300b11aa0315 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -72,7 +72,7 @@ PATH devise-i18n (~> 1.2) diffy (~> 3.3) doorkeeper (~> 5.6, >= 5.6.6) - doorkeeper-i18n (~> 4.0) + doorkeeper-i18n (>= 4, < 6) file_validators (~> 3.0) fog-local (~> 0.6) geocoder (~> 1.8) @@ -382,7 +382,8 @@ GEM docile (1.4.1) doorkeeper (5.8.2) railties (>= 5) - doorkeeper-i18n (4.0.1) + doorkeeper-i18n (5.2.8) + doorkeeper (>= 5.2) drb (2.2.3) dry-auto_inject (1.1.0) dry-core (~> 1.1) From 69d7b1989ebd850d81221c134f5e941df4b17301 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 26 Feb 2026 14:08:28 +0200 Subject: [PATCH 014/135] Fix error state is not visible when translation is missing (#16157) * Fix error display on I18n fields * Add words to expect list * Apply code suggestions * Apply review recommendations * Apply coderabbit recommendation * Fix i18n usage * Add specs for translated field * Fix specs * Debug available locales * Add some more debug * Remove debug --- .github/actions/spelling/expect.txt | 2 + .../system/admin_manages_organization_spec.rb | 36 +++ .../controllers/language_change/controller.js | 1 + .../language_change/language_change.test.js | 13 + decidim-core/lib/decidim/form_builder.rb | 94 ++++--- decidim-core/spec/lib/form_builder_spec.rb | 231 ++++++++++++++++-- 6 files changed, 321 insertions(+), 56 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 54ba642be5ae0..f9315f4e9bcd8 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -19,6 +19,7 @@ activelog activemodel activerecord activestorage +actualitzar actualizado addlabel Adeu @@ -280,6 +281,7 @@ erblint Errbit Esa espa +estar estat etherpad evanfuture diff --git a/decidim-admin/spec/system/admin_manages_organization_spec.rb b/decidim-admin/spec/system/admin_manages_organization_spec.rb index efabd2af563e4..28167accf7538 100644 --- a/decidim-admin/spec/system/admin_manages_organization_spec.rb +++ b/decidim-admin/spec/system/admin_manages_organization_spec.rb @@ -61,6 +61,25 @@ expect(page).to have_content("There is an error in this field.") end + it "displays the form error for official language" do + visit decidim_admin.edit_organization_path(locale: :ca) + + expect(page).to have_css("#organization_name_ca", visible: :visible) + expect(page).to have_field("organization_name_ca", with: organization.name["ca"]) + + within "#organization-name-tabs" do + click_on "English" + end + fill_in :organization_name_en, with: "" + + click_on "Actualitzar" + expect(page).to have_content("S'ha produït un error en actualitzar aquesta organització.") + + expect(page).to have_css("#organization_name_en", visible: :visible) + expect(page).to have_field("organization_name_en", with: "") + expect(page).to have_content("no pot estar en blanc") + end + context "when there are more than 4 locales in the organization" do let(:available_locales) { %w(en ca es fr it) } let(:organization_names) do @@ -93,6 +112,23 @@ load "#{Decidim::Admin::Engine.root}/app/forms/decidim/admin/organization_form.rb" end + it "displays the form error for official language" do + visit decidim_admin.edit_organization_path(locale: :ca) + + expect(page).to have_css("#organization_name_ca", visible: :visible) + expect(page).to have_field("organization_name_ca", with: organization_names[:ca]) + + select "English", from: "organization-name-tabs" + fill_in :organization_name_en, with: "" + + click_on "Actualitzar" + expect(page).to have_content("S'ha produït un error en actualitzar aquesta organització.") + + expect(page).to have_css("#organization_name_en", visible: :visible) + expect(page).to have_field("organization_name_en", with: "") + expect(page).to have_content("no pot estar en blanc") + end + it "renders a dropdown for the language selector and switches between languages" do visit decidim_admin.edit_organization_path diff --git a/decidim-core/app/packs/src/decidim/controllers/language_change/controller.js b/decidim-core/app/packs/src/decidim/controllers/language_change/controller.js index 95a2b6104a129..0b1d82b3cff9f 100644 --- a/decidim-core/app/packs/src/decidim/controllers/language_change/controller.js +++ b/decidim-core/app/packs/src/decidim/controllers/language_change/controller.js @@ -10,6 +10,7 @@ export default class extends Controller { connect() { this.handleChange = this.handleChange.bind(this); this.element.addEventListener("change", this.handleChange); + this.element.dispatchEvent(new Event("change")); } disconnect() { diff --git a/decidim-core/app/packs/src/decidim/controllers/language_change/language_change.test.js b/decidim-core/app/packs/src/decidim/controllers/language_change/language_change.test.js index 1eb2916a3568e..44a13ddee5c69 100644 --- a/decidim-core/app/packs/src/decidim/controllers/language_change/language_change.test.js +++ b/decidim-core/app/packs/src/decidim/controllers/language_change/language_change.test.js @@ -72,6 +72,19 @@ describe("LanguageChangeController", () => { expect(removeSpy).toHaveBeenCalledWith("change", controller.handleChange); removeSpy.mockRestore(); }); + + it("activates the selected option's panel on connect", () => { + const options = selectElement.querySelectorAll("option"); + options[1].selected = true; + + controller.disconnect(); + controller.connect(); + + expect(panel0.classList.contains("is-active")).toBe(false); + expect(panel0.ariaHidden).toBe("true"); + expect(panel1.classList.contains("is-active")).toBe(true); + expect(panel1.ariaHidden).toBe("false"); + }); }); describe("handleChange", () => { diff --git a/decidim-core/lib/decidim/form_builder.rb b/decidim-core/lib/decidim/form_builder.rb index 1d6c8babc2d8d..20685b85696da 100644 --- a/decidim-core/lib/decidim/form_builder.rb +++ b/decidim-core/lib/decidim/form_builder.rb @@ -45,14 +45,6 @@ def collection_radio_buttons(attribute, collection, value_attribute, text_attrib end # rubocop:enable Metrics/ParameterLists - def create_language_selector(locales, tabs_id, name) - if locales.count > 4 - language_selector_select(locales, tabs_id, name) - else - language_tabs(locales, tabs_id, name) - end - end - # Public: Generates a form field for each locale. # # type - The form field's type, like `text_area` or `text_field` @@ -65,23 +57,11 @@ def translated(type, name, options = {}) tabs_id = sanitize_tabs_selector(options[:tabs_id] || "#{object_name}-#{name}-tabs") - label_tabs = content_tag(:div, class: "label--tabs") do - field_label = label_i18n(name, options[:label] || label_for(name), required: options[:required]) + error_on_locale = locales.find { |locale| error?(name_with_locale(name, locale)) } - language_selector = "".html_safe - language_selector = create_language_selector(locales, tabs_id, name) if options[:label] != false + label_tabs = translated_labels(name, options, tabs_id, error_on_locale) - safe_join [field_label, language_selector] - end - - tabs_content = content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do - locales.each_with_index.inject("".html_safe) do |string, (locale, index)| - tab_content_id = "#{tabs_id}-#{name}-panel-#{index}" - string + content_tag(:div, class: tab_element_class_for("panel", index), id: tab_content_id, "aria-hidden": tab_attr_aria_hidden_for(index)) do - send(type, name_with_locale(name, locale), options.merge(label: false)) - end - end - end + tabs_content = translated_tabs(type, name, options, tabs_id, error_on_locale) safe_join [label_tabs, tabs_content] end @@ -452,6 +432,44 @@ def text_area(attribute, options = {}) private + def translated_tabs(type, name, options, tabs_id, error_on_locale = nil) + content_tag(:div, class: "tabs-content", data: { tabs_content: tabs_id }) do + locales.each_with_index.inject("".html_safe) do |string, (locale, index)| + tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}" + + aria_hidden = (error_on_locale.present? ? !locale.eql?(error_on_locale) : index.positive?).to_s + css_class = if error_on_locale.present? + tab_element_class_for("panel", locale.eql?(error_on_locale) ? 0 : 1) + else + tab_element_class_for("panel", index) + end + + string + content_tag(:div, class: css_class, id: tab_content_id, "aria-hidden": aria_hidden) do + send(type, name_with_locale(name, locale), options.merge(label: false)) + end + end + end + end + + def create_language_selector(locales, tabs_id, name, error_on_locale = nil) + if locales.count > 4 + language_selector_select(locales, tabs_id, name, error_on_locale) + else + language_tabs(locales, tabs_id, name, error_on_locale) + end + end + + def translated_labels(name, options, tabs_id, error_on_locale = nil) + content_tag(:div, class: "label--tabs") do + field_label = label_i18n(name, options[:label] || label_for(name), required: options[:required]) + + language_selector = "".html_safe + language_selector = create_language_selector(locales, tabs_id, name, error_on_locale) if options[:label] != false + + safe_join [field_label, language_selector] + end + end + def editor_hidden_options(name, options) hidden_options = extract_validations(name, options).merge(options) if hidden_options[:minlength] || hidden_options[:maxlength] @@ -682,9 +700,7 @@ def tab_element_class_for(type, index) end def tab_attr_aria_hidden_for(index) - return "false" if index.zero? - - "true" + index.positive?.to_s end def locales @@ -799,29 +815,35 @@ def tag_from_options(name, options) class: "columns") end - def language_selector_select(locales, tabs_id, name) + # i18n-tasks-use t('locale.name_with_error') + # i18n-tasks-use t('locale.name') + def language_selector_select(locales, tabs_id, name, error_on_locale = nil) content_tag(:div) do content_tag(:select, id: tabs_id, class: "language-change", data: { controller: "language-change" }) do locales.each_with_index.inject("".html_safe) do |string, (locale, index)| - title = if error?(name_with_locale(name, locale)) - I18n.with_locale(locale) { I18n.t("name_with_error", scope: "locale") } - else - I18n.with_locale(locale) { I18n.t("name", scope: "locale") } - end + title = locale.eql?(error_on_locale) ? "name_with_error" : "name" + title = I18n.with_locale(locale) { I18n.t(title, scope: "locale") } tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}" - string + content_tag(:option, title, value: "##{tab_content_id}") + string + content_tag(:option, title, value: "##{tab_content_id}", selected: locale.eql?(error_on_locale)) end end end end - def language_tabs(locales, tabs_id, name) + def language_tabs(locales, tabs_id, name, error_on_locale = nil) content_tag(:ul, class: "tabs tabs--lang", id: tabs_id, data: { tabs: true }) do locales.each_with_index.inject("".html_safe) do |string, (locale, index)| - string + content_tag(:li, class: tab_element_class_for("title", index)) do + display = if error_on_locale.nil? + index + else + locale.eql?(error_on_locale) ? 0 : 1 + end + + css_class = tab_element_class_for("title", display) + string + content_tag(:li, class: css_class) do title = I18n.with_locale(locale) { I18n.t("name", scope: "locale") } element_class = nil - element_class = "is-tab-error" if error?(name_with_locale(name, locale)) + element_class = "is-tab-error" if locale.eql?(error_on_locale) tab_content_id = sanitize_tabs_selector "#{tabs_id}-#{name}-panel-#{index}" content_tag(:a, title, href: "##{tab_content_id}", class: element_class) end diff --git a/decidim-core/spec/lib/form_builder_spec.rb b/decidim-core/spec/lib/form_builder_spec.rb index 6284b463208fa..7d3614cd038dc 100644 --- a/decidim-core/spec/lib/form_builder_spec.rb +++ b/decidim-core/spec/lib/form_builder_spec.rb @@ -60,6 +60,7 @@ def self.attached_config validates :conditional_presence, presence: true, if: :validate_presence validates :born_at, presence: true validates :start_time, presence: true + validates :short_description, translatable_presence: true def validate_presence false @@ -199,6 +200,7 @@ def organization expect(parsed.css("label[for='resource_short_description']")).not_to be_empty expect(parsed.css("li.tabs-title a").count).to eq 3 + expect(parsed.css(".editor").count).to eq 3 expect(parsed.css(".editor label[for='resource_short_description_en']").first).to be_nil @@ -213,6 +215,7 @@ def organization let(:available_locales) { %w(en) } it "renders a single input and a container for the editor" do + expect(parsed.css(".editor-container").count).to eq 1 expect(parsed.css(".editor input[type='hidden'][name='resource[short_description_en]']")).not_to be_empty expect(parsed.css(".editor label")).not_to be_empty expect(parsed.css(".editor .editor-container")).not_to be_empty @@ -220,35 +223,223 @@ def organization end end - context "with a editor field" do - let(:output) do - builder.translated :editor, :short_description + context "when there are 2 languages" do + let(:available_locales) { %w(en ca) } + + it "calls the correct widget components" do + allow(builder).to receive(:translated).and_call_original + allow(builder).to receive(:translated_labels).and_call_original + allow(builder).to receive(:create_language_selector).and_call_original + allow(builder).to receive(:language_tabs).and_call_original + allow(builder).to receive(:translated_tabs).and_call_original + + builder.translated :text_field, :short_description + + expect(builder).to have_received(:translated_labels) + expect(builder).to have_received(:create_language_selector) + expect(builder).to have_received(:language_tabs) + expect(builder).to have_received(:translated_tabs) end + end - it "renders a tabbed input hidden for each field and a container for the editor" do - expect(parsed.css("label")).not_to be_empty + context "when there are more languages" do + let(:available_locales) { %w(ca en es ro fr it) } - expect(parsed.css("li.tabs-title a").count).to eq 3 - expect(parsed.css(".editor").count).to eq 3 + it "calls the correct widget components" do + allow(builder).to receive(:translated).and_call_original + allow(builder).to receive(:translated_labels).and_call_original + allow(builder).to receive(:create_language_selector).and_call_original + allow(builder).to receive(:language_selector_select).and_call_original + allow(builder).to receive(:translated_tabs).and_call_original - expect(parsed.css(".editor label[for='resource_short_description_en']").first).to be_nil + builder.translated :text_field, :short_description - expect(parsed.css(".tabs-panel .editor input[type='hidden'][name='resource[short_description_ca]']")).not_to be_empty - expect(parsed.css(".tabs-panel .editor input[type='hidden'][name='resource[short_description_en]']")).not_to be_empty - expect(parsed.css(".tabs-panel .editor input[type='hidden'][name='resource[short_description_de__CH]']")).not_to be_empty + expect(builder).to have_received(:translated_labels) + expect(builder).to have_received(:create_language_selector) + expect(builder).to have_received(:language_selector_select) + expect(builder).to have_received(:translated_tabs) + end + end + end - expect(parsed.css(".tabs-panel .editor .editor-container").count).to eq 3 + describe "#translated_tabs" do + context "when there are 2 languages" do + let(:available_locales) { %w(en ca) } + + it "displays the first tab when there is no error" do + allow(builder).to receive(:locales).and_return(available_locales) + + output = builder.send(:translated_tabs, :text_field, :short_description, {}, "resource-short_description-tabs", nil) + + expect(output).to match( + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" + ) end - context "with a single locale" do - let(:available_locales) { %w(en) } + it "displays the errored tab first" do + allow(builder).to receive(:locales).and_return(available_locales) - it "renders a single input and a container for the editor" do - expect(parsed.css(".editor-container").count).to eq 1 - expect(parsed.css(".editor input[type='hidden'][name='resource[short_description_en]']")).not_to be_empty - expect(parsed.css(".editor label")).not_to be_empty - expect(parsed.css(".editor .editor-container")).not_to be_empty - end + output = builder.send(:translated_tabs, :text_field, :short_description, {}, "resource-short_description-tabs", "ca") + + expect(output).to match( + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" + ) + end + end + + context "when there are more languages" do + let(:available_locales) { %w(en ca es ro fr it) } + + it "displays the first tab when there is no error" do + allow(builder).to receive(:locales).and_return(available_locales) + + output = builder.send(:translated_tabs, :text_field, :short_description, {}, "resource-short_description-tabs", nil) + + expect(output).to match( + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" + ) + end + + it "displays the errored tab first" do + allow(builder).to receive(:locales).and_return(available_locales) + + output = builder.send(:translated_tabs, :text_field, :short_description, {}, "resource-short_description-tabs", "ca") + + expect(output).to match( + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" \ + "" \ + "
" \ + "
" + ) + end + end + end + + describe "#create_language_selector" do + context "when there are 2 languages" do + let(:available_locales) { %w(en ca) } + + it "displays the first tab when there is no error" do + allow(builder).to receive(:locales).and_return(available_locales) + + output = builder.send(:create_language_selector, available_locales, :short_description, "resource-short_description-tabs", nil) + + expect(output).to match( + "" + ) + end + + it "displays the errored tab first" do + allow(builder).to receive(:locales).and_return(available_locales) + + output = builder.send(:create_language_selector, available_locales, :short_description, "resource-short_description-tabs", "ca") + + expect(output).to match( + "" + ) + end + end + + context "when there are more languages" do + let(:available_locales) { %w(en ca es ro fr it) } + + before do + builder.remove_instance_variable(:@locales) if builder.instance_variable_defined?(:@locales) + allow(builder).to receive(:locales).and_return(available_locales) + I18n.backend.reload! + end + + it "displays the first tab when there is no error" do + output = builder.send(:create_language_selector, available_locales, :short_description, "resource-short_description-tabs", nil) + + expect(output).to match( + "" + ) + end + + it "displays the errored tab first" do + output = builder.send(:create_language_selector, available_locales, :short_description, "resource-short_description-tabs", "ca") + + expect(output).to match( + "" + ) end end end From 84838647e10cb74c4a1bdf91293a7198b9b2d910 Mon Sep 17 00:00:00 2001 From: Renato Date: Thu, 26 Feb 2026 10:33:17 -0300 Subject: [PATCH 015/135] Fix ux resolve encoding issues in the admin budget title (#16015) --- .../app/views/decidim/budgets/admin/projects/edit.html.erb | 2 +- .../app/views/decidim/budgets/admin/projects/index.html.erb | 2 +- .../views/decidim/budgets/admin/projects/manage_trash.html.erb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/decidim-budgets/app/views/decidim/budgets/admin/projects/edit.html.erb b/decidim-budgets/app/views/decidim/budgets/admin/projects/edit.html.erb index 70bf4c4dc4a39..f341fc9c8e945 100644 --- a/decidim-budgets/app/views/decidim/budgets/admin/projects/edit.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/admin/projects/edit.html.erb @@ -1,7 +1,7 @@ <% add_decidim_page_title("#{translated_attribute(budget.title)} - #{t(".title")}") %>

- <%= "#{decidim_escape_translated(budget.title)} #{t(".title")}" %> + <%= "#{decidim_sanitize_translated(budget.title)} #{t(".title")}" %>

diff --git a/decidim-budgets/app/views/decidim/budgets/admin/projects/index.html.erb b/decidim-budgets/app/views/decidim/budgets/admin/projects/index.html.erb index ad4cef7763585..7c2f6e7f77309 100644 --- a/decidim-budgets/app/views/decidim/budgets/admin/projects/index.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/admin/projects/index.html.erb @@ -3,7 +3,7 @@

- <%= link_to decidim_escape_translated(budget.title), budgets_path %> > + <%= link_to decidim_sanitize_translated(budget.title), budgets_path %> > <%= t(".title") %> ">
diff --git a/decidim-budgets/app/views/decidim/budgets/admin/projects/manage_trash.html.erb b/decidim-budgets/app/views/decidim/budgets/admin/projects/manage_trash.html.erb index 31d279fdcfb01..c31875292b9d6 100644 --- a/decidim-budgets/app/views/decidim/budgets/admin/projects/manage_trash.html.erb +++ b/decidim-budgets/app/views/decidim/budgets/admin/projects/manage_trash.html.erb @@ -4,7 +4,7 @@

- <%= link_to decidim_escape_translated(budget.title), budgets_path %> > + <%= link_to decidim_sanitize_translated(budget.title), budgets_path %> > <%= t(".title") %> ">
From 0c09a06c7eed53107e0b0ef2f769b805dcb582d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:18:45 +0200 Subject: [PATCH 016/135] Bump to dependencies: Bump brakeman from 7.0.2 to 8.0.2 (#16228) * Bump to dependencies: Bump brakeman from 7.0.2 to 8.0.2 Bumps [brakeman](https://github.com/presidentbeef/brakeman) from 7.0.2 to 8.0.2. - [Release notes](https://github.com/presidentbeef/brakeman/releases) - [Changelog](https://github.com/presidentbeef/brakeman/blob/main/CHANGES.md) - [Commits](https://github.com/presidentbeef/brakeman/compare/v7.0.2...v8.0.2) --- updated-dependencies: - dependency-name: brakeman dependency-version: 8.0.2 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock * Patch generators --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile | 2 +- Gemfile.lock | 4 ++-- decidim-generators/Gemfile | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 8f4b191dc6eaa..6bf3cc3d819ba 100644 --- a/Gemfile +++ b/Gemfile @@ -23,7 +23,7 @@ group :development, :test do gem "decidim-dev", path: "." - gem "brakeman", "~> 7.0" + gem "brakeman", "~> 8.0" gem "parallel_tests", "~> 4.2" end diff --git a/Gemfile.lock b/Gemfile.lock index bbf671eddf24e..f7d072d30f27d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -302,7 +302,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.0.2) + brakeman (8.0.2) racc browser (6.2.0) builder (3.3.0) @@ -935,7 +935,7 @@ PLATFORMS DEPENDENCIES bootsnap (~> 1.4) - brakeman (~> 7.0) + brakeman (~> 8.0) byebug (~> 13.0) decidim! decidim-ai! diff --git a/decidim-generators/Gemfile b/decidim-generators/Gemfile index 37a012fbda3c0..9255eaf743d2e 100644 --- a/decidim-generators/Gemfile +++ b/decidim-generators/Gemfile @@ -21,7 +21,7 @@ gem "puma", ">= 6.3.1" group :development, :test do gem "byebug", "~> 11.0", platform: :mri - gem "brakeman", "~> 7.0" + gem "brakeman", "~> 8.0" gem "decidim-dev", path: ".." gem "net-imap", "~> 0.5.0" gem "net-pop", "~> 0.1.1" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 9300b11aa0315..fe900f1ddf552 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -302,7 +302,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (7.0.2) + brakeman (8.0.2) racc browser (6.2.0) builder (3.3.0) @@ -924,7 +924,7 @@ PLATFORMS DEPENDENCIES bootsnap (~> 1.3) - brakeman (~> 7.0) + brakeman (~> 8.0) byebug (~> 11.0) decidim! decidim-ai! From 1c7ff3862081652003163d820ee9ae85ff16afed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:11:10 +0200 Subject: [PATCH 017/135] Bump to dependencies: Bump spring from 4.2.1 to 4.4.2 (#16234) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f7d072d30f27d..0701c0bf7e704 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -862,7 +862,7 @@ GEM snaky_hash (2.0.3) hashie (>= 0.1.0, < 6) version_gem (>= 1.1.8, < 3) - spring (4.2.1) + spring (4.4.2) spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) spring (>= 4) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index fe900f1ddf552..cf252f92b0c96 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -853,7 +853,7 @@ GEM snaky_hash (2.0.3) hashie (>= 0.1.0, < 6) version_gem (>= 1.1.8, < 3) - spring (4.2.1) + spring (4.4.2) spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) spring (>= 4) From bfaec017f562a68afb19848dc5892fc0b8e944bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:11:17 +0200 Subject: [PATCH 018/135] Bump to dependencies: Bump postcss-loader from 7.3.4 to 8.2.1 in /packages/webpacker (#16233) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 100 ++++++++++++++++++++++++++------ packages/webpacker/package.json | 2 +- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2926ad2ea34f..37203ea70f57b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8939,6 +8939,7 @@ }, "node_modules/cosmiconfig": { "version": "8.3.6", + "dev": true, "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", @@ -8963,10 +8964,12 @@ }, "node_modules/cosmiconfig/node_modules/argparse": { "version": "2.0.1", + "dev": true, "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10166,6 +10169,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/envinfo": { "version": "7.11.0", "license": "MIT", @@ -19508,6 +19520,7 @@ }, "node_modules/path-type": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -20155,41 +20168,94 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.4", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.1.tgz", + "integrity": "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==", "license": "MIT", "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "cosmiconfig": "^9.0.0", + "jiti": "^2.5.1", + "semver": "^7.6.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, - "node_modules/postcss-loader/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", + "node_modules/postcss-loader/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/postcss-loader/node_modules/semver": { - "version": "7.5.4", - "license": "ISC", + "node_modules/postcss-loader/node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/postcss-loader/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", "dependencies": { - "lru-cache": "^6.0.0" + "argparse": "^2.0.1" }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -20197,10 +20263,6 @@ "node": ">=10" } }, - "node_modules/postcss-loader/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, "node_modules/postcss-logical": { "version": "7.0.1", "funding": [ @@ -25879,7 +25941,7 @@ "postcss": ">=8.4.31", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^16.1.1", - "postcss-loader": "^7.3.3", + "postcss-loader": "^8.2.1", "postcss-preset-env": "^9.0.0", "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index 54a0dd6b56875..fc61b40673ba9 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -30,7 +30,7 @@ "postcss": ">=8.4.31", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^16.1.1", - "postcss-loader": "^7.3.3", + "postcss-loader": "^8.2.1", "postcss-preset-env": "^9.0.0", "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", From 4434ac575a7d8715653ac84b541f07a2e5c688ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:02:27 +0200 Subject: [PATCH 019/135] Bump to dependencies: Bump postcss-preset-env from 9.6.0 to 11.2.0 in /packages/webpacker (#16238) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 3589 +++++++++++++++++++++++++++---- packages/webpacker/package.json | 2 +- 2 files changed, 3134 insertions(+), 457 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37203ea70f57b..e19ee57c2920e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1675,8 +1675,28 @@ "w3c-keyname": "^2.2.4" } }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.7", + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.5.0", + "dev": true, "funding": [ { "type": "github", @@ -1688,16 +1708,17 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3" } }, - "node_modules/@csstools/color-helpers": { - "version": "4.0.0", + "node_modules/@csstools/css-tokenizer": { + "version": "2.2.3", + "dev": true, "funding": [ { "type": "github", @@ -1708,13 +1729,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", + "peer": true, "engines": { "node": "^14 || ^16 || >=18" } }, - "node_modules/@csstools/css-calc": { - "version": "1.1.6", + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.7", + "dev": true, "funding": [ { "type": "github", @@ -1726,6 +1749,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -1734,8 +1758,10 @@ "@csstools/css-tokenizer": "^2.2.3" } }, - "node_modules/@csstools/css-color-parser": { - "version": "1.5.1", + "node_modules/@csstools/postcss-alpha-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-2.0.3.tgz", + "integrity": "sha512-8GqzD3JnfpKJSVxPIC0KadyAfB5VRzPZdv7XQ4zvK1q0ku+uHVUAS2N/IDavQkW40gkuUci64O0ea6QB/zgCSw==", "funding": [ { "type": "github", @@ -1746,21 +1772,25 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", + "license": "MIT-0", "dependencies": { - "@csstools/color-helpers": "^4.0.0", - "@csstools/css-calc": "^1.1.6" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "postcss": "^8.4" } }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "2.5.0", + "node_modules/@csstools/postcss-alpha-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "funding": [ { "type": "github", @@ -1773,14 +1803,17 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/css-tokenizer": { - "version": "2.2.3", + "node_modules/@csstools/postcss-alpha-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "funding": [ { "type": "github", @@ -1792,12 +1825,22 @@ } ], "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.7", + "node_modules/@csstools/postcss-alpha-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -1810,15 +1853,35 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-alpha-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, "node_modules/@csstools/postcss-cascade-layers": { - "version": "4.0.2", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-6.0.0.tgz", + "integrity": "sha512-WhsECqmrEZQGqaPlBA7JkmF/CJ2/+wetL4fkL9sOPccKd32PQ1qToFM6gqSI5rkpmYqubvbxjEJhyMTHYK0vZQ==", "funding": [ { "type": "github", @@ -1831,18 +1894,20 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^3.0.1", - "postcss-selector-parser": "^6.0.13" + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { - "version": "3.0.1", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", "funding": [ { "type": "github", @@ -1855,14 +1920,16 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" } }, "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -1873,7 +1940,9 @@ } }, "node_modules/@csstools/postcss-color-function": { - "version": "3.0.9", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-5.0.2.tgz", + "integrity": "sha512-CjBdFemUFcAh3087MEJhZcO+QT1b8S75agysa1rU9TEC1YecznzwV+jpMxUc0JRBEV4ET2PjLssqmndR9IygeA==", "funding": [ { "type": "github", @@ -1886,20 +1955,23 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-color-mix-function": { - "version": "2.0.9", + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-2.0.2.tgz", + "integrity": "sha512-TWUwSe1+2KdYGGWTx5LR4JQN07vKHAeSho+bGYRgow+9cs3dqgOqS1f/a1odiX30ESmZvwIudJ86wzeiDR6UGg==", "funding": [ { "type": "github", @@ -1912,20 +1984,23 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-exponential-functions": { - "version": "1.0.3", + "node_modules/@csstools/postcss-color-function-display-p3-linear/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "funding": [ { "type": "github", @@ -1936,21 +2011,46 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "3.0.1", + "node_modules/@csstools/postcss-color-function-display-p3-linear/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -1961,19 +2061,60 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" + "license": "MIT", + "engines": { + "node": ">=20.19.0" }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-color-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-gamut-mapping": { - "version": "1.0.2", + "node_modules/@csstools/postcss-color-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "funding": [ { "type": "github", @@ -1984,21 +2125,64 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "4.0.9", + "node_modules/@csstools/postcss-color-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-4.0.2.tgz", + "integrity": "sha512-PFKQKswFqZrYKpajZsP4lhqjU/6+J5PTOWq1rKiFnniKsf4LgpGXrgHS/C6nn5Rc51LX0n4dWOWqY5ZN2i5IjA==", "funding": [ { "type": "github", @@ -2011,20 +2195,23 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "3.0.8", + "node_modules/@csstools/postcss-color-mix-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "funding": [ { "type": "github", @@ -2035,21 +2222,1912 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-2.0.2.tgz", + "integrity": "sha512-zEchsghpDH/6SytyjKu9TIPm4hiiWcur102cENl54cyIwTZsa+2MBJl/vtyALZ+uQ17h27L4waD+0Ow96sgZow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-3.0.0.tgz", + "integrity": "sha512-OHa+4aCcrJtHpPWB3zptScHwpS1TUbeLR4uO0ntIz0Su/zw9SoWkVu+tDMSySSAsNtNSI3kut4fTliFwIsrHxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-content-alt-text/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-3.0.2.tgz", + "integrity": "sha512-fwOz/m+ytFPz4aIph2foQS9nEDOdOjYcN5bgwbGR2jGUV8mYaeD/EaTVMHTRb/zqB65y2qNwmcFcE6VQty69Pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-contrast-color-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-contrast-color-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-contrast-color-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-3.0.1.tgz", + "integrity": "sha512-WHJ52Uk0AVUIICEYRY9xFHJZAuq0ZVg0f8xzqUN2zRFrZvGgRPpFwxK7h9FWvqKIOueOwN6hnJD23A8FwsUiVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-exponential-functions/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-exponential-functions/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-5.0.0.tgz", + "integrity": "sha512-M1EjCe/J3u8fFhOZgRci74cQhJ7R0UFBX6T+WqoEvjrr8hVfMiV+HTYrzxLY5OW8YllvXYr5Q5t5OvJbsUSeDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-width-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-width-property/-/postcss-font-width-property-1.0.0.tgz", + "integrity": "sha512-AvmySApdijbjYQuXXh95tb7iVnqZBbJrv3oajO927ksE/mDmJBiszm+psW8orL2lRGR8j6ZU5Uv9/ou2Z5KRKA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-3.0.2.tgz", + "integrity": "sha512-IrXAW3KQ3Sxm29C3/4mYQ/iA0Q5OH9YFOPQ2w24iIlXpD06A9MHvmQapP2vAGtQI3tlp2Xw5LIdm9F8khARfOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gamut-mapping/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gamut-mapping/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gamut-mapping/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-6.0.2.tgz", + "integrity": "sha512-saQHvD1PD/zCdn+kxCWCcQOdXZBljr8L6BKlCLs0w8GXYfo3SHdWL1HZQ+I1hVCPlU+MJPJJbZJjG/jHRJSlAw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-5.0.2.tgz", + "integrity": "sha512-ChR0+pKc/2cs900jakiv8dLrb69aez5P3T+g+wfJx1j6mreAe8orKTiMrVBk+DZvCRqpdOA2m8VoFms64A3Dew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-hwb-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-hwb-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-hwb-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-5.0.0.tgz", + "integrity": "sha512-/ws5d6c4uKqfM9zIL3ugcGI+3fvZEOOkJHNzAyTAGJIdZ+aSL9BVPNlHGV4QzmL0vqBSCOdU3+rhcMEj3+KzYw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-3.0.0.tgz", + "integrity": "sha512-UVUrFmrTQyLomVepnjWlbBg7GoscLmXLwYFyjbcEnmpeGW7wde6lNpx5eM3eVwZI2M+7hCE3ykYnAsEPLcLa+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-6.0.0.tgz", + "integrity": "sha512-1Hdy/ykg9RDo8vU8RiM2o+RaXO39WpFPaIkHxlAEJFofle/lc33tdQMKhBk3jR/Fe+uZNLOs3HlowFafyFptVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-3.0.0.tgz", + "integrity": "sha512-s++V5/hYazeRUCYIn2lsBVzUsxdeC46gtwpgW6lu5U/GlPOS5UTDT14kkEyPgXmFbCvaWLREqV7YTMJq1K3G6w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-light-dark-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-light-dark-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-4.0.0.tgz", + "integrity": "sha512-NGzdIRVj/VxOa/TjVdkHeyiJoDihONV0+uB0csUdgWbFFr8xndtfqK8iIGP9IKJzco+w0hvBF2SSk2sDSTAnOQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-3.0.0.tgz", + "integrity": "sha512-5cRg93QXVskM0MNepHpPcL0WLSf5Hncky0DrFDQY/4ozbH5lH7SX5ejayVpNTGSX7IpOvu7ykQDLOdMMGYzwpA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-3.0.0.tgz", + "integrity": "sha512-82Jnl/5Wi5jb19nQE1XlBHrZcNL3PzOgcj268cDkfwf+xi10HBqufGo1Unwf5n8bbbEFhEKgyQW+vFsc9iY1jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-4.0.0.tgz", + "integrity": "sha512-L0T3q0gei/tGetCGZU0c7VN77VTivRpz1YZRNxjXYmW+85PKeI6U9YnSvDqLU2vBT2uN4kLEzfgZ0ThIZpN18A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-4.0.0.tgz", + "integrity": "sha512-TA3AqVN/1IH3dKRC2UUWvprvwyOs2IeD7FDZk5Hz20w4q33yIuSg0i0gjyTUkcn90g8A4n7QpyZ2AgBrnYPnnA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-3.0.1.tgz", + "integrity": "sha512-I+CrmZt23fyejMItpLQFOg9gPXkDBBDjTqRT0UxCTZlYZfGrzZn4z+2kbXLRwDfR59OK8zaf26M4kwYwG0e1MA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-media-minmax/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-media-minmax/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-media-minmax/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-4.0.0.tgz", + "integrity": "sha512-FDdC3lbrj8Vr0SkGIcSLTcRB7ApG6nlJFxOxkEF2C5hIZC1jtgjISFSGn/WjFdVkn8Dqe+Vx9QXI3axS2w1XHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-mixins": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-mixins/-/postcss-mixins-1.0.0.tgz", + "integrity": "sha512-rz6qjT2w9L3k65jGc2dX+3oGiSrYQ70EZPDrINSmSVoVys7lLBFH0tvEa8DW2sr9cbRVD/W+1sy8+7bfu0JUfg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-mixins/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-mixins/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-5.0.0.tgz", + "integrity": "sha512-aPSw8P60e/i9BEfugauhikBqgjiwXcw3I9o4vXs+hktl4NSTgZRI0QHimxk9mst8N01A2TKDBxOln3mssRxiHQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-FcbEmoxDEGYvm2W3rQzVzcuo66+dDJjzzVDs+QwRmZLHYofGmMGwIKPqzF86/YW+euMDa7sh1xjWDvz/fzByZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-5.0.2.tgz", + "integrity": "sha512-3d/Wcnp2uW6Io0Tajl0croeUo46gwOVQI9N32PjA/HVQo6z1iL7yp19Gp+6e5E5CDKGpW7U822MsDVo2XK1z0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-oklab-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-oklab-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-oklab-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-2.0.0.tgz", + "integrity": "sha512-TeEfzsJGB23Syv7yCm8AHCD2XTFujdjr9YYu9ebH64vnfCEvY4BG319jXAYSlNlf3Yc9PNJ6WnkDkUF5XVgSKQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-5.0.0.tgz", + "integrity": "sha512-NsJoZ89rxmDrUsITf8QIk5w+lQZQ8Xw5K6cLFG+cfiffsLYHb3zcbOOrHLetGl1WIhjWWQ4Cr8MMrg46Q+oACg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-2.0.0.tgz", + "integrity": "sha512-qcMAkc9AhpzHgmQCD8hoJgGYifcOAxd1exXjjxilMM6euwRE619xDa4UsKBCv/v4g+sS63sd6c29LPM8s2ylSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-3.0.1.tgz", + "integrity": "sha512-SvKGfmj+WHfn4bWHaBYlkXDyU3SlA3fL8aaYZ8Op6M8tunNf3iV9uZyZZGWMCbDw0sGeoTmYZW9nmKN8Qi/ctg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-random-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/postcss-random-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-4.0.2.tgz", + "integrity": "sha512-HaMN+qMURinllszbps2AhXKaLeibg/2VW6FriYDrqE58ji82+z2S3/eLloywVOY8BQCJ9lZMdy6TcRQNbn9u3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "3.0.3", + "node_modules/@csstools/postcss-relative-color-syntax/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "funding": [ { "type": "github", @@ -2060,20 +4138,23 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.0.3", - "postcss-value-parser": "^4.2.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-initial": { - "version": "1.0.1", + "node_modules/@csstools/postcss-relative-color-syntax/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -2084,16 +4165,18 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "4.0.4", + "node_modules/@csstools/postcss-relative-color-syntax/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "funding": [ { "type": "github", @@ -2104,20 +4187,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^3.0.1", - "postcss-selector-parser": "^6.0.13" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=20.19.0" } }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { - "version": "3.0.1", + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-5.0.0.tgz", + "integrity": "sha512-kBrBFJcAji3MSHS4qQIihPvJfJC5xCabXLbejqDMiQi+86HD4eMBiTayAo46Urg7tlEmZZQFymFiJt+GH6nvXw==", "funding": [ { "type": "github", @@ -2129,15 +4207,20 @@ } ], "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -2147,8 +4230,10 @@ "node": ">=4" } }, - "node_modules/@csstools/postcss-logical-float-and-clear": { + "node_modules/@csstools/postcss-sign-functions": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-2.0.1.tgz", + "integrity": "sha512-C3br0qcHJkQ0qSGUBnDJHXQdO8XObnCpGwai5m1L2tv2nCjt0vRHG6A9aVCQHvh08OqHNM2ty1dYDNNXV99YAQ==", "funding": [ { "type": "github", @@ -2160,15 +4245,22 @@ } ], "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-logical-overflow": { - "version": "1.0.1", + "node_modules/@csstools/postcss-sign-functions/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "funding": [ { "type": "github", @@ -2179,16 +4271,19 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-logical-overscroll-behavior": { - "version": "1.0.1", + "node_modules/@csstools/postcss-sign-functions/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -2199,16 +4294,18 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-logical-resize": { - "version": "2.0.1", + "node_modules/@csstools/postcss-sign-functions/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "funding": [ { "type": "github", @@ -2219,19 +4316,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=20.19.0" } }, - "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "2.0.5", + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-5.0.1.tgz", + "integrity": "sha512-vZf7zPzRb7xIi2o5Z9q6wyeEAjoRCg74O2QvYxmQgxYO5V5cdBv4phgJDyOAOP3JHy4abQlm2YaEUS3gtGQo0g==", "funding": [ { "type": "github", @@ -2244,17 +4337,21 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-tokenizer": "^2.2.3" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-media-minmax": { - "version": "1.1.2", + "node_modules/@csstools/postcss-stepped-value-functions/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "funding": [ { "type": "github", @@ -2266,21 +4363,18 @@ } ], "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/media-query-list-parser": "^2.1.7" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "2.0.5", + "node_modules/@csstools/postcss-stepped-value-functions/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -2291,21 +4385,18 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/media-query-list-parser": "^2.1.7" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "3.0.1", + "node_modules/@csstools/postcss-stepped-value-functions/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "funding": [ { "type": "github", @@ -2316,19 +4407,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=20.19.0" } }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "3.0.2", + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-2.0.0.tgz", + "integrity": "sha512-elYcbdiBXAkPqvojB9kIBRuHY6htUhjSITtFQ+XiXnt6SvZCbNGxQmaaw6uZ7SPHu/+i/XVjzIt09/1k3SIerQ==", "funding": [ { "type": "github", @@ -2341,17 +4428,38 @@ ], "license": "MIT-0", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "3.0.9", + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-2.0.0.tgz", + "integrity": "sha512-FyGZCgchFImFyiHS2x3rD5trAqatf/x23veBLTIgbaqyFfna6RNBD+Qf8HRSjt6HGMXOLhAjxJ3OoZg0bbn7Qw==", "funding": [ { "type": "github", @@ -2364,20 +4472,20 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "3.0.3", + "node_modules/@csstools/postcss-system-ui-font-family/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -2388,19 +4496,37 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "2.0.9", + "node_modules/@csstools/postcss-system-ui-font-family/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-5.0.3.tgz", + "integrity": "sha512-62fjggvIM1YYfDJPcErMUDkEZB6CByG8neTJqexnZe1hRBgCjD4dnXDLoCSSurjs1LzjBq6irFDpDaOvDZfrlw==", "funding": [ { "type": "github", @@ -2413,20 +4539,20 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/color-helpers": "^6.0.2", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "3.0.1", + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-5.0.1.tgz", + "integrity": "sha512-e8me32Mhl8JeBnxVJgsQUYpV4Md4KiyvpILpQlaY/eK1Gwdb04kasiTTswPQ5q7Z8+FppJZ2Z4d8HRfn6rjD3w==", "funding": [ { "type": "github", @@ -2439,28 +4565,44 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "node_modules/@csstools/postcss-trigonometric-functions/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, "engines": { - "node": ">=4" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "3.0.4", + "node_modules/@csstools/postcss-trigonometric-functions/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -2471,21 +4613,18 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "3.0.4", + "node_modules/@csstools/postcss-trigonometric-functions/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "funding": [ { "type": "github", @@ -2496,20 +4635,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "@csstools/color-helpers": "^4.0.0", - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=20.19.0" } }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "3.0.4", + "node_modules/@csstools/postcss-unset-value": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-5.0.0.tgz", + "integrity": "sha512-EoO54sS2KCIfesvHyFYAW99RtzwHdgaJzhl7cqKZSaMYKZv3fXSOehDjAQx8WZBKn1JrMd7xJJI1T1BxPF7/jA==", "funding": [ { "type": "github", @@ -2521,20 +4655,17 @@ } ], "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^1.1.6", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/@csstools/postcss-unset-value": { - "version": "3.0.1", + "node_modules/@csstools/utilities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-3.0.0.tgz", + "integrity": "sha512-etDqA/4jYvOGBM6yfKCOsEXfH96BKztZdgGmGqKi2xHnDe0ILIBraRspwgYatJH9JsCZ5HCGoCst8w18EKOAdg==", "funding": [ { "type": "github", @@ -2547,7 +4678,7 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -7517,7 +9648,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.17", + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", "funding": [ { "type": "opencollective", @@ -7534,11 +9667,10 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -8192,9 +10324,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "funding": [ { "type": "opencollective", @@ -9011,7 +11143,9 @@ } }, "node_modules/css-blank-pseudo": { - "version": "6.0.1", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-8.0.1.tgz", + "integrity": "sha512-C5B2e5hCM4llrQkUms+KnWEMVW8K1n2XvX9G7ppfMZJQ7KAS/4rNnkP1Cs+HhWriOz1mWWTMFD4j1J7s31Dgug==", "funding": [ { "type": "github", @@ -9024,17 +11158,19 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -9054,7 +11190,9 @@ } }, "node_modules/css-has-pseudo": { - "version": "6.0.1", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-8.0.0.tgz", + "integrity": "sha512-Uz/bsHRbOeir/5Oeuz85tq/yLJLxX+3dpoRdjNTshs6jjqwUg8XaEZGDd0ci3fw7l53Srw0EkJ8mYan0eW5uGQ==", "funding": [ { "type": "github", @@ -9067,19 +11205,21 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^3.0.1", - "postcss-selector-parser": "^6.0.13", + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { - "version": "3.0.1", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", "funding": [ { "type": "github", @@ -9092,14 +11232,16 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" } }, "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -9170,7 +11312,9 @@ "license": "ISC" }, "node_modules/css-prefers-color-scheme": { - "version": "9.0.1", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-11.0.0.tgz", + "integrity": "sha512-fv0mgtwUhh2m9iio3Kxc2CkrogjIaRdMFaaqyzSFdii17JF4cfPyMNX72B15ZW2Nrr/NZUpxI4dec1VMHYJvdw==", "funding": [ { "type": "github", @@ -9183,7 +11327,7 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -9229,7 +11373,9 @@ } }, "node_modules/cssdb": { - "version": "7.10.0", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.8.0.tgz", + "integrity": "sha512-QbLeyz2Bgso1iRlh7IpWk6OKa3lLNGXsujVjDMPl9rOZpxKeiG69icLpbLCFxeURwmcdIfZqQyhlooKJYM4f8Q==", "funding": [ { "type": "opencollective", @@ -9240,7 +11386,7 @@ "url": "https://github.com/sponsors/csstools" } ], - "license": "CC0-1.0" + "license": "MIT-0" }, "node_modules/cssesc": { "version": "3.0.0", @@ -12551,13 +14697,15 @@ } }, "node_modules/fraction.js": { - "version": "4.3.7", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "license": "MIT", "engines": { "node": "*" }, "funding": { - "type": "patreon", + "type": "github", "url": "https://github.com/sponsors/rawify" } }, @@ -18985,15 +21133,8 @@ "dev": true, "license": "ISC" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", + "node_modules/normalize-path": { + "version": "3.0.0", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -19703,37 +21844,376 @@ } }, "node_modules/postcss-attribute-case-insensitive": { - "version": "6.0.2", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-8.0.0.tgz", + "integrity": "sha512-fovIPEV35c2JzVXdmP+sp2xirbBMt54J+upU8u6TSj410kUU5+axgEzvBBSAX8KCybze8CFCelzFAw/FfWg2TA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-8.0.2.tgz", + "integrity": "sha512-tbmkk6teYpJzFcGwPIhN1gkvxqGHvNx2PMb8Y3S5Ktyn7xOlvD98XzQ99MFY5mAyvXWclDG+BgoJKYJXFJOp5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-functional-notation/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-color-functional-notation/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-color-functional-notation/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-color-functional-notation/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-11.0.0.tgz", + "integrity": "sha512-NCGa6vjIyrjosz9GqRxVKbONBklz5TeipYqTJp3IqbnBWlBq5e5EMtG6MaX4vqk9LzocPfMQkuRK9tfk+OQuKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-11.0.0.tgz", + "integrity": "sha512-g9561mx7cbdqx7XeO/L+lJzVlzu7bICyXr72efBVKZGxIhvBBJf9fGXn3Cb6U4Bwh3LbzQO2e9NWBLVYdX5Eag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^3.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-media": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.1.tgz", + "integrity": "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-media/node_modules/@csstools/cascade-layer-name-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz", + "integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-custom-media/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-custom-media/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/postcss-custom-media/node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/postcss-clamp": { - "version": "4.1.0", + "node_modules/postcss-custom-properties": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-15.0.1.tgz", + "integrity": "sha512-cuyq8sd8dLY0GLbelz1KB8IMIoDECo6RVXMeHeXY2Uw3Q05k/d1GVITdaKLsheqrHbnxlwxzSRZQQ5u+rNtbMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=7.6.0" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4.6" + "postcss": "^8.4" } }, - "node_modules/postcss-color-functional-notation": { - "version": "6.0.4", + "node_modules/postcss-custom-properties/node_modules/@csstools/cascade-layer-name-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz", + "integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==", "funding": [ { "type": "github", @@ -19744,22 +22224,19 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/postcss-color-hex-alpha": { - "version": "9.0.3", + "node_modules/postcss-custom-properties/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -19771,18 +22248,17 @@ } ], "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/postcss-color-rebeccapurple": { - "version": "9.0.2", + "node_modules/postcss-custom-properties/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "funding": [ { "type": "github", @@ -19793,19 +22269,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "node": ">=20.19.0" } }, - "node_modules/postcss-custom-media": { - "version": "10.0.2", + "node_modules/postcss-custom-selectors": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-9.0.1.tgz", + "integrity": "sha512-2XBELy4DmdVKimChfaZ2id9u9CSGYQhiJ53SvlfBvMTzLMW2VxuMb9rHsMSQw9kRq/zSbhT5x13EaK8JSmK8KQ==", "funding": [ { "type": "github", @@ -19818,20 +22290,22 @@ ], "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.5", - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "@csstools/media-query-list-parser": "^2.1.5" + "@csstools/cascade-layer-name-parser": "^3.0.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, - "node_modules/postcss-custom-properties": { - "version": "13.3.4", + "node_modules/postcss-custom-selectors/node_modules/@csstools/cascade-layer-name-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz", + "integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==", "funding": [ { "type": "github", @@ -19843,21 +22317,18 @@ } ], "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.7", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/postcss-custom-selectors": { - "version": "7.1.6", + "node_modules/postcss-custom-selectors/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "funding": [ { "type": "github", @@ -19869,21 +22340,36 @@ } ], "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.5", - "@csstools/css-parser-algorithms": "^2.3.2", - "@csstools/css-tokenizer": "^2.2.1", - "postcss-selector-parser": "^6.0.13" - }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss": "^8.4" + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-custom-selectors/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -19894,7 +22380,9 @@ } }, "node_modules/postcss-dir-pseudo-class": { - "version": "8.0.1", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-10.0.0.tgz", + "integrity": "sha512-DmtIzULpyC8XaH4b5AaUgt4Jic4QmrECqidNCdR7u7naQFdnxX80YI06u238a+ZVRXwURDxVzy0s/UQnWmpVeg==", "funding": [ { "type": "github", @@ -19907,17 +22395,19 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -19928,7 +22418,9 @@ } }, "node_modules/postcss-double-position-gradients": { - "version": "5.0.3", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-7.0.0.tgz", + "integrity": "sha512-Msr/dxj8Os7KLJE5Hdhvprwm3K5Zrh1KTY0eFN3ngPKNkej/Usy4BM9JQmqE6CLAkDpHoQVsi4snbL72CPt6qg==", "funding": [ { "type": "github", @@ -19941,11 +22433,12 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -19959,7 +22452,9 @@ } }, "node_modules/postcss-focus-visible": { - "version": "9.0.1", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-11.0.0.tgz", + "integrity": "sha512-VG1a9kBKizUBWS66t5xyB4uLONBnvZLCmZXxT40FALu8EF0QgVZBYy5ApC0KhmpHsv+pvHMJHB3agKHwmocWjw==", "funding": [ { "type": "github", @@ -19972,17 +22467,19 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -19993,7 +22490,9 @@ } }, "node_modules/postcss-focus-within": { - "version": "8.0.1", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-10.0.0.tgz", + "integrity": "sha512-dvql0fzUTG+gcJYp+KTbag5vAjuo94LDYZHkqDV1rnf5gPGer1v/SrmIZBdvKU8moep3HbcbujqGjzSb3DL53Q==", "funding": [ { "type": "github", @@ -20006,17 +22505,19 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -20034,7 +22535,9 @@ } }, "node_modules/postcss-gap-properties": { - "version": "5.0.1", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-7.0.0.tgz", + "integrity": "sha512-PSDF2QoZMRUbsINvXObQgxx4HExRP85QTT8qS/YN9fBsCPWCqUuwqAD6E6PNp0BqL/jU1eyWUBORaOK/J/9LDA==", "funding": [ { "type": "github", @@ -20047,14 +22550,16 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-image-set-function": { - "version": "6.0.2", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-8.0.0.tgz", + "integrity": "sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA==", "funding": [ { "type": "github", @@ -20067,10 +22572,11 @@ ], "license": "MIT-0", "dependencies": { + "@csstools/utilities": "^3.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -20109,7 +22615,9 @@ } }, "node_modules/postcss-lab-function": { - "version": "6.0.9", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-8.0.2.tgz", + "integrity": "sha512-1ZIAh8ODhZdnAb09Aq2BTenePKS1G/kUR0FwvzkQDfFtSOV64Ycv27YvV11fDycEvhIcEmgYkLABXKRiWcXRuA==", "funding": [ { "type": "github", @@ -20122,18 +22630,110 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/css-color-parser": "^1.5.1", - "@csstools/css-parser-algorithms": "^2.5.0", - "@csstools/css-tokenizer": "^2.2.3", - "@csstools/postcss-progressive-custom-properties": "^3.0.3" + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/utilities": "^3.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-lab-function/node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-lab-function/node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-lab-function/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/postcss-lab-function/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/postcss-load-config": { "version": "4.0.2", "funding": [ @@ -20264,7 +22864,9 @@ } }, "node_modules/postcss-logical": { - "version": "7.0.1", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-9.0.0.tgz", + "integrity": "sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ==", "funding": [ { "type": "github", @@ -20280,7 +22882,7 @@ "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -20376,7 +22978,9 @@ } }, "node_modules/postcss-nesting": { - "version": "12.0.2", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-14.0.0.tgz", + "integrity": "sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==", "funding": [ { "type": "github", @@ -20389,18 +22993,43 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-specificity": "^3.0.1", - "postcss-selector-parser": "^6.0.13" + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", + "integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { - "version": "3.0.1", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", "funding": [ { "type": "github", @@ -20413,14 +23042,16 @@ ], "license": "MIT-0", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" } }, "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -20431,7 +23062,9 @@ } }, "node_modules/postcss-opacity-percentage": { - "version": "2.0.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", "funding": [ { "type": "kofi", @@ -20444,14 +23077,16 @@ ], "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.2" + "postcss": "^8.4" } }, "node_modules/postcss-overflow-shorthand": { - "version": "5.0.1", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-7.0.0.tgz", + "integrity": "sha512-9SLpjoUdGRoRrzoOdX66HbUs0+uDwfIAiXsRa7piKGOqPd6F4ZlON9oaDSP5r1Qpgmzw5L9Ht0undIK6igJPMA==", "funding": [ { "type": "github", @@ -20467,7 +23102,7 @@ "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" @@ -20481,7 +23116,9 @@ } }, "node_modules/postcss-place": { - "version": "9.0.1", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-11.0.0.tgz", + "integrity": "sha512-fAifpyjQ+fuDRp2nmF95WbotqbpjdazebedahXdfBxy5sHembOLpBQ1cHveZD9ZmjK26tYM8tikeNaUlp/KfHA==", "funding": [ { "type": "github", @@ -20497,14 +23134,16 @@ "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-preset-env": { - "version": "9.3.0", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-11.2.0.tgz", + "integrity": "sha512-eNYpuj68cjGjvZMoSAbHilaCt3yIyzBL1cVuSGJfvJewsaBW/U6dI2bqCJl3iuZsL+yvBobcy4zJFA/3I68IHQ==", "funding": [ { "type": "github", @@ -20517,76 +23156,91 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/postcss-cascade-layers": "^4.0.1", - "@csstools/postcss-color-function": "^3.0.7", - "@csstools/postcss-color-mix-function": "^2.0.7", - "@csstools/postcss-exponential-functions": "^1.0.1", - "@csstools/postcss-font-format-keywords": "^3.0.0", - "@csstools/postcss-gamut-mapping": "^1.0.0", - "@csstools/postcss-gradients-interpolation-method": "^4.0.7", - "@csstools/postcss-hwb-function": "^3.0.6", - "@csstools/postcss-ic-unit": "^3.0.2", - "@csstools/postcss-initial": "^1.0.0", - "@csstools/postcss-is-pseudo-class": "^4.0.3", - "@csstools/postcss-logical-float-and-clear": "^2.0.0", - "@csstools/postcss-logical-overflow": "^1.0.0", - "@csstools/postcss-logical-overscroll-behavior": "^1.0.0", - "@csstools/postcss-logical-resize": "^2.0.0", - "@csstools/postcss-logical-viewport-units": "^2.0.3", - "@csstools/postcss-media-minmax": "^1.1.0", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^2.0.3", - "@csstools/postcss-nested-calc": "^3.0.0", - "@csstools/postcss-normalize-display-values": "^3.0.1", - "@csstools/postcss-oklab-function": "^3.0.7", - "@csstools/postcss-progressive-custom-properties": "^3.0.2", - "@csstools/postcss-relative-color-syntax": "^2.0.7", - "@csstools/postcss-scope-pseudo-class": "^3.0.0", - "@csstools/postcss-stepped-value-functions": "^3.0.2", - "@csstools/postcss-text-decoration-shorthand": "^3.0.3", - "@csstools/postcss-trigonometric-functions": "^3.0.2", - "@csstools/postcss-unset-value": "^3.0.0", - "autoprefixer": "^10.4.16", - "browserslist": "^4.22.1", - "css-blank-pseudo": "^6.0.0", - "css-has-pseudo": "^6.0.0", - "css-prefers-color-scheme": "^9.0.0", - "cssdb": "^7.9.0", - "postcss-attribute-case-insensitive": "^6.0.2", + "@csstools/postcss-alpha-function": "^2.0.3", + "@csstools/postcss-cascade-layers": "^6.0.0", + "@csstools/postcss-color-function": "^5.0.2", + "@csstools/postcss-color-function-display-p3-linear": "^2.0.2", + "@csstools/postcss-color-mix-function": "^4.0.2", + "@csstools/postcss-color-mix-variadic-function-arguments": "^2.0.2", + "@csstools/postcss-content-alt-text": "^3.0.0", + "@csstools/postcss-contrast-color-function": "^3.0.2", + "@csstools/postcss-exponential-functions": "^3.0.1", + "@csstools/postcss-font-format-keywords": "^5.0.0", + "@csstools/postcss-font-width-property": "^1.0.0", + "@csstools/postcss-gamut-mapping": "^3.0.2", + "@csstools/postcss-gradients-interpolation-method": "^6.0.2", + "@csstools/postcss-hwb-function": "^5.0.2", + "@csstools/postcss-ic-unit": "^5.0.0", + "@csstools/postcss-initial": "^3.0.0", + "@csstools/postcss-is-pseudo-class": "^6.0.0", + "@csstools/postcss-light-dark-function": "^3.0.0", + "@csstools/postcss-logical-float-and-clear": "^4.0.0", + "@csstools/postcss-logical-overflow": "^3.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^3.0.0", + "@csstools/postcss-logical-resize": "^4.0.0", + "@csstools/postcss-logical-viewport-units": "^4.0.0", + "@csstools/postcss-media-minmax": "^3.0.1", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^4.0.0", + "@csstools/postcss-mixins": "^1.0.0", + "@csstools/postcss-nested-calc": "^5.0.0", + "@csstools/postcss-normalize-display-values": "^5.0.1", + "@csstools/postcss-oklab-function": "^5.0.2", + "@csstools/postcss-position-area-property": "^2.0.0", + "@csstools/postcss-progressive-custom-properties": "^5.0.0", + "@csstools/postcss-property-rule-prelude-list": "^2.0.0", + "@csstools/postcss-random-function": "^3.0.1", + "@csstools/postcss-relative-color-syntax": "^4.0.2", + "@csstools/postcss-scope-pseudo-class": "^5.0.0", + "@csstools/postcss-sign-functions": "^2.0.1", + "@csstools/postcss-stepped-value-functions": "^5.0.1", + "@csstools/postcss-syntax-descriptor-syntax-production": "^2.0.0", + "@csstools/postcss-system-ui-font-family": "^2.0.0", + "@csstools/postcss-text-decoration-shorthand": "^5.0.3", + "@csstools/postcss-trigonometric-functions": "^5.0.1", + "@csstools/postcss-unset-value": "^5.0.0", + "autoprefixer": "^10.4.24", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^8.0.1", + "css-has-pseudo": "^8.0.0", + "css-prefers-color-scheme": "^11.0.0", + "cssdb": "^8.8.0", + "postcss-attribute-case-insensitive": "^8.0.0", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^6.0.2", - "postcss-color-hex-alpha": "^9.0.2", - "postcss-color-rebeccapurple": "^9.0.1", - "postcss-custom-media": "^10.0.2", - "postcss-custom-properties": "^13.3.2", - "postcss-custom-selectors": "^7.1.6", - "postcss-dir-pseudo-class": "^8.0.0", - "postcss-double-position-gradients": "^5.0.2", - "postcss-focus-visible": "^9.0.0", - "postcss-focus-within": "^8.0.0", + "postcss-color-functional-notation": "^8.0.2", + "postcss-color-hex-alpha": "^11.0.0", + "postcss-color-rebeccapurple": "^11.0.0", + "postcss-custom-media": "^12.0.1", + "postcss-custom-properties": "^15.0.1", + "postcss-custom-selectors": "^9.0.1", + "postcss-dir-pseudo-class": "^10.0.0", + "postcss-double-position-gradients": "^7.0.0", + "postcss-focus-visible": "^11.0.0", + "postcss-focus-within": "^10.0.0", "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^5.0.0", - "postcss-image-set-function": "^6.0.1", - "postcss-lab-function": "^6.0.7", - "postcss-logical": "^7.0.0", - "postcss-nesting": "^12.0.1", - "postcss-opacity-percentage": "^2.0.0", - "postcss-overflow-shorthand": "^5.0.0", + "postcss-gap-properties": "^7.0.0", + "postcss-image-set-function": "^8.0.0", + "postcss-lab-function": "^8.0.2", + "postcss-logical": "^9.0.0", + "postcss-nesting": "^14.0.0", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^7.0.0", "postcss-page-break": "^3.0.4", - "postcss-place": "^9.0.0", - "postcss-pseudo-class-any-link": "^9.0.0", + "postcss-place": "^11.0.0", + "postcss-pseudo-class-any-link": "^11.0.0", "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^7.0.1", - "postcss-value-parser": "^4.2.0" + "postcss-selector-not": "^9.0.0" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-pseudo-class-any-link": { - "version": "9.0.1", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-11.0.0.tgz", + "integrity": "sha512-DNFZ4GMa3C3pU5dM+UCTG1CEeLtS1ZqV5DKSqCTJQMn1G5jnd/30fS8+A7H4o5bSD3MOcnx+VgI+xPE9Z5Wvig==", "funding": [ { "type": "github", @@ -20599,17 +23253,19 @@ ], "license": "MIT-0", "dependencies": { - "postcss-selector-parser": "^6.0.13" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "6.0.15", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -20673,22 +23329,43 @@ } }, "node_modules/postcss-selector-not": { - "version": "7.0.1", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-9.0.0.tgz", + "integrity": "sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.10" + "postcss-selector-parser": "^7.1.1" }, "engines": { - "node": "^14 || ^16 || >=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" + "node": ">=20.19.0" }, "peerDependencies": { "postcss": "^8.4" } }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.0.10", "license": "MIT", @@ -25942,7 +28619,7 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^16.1.1", "postcss-loader": "^8.2.1", - "postcss-preset-env": "^9.0.0", + "postcss-preset-env": "^11.2.0", "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", "shakapacker": "~8.3.0", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index fc61b40673ba9..66feca570b6d8 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -31,7 +31,7 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^16.1.1", "postcss-loader": "^8.2.1", - "postcss-preset-env": "^9.0.0", + "postcss-preset-env": "^11.2.0", "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", "shakapacker": "~8.3.0", From 98f17de891f2954de526c5e8c043bd11dbbc6884 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:13:54 +0200 Subject: [PATCH 020/135] Bump to dependencies: Bump erb_lint from 0.8.0 to 0.9.0 (#16237) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 10 +++++----- decidim-dev/decidim-dev.gemspec | 2 +- decidim-generators/Gemfile.lock | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0701c0bf7e704..8639ce13afb15 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -128,7 +128,7 @@ PATH decidim-core (= 0.32.0.dev) decidim-generators (= 0.32.0.dev) decidim-verifications (= 0.32.0.dev) - erb_lint (~> 0.8.0) + erb_lint (>= 0.8, < 0.10) factory_bot_rails (~> 6.2) faker (~> 3.2) i18n-tasks (~> 1.0) @@ -291,9 +291,9 @@ GEM batch-loader (2.0.5) bcrypt (3.1.20) benchmark (0.5.0) - better_html (2.1.1) - actionview (>= 6.0) - activesupport (>= 6.0) + better_html (2.2.0) + actionview (>= 7.0) + activesupport (>= 7.0) ast (~> 2.0) erubi (~> 1.4) parser (>= 2.4) @@ -397,7 +397,7 @@ GEM logger zeitwerk (~> 2.6) erb (6.0.2) - erb_lint (0.8.0) + erb_lint (0.9.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index c7cf427b5d4ab..b68fa7ade1d95 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -41,7 +41,7 @@ Gem::Specification.new do |s| s.add_dependency "bullet", "~> 8.0.0" s.add_dependency "byebug", ">= 11", "< 14" - s.add_dependency "erb_lint", "~> 0.8.0" + s.add_dependency "erb_lint", ">= 0.8", "< 0.10" s.add_dependency "i18n-tasks", "~> 1.0" s.add_dependency "nokogiri", "~> 1.16", ">= 1.16.2" s.add_dependency "parallel_tests", "~> 4.2" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index cf252f92b0c96..628d427f896b0 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -128,7 +128,7 @@ PATH decidim-core (= 0.32.0.dev) decidim-generators (= 0.32.0.dev) decidim-verifications (= 0.32.0.dev) - erb_lint (~> 0.8.0) + erb_lint (>= 0.8, < 0.10) factory_bot_rails (~> 6.2) faker (~> 3.2) i18n-tasks (~> 1.0) @@ -291,9 +291,9 @@ GEM batch-loader (2.0.5) bcrypt (3.1.20) benchmark (0.5.0) - better_html (2.1.1) - actionview (>= 6.0) - activesupport (>= 6.0) + better_html (2.2.0) + actionview (>= 7.0) + activesupport (>= 7.0) ast (~> 2.0) erubi (~> 1.4) parser (>= 2.4) @@ -396,7 +396,7 @@ GEM logger zeitwerk (~> 2.6) erb (6.0.2) - erb_lint (0.8.0) + erb_lint (0.9.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) From 2545b0bbd4fceafa3297a0975672017793f069f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:27:47 +0200 Subject: [PATCH 021/135] Bump to dependencies: Bump graphql-docs from 5.1.0 to 6.0.0 (#16240) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 40 ++++++++++++++++----------------- decidim-api/decidim-api.gemspec | 2 +- decidim-generators/Gemfile.lock | 32 +++++++++++++------------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8639ce13afb15..8ab00940b8564 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,7 +36,7 @@ PATH decidim-core (= 0.32.0.dev) devise-jwt (~> 0.12.1) graphql (>= 2.4.17, < 2.6) - graphql-docs (~> 5.0) + graphql-docs (>= 5, < 7) rack-cors (~> 1.0) decidim-assemblies (0.32.0.dev) decidim-core (= 0.32.0.dev) @@ -341,7 +341,8 @@ GEM fast-stemmer (~> 1.0) matrix (~> 0.4) cmdparse (3.0.7) - commonmarker (0.23.12) + commonmarker (2.6.3-arm64-darwin) + commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) connection_pool (2.5.5) crack (1.0.0) @@ -410,8 +411,6 @@ GEM escape_utils (1.3.0) excon (1.2.8) logger - extended-markdown-filter (0.7.0) - html-pipeline (~> 2.9) factory_bot (6.5.4) activesupport (>= 6.1.0) factory_bot_rails (6.4.4) @@ -440,30 +439,29 @@ GEM fog-local (0.8.0) fog-core (>= 1.27, < 3.0) formatador (1.1.1) - gemoji (3.0.1) + gemoji (4.1.0) geocoder (1.8.5) base64 (>= 0.1.0) csv (>= 3.0.0) geom2d (0.4.1) globalid (1.3.0) activesupport (>= 6.1) - google-protobuf (4.29.3-arm64-darwin) + google-protobuf (4.33.5-arm64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.29.3-x86_64-linux) + google-protobuf (4.33.5-x86_64-linux-gnu) bigdecimal rake (>= 13) - graphql (2.5.19) + graphql (2.5.20) base64 fiber-storage logger - graphql-docs (5.1.0) - commonmarker (~> 0.23, >= 0.23.6) + graphql-docs (6.0.0) + commonmarker (~> 2.0) escape_utils (~> 1.2) - extended-markdown-filter (~> 0.4) - gemoji (~> 3.0) + gemoji (~> 4.0) graphql (~> 2.0) - html-pipeline (~> 2.14, >= 2.14.3) + html-pipeline (~> 3.0) logger (~> 1.6) ostruct (~> 0.6) sass-embedded (~> 1.58) @@ -476,9 +474,9 @@ GEM strscan (>= 3.1.2) highline (3.1.2) reline - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) + html-pipeline (3.2.4) + selma (~> 0.4) + zeitwerk (~> 2.5) htmlentities (4.3.4) i18n (1.14.8) concurrent-ruby (~> 1.0) @@ -831,10 +829,10 @@ GEM nokogiri (>= 1.10.8) rubyzip (>= 1.3.0) rubyzip (2.3.2) - sass-embedded (1.83.4-arm64-darwin) - google-protobuf (~> 4.29) - sass-embedded (1.83.4-x86_64-linux-gnu) - google-protobuf (~> 4.29) + sass-embedded (1.97.3-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.97.3-x86_64-linux-gnu) + google-protobuf (~> 4.31) securerandom (0.4.1) selenium-webdriver (4.27.0) base64 (~> 0.2) @@ -842,6 +840,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + selma (0.4.15-arm64-darwin) + selma (0.4.15-x86_64-linux) semantic_range (3.1.0) shakapacker (8.3.0) activesupport (>= 5.2) diff --git a/decidim-api/decidim-api.gemspec b/decidim-api/decidim-api.gemspec index 479b5af792aa2..d58fd20a6ca97 100644 --- a/decidim-api/decidim-api.gemspec +++ b/decidim-api/decidim-api.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |s| s.add_dependency "decidim-core", version s.add_dependency "devise-jwt", "~> 0.12.1" s.add_dependency "graphql", ">= 2.4.17", "< 2.6" - s.add_dependency "graphql-docs", "~> 5.0" + s.add_dependency "graphql-docs", ">= 5", "< 7" s.add_dependency "rack-cors", "~> 1.0" s.add_development_dependency "decidim-assemblies", version diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 628d427f896b0..95bd491148113 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -36,7 +36,7 @@ PATH decidim-core (= 0.32.0.dev) devise-jwt (~> 0.12.1) graphql (>= 2.4.17, < 2.6) - graphql-docs (~> 5.0) + graphql-docs (>= 5, < 7) rack-cors (~> 1.0) decidim-assemblies (0.32.0.dev) decidim-core (= 0.32.0.dev) @@ -340,7 +340,7 @@ GEM fast-stemmer (~> 1.0) matrix (~> 0.4) cmdparse (3.0.7) - commonmarker (0.23.12) + commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) connection_pool (2.5.5) crack (1.0.0) @@ -409,8 +409,6 @@ GEM escape_utils (1.3.0) excon (1.2.8) logger - extended-markdown-filter (0.7.0) - html-pipeline (~> 2.9) factory_bot (6.5.4) activesupport (>= 6.1.0) factory_bot_rails (6.4.4) @@ -438,27 +436,26 @@ GEM fog-local (0.8.0) fog-core (>= 1.27, < 3.0) formatador (1.1.1) - gemoji (3.0.1) + gemoji (4.1.0) geocoder (1.8.5) base64 (>= 0.1.0) csv (>= 3.0.0) geom2d (0.4.1) globalid (1.3.0) activesupport (>= 6.1) - google-protobuf (4.29.3-x86_64-linux) + google-protobuf (4.33.5-x86_64-linux-gnu) bigdecimal rake (>= 13) - graphql (2.5.19) + graphql (2.5.20) base64 fiber-storage logger - graphql-docs (5.1.0) - commonmarker (~> 0.23, >= 0.23.6) + graphql-docs (6.0.0) + commonmarker (~> 2.0) escape_utils (~> 1.2) - extended-markdown-filter (~> 0.4) - gemoji (~> 3.0) + gemoji (~> 4.0) graphql (~> 2.0) - html-pipeline (~> 2.14, >= 2.14.3) + html-pipeline (~> 3.0) logger (~> 1.6) ostruct (~> 0.6) sass-embedded (~> 1.58) @@ -471,9 +468,9 @@ GEM strscan (>= 3.1.2) highline (3.1.2) reline - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) + html-pipeline (3.2.4) + selma (~> 0.4) + zeitwerk (~> 2.5) htmlentities (4.3.4) i18n (1.14.8) concurrent-ruby (~> 1.0) @@ -824,8 +821,8 @@ GEM nokogiri (>= 1.10.8) rubyzip (>= 1.3.0) rubyzip (2.3.2) - sass-embedded (1.83.4-x86_64-linux-gnu) - google-protobuf (~> 4.29) + sass-embedded (1.97.3-x86_64-linux-gnu) + google-protobuf (~> 4.31) securerandom (0.4.1) selenium-webdriver (4.27.0) base64 (~> 0.2) @@ -833,6 +830,7 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + selma (0.4.15-x86_64-linux) semantic_range (3.1.0) shakapacker (8.3.0) activesupport (>= 5.2) From 460065b6ec1c5a272cea0b56fcfaf9650dd109d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:04:28 +0200 Subject: [PATCH 022/135] Bump to dependencies: Bump css-loader from 6.11.0 to 7.1.4 in /packages/webpacker (#16239) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 85 ++++++++++++++++++++------------- packages/webpacker/package.json | 2 +- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index e19ee57c2920e..a5d0ebe38b3b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11252,28 +11252,30 @@ } }, "node_modules/css-loader": { - "version": "6.10.0", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" + "semver": "^7.6.3" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" }, "peerDependenciesMeta": { "@rspack/core": { @@ -11284,22 +11286,11 @@ } } }, - "node_modules/css-loader/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/css-loader/node_modules/semver": { - "version": "7.5.4", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -11307,10 +11298,6 @@ "node": ">=10" } }, - "node_modules/css-loader/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, "node_modules/css-prefers-color-scheme": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-11.0.0.tgz", @@ -22889,7 +22876,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" @@ -22899,11 +22888,13 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.4", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -22913,11 +22904,26 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-scope": { - "version": "3.1.1", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -22926,6 +22932,19 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-values": { "version": "4.0.0", "license": "ISC", @@ -28605,7 +28624,7 @@ "autoprefixer": "^10.4.14", "compression-webpack-plugin": "^10.0.0", "core-js": "~3.33.1", - "css-loader": "^6.8.1", + "css-loader": "^7.1.4", "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", "esbuild-sass-plugin": "^2.16.1", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index 66feca570b6d8..ec21ecf0c495d 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -17,7 +17,7 @@ "autoprefixer": "^10.4.14", "compression-webpack-plugin": "^10.0.0", "core-js": "~3.33.1", - "css-loader": "^6.8.1", + "css-loader": "^7.1.4", "expose-loader": "^4.1.0", "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", From 06b8ff0c8b93006f1dc077b2a8c27088585b6bf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:35:45 +0200 Subject: [PATCH 023/135] Bump to dependencies: Bump omniauth from 2.1.3 to 2.1.4 (#16243) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 6 ++++-- decidim-generators/Gemfile.lock | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8ab00940b8564..fc10f112f12e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -466,7 +466,8 @@ GEM ostruct (~> 0.6) sass-embedded (~> 1.58) hashdiff (1.1.2) - hashie (5.0.0) + hashie (5.1.0) + logger hexapdf (1.6.0) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) @@ -593,8 +594,9 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0, >= 2.0.3) version_gem (>= 1.1.8, < 3) - omniauth (2.1.3) + omniauth (2.1.4) hashie (>= 3.4.6) + logger rack (>= 2.2.3) rack-protection omniauth-facebook (5.0.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 95bd491148113..ae57b5bd8ee63 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -460,7 +460,8 @@ GEM ostruct (~> 0.6) sass-embedded (~> 1.58) hashdiff (1.1.2) - hashie (5.0.0) + hashie (5.1.0) + logger hexapdf (1.6.0) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) @@ -585,8 +586,9 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0, >= 2.0.3) version_gem (>= 1.1.8, < 3) - omniauth (2.1.3) + omniauth (2.1.4) hashie (>= 3.4.6) + logger rack (>= 2.2.3) rack-protection omniauth-facebook (5.0.0) From 433335714d8beac3bdbb0485e7e74f4ea44c907d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:55:00 +0200 Subject: [PATCH 024/135] Bump to dependencies: Bump esbuild-sass-plugin from 2.16.1 to 3.6.0 in /packages/webpacker (#16244) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 1412 ++++++++++++++++++------------- packages/webpacker/package.json | 2 +- 2 files changed, 826 insertions(+), 588 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5d0ebe38b3b3..caa0305619379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1634,7 +1634,9 @@ "license": "MIT" }, "node_modules/@bufbuild/protobuf": { - "version": "1.7.1", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@codemirror/language": { @@ -5030,21 +5032,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", @@ -7276,6 +7263,315 @@ "version": "1.0.4", "license": "Apache-2.0" }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "license": "MIT", @@ -10083,10 +10379,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-builder": { - "version": "0.2.0", - "license": "MIT/X11" - }, "node_modules/buffer-from": { "version": "1.1.2", "license": "MIT" @@ -10772,6 +11064,12 @@ "version": "2.0.20", "license": "MIT" }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "dev": true, @@ -12022,6 +12320,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -12521,47 +12829,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.19.12", - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "node_modules/esbuild-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.3.0.tgz", - "integrity": "sha512-D7HeJNdkDKKMarPQO/3dlJT6RwN2YJO7ENU6RPlpOz5YxSHnUNi2yvW41Bckvi1EVwctIaLzlb0ni5ag2GINYA==", + "node_modules/esbuild-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esbuild-loader/-/esbuild-loader-4.3.0.tgz", + "integrity": "sha512-D7HeJNdkDKKMarPQO/3dlJT6RwN2YJO7ENU6RPlpOz5YxSHnUNi2yvW41Bckvi1EVwctIaLzlb0ni5ag2GINYA==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -12641,391 +12912,6 @@ "source-map": "~0.6.1" } }, - "node_modules/esbuild-sass-plugin": { - "version": "2.16.1", - "license": "MIT", - "dependencies": { - "resolve": "^1.22.6", - "sass": "^1.7.3" - }, - "peerDependencies": { - "esbuild": "^0.19.4" - } - }, - "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -15514,7 +15400,9 @@ } }, "node_modules/immutable": { - "version": "4.3.5", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", + "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "license": "MIT" }, "node_modules/import-fresh": { @@ -15848,10 +15736,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21058,6 +20951,13 @@ "version": "2.6.2", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-forge": { "version": "1.3.1", "license": "(BSD-3-Clause OR GPL-2.0)", @@ -24307,16 +24207,21 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -24649,11 +24554,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.70.0", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -24661,43 +24568,248 @@ }, "engines": { "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, "node_modules/sass-embedded": { - "version": "1.70.0", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.97.3.tgz", + "integrity": "sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==", "license": "MIT", "dependencies": { - "@bufbuild/protobuf": "^1.0.0", - "buffer-builder": "^0.2.0", - "immutable": "^4.0.0", + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.5.0", + "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, + "bin": { + "sass": "dist/bin/sass.js" + }, "engines": { "node": ">=16.0.0" }, "optionalDependencies": { - "sass-embedded-android-arm": "1.70.0", - "sass-embedded-android-arm64": "1.70.0", - "sass-embedded-android-ia32": "1.70.0", - "sass-embedded-android-x64": "1.70.0", - "sass-embedded-darwin-arm64": "1.70.0", - "sass-embedded-darwin-x64": "1.70.0", - "sass-embedded-linux-arm": "1.70.0", - "sass-embedded-linux-arm64": "1.70.0", - "sass-embedded-linux-ia32": "1.70.0", - "sass-embedded-linux-musl-arm": "1.70.0", - "sass-embedded-linux-musl-arm64": "1.70.0", - "sass-embedded-linux-musl-ia32": "1.70.0", - "sass-embedded-linux-musl-x64": "1.70.0", - "sass-embedded-linux-x64": "1.70.0", - "sass-embedded-win32-ia32": "1.70.0", - "sass-embedded-win32-x64": "1.70.0" + "sass-embedded-all-unknown": "1.97.3", + "sass-embedded-android-arm": "1.97.3", + "sass-embedded-android-arm64": "1.97.3", + "sass-embedded-android-riscv64": "1.97.3", + "sass-embedded-android-x64": "1.97.3", + "sass-embedded-darwin-arm64": "1.97.3", + "sass-embedded-darwin-x64": "1.97.3", + "sass-embedded-linux-arm": "1.97.3", + "sass-embedded-linux-arm64": "1.97.3", + "sass-embedded-linux-musl-arm": "1.97.3", + "sass-embedded-linux-musl-arm64": "1.97.3", + "sass-embedded-linux-musl-riscv64": "1.97.3", + "sass-embedded-linux-musl-x64": "1.97.3", + "sass-embedded-linux-riscv64": "1.97.3", + "sass-embedded-linux-x64": "1.97.3", + "sass-embedded-unknown-all": "1.97.3", + "sass-embedded-win32-arm64": "1.97.3", + "sass-embedded-win32-x64": "1.97.3" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.97.3.tgz", + "integrity": "sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.97.3.tgz", + "integrity": "sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.97.3.tgz", + "integrity": "sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.97.3.tgz", + "integrity": "sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.97.3.tgz", + "integrity": "sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.97.3.tgz", + "integrity": "sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.97.3.tgz", + "integrity": "sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.97.3.tgz", + "integrity": "sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.97.3.tgz", + "integrity": "sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.97.3.tgz", + "integrity": "sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.97.3.tgz", + "integrity": "sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.97.3.tgz", + "integrity": "sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" } }, "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.70.0", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.97.3.tgz", + "integrity": "sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==", "cpu": [ "x64" ], @@ -24710,8 +24822,26 @@ "node": ">=14.0.0" } }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.97.3.tgz", + "integrity": "sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/sass-embedded-linux-x64": { - "version": "1.70.0", + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.97.3.tgz", + "integrity": "sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==", "cpu": [ "x64" ], @@ -24720,9 +24850,54 @@ "os": [ "linux" ], - "bin": { - "sass": "dart-sass/sass" - }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.97.3.tgz", + "integrity": "sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==", + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.97.3" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.97.3.tgz", + "integrity": "sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.97.3.tgz", + "integrity": "sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=14.0.0" } @@ -24740,6 +24915,34 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/saxes": { "version": "6.0.0", "dev": true, @@ -26020,6 +26223,27 @@ "dev": true, "license": "MIT" }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -28627,7 +28851,7 @@ "css-loader": "^7.1.4", "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", - "esbuild-sass-plugin": "^2.16.1", + "esbuild-sass-plugin": "^3.6.0", "expose-loader": "^4.1.0", "glob": "^10.3.3", "js-yaml": "^4.1.0", @@ -28657,9 +28881,9 @@ } }, "packages/webpacker/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -28673,9 +28897,9 @@ } }, "packages/webpacker/node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -28689,9 +28913,9 @@ } }, "packages/webpacker/node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -28705,9 +28929,9 @@ } }, "packages/webpacker/node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -28721,9 +28945,9 @@ } }, "packages/webpacker/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -28737,9 +28961,9 @@ } }, "packages/webpacker/node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -28753,9 +28977,9 @@ } }, "packages/webpacker/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -28769,9 +28993,9 @@ } }, "packages/webpacker/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -28785,9 +29009,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -28801,9 +29025,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -28817,9 +29041,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -28833,9 +29057,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -28849,9 +29073,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -28865,9 +29089,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -28881,9 +29105,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -28897,9 +29121,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -28913,9 +29137,9 @@ } }, "packages/webpacker/node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -28929,9 +29153,9 @@ } }, "packages/webpacker/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -28945,9 +29169,9 @@ } }, "packages/webpacker/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -28961,9 +29185,9 @@ } }, "packages/webpacker/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -28977,9 +29201,9 @@ } }, "packages/webpacker/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -28993,9 +29217,9 @@ } }, "packages/webpacker/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -29009,9 +29233,9 @@ } }, "packages/webpacker/node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -29025,9 +29249,9 @@ } }, "packages/webpacker/node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -29041,9 +29265,9 @@ } }, "packages/webpacker/node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -29057,9 +29281,9 @@ } }, "packages/webpacker/node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -29086,9 +29310,9 @@ } }, "packages/webpacker/node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -29098,32 +29322,46 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "packages/webpacker/node_modules/esbuild-sass-plugin": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.6.0.tgz", + "integrity": "sha512-lzPJQSEXcnj5amBPPib5lBjsDNPzvdMnX+1Rf7eha9BIpLSM5Ad2pi+Rqg5CAlWMduCgLntS2hLAqG7v1fxWGw==", + "license": "MIT", + "dependencies": { + "resolve": "^1.22.11", + "sass": "^1.97.2" + }, + "peerDependencies": { + "esbuild": ">=0.27.2", + "sass-embedded": "^1.97.2" } }, "packages/webpacker/node_modules/js-yaml": { diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index ec21ecf0c495d..1f2366bf7231a 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -21,7 +21,7 @@ "expose-loader": "^4.1.0", "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", - "esbuild-sass-plugin": "^2.16.1", + "esbuild-sass-plugin": "^3.6.0", "glob": "^10.3.3", "js-yaml": "^4.1.0", "mini-css-extract-plugin": "^2.7.6", From 954718bd3dbc65ca0fbf1a18972f209962342c8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:29:15 +0200 Subject: [PATCH 025/135] Bump to dependencies: Bump expose-loader from 4.1.0 to 5.0.1 in /packages/webpacker (#16245) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 8 +++++--- packages/webpacker/package.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index caa0305619379..ea5c9e266d3fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14181,10 +14181,12 @@ } }, "node_modules/expose-loader": { - "version": "4.1.0", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz", + "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==", "license": "MIT", "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -28852,7 +28854,7 @@ "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", "esbuild-sass-plugin": "^3.6.0", - "expose-loader": "^4.1.0", + "expose-loader": "^5.0.1", "glob": "^10.3.3", "js-yaml": "^4.1.0", "mini-css-extract-plugin": "^2.7.6", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index 1f2366bf7231a..22561bd8f6b7e 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -18,7 +18,7 @@ "compression-webpack-plugin": "^10.0.0", "core-js": "~3.33.1", "css-loader": "^7.1.4", - "expose-loader": "^4.1.0", + "expose-loader": "^5.0.1", "esbuild": "^0.27.0", "esbuild-loader": "^4.0.2", "esbuild-sass-plugin": "^3.6.0", From f9914cf40fe510214cd1e4cb251e437c833c7085 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:29:53 +0200 Subject: [PATCH 026/135] Bump to dependencies: Bump connection_pool from 2.5.5 to 3.0.2 (#16246) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index fc10f112f12e8..914d3662ce618 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,7 +65,7 @@ PATH charlock_holmes (~> 0.7) chartkick (>= 5.1.2, < 5.3.0) concurrent-ruby (~> 1.3.0) - connection_pool (< 3) + connection_pool (< 4) data_migrate (~> 11.3) date_validator (~> 0.12.0) devise (~> 4.7) @@ -344,7 +344,7 @@ GEM commonmarker (2.6.3-arm64-darwin) commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) - connection_pool (2.5.5) + connection_pool (3.0.2) crack (1.0.0) bigdecimal rexml diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 48af1c03e80ac..727e917c4d60f 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -38,7 +38,7 @@ Gem::Specification.new do |s| s.add_dependency "cells-rails", "~> 0.1.3" s.add_dependency "charlock_holmes", "~> 0.7" s.add_dependency "chartkick", ">= 5.1.2", "< 5.3.0" - s.add_dependency "connection_pool", "< 3" + s.add_dependency "connection_pool", "< 4" s.add_dependency "data_migrate", "~> 11.3" s.add_dependency "date_validator", "~> 0.12.0" s.add_dependency "devise", "~> 4.7" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index ae57b5bd8ee63..b32e00c2f3228 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -65,7 +65,7 @@ PATH charlock_holmes (~> 0.7) chartkick (>= 5.1.2, < 5.3.0) concurrent-ruby (~> 1.3.0) - connection_pool (< 3) + connection_pool (< 4) data_migrate (~> 11.3) date_validator (~> 0.12.0) devise (~> 4.7) @@ -342,7 +342,7 @@ GEM cmdparse (3.0.7) commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) - connection_pool (2.5.5) + connection_pool (3.0.2) crack (1.0.0) bigdecimal rexml From 419a56facbf4badfc155621d696d989010fd51cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:57:15 +0200 Subject: [PATCH 027/135] Bump to dependencies: Bump the github-actions group across 2 directories with 1 update (#16247) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test_app.yml | 2 +- .../lib/decidim/generators/component_templates/github/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index 572b557bc604d..813b796847a64 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -141,7 +141,7 @@ jobs: with: name: ${{ inputs.working-directory }} flags: ${{ inputs.working-directory }} - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: always() with: name: Screenshots of run ${{ github.run_id }} diff --git a/decidim-generators/lib/decidim/generators/component_templates/github/ci.yml b/decidim-generators/lib/decidim/generators/component_templates/github/ci.yml index 848524df666a4..803c4911936d3 100644 --- a/decidim-generators/lib/decidim/generators/component_templates/github/ci.yml +++ b/decidim-generators/lib/decidim/generators/component_templates/github/ci.yml @@ -69,7 +69,7 @@ jobs: - run: bundle exec rspec name: RSpec - uses: codecov/codecov-action@v5 - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 if: always() with: name: screenshots From 16c178e05c8594090c4510ffb79640149393610b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 27 Feb 2026 12:27:27 +0100 Subject: [PATCH 028/135] Fix long names and components counter in admin (#16236) * Fix long names and components counter in admin * Change truncate strategy for min-width (suggested by CodeRabbit) --- .../packs/stylesheets/decidim/admin/_sidebar-menu.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_sidebar-menu.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_sidebar-menu.scss index 1680ab240534d..4db16bb3b817b 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_sidebar-menu.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_sidebar-menu.scss @@ -4,7 +4,7 @@ &__item { a, &-disabled { - @apply gap-x-2 p-2 flex items-center text-sm truncate border border-gray-5 rounded; + @apply gap-x-2 p-2 flex items-center text-sm border border-gray-5 rounded; > svg { @apply w-4 h-4 flex-none text-gray fill-current; @@ -25,7 +25,11 @@ } & .component-counter { - @apply ml-auto inline-flex items-center justify-center w-5 h-5 text-xs font-semibold rounded-full bg-background-4 border-secondary; + @apply flex-shrink-0 ml-auto inline-flex items-center justify-center w-5 h-5 text-xs font-semibold rounded-full bg-background-4 border-secondary; + } + + a > span:first-child { + @apply min-w-0 whitespace-normal break-words; } ul { From bd53bd6e4763b93d50dc8ba36d771790f1945137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:18:23 +0200 Subject: [PATCH 029/135] Bump to dependencies: Bump factory_bot_rails from 6.4.4 to 6.5.1 (#16249) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 6 +++--- decidim-core/lib/decidim/core/test/factories.rb | 4 ++-- decidim-generators/Gemfile.lock | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 914d3662ce618..32e5e4b02667e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -411,11 +411,11 @@ GEM escape_utils (1.3.0) excon (1.2.8) logger - factory_bot (6.5.4) + factory_bot (6.5.6) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.1) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faker (3.5.3) i18n (>= 1.8.11, < 2) faraday (2.14.1) diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index f6974216dc600..c2f24ce3dcaaf 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -830,8 +830,8 @@ def generate_title(field = nil, skip_injection:) user { create(:user) } organization { user.organization } - user_id { user.id } - user_type { user.class.name } + user_id { user.try(:id) } + user_type { user.try(:class).try(:name) } participatory_space { build(:participatory_process, organization:, skip_injection:) } component { build(:component, participatory_space:, skip_injection:) } resource { build(:dummy_resource, component:, skip_injection:) } diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index b32e00c2f3228..bdf6057aab1a9 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -409,11 +409,11 @@ GEM escape_utils (1.3.0) excon (1.2.8) logger - factory_bot (6.5.4) + factory_bot (6.5.6) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.1) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faker (3.5.3) i18n (>= 1.8.11, < 2) faraday (2.14.1) From 70851a8f67176c2d42604c5e48a0151a58d076ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:36:47 +0100 Subject: [PATCH 030/135] Bump to dependencies: Bump minimatch (#16248) Bumps the npm_and_yarn group with 1 update in the / directory: [minimatch](https://github.com/isaacs/minimatch). Updates `minimatch` from 3.1.3 to 3.1.5 - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.3...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 113 +++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea5c9e266d3fc..bec1efd6b5c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5291,9 +5291,9 @@ "peer": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -5396,9 +5396,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -10479,9 +10479,9 @@ } }, "node_modules/bulk-require/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -13255,9 +13255,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -13320,9 +13320,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -13401,9 +13401,9 @@ } }, "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -13486,9 +13486,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -13578,9 +13578,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -13814,9 +13814,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -14367,9 +14367,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz", - "integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -16318,9 +16318,9 @@ } }, "node_modules/jake/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -20760,12 +20760,12 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", - "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -20774,27 +20774,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimatch/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/minimist": { "version": "1.2.8", "dev": true, @@ -24346,9 +24325,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "peer": true, @@ -26612,9 +26591,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -27922,9 +27901,9 @@ } }, "node_modules/workbox-build/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" From 51802b793ff5a78adfd85f014bc72827cb20df1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:16:16 +0200 Subject: [PATCH 031/135] Bump to dependencies: Bump ruby-vips from 2.2.5 to 2.3.0 (#16255) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 32e5e4b02667e..42929ac5c365f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -824,7 +824,7 @@ GEM rubocop (~> 1.72) yard ruby-progressbar (1.13.0) - ruby-vips (2.2.5) + ruby-vips (2.3.0) ffi (~> 1.12) logger rubyXL (3.4.33) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index bdf6057aab1a9..9ae4d7c9748c0 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -816,7 +816,7 @@ GEM rubocop (~> 1.72) yard ruby-progressbar (1.13.0) - ruby-vips (2.2.5) + ruby-vips (2.3.0) ffi (~> 1.12) logger rubyXL (3.4.33) From c7cfc2f3198844af62f02dc2eb4ff7ce1d73eb59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:34:48 +0200 Subject: [PATCH 032/135] Bump to dependencies: Bump webmock from 3.24.0 to 3.26.1 (#16257) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 6 +++--- decidim-generators/Gemfile.lock | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 42929ac5c365f..63d9d3d669448 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -345,7 +345,7 @@ GEM commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) connection_pool (3.0.2) - crack (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -465,7 +465,7 @@ GEM logger (~> 1.6) ostruct (~> 0.6) sass-embedded (~> 1.58) - hashdiff (1.1.2) + hashdiff (1.2.1) hashie (5.1.0) logger hexapdf (1.6.0) @@ -914,7 +914,7 @@ GEM web-push (3.1.0) jwt (~> 3.0) openssl (>= 3.0) - webmock (3.24.0) + webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 9ae4d7c9748c0..b25b76b8bd4d8 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -284,7 +284,7 @@ GEM acts_as_list (1.2.4) activerecord (>= 6.1) activesupport (>= 6.1) - addressable (2.8.8) + addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) @@ -343,7 +343,7 @@ GEM commonmarker (2.6.3-x86_64-linux) concurrent-ruby (1.3.6) connection_pool (3.0.2) - crack (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -459,7 +459,7 @@ GEM logger (~> 1.6) ostruct (~> 0.6) sass-embedded (~> 1.58) - hashdiff (1.1.2) + hashdiff (1.2.1) hashie (5.1.0) logger hexapdf (1.6.0) @@ -903,7 +903,7 @@ GEM web-push (3.1.0) jwt (~> 3.0) openssl (>= 3.0) - webmock (3.24.0) + webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) From 6e757cd98c4642f9f36be5262638fbbb613e6583 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:24:23 +0200 Subject: [PATCH 033/135] Bump to dependencies: Bump brakeman from 8.0.2 to 8.0.4 (#16258) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 63d9d3d669448..ba9993db32f84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -302,7 +302,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (8.0.2) + brakeman (8.0.4) racc browser (6.2.0) builder (3.3.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index b25b76b8bd4d8..c8f3eded7f590 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -302,7 +302,7 @@ GEM bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (8.0.2) + brakeman (8.0.4) racc browser (6.2.0) builder (3.3.0) From bcde88d6e46eccb39b858c82476446349cba72a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:07:56 +0200 Subject: [PATCH 034/135] Bump to dependencies: Bump rubocop-factory_bot from 2.27.1 to 2.28.0 (#16259) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 4 ++-- .../spec/cells/decidim/budgets/budgets_list_cell_spec.rb | 4 ++-- decidim-comments/spec/types/comment_type_spec.rb | 6 +++--- decidim-core/spec/lib/file_validator_humanizer_spec.rb | 2 ++ decidim-dev/decidim-dev.gemspec | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba9993db32f84..f46badad3037b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,7 +144,7 @@ PATH rspec_junit_formatter (~> 0.6.0) rubocop (~> 1.78.0) rubocop-capybara (~> 2.22.0, >= 2.22.1) - rubocop-factory_bot (~> 2.27.0) + rubocop-factory_bot (>= 2.27, < 2.29) rubocop-faker (~> 1.3, >= 1.3.0) rubocop-graphql (~> 1.5, >= 1.5.6) rubocop-performance (~> 1.25, >= 1.25.0) @@ -789,7 +789,7 @@ GEM rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-factory_bot (2.27.1) + rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-faker (1.3.0) diff --git a/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb b/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb index cfac7d4a9a5d7..b16e03f5b2727 100644 --- a/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb +++ b/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb @@ -25,11 +25,11 @@ module Decidim::Budgets allow(my_cell).to receive(:url_for).and_return("/") - # rubocop:disable Rspec/AnyInstance + # rubocop:disable RSpec/AnyInstance allow_any_instance_of(BudgetListItemCell).to receive(:budget_projects_path) do |_subcell, budget, **| "/budgets/#{budget.id}/projects" end - # rubocop:enable Rspec/AnyInstance + # rubocop:enable RSpec/AnyInstance end describe "#main_list" do diff --git a/decidim-comments/spec/types/comment_type_spec.rb b/decidim-comments/spec/types/comment_type_spec.rb index 26b6cfe1d80c3..35628ae2bbf29 100644 --- a/decidim-comments/spec/types/comment_type_spec.rb +++ b/decidim-comments/spec/types/comment_type_spec.rb @@ -136,7 +136,7 @@ module Comments end it "returns true if the comment has comments" do - FactoryBot.create(:comment, commentable: model) + create(:comment, commentable: model) expect(response).to include("hasComments" => true) end @@ -160,8 +160,8 @@ module Comments end describe "comments" do - let!(:random_comment) { FactoryBot.create(:comment) } - let!(:replies) { Array.new(3) { |n| FactoryBot.create(:comment, commentable: model, created_at: Time.current - n.days) } } + let!(:random_comment) { create(:comment) } + let!(:replies) { Array.new(3) { |n| create(:comment, commentable: model, created_at: Time.current - n.days) } } let(:query) { "{ comments { id } }" } diff --git a/decidim-core/spec/lib/file_validator_humanizer_spec.rb b/decidim-core/spec/lib/file_validator_humanizer_spec.rb index 4897c2362b082..ee5625e1c101d 100644 --- a/decidim-core/spec/lib/file_validator_humanizer_spec.rb +++ b/decidim-core/spec/lib/file_validator_humanizer_spec.rb @@ -32,9 +32,11 @@ def self.model_name validates_upload(:file, **validation_options, uploader: mount_class) + # rubocop:disable FactoryBot/SyntaxMethods def organization @organization ||= FactoryBot.create(:organization) end + # rubocop:enable FactoryBot/SyntaxMethods end end diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index b68fa7ade1d95..408ec012f3361 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -55,7 +55,7 @@ Gem::Specification.new do |s| s.add_dependency "rspec-retry", "~> 0.6.2" s.add_dependency "rubocop", "~> 1.78.0" s.add_dependency "rubocop-capybara", "~> 2.22.0", ">= 2.22.1" - s.add_dependency "rubocop-factory_bot", "~> 2.27.0" + s.add_dependency "rubocop-factory_bot", ">= 2.27", "< 2.29" s.add_dependency "rubocop-faker", "~> 1.3", ">= 1.3.0" s.add_dependency "rubocop-graphql", "~> 1.5", ">= 1.5.6" s.add_dependency "rubocop-performance", "~> 1.25", ">= 1.25.0" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index c8f3eded7f590..2f3c98c1b5775 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -144,7 +144,7 @@ PATH rspec_junit_formatter (~> 0.6.0) rubocop (~> 1.78.0) rubocop-capybara (~> 2.22.0, >= 2.22.1) - rubocop-factory_bot (~> 2.27.0) + rubocop-factory_bot (>= 2.27, < 2.29) rubocop-faker (~> 1.3, >= 1.3.0) rubocop-graphql (~> 1.5, >= 1.5.6) rubocop-performance (~> 1.25, >= 1.25.0) @@ -781,7 +781,7 @@ GEM rubocop-capybara (2.22.1) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-factory_bot (2.27.1) + rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-faker (1.3.0) From 244a5f9ed772c8a1b909754fb576cdbb52cf6d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 27 Feb 2026 23:33:35 +0100 Subject: [PATCH 035/135] Add a validation and increase the amounts for budgets (#16250) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../decidim/budgets/admin/budget_form.rb | 2 +- .../decidim/budgets/admin/project_form.rb | 2 +- ...2110213_change_budget_columns_to_bigint.rb | 26 +++++++++++++++++++ .../decidim/budgets/admin/budget_form_spec.rb | 18 +++++++++++++ .../budgets/admin/project_form_spec.rb | 18 +++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 decidim-budgets/db/migrate/20250912110213_change_budget_columns_to_bigint.rb diff --git a/decidim-budgets/app/forms/decidim/budgets/admin/budget_form.rb b/decidim-budgets/app/forms/decidim/budgets/admin/budget_form.rb index 4918e044c4329..eb4b8f4425d82 100644 --- a/decidim-budgets/app/forms/decidim/budgets/admin/budget_form.rb +++ b/decidim-budgets/app/forms/decidim/budgets/admin/budget_form.rb @@ -16,7 +16,7 @@ class BudgetForm < Decidim::Form validates :title, translatable_presence: true validates :weight, numericality: { greater_than_or_equal_to: 0 } - validates :total_budget, numericality: { greater_than: 0 } + validates :total_budget, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 9_223_372_036_854_775_807 } end end end diff --git a/decidim-budgets/app/forms/decidim/budgets/admin/project_form.rb b/decidim-budgets/app/forms/decidim/budgets/admin/project_form.rb index 339ab98e8968f..530e4099dac94 100644 --- a/decidim-budgets/app/forms/decidim/budgets/admin/project_form.rb +++ b/decidim-budgets/app/forms/decidim/budgets/admin/project_form.rb @@ -26,7 +26,7 @@ class ProjectForm < Decidim::Form validates :title, translatable_presence: true validates :description, translatable_presence: true - validates :budget_amount, presence: true, numericality: { greater_than: 0 } + validates :budget_amount, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 9_223_372_036_854_775_807 } validates :address, geocoding: true, if: ->(form) { form.has_address? && !form.geocoded? } validate :notify_missing_attachment_if_errored diff --git a/decidim-budgets/db/migrate/20250912110213_change_budget_columns_to_bigint.rb b/decidim-budgets/db/migrate/20250912110213_change_budget_columns_to_bigint.rb new file mode 100644 index 0000000000000..a0cc1c834b5d4 --- /dev/null +++ b/decidim-budgets/db/migrate/20250912110213_change_budget_columns_to_bigint.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ChangeBudgetColumnsToBigint < ActiveRecord::Migration[7.1] + def up + change_column :decidim_budgets_budgets, :total_budget, :bigint + change_column :decidim_budgets_projects, :budget_amount, :bigint + end + + def down + budget_overflow = select_value(<<~SQL.squish) + SELECT 1 FROM decidim_budgets_budgets + WHERE total_budget > 2147483647 OR total_budget < -2147483648 + LIMIT 1 + SQL + project_overflow = select_value(<<~SQL.squish) + SELECT 1 FROM decidim_budgets_projects + WHERE budget_amount > 2147483647 OR budget_amount < -2147483648 + LIMIT 1 + SQL + + raise ActiveRecord::IrreversibleMigration, "Cannot safely convert bigint budgets back to integer: out-of-range values exist" if budget_overflow || project_overflow + + change_column :decidim_budgets_budgets, :total_budget, :integer + change_column :decidim_budgets_projects, :budget_amount, :integer + end +end diff --git a/decidim-budgets/spec/forms/decidim/budgets/admin/budget_form_spec.rb b/decidim-budgets/spec/forms/decidim/budgets/admin/budget_form_spec.rb index 7fed256d1a4c5..6f86494651ff8 100644 --- a/decidim-budgets/spec/forms/decidim/budgets/admin/budget_form_spec.rb +++ b/decidim-budgets/spec/forms/decidim/budgets/admin/budget_form_spec.rb @@ -53,4 +53,22 @@ it { is_expected.not_to be_valid } end + + describe "when total_budget is negative" do + let(:total_budget) { -1 } + + it { is_expected.not_to be_valid } + end + + describe "when total_budget is too large" do + let(:total_budget) { 9_223_372_036_854_775_808 } + + it { is_expected.not_to be_valid } + end + + describe "when total_budget is at the maximum allowed value" do + let(:total_budget) { 9_223_372_036_854_775_807 } + + it { is_expected.to be_valid } + end end diff --git a/decidim-budgets/spec/forms/decidim/budgets/admin/project_form_spec.rb b/decidim-budgets/spec/forms/decidim/budgets/admin/project_form_spec.rb index b065c44326a20..b128df08de0b3 100644 --- a/decidim-budgets/spec/forms/decidim/budgets/admin/project_form_spec.rb +++ b/decidim-budgets/spec/forms/decidim/budgets/admin/project_form_spec.rb @@ -100,6 +100,24 @@ module Decidim::Budgets it { is_expected.not_to be_valid } end + describe "when budget_amount is negative" do + let(:budget_amount) { -1 } + + it { is_expected.not_to be_valid } + end + + describe "when budget_amount is too large" do + let(:budget_amount) { 9_223_372_036_854_775_808 } + + it { is_expected.not_to be_valid } + end + + describe "when budget_amount is at the maximum allowed value" do + let(:budget_amount) { 9_223_372_036_854_775_807 } + + it { is_expected.to be_valid } + end + context "with proposals" do subject { described_class.from_model(project).with_context(context) } From f0f8549bb14246bd350c803c9764cfdebb1ddb6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:51:57 +0200 Subject: [PATCH 036/135] Bump to dependencies: Bump devise-i18n from 1.12.1 to 1.15.0 (#16261) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 11 ++++++----- decidim-generators/Gemfile.lock | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f46badad3037b..300356ecd2ee9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,7 +289,7 @@ GEM ast (2.4.3) base64 (0.3.0) batch-loader (2.0.5) - bcrypt (3.1.20) + bcrypt (3.1.21) benchmark (0.5.0) better_html (2.2.0) actionview (>= 7.0) @@ -368,8 +368,9 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.12.1) + devise-i18n (1.15.0) devise (>= 4.9.0) + rails-i18n devise-jwt (0.12.1) devise (~> 4.0) warden-jwt_auth (~> 0.10) @@ -733,9 +734,9 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) rexml (3.4.4) rqrcode (2.2.0) chunky_png (~> 1.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 2f3c98c1b5775..52e7bb60c789c 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -289,7 +289,7 @@ GEM ast (2.4.3) base64 (0.3.0) batch-loader (2.0.5) - bcrypt (3.1.20) + bcrypt (3.1.21) benchmark (0.5.0) better_html (2.2.0) actionview (>= 7.0) @@ -366,8 +366,9 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.12.1) + devise-i18n (1.15.0) devise (>= 4.9.0) + rails-i18n devise-jwt (0.12.1) devise (~> 4.0) warden-jwt_auth (~> 0.10) @@ -725,9 +726,9 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) rexml (3.4.4) rqrcode (2.2.0) chunky_png (~> 1.0) From 714a81c1ec6cbcebfc2d43e6a9180c8bbb03cba0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:52:41 +0200 Subject: [PATCH 037/135] Bump to dependencies: Bump source-map-loader from 4.0.2 to 5.0.0 in /packages/webpacker (#16260) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- package-lock.json | 8 +++++--- packages/webpacker/package.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index bec1efd6b5c52..7478d8192937b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25348,14 +25348,16 @@ } }, "node_modules/source-map-loader": { - "version": "4.0.2", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "license": "MIT", "dependencies": { "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -28847,7 +28849,7 @@ "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", "shakapacker": "~8.3.0", - "source-map-loader": "^4.0.1", + "source-map-loader": "^5.0.0", "style-loader": "^3.3.3", "tailwindcss": "^3.4.19", "terser-webpack-plugin": "^5.3.9", diff --git a/packages/webpacker/package.json b/packages/webpacker/package.json index 22561bd8f6b7e..e0a04e2bb2de2 100644 --- a/packages/webpacker/package.json +++ b/packages/webpacker/package.json @@ -35,7 +35,7 @@ "postcss-scss": "^4.0.6", "sass-embedded": "^1.63.6", "shakapacker": "~8.3.0", - "source-map-loader": "^4.0.1", + "source-map-loader": "^5.0.0", "style-loader": "^3.3.3", "tailwindcss": "^3.4.19", "terser-webpack-plugin": "^5.3.9", From b0ee99ba1629a10479cf59d8b9ab1c749d6c80ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sat, 28 Feb 2026 00:24:26 +0100 Subject: [PATCH 038/135] Fix count of valid questions types in survey (#16254) --- .../app/models/decidim/forms/questionnaire.rb | 5 ++++ .../models/decidim/forms/question_spec.rb | 26 +++++++++++++++++++ .../decidim/forms/questionnaire_spec.rb | 22 ++++++++++++++++ .../surveys/survey_card_metadata_cell.rb | 2 +- .../surveys/admin/surveys/index.html.erb | 2 +- .../surveys/survey_card_metadata_cell_spec.rb | 10 +++++-- 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/decidim-forms/app/models/decidim/forms/questionnaire.rb b/decidim-forms/app/models/decidim/forms/questionnaire.rb index a70004f0a76ad..86108e4a8e80a 100644 --- a/decidim-forms/app/models/decidim/forms/questionnaire.rb +++ b/decidim-forms/app/models/decidim/forms/questionnaire.rb @@ -48,6 +48,11 @@ def count_participants Decidim::Forms::QuestionnaireParticipants.new(self).count_participants end + # Public: Returns only the actual question types (excludes separators and title_and_description) + def question_types + questions.not_separator.not_title_and_description + end + private # salt is used to generate secure hash in anonymous responses diff --git a/decidim-forms/spec/models/decidim/forms/question_spec.rb b/decidim-forms/spec/models/decidim/forms/question_spec.rb index 126533728fadb..21e9c84e0b4f1 100644 --- a/decidim-forms/spec/models/decidim/forms/question_spec.rb +++ b/decidim-forms/spec/models/decidim/forms/question_spec.rb @@ -67,6 +67,32 @@ module Forms expect(subject.class.not_conditioned).not_to include(question_conditioned) end end + + describe "#not_separator" do + let(:question_separator) { create(:questionnaire_question, questionnaire:, question_type: "separator") } + let(:question_regular) { create(:questionnaire_question, questionnaire:, question_type: "short_response") } + + it "excludes separator questions" do + expect(subject.class.not_separator).not_to include(question_separator) + end + + it "includes regular questions" do + expect(subject.class.not_separator).to include(question_regular) + end + end + + describe "#not_title_and_description" do + let(:question_title_desc) { create(:questionnaire_question, questionnaire:, question_type: "title_and_description") } + let(:question_regular) { create(:questionnaire_question, questionnaire:, question_type: "short_response") } + + it "excludes title_and_description questions" do + expect(subject.class.not_title_and_description).not_to include(question_title_desc) + end + + it "includes regular questions" do + expect(subject.class.not_title_and_description).to include(question_regular) + end + end end describe ".log_presenter_class_for" do diff --git a/decidim-forms/spec/models/decidim/forms/questionnaire_spec.rb b/decidim-forms/spec/models/decidim/forms/questionnaire_spec.rb index c699b11e1374c..070f7e671b96e 100644 --- a/decidim-forms/spec/models/decidim/forms/questionnaire_spec.rb +++ b/decidim-forms/spec/models/decidim/forms/questionnaire_spec.rb @@ -93,6 +93,28 @@ module Forms end end end + + describe "#question_types" do + let!(:question_separator) { create(:questionnaire_question, questionnaire:, question_type: "separator") } + let!(:question_title_desc) { create(:questionnaire_question, questionnaire:, question_type: "title_and_description") } + let!(:question_short_answer) { create(:questionnaire_question, questionnaire:, question_type: "short_response") } + + it "returns only actual question types" do + expect(subject.question_types).to include(question_short_answer) + end + + it "does not include separator questions" do + expect(subject.question_types).not_to include(question_separator) + end + + it "does not include title_and_description questions" do + expect(subject.question_types).not_to include(question_title_desc) + end + + it "returns the correct count" do + expect(subject.question_types.size).to eq(1) + end + end end end end diff --git a/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb b/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb index c8d5b42712db2..412a9cbf3b9f4 100644 --- a/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb +++ b/decidim-surveys/app/cells/decidim/surveys/survey_card_metadata_cell.rb @@ -31,7 +31,7 @@ def duration end def questions_count_item - text = "#{survey.questionnaire.questions.not_separator.size} #{t("questions", scope: "decidim.surveys.surveys.show")}" + text = "#{survey.questionnaire.question_types.size} #{t("questions", scope: "decidim.surveys.surveys.show")}" { text:, diff --git a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb index 308db89b9beaf..f0577ad8ff074 100644 --- a/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb +++ b/decidim-surveys/app/views/decidim/surveys/admin/surveys/index.html.erb @@ -26,7 +26,7 @@ <%= link_to decidim_sanitize_translated(survey.title), edit_survey_path(survey) %> "> - <%= survey.questionnaire.questions.not_separator.size %> + <%= survey.questionnaire.question_types.size %> "> <%= survey.questionnaire.count_participants %> diff --git a/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb b/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb index edf798db0f3f0..42f56f9acf087 100644 --- a/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb +++ b/decidim-surveys/spec/cells/decidim/surveys/survey_card_metadata_cell_spec.rb @@ -39,10 +39,16 @@ module Decidim::Surveys end describe "questions_count_item" do - it "renders the correct number of questions and survey icon" do - questions_count = survey.questionnaire.questions.size + let!(:question_separator) { create(:questionnaire_question, questionnaire: survey.questionnaire, question_type: "separator") } + let!(:question_title_desc) { create(:questionnaire_question, questionnaire: survey.questionnaire, question_type: "title_and_description") } + let!(:question_regular) { create(:questionnaire_question, questionnaire: survey.questionnaire, question_type: "short_response") } + + it "renders only the number of actual questions, excluding separators and title_and_description" do + questions_count = survey.questionnaire.question_types.size expect(subject.to_s).to include("#{questions_count} #{I18n.t("questions", scope: "decidim.surveys.surveys.show")}") expect(subject.to_s).to include("survey-line") + # 3 from the factory + 1 question_regular from this spec + expect(questions_count).to eq(4) end end end From eff46f87be86db9136d2ac20c1c7cffe187eac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sat, 28 Feb 2026 00:39:42 +0100 Subject: [PATCH 039/135] Remove 'free text' option from sorting questions' responses options (#16235) --- .../packs/src/decidim/forms/admin/forms.js | 12 ++++- .../questionnaires/_response_option.html.erb | 18 ++++---- .../manage_questionnaires/add_questions.rb | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/decidim-forms/app/packs/src/decidim/forms/admin/forms.js b/decidim-forms/app/packs/src/decidim/forms/admin/forms.js index c07471077963d..4c7f8f8eb26bd 100644 --- a/decidim-forms/app/packs/src/decidim/forms/admin/forms.js +++ b/decidim-forms/app/packs/src/decidim/forms/admin/forms.js @@ -22,6 +22,7 @@ export default function createEditableForm() { const matrixRowRemoveFieldButtonSelector = ".remove-matrix-row"; const addMatrixRowButtonSelector = ".add-matrix-row"; const maxChoicesWrapperSelector = ".questionnaire-question-max-choices"; + const responseOptionFreeTextSelector = ".questionnaire-question-response-option-free-text"; const displayConditionFieldSelector = ".questionnaire-question-display-condition"; const displayConditionsWrapperSelector = ".questionnaire-question-display-conditions"; @@ -335,7 +336,10 @@ export default function createEditableForm() { const dynamicFieldsMatrixRows = dynamicFieldsForMatrixRows[fieldId]; const onQuestionTypeChange = () => { - if (isMultipleChoiceOption($fieldQuestionTypeSelect.val())) { + const $currentField = $fieldQuestionTypeSelect.parents(fieldSelector); + const questionType = $fieldQuestionTypeSelect.val(); + + if (isMultipleChoiceOption(questionType)) { const nOptions = $fieldQuestionTypeSelect.parents(fieldSelector).find(responseOptionFieldSelector).length; if (nOptions === 0) { @@ -352,6 +356,12 @@ export default function createEditableForm() { dynamicFieldsMatrixRows._addField(); } } + + if (questionType === "sorting") { + $currentField.find(responseOptionFreeTextSelector).addClass("hidden"); + } else { + $currentField.find(responseOptionFreeTextSelector).removeClass("hidden"); + } }; $fieldQuestionTypeSelect.on("change", onQuestionTypeChange); diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_response_option.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_response_option.html.erb index e7d3c77975fcc..2ab8fee291c20 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_response_option.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_response_option.html.erb @@ -26,15 +26,15 @@ %>

-
- <%= - form.check_box( - :free_text, - label: t(".free_text"), - disabled: !questionnaire.questions_editable? - ) - %> -
+
+ <%= + form.check_box( + :free_text, + label: t(".free_text"), + disabled: !questionnaire.questions_editable? + ) + %> +

<% if response_option.persisted? %> diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb index 05372513eb088..1f9ba4c9ea80a 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/add_questions.rb @@ -412,11 +412,57 @@ select "Single option", from: "Type" expect(page).to have_css("input[type=checkbox][id$=_free_text]") + + select "Sorting", from: "Type" + expect(page).to have_no_css("input[type=checkbox][id$=_free_text]", visible: :visible) end it_behaves_like "updating the max choices selector according to the configured options" end + context "when adding a sorting question" do + before do + click_on "Add question" + + expand_all_questions + + within ".questionnaire-question" do + fill_in find_nested_form_field_locator("body_en"), with: "This is a sorting question" + select "Single option", from: "Type" + end + end + + it "does not display the free text option when switching to sorting type" do + within ".questionnaire-question" do + expect(page).to have_css("input[type=checkbox][id$=_free_text]") + + select "Sorting", from: "Type" + + expect(page).to have_no_css("input[type=checkbox][id$=_free_text]", visible: :visible) + end + end + + it "shows the free text option when switching back from sorting to single option" do + within ".questionnaire-question" do + select "Sorting", from: "Type" + expect(page).to have_no_css("input[type=checkbox][id$=_free_text]", visible: :visible) + + select "Single option", from: "Type" + expect(page).to have_css("input[type=checkbox][id$=_free_text]") + end + end + + it "hides free text option when switching from multiple option to sorting" do + within ".questionnaire-question" do + select "Multiple option", from: "Type" + expect(page).to have_css("input[type=checkbox][id$=_free_text]") + + select "Sorting", from: "Type" + expect(page).to have_no_css("input[type=checkbox][id$=_free_text]", visible: :visible) + end + end + end + context "when adding a matrix question" do let(:multiple_option_string) { "Matrix (Multiple option)" } let(:single_option_string) { "Matrix (Single option)" } From 47430ab14d9aa80ecdc390c36e9fcac2a335e68c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:19:48 +0200 Subject: [PATCH 040/135] Bump to dependencies: Bump fog-local from 0.8.0 to 0.9.0 (#16264) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 13 +++++++------ decidim-generators/Gemfile.lock | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 300356ecd2ee9..bb69cc5b86a35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -410,7 +410,7 @@ GEM temple erubi (1.13.1) escape_utils (1.3.0) - excon (1.2.8) + excon (1.3.2) logger factory_bot (6.5.6) activesupport (>= 6.1.0) @@ -437,9 +437,10 @@ GEM excon (~> 1.0) formatador (>= 0.2, < 2.0) mime-types - fog-local (0.8.0) + fog-local (0.9.0) fog-core (>= 1.27, < 3.0) - formatador (1.1.1) + formatador (1.2.3) + reline gemoji (4.1.0) geocoder (1.8.5) base64 (>= 0.1.0) @@ -552,10 +553,10 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) - mime-types (3.6.0) + mime-types (3.7.0) logger - mime-types-data (~> 3.2015) - mime-types-data (3.2025.0722) + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2026.0224) mini_magick (5.3.1) logger mini_mime (1.1.5) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 52e7bb60c789c..bbe6cd8d48186 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -408,7 +408,7 @@ GEM temple erubi (1.13.1) escape_utils (1.3.0) - excon (1.2.8) + excon (1.3.2) logger factory_bot (6.5.6) activesupport (>= 6.1.0) @@ -434,9 +434,10 @@ GEM excon (~> 1.0) formatador (>= 0.2, < 2.0) mime-types - fog-local (0.8.0) + fog-local (0.9.0) fog-core (>= 1.27, < 3.0) - formatador (1.1.1) + formatador (1.2.3) + reline gemoji (4.1.0) geocoder (1.8.5) base64 (>= 0.1.0) @@ -546,10 +547,10 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) - mime-types (3.6.0) + mime-types (3.7.0) logger - mime-types-data (~> 3.2015) - mime-types-data (3.2025.0722) + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2026.0224) mini_magick (5.3.1) logger mini_mime (1.1.5) From 90b3e322400d6ac016100bcc9e10a395703211e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 07:49:21 +0200 Subject: [PATCH 041/135] Bump to dependencies: Bump bootsnap from 1.18.6 to 1.23.0 (#16269) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile | 2 +- Gemfile.lock | 4 ++-- decidim-generators/Gemfile.lock | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 6bf3cc3d819ba..ef3c913668fa3 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ gem "decidim-elections", path: "." gem "decidim-initiatives", path: "." gem "decidim-templates", path: "." -gem "bootsnap", "~> 1.4" +gem "bootsnap", "~> 1.23" gem "puma", ">= 6.3.1" diff --git a/Gemfile.lock b/Gemfile.lock index bb69cc5b86a35..8e0c88530a74d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -300,7 +300,7 @@ GEM smart_properties bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.18.6) + bootsnap (1.23.0) msgpack (~> 1.2) brakeman (8.0.4) racc @@ -938,7 +938,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - bootsnap (~> 1.4) + bootsnap (~> 1.23) brakeman (~> 8.0) byebug (~> 13.0) decidim! diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index bbe6cd8d48186..897a8059d86b3 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -300,7 +300,7 @@ GEM smart_properties bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.18.6) + bootsnap (1.23.0) msgpack (~> 1.2) brakeman (8.0.4) racc From 761e7a421dcaec21e145df2690393b99a2479628 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:32:23 +0200 Subject: [PATCH 042/135] Bump to dependencies: Bump omniauth-google-oauth2 from 1.2.1 to 1.2.2 (#16270) * Bump to dependencies: Bump omniauth-google-oauth2 from 1.2.1 to 1.2.2 Bumps [omniauth-google-oauth2](https://github.com/zquestz/omniauth-google-oauth2) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/zquestz/omniauth-google-oauth2/releases) - [Changelog](https://github.com/zquestz/omniauth-google-oauth2/blob/master/CHANGELOG.md) - [Commits](https://github.com/zquestz/omniauth-google-oauth2/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: omniauth-google-oauth2 dependency-version: 1.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 12 ++++++------ decidim-generators/Gemfile.lock | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8e0c88530a74d..d7060e6d30659 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -588,14 +588,14 @@ GEM version_gem (~> 1.1) oauth-tty (1.0.5) version_gem (~> 1.1, >= 1.1.1) - oauth2 (2.0.12) + oauth2 (2.0.18) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) snaky_hash (~> 2.0, >= 2.0.3) - version_gem (>= 1.1.8, < 3) + version_gem (~> 1.1, >= 1.1.9) omniauth (2.1.4) hashie (>= 3.4.6) logger @@ -603,7 +603,7 @@ GEM rack-protection omniauth-facebook (5.0.0) omniauth-oauth2 (~> 1.2) - omniauth-google-oauth2 (1.2.1) + omniauth-google-oauth2 (1.2.2) jwt (>= 2.9.2) oauth2 (~> 2.0) omniauth (~> 2.0) @@ -612,8 +612,8 @@ GEM oauth omniauth (>= 1.0, < 3) rack (>= 1.6.2, < 4) - omniauth-oauth2 (1.8.0) - oauth2 (>= 1.4, < 3) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) omniauth (~> 2.0) omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) @@ -892,7 +892,7 @@ GEM valid_email2 (7.0.15) activemodel (>= 6.0) mail (~> 2.5) - version_gem (1.1.8) + version_gem (1.1.9) w3c_rspec_validators (0.3.0) rails rspec diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 897a8059d86b3..131129b050fac 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -580,14 +580,14 @@ GEM version_gem (~> 1.1) oauth-tty (1.0.5) version_gem (~> 1.1, >= 1.1.1) - oauth2 (2.0.12) + oauth2 (2.0.18) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) snaky_hash (~> 2.0, >= 2.0.3) - version_gem (>= 1.1.8, < 3) + version_gem (~> 1.1, >= 1.1.9) omniauth (2.1.4) hashie (>= 3.4.6) logger @@ -595,7 +595,7 @@ GEM rack-protection omniauth-facebook (5.0.0) omniauth-oauth2 (~> 1.2) - omniauth-google-oauth2 (1.2.1) + omniauth-google-oauth2 (1.2.2) jwt (>= 2.9.2) oauth2 (~> 2.0) omniauth (~> 2.0) @@ -604,8 +604,8 @@ GEM oauth omniauth (>= 1.0, < 3) rack (>= 1.6.2, < 4) - omniauth-oauth2 (1.8.0) - oauth2 (>= 1.4, < 3) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) omniauth (~> 2.0) omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) @@ -881,7 +881,7 @@ GEM valid_email2 (7.0.15) activemodel (>= 6.0) mail (~> 2.5) - version_gem (1.1.8) + version_gem (1.1.9) w3c_rspec_validators (0.3.0) rails rspec From 71f593670ae35d8b911758d07eaaabf990430c4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:52 +0200 Subject: [PATCH 043/135] Bump to dependencies: Bump devise-jwt from 0.12.1 to 0.13.0 (#16271) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 8 ++++---- decidim-api/decidim-api.gemspec | 2 +- decidim-generators/Gemfile.lock | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d7060e6d30659..6bd0e6ced952c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -34,7 +34,7 @@ PATH decidim-core (= 0.32.0.dev) decidim-api (0.32.0.dev) decidim-core (= 0.32.0.dev) - devise-jwt (~> 0.12.1) + devise-jwt (>= 0.12.1, < 0.14.0) graphql (>= 2.4.17, < 2.6) graphql-docs (>= 5, < 7) rack-cors (~> 1.0) @@ -371,8 +371,8 @@ GEM devise-i18n (1.15.0) devise (>= 4.9.0) rails-i18n - devise-jwt (0.12.1) - devise (~> 4.0) + devise-jwt (0.13.0) + devise (>= 4.0.0, < 6.0.0) warden-jwt_auth (~> 0.10) devise_invitable (2.0.11) actionmailer (>= 5.0) @@ -394,7 +394,7 @@ GEM dry-configurable (1.3.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-core (1.1.0) + dry-core (1.2.0) concurrent-ruby (~> 1.0) logger zeitwerk (~> 2.6) diff --git a/decidim-api/decidim-api.gemspec b/decidim-api/decidim-api.gemspec index d58fd20a6ca97..79a644a4bb458 100644 --- a/decidim-api/decidim-api.gemspec +++ b/decidim-api/decidim-api.gemspec @@ -30,7 +30,7 @@ Gem::Specification.new do |s| end s.add_dependency "decidim-core", version - s.add_dependency "devise-jwt", "~> 0.12.1" + s.add_dependency "devise-jwt", ">= 0.12.1", "< 0.14.0" s.add_dependency "graphql", ">= 2.4.17", "< 2.6" s.add_dependency "graphql-docs", ">= 5", "< 7" s.add_dependency "rack-cors", "~> 1.0" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 131129b050fac..f55f6f35aa66f 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -34,7 +34,7 @@ PATH decidim-core (= 0.32.0.dev) decidim-api (0.32.0.dev) decidim-core (= 0.32.0.dev) - devise-jwt (~> 0.12.1) + devise-jwt (>= 0.12.1, < 0.14.0) graphql (>= 2.4.17, < 2.6) graphql-docs (>= 5, < 7) rack-cors (~> 1.0) @@ -369,8 +369,8 @@ GEM devise-i18n (1.15.0) devise (>= 4.9.0) rails-i18n - devise-jwt (0.12.1) - devise (~> 4.0) + devise-jwt (0.13.0) + devise (>= 4.0.0, < 6.0.0) warden-jwt_auth (~> 0.10) devise_invitable (2.0.11) actionmailer (>= 5.0) From 2af91496579173d06d94d123cea4b727da603b96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:49:40 +0200 Subject: [PATCH 044/135] Bump to dependencies: Bump rspec from 3.13.1 to 3.13.2 (#16272) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-generators/Gemfile.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6bd0e6ced952c..eedf58297f78c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -743,7 +743,7 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) @@ -758,7 +758,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (8.0.3) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index f55f6f35aa66f..1bc2da9489977 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -735,7 +735,7 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) @@ -750,7 +750,7 @@ GEM rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (8.0.3) From 780b83a3b5451888868ae33355389a96bab9e5d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:51:03 +0200 Subject: [PATCH 045/135] Bump to dependencies: Bump rubocop-rspec_rails from 2.31.0 to 2.32.0 (#16273) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 4 ++-- .../decidim/budgets/admin/projects_controller_spec.rb | 2 +- decidim-budgets/spec/requests/line_items_spec.rb | 2 +- .../collaborative_texts/suggestions_controller_spec.rb | 4 ++-- .../controllers/registration_types_controller_spec.rb | 4 ++-- .../active_storage/direct_uploads_controller_spec.rb | 8 ++++---- .../spec/controllers/editor_images_controller_spec.rb | 2 +- .../spec/controllers/geolocation_controller_spec.rb | 4 ++-- decidim-dev/decidim-dev.gemspec | 2 +- .../decidim/elections/admin/elections_controller_spec.rb | 2 +- decidim-generators/Gemfile.lock | 4 ++-- .../initiatives/initiative_signatures_controller_spec.rb | 2 +- .../decidim/initiatives/initiatives_controller_spec.rb | 4 ++-- .../decidim/proposals/admin/proposals_controller_spec.rb | 2 +- .../decidim/proposals/proposals_controller_spec.rb | 2 +- 15 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eedf58297f78c..046780fff8077 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ PATH rubocop-performance (~> 1.25, >= 1.25.0) rubocop-rails (~> 2.32.0, >= 2.32.0) rubocop-rspec (~> 3.0, >= 3.6.0) - rubocop-rspec_rails (~> 2.31.0) + rubocop-rspec_rails (>= 2.31, < 2.33) rubocop-rubycw (~> 0.2.0) rubocop-yard (~> 1.0.0) selenium-webdriver (~> 4.9) @@ -814,7 +814,7 @@ GEM rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec_rails (2.31.0) + rubocop-rspec_rails (2.32.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) diff --git a/decidim-budgets/spec/controllers/decidim/budgets/admin/projects_controller_spec.rb b/decidim-budgets/spec/controllers/decidim/budgets/admin/projects_controller_spec.rb index 048530a998e93..08ba2ac357b2a 100644 --- a/decidim-budgets/spec/controllers/decidim/budgets/admin/projects_controller_spec.rb +++ b/decidim-budgets/spec/controllers/decidim/budgets/admin/projects_controller_spec.rb @@ -64,7 +64,7 @@ def proposals_picker_projects_path patch(:update, params:) expect(flash[:alert]).not_to be_empty - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(subject).to render_template(:edit) expect(response.body).to include("There was a problem updating this project") end diff --git a/decidim-budgets/spec/requests/line_items_spec.rb b/decidim-budgets/spec/requests/line_items_spec.rb index 4f169a883bba8..150994820e5fb 100644 --- a/decidim-budgets/spec/requests/line_items_spec.rb +++ b/decidim-budgets/spec/requests/line_items_spec.rb @@ -35,7 +35,7 @@ expect(response).to have_http_status(:ok) post(request_path, xhr: true, params: { project_id: project.id }, headers:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(Decidim::Budgets::Order.count).to eq(1) expect(Decidim::Budgets::LineItem.count).to eq(1) diff --git a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb index bf1a9103e38e5..24015ea52e705 100644 --- a/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb +++ b/decidim-collaborative_texts/spec/controllers/decidim/collaborative_texts/suggestions_controller_spec.rb @@ -61,7 +61,7 @@ module CollaborativeTexts it "returns an error when user is not signed in" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) body = JSON.parse(response.body) expect(body["message"]).to eq("You are not authorized to perform this action.") end @@ -85,7 +85,7 @@ module CollaborativeTexts it "returns an error" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) body = JSON.parse(response.body) expect(body["message"]).to eq("There was a problem creating the suggestion. Invalid selected nodes.") end diff --git a/decidim-conferences/spec/controllers/registration_types_controller_spec.rb b/decidim-conferences/spec/controllers/registration_types_controller_spec.rb index d406c5c84acf4..a03180a324883 100644 --- a/decidim-conferences/spec/controllers/registration_types_controller_spec.rb +++ b/decidim-conferences/spec/controllers/registration_types_controller_spec.rb @@ -30,7 +30,7 @@ module Conferences context "when registration_types is present" do it "does not raise an error" do get :index, params: { conference_slug: conference.slug, locale: I18n.locale } - assert_response :success + expect(response).to have_http_status(:success) end end @@ -49,7 +49,7 @@ module Conferences it "does not raise an error" do get :index, params: { conference_slug: conference.slug, locale: I18n.locale } - assert_response :success + expect(response).to have_http_status(:success) end end end diff --git a/decidim-core/spec/controllers/active_storage/direct_uploads_controller_spec.rb b/decidim-core/spec/controllers/active_storage/direct_uploads_controller_spec.rb index be5afe88ba9a1..66c71ee600a63 100644 --- a/decidim-core/spec/controllers/active_storage/direct_uploads_controller_spec.rb +++ b/decidim-core/spec/controllers/active_storage/direct_uploads_controller_spec.rb @@ -87,7 +87,7 @@ module ActiveStorage it "returns renders unprocessable entity" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -97,7 +97,7 @@ module ActiveStorage it "returns renders unprocessable entity" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -107,7 +107,7 @@ module ActiveStorage it "returns renders unprocessable entity" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end @@ -124,7 +124,7 @@ module ActiveStorage it "returns renders unprocessable entity" do post(:create, params:) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end end end diff --git a/decidim-core/spec/controllers/editor_images_controller_spec.rb b/decidim-core/spec/controllers/editor_images_controller_spec.rb index e7dae8e8131f1..f5d6978025381 100644 --- a/decidim-core/spec/controllers/editor_images_controller_spec.rb +++ b/decidim-core/spec/controllers/editor_images_controller_spec.rb @@ -59,7 +59,7 @@ module Decidim post :create, params: invalid_params end.not_to(change(Decidim::EditorImage, :count)) - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.body).to include("Error uploading image") end end diff --git a/decidim-core/spec/controllers/geolocation_controller_spec.rb b/decidim-core/spec/controllers/geolocation_controller_spec.rb index 5dd84529819f9..e2bbcfb2e5fcb 100644 --- a/decidim-core/spec/controllers/geolocation_controller_spec.rb +++ b/decidim-core/spec/controllers/geolocation_controller_spec.rb @@ -34,7 +34,7 @@ module Decidim it "fails" do post :locate, params:, xhr: true - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(json["message"]).to have_content("not configured") expect(json["found"]).to be_blank end @@ -43,7 +43,7 @@ module Decidim shared_examples "not found" do it "fails" do post :locate, params:, xhr: true - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(json["address"]).not_to eq(address) expect(json["message"]).to have_content("not authorized") expect(json["found"]).to be_blank diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index 408ec012f3361..f4d0dc1a669b0 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -61,7 +61,7 @@ Gem::Specification.new do |s| s.add_dependency "rubocop-performance", "~> 1.25", ">= 1.25.0" s.add_dependency "rubocop-rails", "~> 2.32.0", ">= 2.32.0" s.add_dependency "rubocop-rspec", "~> 3.0", ">= 3.6.0" - s.add_dependency "rubocop-rspec_rails", "~> 2.31.0" + s.add_dependency "rubocop-rspec_rails", ">= 2.31", "< 2.33" s.add_dependency "rubocop-rubycw", "~> 0.2.0" s.add_dependency "rubocop-yard", "~> 1.0.0" s.add_dependency "selenium-webdriver", "~> 4.9" diff --git a/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb b/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb index eadcfcb5697c1..5860c5d21c6d9 100644 --- a/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb +++ b/decidim-elections/spec/controllers/decidim/elections/admin/elections_controller_spec.rb @@ -196,7 +196,7 @@ def dashboard_path(election) patch :toggle_census_check, params: { id: election.id, allow_census_check_before_start: true }, format: :json - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(JSON.parse(response.body)).to include( "success" => false, "error" => I18n.t("elections.toggle_census_check.error", scope: "decidim.elections.admin") diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 1bc2da9489977..c29612bfe803d 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -150,7 +150,7 @@ PATH rubocop-performance (~> 1.25, >= 1.25.0) rubocop-rails (~> 2.32.0, >= 2.32.0) rubocop-rspec (~> 3.0, >= 3.6.0) - rubocop-rspec_rails (~> 2.31.0) + rubocop-rspec_rails (>= 2.31, < 2.33) rubocop-rubycw (~> 0.2.0) rubocop-yard (~> 1.0.0) selenium-webdriver (~> 4.9) @@ -806,7 +806,7 @@ GEM rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec_rails (2.31.0) + rubocop-rspec_rails (2.32.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec (~> 3.5) diff --git a/decidim-initiatives/spec/controllers/decidim/initiatives/initiative_signatures_controller_spec.rb b/decidim-initiatives/spec/controllers/decidim/initiatives/initiative_signatures_controller_spec.rb index 8e9a3914e475c..60972de2a775a 100644 --- a/decidim-initiatives/spec/controllers/decidim/initiatives/initiative_signatures_controller_spec.rb +++ b/decidim-initiatives/spec/controllers/decidim/initiatives/initiative_signatures_controller_spec.rb @@ -22,7 +22,7 @@ module Initiatives it "cannot vote" do sign_in initiative_with_user_extra_fields.author, scope: :user post :create, params: { initiative_slug: initiative_with_user_extra_fields.slug, locale: I18n.locale, format: :js } - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(response.content_type).to eq("text/javascript; charset=utf-8") end end diff --git a/decidim-initiatives/spec/controllers/decidim/initiatives/initiatives_controller_spec.rb b/decidim-initiatives/spec/controllers/decidim/initiatives/initiatives_controller_spec.rb index c068bdbdf4b42..6b2ab2a1ad83c 100644 --- a/decidim-initiatives/spec/controllers/decidim/initiatives/initiatives_controller_spec.rb +++ b/decidim-initiatives/spec/controllers/decidim/initiatives/initiatives_controller_spec.rb @@ -151,7 +151,7 @@ } expect(flash[:alert]).not_to be_empty - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) end context "when the existing initiative has attachments and there are other errors on the form" do @@ -182,7 +182,7 @@ } expect(flash[:alert]).not_to be_empty - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(subject).to render_template(:edit) expect(response.body).to include("There was a problem updating the initiative.") end diff --git a/decidim-proposals/spec/controllers/decidim/proposals/admin/proposals_controller_spec.rb b/decidim-proposals/spec/controllers/decidim/proposals/admin/proposals_controller_spec.rb index 3f73c8072cab1..225c6b21108d9 100644 --- a/decidim-proposals/spec/controllers/decidim/proposals/admin/proposals_controller_spec.rb +++ b/decidim-proposals/spec/controllers/decidim/proposals/admin/proposals_controller_spec.rb @@ -61,7 +61,7 @@ patch(:update, params:) expect(flash[:alert]).not_to be_empty - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(subject).to render_template(:edit) expect(response.body).to include("There was a problem saving") end diff --git a/decidim-proposals/spec/controllers/decidim/proposals/proposals_controller_spec.rb b/decidim-proposals/spec/controllers/decidim/proposals/proposals_controller_spec.rb index 1a5b7819cc181..e742904fb1afe 100644 --- a/decidim-proposals/spec/controllers/decidim/proposals/proposals_controller_spec.rb +++ b/decidim-proposals/spec/controllers/decidim/proposals/proposals_controller_spec.rb @@ -187,7 +187,7 @@ module Proposals patch(:update, params:) expect(flash[:alert]).not_to be_empty - expect(response).to have_http_status(:unprocessable_entity) + expect(response).to have_http_status(:unprocessable_content) expect(subject).to render_template(:edit) expect(response.body).to include("There was a problem saving") end From 24423c52b5785ba1c0491cf1c553185fefa202c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:49:39 +0200 Subject: [PATCH 046/135] Bump to dependencies: Bump batch-loader from 2.0.5 to 2.0.6 (#16274) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 046780fff8077..bcca1693e2b27 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -288,7 +288,7 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) - batch-loader (2.0.5) + batch-loader (2.0.6) bcrypt (3.1.21) benchmark (0.5.0) better_html (2.2.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index c29612bfe803d..197a336f0213c 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -288,7 +288,7 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) - batch-loader (2.0.5) + batch-loader (2.0.6) bcrypt (3.1.21) benchmark (0.5.0) better_html (2.2.0) From 85eae5484c7acfc5b9f9db8a5b3cd81161a50d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 2 Mar 2026 11:17:43 +0100 Subject: [PATCH 047/135] Fix importing attachments within processes/assemblies (#16202) --- .../decidim/assemblies/assembly_importer.rb | 27 ++--- .../assemblies/assembly_importer_spec.rb | 108 ++++++++++++++++++ .../participatory_process_importer.rb | 27 ++--- .../participatory_process_importer_spec.rb | 108 ++++++++++++++++++ 4 files changed, 240 insertions(+), 30 deletions(-) diff --git a/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb b/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb index b0b825f029a1f..8590c8b67b452 100644 --- a/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb +++ b/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb @@ -84,27 +84,24 @@ def import_folders_and_attachments(attachments) next end - begin - file_tmp = URI.parse(url).open - rescue OpenURI::HTTPError, Errno::ENOENT, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e - @warnings << I18n.t( - "decidim.assemblies.admin.imports.attachment_error", - title: attachment_title(file), - error: format_error(e) - ) - next - end - Decidim.traceability.perform_action!("create", Attachment, @user) do attachment = Attachment.new( title: file["title"], description: file["description"], - content_type: file_tmp.content_type, attached_to: @imported_assembly, - weight: file["weight"], - file: file_tmp, # Define attached_to before this - file_size: file_tmp.size + weight: file["weight"] ) + begin + attachment.attached_uploader(:file).remote_url = url + attachment.set_content_type_and_size + rescue OpenURI::HTTPError, Errno::ENOENT, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e + @warnings << I18n.t( + "decidim.assemblies.admin.imports.attachment_error", + title: attachment_title(file), + error: format_error(e) + ) + next + end attachment.create_attachment_collection(file["attachment_collection"]) attachment.save! attachment diff --git a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb index f7c15cbab8bc3..dcc537f968bbe 100644 --- a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb +++ b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb @@ -311,6 +311,114 @@ module Decidim::Assemblies expect(importer.warnings).to include(a_string_matching(/The attachment "Test File" could not be imported \(500 Internal Server Error\)\./i)) end end + + context "when remote file is accessible and downloadable (PDF)" do + let(:remote_file_url) { "http://example.com/document.pdf" } + + before do + stub_request(:head, remote_file_url) + .to_return(status: 200, headers: { "Content-Type" => "application/pdf" }) + stub_request(:get, remote_file_url) + .to_return(status: 200, body: File.read(Decidim::Dev.asset("Exampledocument.pdf"))) + end + + it "successfully imports the attachment" do + expect { importer.import_folders_and_attachments(attachments_data) } + .to change(Decidim::Attachment, :count).by(1) + end + + it "attaches the file to the assembly" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file).to be_attached + expect(attachment.file.filename.to_s).to eq("document.pdf") + end + + it "sets the content type automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.content_type).to eq("application/pdf") + end + + it "sets the file size automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file_size).to be_present + end + + it "has no warnings" do + importer.import_folders_and_attachments(attachments_data) + expect(importer.warnings).to be_empty + end + end + + context "when remote file is accessible and downloadable (image)" do + let(:remote_file_url) { "http://example.com/image.jpg" } + + before do + stub_request(:head, remote_file_url) + .to_return(status: 200, headers: { "Content-Type" => "image/jpeg" }) + stub_request(:get, remote_file_url) + .to_return(status: 200, body: File.read(Decidim::Dev.asset("city.jpeg"))) + end + + it "successfully imports the attachment" do + expect { importer.import_folders_and_attachments(attachments_data) } + .to change(Decidim::Attachment, :count).by(1) + end + + it "attaches the file to the assembly" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file).to be_attached + end + + it "sets the content type automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.content_type).to eq("image/jpeg") + end + + it "has no warnings" do + importer.import_folders_and_attachments(attachments_data) + expect(importer.warnings).to be_empty + end + end + + context "when remote file URL is blank" do + let(:attachments_data) do + { + "files" => [ + { + "title" => { "en" => "Test File" }, + "description" => { "en" => "Test Description" }, + "weight" => 1, + "remote_file_url" => "" + } + ], + "attachment_collections" => [] + } + end + + it "does not create any attachments" do + expect { importer.import_folders_and_attachments(attachments_data) } + .not_to change(Decidim::Attachment, :count) + end + end + + context "when files array is nil" do + let(:attachments_data) do + { + "files" => nil, + "attachment_collections" => [] + } + end + + it "does not create any attachments" do + expect { importer.import_folders_and_attachments(attachments_data) } + .not_to change(Decidim::Attachment, :count) + end + end end end end diff --git a/decidim-participatory_processes/app/serializers/decidim/participatory_processes/participatory_process_importer.rb b/decidim-participatory_processes/app/serializers/decidim/participatory_processes/participatory_process_importer.rb index 126133e444e13..1af3fc62f7938 100644 --- a/decidim-participatory_processes/app/serializers/decidim/participatory_processes/participatory_process_importer.rb +++ b/decidim-participatory_processes/app/serializers/decidim/participatory_processes/participatory_process_importer.rb @@ -106,27 +106,24 @@ def import_folders_and_attachments(attachments) next end - begin - file_tmp = URI.parse(url).open - rescue OpenURI::HTTPError, Errno::ENOENT, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e - @warnings << I18n.t( - "decidim.participatory_processes.admin.imports.attachment_error", - title: attachment_title(file), - error: format_error(e) - ) - next - end - Decidim.traceability.perform_action!("create", Attachment, @user) do attachment = Attachment.new( title: file["title"], description: file["description"], - content_type: file_tmp.content_type, attached_to: @imported_process, - weight: file["weight"], - file: file_tmp, # Define attached_to before this - file_size: file_tmp.size + weight: file["weight"] ) + begin + attachment.attached_uploader(:file).remote_url = url + attachment.set_content_type_and_size + rescue OpenURI::HTTPError, Errno::ENOENT, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e + @warnings << I18n.t( + "decidim.participatory_processes.admin.imports.attachment_error", + title: attachment_title(file), + error: format_error(e) + ) + next + end attachment.create_attachment_collection(file["attachment_collection"]) attachment.save! attachment diff --git a/decidim-participatory_processes/spec/serializers/decidim/participatory_processes/participatory_process_importer_spec.rb b/decidim-participatory_processes/spec/serializers/decidim/participatory_processes/participatory_process_importer_spec.rb index 4ab4d7159ba7b..6408dd7ddc46b 100644 --- a/decidim-participatory_processes/spec/serializers/decidim/participatory_processes/participatory_process_importer_spec.rb +++ b/decidim-participatory_processes/spec/serializers/decidim/participatory_processes/participatory_process_importer_spec.rb @@ -387,6 +387,114 @@ module Decidim::ParticipatoryProcesses expect(importer.warnings).to include(a_string_matching(/The attachment "Test File" could not be imported \(500 Internal Server Error\)\./i)) end end + + context "when remote file is accessible and downloadable (PDF)" do + let(:remote_file_url) { "http://example.com/document.pdf" } + + before do + stub_request(:head, remote_file_url) + .to_return(status: 200, headers: { "Content-Type" => "application/pdf" }) + stub_request(:get, remote_file_url) + .to_return(status: 200, body: File.read(Decidim::Dev.asset("Exampledocument.pdf"))) + end + + it "successfully imports the attachment" do + expect { importer.import_folders_and_attachments(attachments_data) } + .to change(Decidim::Attachment, :count).by(1) + end + + it "attaches the file to the process" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file).to be_attached + expect(attachment.file.filename.to_s).to eq("document.pdf") + end + + it "sets the content type automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.content_type).to eq("application/pdf") + end + + it "sets the file size automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file_size).to be_present + end + + it "has no warnings" do + importer.import_folders_and_attachments(attachments_data) + expect(importer.warnings).to be_empty + end + end + + context "when remote file is accessible and downloadable (image)" do + let(:remote_file_url) { "http://example.com/image.jpg" } + + before do + stub_request(:head, remote_file_url) + .to_return(status: 200, headers: { "Content-Type" => "image/jpeg" }) + stub_request(:get, remote_file_url) + .to_return(status: 200, body: File.read(Decidim::Dev.asset("city.jpeg"))) + end + + it "successfully imports the attachment" do + expect { importer.import_folders_and_attachments(attachments_data) } + .to change(Decidim::Attachment, :count).by(1) + end + + it "attaches the file to the process" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.file).to be_attached + end + + it "sets the content type automatically" do + importer.import_folders_and_attachments(attachments_data) + attachment = Decidim::Attachment.last + expect(attachment.content_type).to eq("image/jpeg") + end + + it "has no warnings" do + importer.import_folders_and_attachments(attachments_data) + expect(importer.warnings).to be_empty + end + end + + context "when remote file URL is blank" do + let(:attachments_data) do + { + "files" => [ + { + "title" => { "en" => "Test File" }, + "description" => { "en" => "Test Description" }, + "weight" => 1, + "remote_file_url" => "" + } + ], + "attachment_collections" => [] + } + end + + it "does not create any attachments" do + expect { importer.import_folders_and_attachments(attachments_data) } + .not_to change(Decidim::Attachment, :count) + end + end + + context "when files array is nil" do + let(:attachments_data) do + { + "files" => nil, + "attachment_collections" => [] + } + end + + it "does not create any attachments" do + expect { importer.import_folders_and_attachments(attachments_data) } + .not_to change(Decidim::Attachment, :count) + end + end end end end From f0810cbbbe7c15c2409f1fb71d2ab235a93bde06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 2 Mar 2026 16:26:57 +0100 Subject: [PATCH 048/135] Avoid removing questions with "enter" key in surveys' admin (#16256) --- decidim-core/app/packs/src/decidim/confirm.js | 9 +- .../app/packs/src/decidim/confirm.test.js | 225 ++++++++++++++++++ .../admin/questions/_question.html.erb | 2 +- .../admin/questionnaires/_question.html.erb | 2 +- .../admin/questionnaires/_separator.html.erb | 2 +- .../_title_and_description.html.erb | 2 +- .../meetings/admin/poll/_question.html.erb | 2 +- 7 files changed, 238 insertions(+), 6 deletions(-) create mode 100644 decidim-core/app/packs/src/decidim/confirm.test.js diff --git a/decidim-core/app/packs/src/decidim/confirm.js b/decidim-core/app/packs/src/decidim/confirm.js index 4e18545b64905..e833fa606b169 100644 --- a/decidim-core/app/packs/src/decidim/confirm.js +++ b/decidim-core/app/packs/src/decidim/confirm.js @@ -169,7 +169,9 @@ export const initializeConfirm = () => { return handleDocumentEvent(ev, [ Rails.linkClickSelector, Rails.buttonClickSelector, - Rails.formInputClickSelector + Rails.formInputClickSelector, + 'button[data-confirm][type="button"]', + "form button[data-confirm]" ]); }); document.addEventListener("change", (ev) => { @@ -187,6 +189,11 @@ export const initializeConfirm = () => { $(Rails.formInputClickSelector).on("click.confirm", (ev) => { handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector)); }); + + // Handle button[type="button"] with data-confirm inside forms + $('button[data-confirm][type="button"]').on("click.confirm", (ev) => { + handleConfirm(ev, ev.currentTarget); + }); }); }; diff --git a/decidim-core/app/packs/src/decidim/confirm.test.js b/decidim-core/app/packs/src/decidim/confirm.test.js new file mode 100644 index 0000000000000..b71c6c83adfa1 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/confirm.test.js @@ -0,0 +1,225 @@ +/* global jest */ + +jest.mock("src/decidim/refactor/moved/icon", () => () => ""); + +describe("Confirm dialog for button[type='button']", () => { + let mockRails = null; + let mockDecidim = null; + + beforeEach(() => { + jest.clearAllMocks(); + document.body.innerHTML = ` + + `; + + mockRails = { + linkClickSelector: "a[data-confirm]", + buttonClickSelector: "button[data-confirm]:not([form])", + formInputClickSelector: 'form button[type="submit"], form button:not([type])', + inputChangeSelector: "input[data-confirm], select[data-confirm]", + formSubmitSelector: "form[data-confirm]", + stopEverything: jest.fn(), + fire: jest.fn((el, event) => { + const evt = new CustomEvent(event); + el.dispatchEvent(evt); + return true; + }), + matches: function(element, selector) { + if (element instanceof Element) { + return element.matches(selector); + } + return false; + } + }; + + mockDecidim = { + currentDialogs: { + "confirm-modal": { + open: jest.fn(), + close: jest.fn() + } + } + }; + + window.Rails = mockRails; + window.Decidim = mockDecidim; + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + describe("selector matching for button[type='button'] with data-confirm", () => { + it("matches button[data-confirm][type='button'] selector", () => { + const button = document.createElement("button"); + button.type = "button"; + button.setAttribute("data-confirm", "Are you sure?"); + + expect(button.matches('button[data-confirm][type="button"]')).toBe(true); + }); + + it("matches form button[data-confirm] selector", () => { + const form = document.createElement("form"); + const button = document.createElement("button"); + button.setAttribute("data-confirm", "Are you sure?"); + form.appendChild(button); + + expect(button.matches("form button[data-confirm]")).toBe(true); + }); + + it("does not match regular button without data-confirm", () => { + const button = document.createElement("button"); + button.type = "button"; + + expect(button.matches('button[data-confirm][type="button"]')).toBe(false); + }); + + it("does not match button[type='submit'] with the type='button' selector", () => { + const button = document.createElement("button"); + button.type = "submit"; + button.setAttribute("data-confirm", "Are you sure?"); + + expect(button.matches('button[data-confirm][type="button"]')).toBe(false); + }); + + it("matches button[type='button'] inside form", () => { + document.body.innerHTML = ` +
+ +
+ `; + + const button = document.querySelector('button[type="button"]'); + expect(button.matches('button[data-confirm][type="button"]')).toBe(true); + expect(button.matches("form button[data-confirm]")).toBe(true); + }); + + it("does not match button[type='button'] without data-confirm inside form", () => { + document.body.innerHTML = ` +
+ +
+ `; + + const button = document.querySelector('button[type="button"]'); + expect(button.matches('button[data-confirm][type="button"]')).toBe(false); + expect(button.matches("form button[data-confirm]")).toBe(false); + }); + }); + + describe("initializeConfirm - selectors registration", () => { + it("adds click event listener with proper selectors including button[type='button'] support", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + + initializeConfirm(); + + expect(addEventListenerSpy).toHaveBeenCalledWith("click", expect.any(Function)); + }); + + it("adds change event listener for input change selector", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + + initializeConfirm(); + + const changeHandlerCalls = addEventListenerSpy.mock.calls.filter( + (call) => call[0] === "change" + ); + expect(changeHandlerCalls.length).toBeGreaterThan(0); + }); + + it("adds submit event listener for form submit selector", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + + initializeConfirm(); + + const submitHandlerCalls = addEventListenerSpy.mock.calls.filter( + (call) => call[0] === "submit" + ); + expect(submitHandlerCalls.length).toBeGreaterThan(0); + }); + + it("adds turbo:load event listener for Foundation Abide compatibility", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + const addEventListenerSpy = jest.spyOn(document, "addEventListener"); + + initializeConfirm(); + + const turboLoadCalls = addEventListenerSpy.mock.calls.filter( + (call) => call[0] === "turbo:load" + ); + expect(turboLoadCalls.length).toBeGreaterThan(0); + }); + }); + + describe("handleDocumentEvent with button[type='button'] support", () => { + it("handles click on button[type='button'] with data-confirm and form attribute", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + document.body.innerHTML = ` +
+ +
+ `; + + const button = document.querySelector('button[type="button"]'); + const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open"); + + initializeConfirm(); + + button.click(); + + expect(openSpy).toHaveBeenCalled(); + }); + + it("handles click on button[type='button'] with data-confirm outside form", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + document.body.innerHTML = ` + + `; + + const button = document.querySelector('button[type="button"]'); + const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open"); + + initializeConfirm(); + + button.click(); + + expect(openSpy).toHaveBeenCalled(); + }); + + it("does not trigger confirm for button without data-confirm attribute", async () => { + const { initializeConfirm } = await import("src/decidim/confirm.js"); + + document.body.innerHTML = ` +
+ +
+ `; + + const button = document.querySelector('button[type="button"]'); + const openSpy = jest.spyOn(mockDecidim.currentDialogs["confirm-modal"], "open"); + + initializeConfirm(); + + button.click(); + + expect(openSpy).not.toHaveBeenCalled(); + }); + }); +}); + +/* dummy end */ diff --git a/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb b/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb index f952b6a0a2df2..c3e1c339d8996 100644 --- a/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb +++ b/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb @@ -25,7 +25,7 @@ <% if editable %> - diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb index 9a0f5896a216a..448407fae80dd 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb @@ -23,7 +23,7 @@ <% if editable %> - diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb index 5618fff56f7fc..3434d2004ea1f 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb @@ -12,7 +12,7 @@
<% if editable %> - diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb index 7f2f26edba042..6f5c0418be9fc 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb @@ -23,7 +23,7 @@ <% if editable %> - diff --git a/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb index a613741df50b5..95d61868e8317 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb @@ -25,7 +25,7 @@ <% if editable %> - <% end %> From 4be7960f5a639ec31011e60caf1bfbfa02724b38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:49:19 +0200 Subject: [PATCH 049/135] Bump to dependencies: Bump rubocop from 1.78.0 to 1.85.0 (#16276) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 12 ++++++--- .../accountability/result_show_cell.rb | 1 + .../accountability/admin/filterable.rb | 2 +- .../accountability/results_controller.rb | 1 + .../accountability/import_projects_mailer.rb | 1 + .../accountability/import_proposals_mailer.rb | 1 + .../admin/content_blocks/landing_page.rb | 1 + .../landing_page_content_blocks.rb | 1 + .../concerns/decidim/admin/filterable.rb | 6 ++--- .../participatory_space_admin_context.rb | 1 + .../decidim/admin/areas_controller.rb | 1 + .../admin/components/base_controller.rb | 1 + .../decidim/admin/components_controller.rb | 1 + .../decidim/admin/imports_controller.rb | 1 + .../decidim/admin/newsletters_controller.rb | 1 + .../concerns/has_members.rb | 1 + .../decidim/admin/scopes_controller.rb | 1 + .../decidim/admin/taxonomy_filter_form.rb | 1 + .../controllers/taxonomies_controller_spec.rb | 2 +- .../admin/concerns/assembly_admin.rb | 1 + .../decidim/assemblies/admin/admin_users.rb | 2 +- ...add_parent_child_relation_to_assemblies.rb | 8 +++--- .../decidim/budgets/admin/create_project.rb | 1 + .../decidim/budgets/admin/update_project.rb | 1 + .../budgets/admin/budgets_controller.rb | 1 + .../budgets/admin/projects_controller.rb | 1 + .../app/models/decidim/budgets/project.rb | 16 ++++++------ .../lib/decidim/budgets/project_serializer.rb | 4 +-- .../cells/decidim/comments/comments_cell.rb | 1 + .../lib/decidim/api/commentable_interface.rb | 1 + .../conferences/conference_speaker_cell.rb | 1 + .../admin/concerns/conference_admin.rb | 1 + .../conference_registration_invite_form.rb | 1 + .../decidim/conferences/admin/admin_users.rb | 2 +- .../spec/system/conference_program_spec.rb | 1 + .../concerns/decidim/direct_upload.rb | 1 + .../decidim/amendments_controller.rb | 1 + .../decidim/components/base_controller.rb | 1 + .../controllers/decidim/follows_controller.rb | 1 + .../controllers/decidim/likes_controller.rb | 1 + .../helpers/decidim/translations_helper.rb | 8 +++--- .../app/mailers/decidim/application_mailer.rb | 1 + .../mailers/decidim/decidim_devise_mailer.rb | 1 + .../app/models/decidim/content_block.rb | 4 +-- .../presenters/decidim/log/user_presenter.rb | 1 + ...0800_convert_private_exports_id_to_uuid.rb | 4 +-- decidim-core/lib/decidim/acts_as_tree.rb | 24 ++++++++--------- .../decidim/api/functions/category_list.rb | 1 + .../decidim/api/functions/component_list.rb | 1 + .../api/functions/component_list_base.rb | 1 + .../participatory_space_list_base.rb | 1 + .../interfaces/amendable_entity_interface.rb | 1 + .../api/interfaces/amendable_interface.rb | 1 + .../attachable_collection_interface.rb | 1 + .../api/interfaces/attachable_interface.rb | 1 + .../api/interfaces/author_interface.rb | 1 + .../api/interfaces/authorable_interface.rb | 1 + .../categories_container_interface.rb | 1 + .../api/interfaces/categorizable_interface.rb | 1 + .../api/interfaces/coauthorable_interface.rb | 1 + .../api/interfaces/component_interface.rb | 1 + .../api/interfaces/fingerprint_interface.rb | 1 + .../api/interfaces/followable_interface.rb | 1 + .../api/interfaces/likeable_interface.rb | 1 + .../api/interfaces/localizable_interface.rb | 1 + .../participatory_space_interface.rb | 1 + .../api/interfaces/referable_interface.rb | 1 + .../api/interfaces/scopable_interface.rb | 1 + .../api/interfaces/taxonomizable_interface.rb | 1 + .../api/interfaces/timestamps_interface.rb | 1 + .../api/interfaces/traceable_interface.rb | 1 + decidim-core/lib/decidim/command.rb | 1 + decidim-core/lib/decidim/core.rb | 1 + .../core/test/shared_examples/simple_event.rb | 1 + decidim-core/lib/decidim/exporters/csv.rb | 2 +- .../lib/decidim/filter_form_builder.rb | 2 +- decidim-core/lib/decidim/has_settings.rb | 4 +-- .../lib/decidim/legacy_form_builder.rb | 1 + decidim-core/lib/decidim/moderation_tools.rb | 2 +- .../participatory_space/has_members.rb | 4 +-- .../active_storage/disk_controller_spec.rb | 1 + .../spec/lib/attribute_object/model_spec.rb | 4 +-- .../decidim/notification_presenter_spec.rb | 1 + .../types/attachment_collection_type_spec.rb | 2 +- .../translatable_presence_validator_spec.rb | 1 + .../spec/validators/url_validator_spec.rb | 1 + .../cells/decidim/debates/debate_l_cell.rb | 1 + .../debates/admin/debates_controller.rb | 1 + .../admin/responses_controller.rb | 1 + .../demographics/application_controller.rb | 1 + .../decidim/dev/nested_dummy_resource.rb | 1 + decidim-dev/config/rubocop/ruby/disabled.yml | 9 +++++++ decidim-dev/decidim-dev.gemspec | 2 +- decidim-dev/lib/decidim/dev.rb | 1 + .../elections/admin/update_election.rb | 1 + .../app/models/decidim/elections/vote.rb | 1 + .../app/models/decidim/elections/voter.rb | 1 + .../forms/admin/concerns/has_questionnaire.rb | 1 + .../api/questionnaire_entity_interface.rb | 1 + ...oad_your_data_user_responses_serializer.rb | 1 + decidim-generators/Gemfile.lock | 12 ++++++--- .../generators/test/generator_examples.rb | 2 -- .../decidim/initiatives/update_initiative.rb | 1 + .../initiatives/admin/initiative_admin.rb | 1 + .../admin/initiatives_types_controller.rb | 1 + .../initiatives/application_controller.rb | 1 + .../initiatives/versions_controller.rb | 1 + .../initiatives/admin/initiative_form.rb | 4 +-- ...nable_pg_trgm_extension_for_initiatives.rb | 2 +- .../decidim/api/initiative_type_interface.rb | 1 + .../initiatives/application_form_pdf.rb | 1 + .../meetings/question_responses_cell.rb | 2 +- .../meetings/registration_serializer.rb | 1 + .../decidim/api/linked_resources_interface.rb | 1 + .../lib/decidim/api/services_interface.rb | 1 + .../concerns/participatory_process_admin.rb | 1 + .../admin/admin_users.rb | 2 +- .../admin/moderators.rb | 2 +- .../proposals/admin/merge_proposals.rb | 1 + .../admin/update_proposal_taxonomies.rb | 1 + .../proposals/admin/proposals_import_form.rb | 1 + .../proposals/admin/proposals_merge_form.rb | 1 + .../app/models/decidim/proposals/proposal.rb | 26 +++++++++---------- ..._enable_pg_trgm_extension_for_proposals.rb | 2 +- ...c_proposals_state_with_amendments_state.rb | 4 +-- ...123652_publish_existing_proposals_state.rb | 4 +-- .../proposals/admin/create_proposal_spec.rb | 2 +- .../proposals/admin/update_proposal_spec.rb | 2 +- .../decidim/proposals/update_proposal_spec.rb | 2 +- ...load_your_data_proposal_serializer_spec.rb | 12 ++++----- .../proposals/proposal_serializer_spec.rb | 12 ++++----- .../spec/system/proposals_spec.rb | 1 + .../surveys/survey_confirmation_mailer.rb | 1 + .../system/register_organization_form.rb | 1 + decidim-system/lib/decidim/system/menu.rb | 2 +- .../questionnaire_templates_controller.rb | 1 + .../decidim/verifications/renewable.rb | 1 + .../authorizations_controller.rb | 1 + .../csv_census/admin/census_data_form.rb | 1 + .../id_documents/admin/config_form.rb | 1 + .../lib/decidim/verifications.rb | 1 + 141 files changed, 221 insertions(+), 100 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bcca1693e2b27..c0cb60bbc86fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,7 +142,7 @@ PATH rspec-rails (>= 6, < 9) rspec-retry (~> 0.6.2) rspec_junit_formatter (~> 0.6.0) - rubocop (~> 1.78.0) + rubocop (>= 1.78, < 1.86) rubocop-capybara (~> 2.22.0, >= 2.22.1) rubocop-factory_bot (>= 2.27, < 2.29) rubocop-faker (~> 1.3, >= 1.3.0) @@ -510,6 +510,9 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.18.1) + json-schema (6.1.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) jwt (3.1.2) base64 kaminari (1.2.2) @@ -553,6 +556,8 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) + mcp (0.7.1) + json-schema (>= 4.1) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) @@ -774,15 +779,16 @@ GEM rspec-support (3.13.7) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.78.0) + rubocop (1.85.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) + mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.45.1, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) diff --git a/decidim-accountability/app/cells/decidim/accountability/result_show_cell.rb b/decidim-accountability/app/cells/decidim/accountability/result_show_cell.rb index 5199b13934590..d9eb0bc0b56d3 100644 --- a/decidim-accountability/app/cells/decidim/accountability/result_show_cell.rb +++ b/decidim-accountability/app/cells/decidim/accountability/result_show_cell.rb @@ -8,6 +8,7 @@ module Accountability class ResultShowCell < Decidim::ViewModel include Decidim::Accountability::ApplicationHelper include Cell::ViewModel::Partial + delegate :children, :milestones, to: :model alias result model diff --git a/decidim-accountability/app/controllers/concerns/decidim/accountability/admin/filterable.rb b/decidim-accountability/app/controllers/concerns/decidim/accountability/admin/filterable.rb index d81e1646cb74e..ac879484908ad 100644 --- a/decidim-accountability/app/controllers/concerns/decidim/accountability/admin/filterable.rb +++ b/decidim-accountability/app/controllers/concerns/decidim/accountability/admin/filterable.rb @@ -44,7 +44,7 @@ def dynamically_translated_filters end def status_ids_hash(statuses) - statuses.each_with_object({}) { |status, hash| hash[status.id] = status.id } + statuses.to_h { |status| [status.id, status.id] } end def translated_status_id_eq(id) diff --git a/decidim-accountability/app/controllers/decidim/accountability/results_controller.rb b/decidim-accountability/app/controllers/decidim/accountability/results_controller.rb index 4b5bf0fb88435..b4fbc7f4d9b8f 100644 --- a/decidim-accountability/app/controllers/decidim/accountability/results_controller.rb +++ b/decidim-accountability/app/controllers/decidim/accountability/results_controller.rb @@ -5,6 +5,7 @@ module Accountability # Exposes the result resource so users can view them class ResultsController < Decidim::Accountability::ApplicationController include FilterResource + helper Decidim::TraceabilityHelper helper Decidim::Accountability::BreadcrumbHelper diff --git a/decidim-accountability/app/mailers/decidim/accountability/import_projects_mailer.rb b/decidim-accountability/app/mailers/decidim/accountability/import_projects_mailer.rb index 444c8dffbc9e2..b070e2347f39b 100644 --- a/decidim-accountability/app/mailers/decidim/accountability/import_projects_mailer.rb +++ b/decidim-accountability/app/mailers/decidim/accountability/import_projects_mailer.rb @@ -6,6 +6,7 @@ module Accountability # projects from one budget component to accountability. class ImportProjectsMailer < Decidim::ApplicationMailer include Decidim::TranslatableAttributes + helper Decidim::TranslationsHelper # Public: Sends a notification email with the result of importing projects diff --git a/decidim-accountability/app/mailers/decidim/accountability/import_proposals_mailer.rb b/decidim-accountability/app/mailers/decidim/accountability/import_proposals_mailer.rb index 1b301987ccae7..2e52d41417f0b 100644 --- a/decidim-accountability/app/mailers/decidim/accountability/import_proposals_mailer.rb +++ b/decidim-accountability/app/mailers/decidim/accountability/import_proposals_mailer.rb @@ -6,6 +6,7 @@ module Accountability # proposals to the results. class ImportProposalsMailer < Decidim::ApplicationMailer include Decidim::TranslatableAttributes + helper Decidim::TranslationsHelper # Public: Sends a notification email with the result of proposals import selected proposals to Accountability diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page.rb b/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page.rb index ca2606884bf47..4ef394de1ba6d 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page.rb @@ -5,6 +5,7 @@ module Admin module ContentBlocks module LandingPage extend ActiveSupport::Concern + included do helper_method :active_blocks, :active_content_blocks_title, :add_content_block_text, :available_manifests, :content_block_destroy_confirmation_text, :content_blocks_title, :inactive_blocks, diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page_content_blocks.rb b/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page_content_blocks.rb index 442455b728e78..2600c70e5228e 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page_content_blocks.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/content_blocks/landing_page_content_blocks.rb @@ -5,6 +5,7 @@ module Admin module ContentBlocks module LandingPageContentBlocks extend ActiveSupport::Concern + included do helper_method :content_block, :resource_landing_page_content_block_path, :scoped_resource, :submit_button_text diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb b/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb index 0a6efac8893d5..6061e771fc266 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb @@ -77,7 +77,7 @@ def session_filtered_collection # the query def adjacent_items(item) query = - <<-SQL.squish + <<~SQL.squish WITH collection AS (#{session_filtered_collection.select(:id).to_sql}), successors AS ( @@ -198,8 +198,8 @@ def taxonomy_ids_hash(taxonomies) filtered_taxonomies = taxonomies.roots.or(taxonomies.where(id: available_taxonomy_ids)) return nil if filtered_taxonomies.blank? - filtered_taxonomies.each_with_object({}) do |taxonomy, hash| - hash[taxonomy.id] = taxonomy_ids_hash(taxonomy.children) + filtered_taxonomies.to_h do |taxonomy| + [taxonomy.id, taxonomy_ids_hash(taxonomy.children)] end end diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/participatory_space_admin_context.rb b/decidim-admin/app/controllers/concerns/decidim/admin/participatory_space_admin_context.rb index 330814cd27dad..b29432c6087f3 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/participatory_space_admin_context.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/participatory_space_admin_context.rb @@ -25,6 +25,7 @@ def participatory_space_admin_layout(options = {}) included do include Decidim::NeedsOrganization include Decidim::Admin::ParticipatorySpaceAdminBreadcrumb + helper ParticipatorySpaceHelpers helper_method :current_participatory_space diff --git a/decidim-admin/app/controllers/decidim/admin/areas_controller.rb b/decidim-admin/app/controllers/decidim/admin/areas_controller.rb index 633128e18fc6b..1b2dfe2c5385f 100644 --- a/decidim-admin/app/controllers/decidim/admin/areas_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/areas_controller.rb @@ -6,6 +6,7 @@ module Admin # class AreasController < Decidim::Admin::ApplicationController include Decidim::Admin::Concerns::HasTabbedMenu + helper Decidim::Admin::AreasHelper layout "decidim/admin/settings" diff --git a/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb index f2dd0a5b924ad..956bc922c6223 100644 --- a/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/components/base_controller.rb @@ -10,6 +10,7 @@ class BaseController < Decidim::Admin::ApplicationController include Decidim::Admin::ParticipatorySpaceAdminContext include Decidim::NeedsPermission + participatory_space_admin_layout helper Decidim::ResourceHelper diff --git a/decidim-admin/app/controllers/decidim/admin/components_controller.rb b/decidim-admin/app/controllers/decidim/admin/components_controller.rb index a25e5e55d0b26..02c04e9f0d630 100644 --- a/decidim-admin/app/controllers/decidim/admin/components_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/components_controller.rb @@ -7,6 +7,7 @@ module Admin # class ComponentsController < Decidim::Admin::ApplicationController include Decidim::Admin::HasTrashableResources + helper_method :manifest def index diff --git a/decidim-admin/app/controllers/decidim/admin/imports_controller.rb b/decidim-admin/app/controllers/decidim/admin/imports_controller.rb index 1bf139e13f007..4addafe2f0921 100644 --- a/decidim-admin/app/controllers/decidim/admin/imports_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/imports_controller.rb @@ -5,6 +5,7 @@ module Admin # This controller allows admins to import resources from a file. class ImportsController < Decidim::Admin::ApplicationController include Decidim::ComponentPathHelper + before_action :set_import_breadcrumb_item helper_method :import_manifest diff --git a/decidim-admin/app/controllers/decidim/admin/newsletters_controller.rb b/decidim-admin/app/controllers/decidim/admin/newsletters_controller.rb index 2112c9528b1dc..2025d406037da 100644 --- a/decidim-admin/app/controllers/decidim/admin/newsletters_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/newsletters_controller.rb @@ -7,6 +7,7 @@ class NewslettersController < Decidim::Admin::ApplicationController include Decidim::NewslettersHelper include Decidim::Admin::NewslettersHelper include Paginable + helper_method :newsletter, :recipients_count_query, :content_block, :selected_options, :newsletter_params def index diff --git a/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb index 4d0e713998dd4..49b6eadd7861a 100644 --- a/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb +++ b/decidim-admin/app/controllers/decidim/admin/participatory_space/concerns/has_members.rb @@ -15,6 +15,7 @@ module HasMembers included do include Decidim::Admin::ParticipatorySpace::Concerns::MembersFilterable + helper PaginateHelper helper_method :members diff --git a/decidim-admin/app/controllers/decidim/admin/scopes_controller.rb b/decidim-admin/app/controllers/decidim/admin/scopes_controller.rb index 635a3f5a0cf9b..640319fb7b8c5 100644 --- a/decidim-admin/app/controllers/decidim/admin/scopes_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/scopes_controller.rb @@ -6,6 +6,7 @@ module Admin # class ScopesController < Decidim::Admin::ApplicationController include Decidim::Admin::Concerns::HasTabbedMenu + helper Decidim::Admin::ScopesHelper layout "decidim/admin/settings" diff --git a/decidim-admin/app/forms/decidim/admin/taxonomy_filter_form.rb b/decidim-admin/app/forms/decidim/admin/taxonomy_filter_form.rb index a0eacb5de4632..da3ff1c8ae269 100644 --- a/decidim-admin/app/forms/decidim/admin/taxonomy_filter_form.rb +++ b/decidim-admin/app/forms/decidim/admin/taxonomy_filter_form.rb @@ -5,6 +5,7 @@ module Admin # A form object to create or update areas. class TaxonomyFilterForm < Form include TranslatableAttributes + Item = Struct.new(:name, :value, :children) Manifest = Struct.new(:id, :name) diff --git a/decidim-admin/spec/controllers/taxonomies_controller_spec.rb b/decidim-admin/spec/controllers/taxonomies_controller_spec.rb index 8b263c5f5a110..0d0425c9c6fff 100644 --- a/decidim-admin/spec/controllers/taxonomies_controller_spec.rb +++ b/decidim-admin/spec/controllers/taxonomies_controller_spec.rb @@ -33,7 +33,7 @@ module Admin it "breadcrumbs are set" do get :index - expect(controller.helpers.breadcrumb_items).to eq([label: "Taxonomies", url: taxonomies_path]) + expect(controller.helpers.breadcrumb_items).to eq([{ label: "Taxonomies", url: taxonomies_path }]) end end diff --git a/decidim-assemblies/app/controllers/decidim/assemblies/admin/concerns/assembly_admin.rb b/decidim-assemblies/app/controllers/decidim/assemblies/admin/concerns/assembly_admin.rb index 31b07defed14e..afb1fd8012218 100644 --- a/decidim-assemblies/app/controllers/decidim/assemblies/admin/concerns/assembly_admin.rb +++ b/decidim-assemblies/app/controllers/decidim/assemblies/admin/concerns/assembly_admin.rb @@ -19,6 +19,7 @@ module AssemblyAdmin included do include Decidim::Admin::ParticipatorySpaceAdminContext + helper_method :current_assembly add_breadcrumb_item_from_menu :admin_assembly_menu diff --git a/decidim-assemblies/app/queries/decidim/assemblies/admin/admin_users.rb b/decidim-assemblies/app/queries/decidim/assemblies/admin/admin_users.rb index 35be379dc5886..8779856149558 100644 --- a/decidim-assemblies/app/queries/decidim/assemblies/admin/admin_users.rb +++ b/decidim-assemblies/app/queries/decidim/assemblies/admin/admin_users.rb @@ -42,7 +42,7 @@ def query def assemblies_user_admins Decidim::User.where( id: Decidim::AssemblyUserRole.where(assembly: assemblies, role: :admin) - .select(:decidim_user_id) + .select(:decidim_user_id) ) end diff --git a/decidim-assemblies/db/migrate/20180226103942_add_parent_child_relation_to_assemblies.rb b/decidim-assemblies/db/migrate/20180226103942_add_parent_child_relation_to_assemblies.rb index f5dbd012845c8..063ce22662adc 100644 --- a/decidim-assemblies/db/migrate/20180226103942_add_parent_child_relation_to_assemblies.rb +++ b/decidim-assemblies/db/migrate/20180226103942_add_parent_child_relation_to_assemblies.rb @@ -7,10 +7,10 @@ def change # required so that test suite works in ci env enable_extension "ltree" rescue StandardError - raise <<-MSG.squish - Decidim requires the ltree extension to be enabled in your PostgreSQL. - You can do so by running `CREATE EXTENSION IF NOT EXISTS "ltree";` on the current DB as a PostgreSQL - super user. + raise <<~MSG.squish + Decidim requires the ltree extension to be enabled in your PostgreSQL. + You can do so by running `CREATE EXTENSION IF NOT EXISTS "ltree";` on the current DB as a PostgreSQL + super user. MSG end end diff --git a/decidim-budgets/app/commands/decidim/budgets/admin/create_project.rb b/decidim-budgets/app/commands/decidim/budgets/admin/create_project.rb index 66e06267db52b..bd80489593723 100644 --- a/decidim-budgets/app/commands/decidim/budgets/admin/create_project.rb +++ b/decidim-budgets/app/commands/decidim/budgets/admin/create_project.rb @@ -7,6 +7,7 @@ module Admin # panel. class CreateProject < Decidim::Commands::CreateResource include ::Decidim::GalleryMethods + fetch_form_attributes :budget, :taxonomizations, :title, :description, :budget_amount, :address, :latitude, :longitude private diff --git a/decidim-budgets/app/commands/decidim/budgets/admin/update_project.rb b/decidim-budgets/app/commands/decidim/budgets/admin/update_project.rb index 3cd42cd6d0652..522afcf3e0697 100644 --- a/decidim-budgets/app/commands/decidim/budgets/admin/update_project.rb +++ b/decidim-budgets/app/commands/decidim/budgets/admin/update_project.rb @@ -7,6 +7,7 @@ module Admin # panel. class UpdateProject < Decidim::Commands::UpdateResource include ::Decidim::GalleryMethods + fetch_form_attributes :taxonomizations, :title, :description, :budget_amount, :address, :latitude, :longitude def initialize(form, project) diff --git a/decidim-budgets/app/controllers/decidim/budgets/admin/budgets_controller.rb b/decidim-budgets/app/controllers/decidim/budgets/admin/budgets_controller.rb index 41dc4fcfe8b1b..477f9d296ddb4 100644 --- a/decidim-budgets/app/controllers/decidim/budgets/admin/budgets_controller.rb +++ b/decidim-budgets/app/controllers/decidim/budgets/admin/budgets_controller.rb @@ -6,6 +6,7 @@ module Admin # This controller allows the create or update a budget. class BudgetsController < Admin::ApplicationController include Decidim::Admin::HasTrashableResources + helper_method :budgets, :budget, :finished_orders, :pending_orders, :users_with_pending_orders, :users_with_finished_orders diff --git a/decidim-budgets/app/controllers/decidim/budgets/admin/projects_controller.rb b/decidim-budgets/app/controllers/decidim/budgets/admin/projects_controller.rb index 7eb5bd00861e6..0111aa84a482c 100644 --- a/decidim-budgets/app/controllers/decidim/budgets/admin/projects_controller.rb +++ b/decidim-budgets/app/controllers/decidim/budgets/admin/projects_controller.rb @@ -9,6 +9,7 @@ class ProjectsController < Admin::ApplicationController include Decidim::Admin::HasTrashableResources include Decidim::Admin::ComponentTaxonomiesHelper include Decidim::Budgets::Admin::Filterable + helper Decidim::Budgets::Admin::ProjectBulkActionsHelper helper Decidim::Budgets::ProjectsHelper diff --git a/decidim-budgets/app/models/decidim/budgets/project.rb b/decidim-budgets/app/models/decidim/budgets/project.rb index 9990eab6dddcb..e5dcb4b0015a9 100644 --- a/decidim-budgets/app/models/decidim/budgets/project.rb +++ b/decidim-budgets/app/models/decidim/budgets/project.rb @@ -134,14 +134,14 @@ def attachment_context end ransacker :confirmed_orders_count do - query = <<-SQL.squish - ( - SELECT COUNT(decidim_budgets_line_items.decidim_order_id) - FROM decidim_budgets_line_items - LEFT JOIN decidim_budgets_orders ON decidim_budgets_orders.id = decidim_budgets_line_items.decidim_order_id - WHERE decidim_budgets_orders.checked_out_at IS NOT NULL - AND decidim_budgets_projects.id = decidim_budgets_line_items.decidim_project_id - ) + query = <<~SQL.squish + ( + SELECT COUNT(decidim_budgets_line_items.decidim_order_id) + FROM decidim_budgets_line_items + LEFT JOIN decidim_budgets_orders ON decidim_budgets_orders.id = decidim_budgets_line_items.decidim_order_id + WHERE decidim_budgets_orders.checked_out_at IS NOT NULL + AND decidim_budgets_projects.id = decidim_budgets_line_items.decidim_project_id + ) SQL Arel.sql(query) end diff --git a/decidim-budgets/lib/decidim/budgets/project_serializer.rb b/decidim-budgets/lib/decidim/budgets/project_serializer.rb index 0779beb5c0083..5ad61435cac1c 100644 --- a/decidim-budgets/lib/decidim/budgets/project_serializer.rb +++ b/decidim-budgets/lib/decidim/budgets/project_serializer.rb @@ -76,8 +76,8 @@ def budget_url end def empty_translatable(locales = Decidim.available_locales) - locales.each_with_object({}) do |locale, result| - result[locale.to_s] = "" + locales.to_h do |locale| + [locale.to_s, ""] end end end diff --git a/decidim-comments/app/cells/decidim/comments/comments_cell.rb b/decidim-comments/app/cells/decidim/comments/comments_cell.rb index 756b4b33fc39a..8df08b549a893 100644 --- a/decidim-comments/app/cells/decidim/comments/comments_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comments_cell.rb @@ -5,6 +5,7 @@ module Comments # A cell to display a comments section for a commentable object. class CommentsCell < Decidim::ViewModel include UserRoleChecker + delegate :user_signed_in?, to: :controller def render_comments diff --git a/decidim-comments/lib/decidim/api/commentable_interface.rb b/decidim-comments/lib/decidim/api/commentable_interface.rb index 30f23e7c29eca..e55bba0f54473 100644 --- a/decidim-comments/lib/decidim/api/commentable_interface.rb +++ b/decidim-comments/lib/decidim/api/commentable_interface.rb @@ -5,6 +5,7 @@ module Comments # This interface represents a commentable object. module CommentableInterface include Decidim::Api::Types::BaseInterface + description "A commentable interface" field :accepts_new_comments, GraphQL::Types::Boolean, "Whether the object can have new comments or not", method: :accepts_new_comments?, null: false diff --git a/decidim-conferences/app/cells/decidim/conferences/conference_speaker_cell.rb b/decidim-conferences/app/cells/decidim/conferences/conference_speaker_cell.rb index 8c35ab9b6da09..c484329e9e327 100644 --- a/decidim-conferences/app/cells/decidim/conferences/conference_speaker_cell.rb +++ b/decidim-conferences/app/cells/decidim/conferences/conference_speaker_cell.rb @@ -7,6 +7,7 @@ class ConferenceSpeakerCell < Decidim::AuthorCell include Decidim::Meetings::MeetingCellsHelper include Cell::ViewModel::Partial include Decidim::Conferences::Engine.routes.url_helpers + property :name property :nickname property :profile_path diff --git a/decidim-conferences/app/controllers/decidim/conferences/admin/concerns/conference_admin.rb b/decidim-conferences/app/controllers/decidim/conferences/admin/concerns/conference_admin.rb index b12e625a8a1d3..0a2f1c34adeef 100644 --- a/decidim-conferences/app/controllers/decidim/conferences/admin/concerns/conference_admin.rb +++ b/decidim-conferences/app/controllers/decidim/conferences/admin/concerns/conference_admin.rb @@ -19,6 +19,7 @@ module ConferenceAdmin included do include Decidim::Admin::ParticipatorySpaceAdminContext + helper_method :current_conference add_breadcrumb_item_from_menu :conference_admin_menu diff --git a/decidim-conferences/app/forms/decidim/conferences/admin/conference_registration_invite_form.rb b/decidim-conferences/app/forms/decidim/conferences/admin/conference_registration_invite_form.rb index 4d9ebc50b5a76..be4c91f9c29f1 100644 --- a/decidim-conferences/app/forms/decidim/conferences/admin/conference_registration_invite_form.rb +++ b/decidim-conferences/app/forms/decidim/conferences/admin/conference_registration_invite_form.rb @@ -7,6 +7,7 @@ module Admin # class ConferenceRegistrationInviteForm < Form include TranslatableAttributes + attribute :name, String attribute :email, String attribute :user_id, Integer diff --git a/decidim-conferences/app/queries/decidim/conferences/admin/admin_users.rb b/decidim-conferences/app/queries/decidim/conferences/admin/admin_users.rb index 547cd7dd1ccb5..28d3185b9971c 100644 --- a/decidim-conferences/app/queries/decidim/conferences/admin/admin_users.rb +++ b/decidim-conferences/app/queries/decidim/conferences/admin/admin_users.rb @@ -42,7 +42,7 @@ def query def conferences_user_admins Decidim::User.where( id: Decidim::ConferenceUserRole.where(conference: conferences, role: :admin) - .select(:decidim_user_id) + .select(:decidim_user_id) ) end diff --git a/decidim-conferences/spec/system/conference_program_spec.rb b/decidim-conferences/spec/system/conference_program_spec.rb index 15788481ec465..3a05ec099f3ae 100644 --- a/decidim-conferences/spec/system/conference_program_spec.rb +++ b/decidim-conferences/spec/system/conference_program_spec.rb @@ -4,6 +4,7 @@ describe "Conference program" do include Decidim::TranslationsHelper + let(:organization) { create(:organization) } let(:conference) { create(:conference, organization:) } let!(:component) do diff --git a/decidim-core/app/controllers/concerns/decidim/direct_upload.rb b/decidim-core/app/controllers/concerns/decidim/direct_upload.rb index 57f8360db0d26..c6b74054f7545 100644 --- a/decidim-core/app/controllers/concerns/decidim/direct_upload.rb +++ b/decidim-core/app/controllers/concerns/decidim/direct_upload.rb @@ -6,6 +6,7 @@ module DirectUpload included do include Decidim::NeedsOrganization + skip_before_action :verify_organization before_action :check_organization!, diff --git a/decidim-core/app/controllers/decidim/amendments_controller.rb b/decidim-core/app/controllers/decidim/amendments_controller.rb index 9e3714d5168ed..ab50bf647feaf 100644 --- a/decidim-core/app/controllers/decidim/amendments_controller.rb +++ b/decidim-core/app/controllers/decidim/amendments_controller.rb @@ -5,6 +5,7 @@ class AmendmentsController < Decidim::ApplicationController include Decidim::ApplicationHelper include FormFactory include HasSpecificBreadcrumb + helper Decidim::ResourceReferenceHelper before_action :authenticate_user! diff --git a/decidim-core/app/controllers/decidim/components/base_controller.rb b/decidim-core/app/controllers/decidim/components/base_controller.rb index da45894553027..a23596474ae34 100644 --- a/decidim-core/app/controllers/decidim/components/base_controller.rb +++ b/decidim-core/app/controllers/decidim/components/base_controller.rb @@ -10,6 +10,7 @@ class BaseController < Decidim::ApplicationController include Decidim::NeedsPermission include ParticipatorySpaceContext + before_action :authorize_participatory_space helper Decidim::FiltersHelper diff --git a/decidim-core/app/controllers/decidim/follows_controller.rb b/decidim-core/app/controllers/decidim/follows_controller.rb index 24f560bc6b86a..056526d29bc91 100644 --- a/decidim-core/app/controllers/decidim/follows_controller.rb +++ b/decidim-core/app/controllers/decidim/follows_controller.rb @@ -3,6 +3,7 @@ module Decidim class FollowsController < Decidim::ApplicationController include FormFactory + before_action :authenticate_user! helper_method :resource, :button_cell, :button_cell_mobile diff --git a/decidim-core/app/controllers/decidim/likes_controller.rb b/decidim-core/app/controllers/decidim/likes_controller.rb index 215753cd4b14e..45df4f7328417 100644 --- a/decidim-core/app/controllers/decidim/likes_controller.rb +++ b/decidim-core/app/controllers/decidim/likes_controller.rb @@ -5,6 +5,7 @@ module Decidim class LikesController < Decidim::Components::BaseController # we need to +include+ to be able to call :like_button from the view include Decidim::LikeableHelper + helper_method :like_button # we need to declare with +helper+ to be able to call :render_like_identity from the views helper Decidim::LikeableHelper diff --git a/decidim-core/app/helpers/decidim/translations_helper.rb b/decidim-core/app/helpers/decidim/translations_helper.rb index 66d4bf082eee6..1ee7da24b1b77 100644 --- a/decidim-core/app/helpers/decidim/translations_helper.rb +++ b/decidim-core/app/helpers/decidim/translations_helper.rb @@ -30,8 +30,8 @@ def multi_translation(key, locales = Decidim.available_locales, **) # # Returns a Hash with the locales as keys and the empty strings as values. def empty_translatable(locales = Decidim.available_locales) - locales.each_with_object({}) do |locale, result| - result[locale.to_s] = "" + locales.to_h do |locale| + [locale.to_s, ""] end end @@ -49,8 +49,8 @@ def empty_translatable(locales = Decidim.available_locales) def ensure_translatable(value, locales = Decidim.available_locales) return empty_translatable(locales) unless value.is_a?(Hash) - locales.each_with_object({}) do |locale, result| - result[locale.to_s] = value[locale.to_s] || value[locale] || "" + locales.to_h do |locale| + [locale.to_s, value[locale.to_s] || value[locale] || ""] end end diff --git a/decidim-core/app/mailers/decidim/application_mailer.rb b/decidim-core/app/mailers/decidim/application_mailer.rb index c7866a36ebbaf..53a2c128ae5e8 100644 --- a/decidim-core/app/mailers/decidim/application_mailer.rb +++ b/decidim-core/app/mailers/decidim/application_mailer.rb @@ -8,6 +8,7 @@ class ApplicationMailer < ActionMailer::Base include MultitenantAssetHost include Decidim::SanitizeHelper include Decidim::OrganizationHelper + helper_method :organization_name, :current_locale, :decidim_escape_translated, :decidim_sanitize_translated, :translated_attribute, :decidim_sanitize, :decidim_sanitize_newsletter diff --git a/decidim-core/app/mailers/decidim/decidim_devise_mailer.rb b/decidim-core/app/mailers/decidim/decidim_devise_mailer.rb index 48e04822f3842..8f38b9722f504 100644 --- a/decidim-core/app/mailers/decidim/decidim_devise_mailer.rb +++ b/decidim-core/app/mailers/decidim/decidim_devise_mailer.rb @@ -6,6 +6,7 @@ module Decidim class DecidimDeviseMailer < ::Devise::Mailer include LocalisedMailer include Decidim::SanitizeHelper + helper_method :decidim_escape_translated, :decidim_sanitize_translated, :translated_attribute layout "decidim/mailer" diff --git a/decidim-core/app/models/decidim/content_block.rb b/decidim-core/app/models/decidim/content_block.rb index f2a38c749e8ec..75ffac30c8f3b 100644 --- a/decidim-core/app/models/decidim/content_block.rb +++ b/decidim-core/app/models/decidim/content_block.rb @@ -123,8 +123,8 @@ def save private def manifest_attachments - @manifest_attachments ||= manifest.images.each_with_object({}) do |attachment_config, list| - list[attachment_config[:name]] = attachments.find_or_initialize_by(name: attachment_config[:name]) + @manifest_attachments ||= manifest.images.to_h do |attachment_config| + [attachment_config[:name], attachments.find_or_initialize_by(name: attachment_config[:name])] end end diff --git a/decidim-core/app/presenters/decidim/log/user_presenter.rb b/decidim-core/app/presenters/decidim/log/user_presenter.rb index 9804a3c58e711..06b2a656c144e 100644 --- a/decidim-core/app/presenters/decidim/log/user_presenter.rb +++ b/decidim-core/app/presenters/decidim/log/user_presenter.rb @@ -11,6 +11,7 @@ module Log # The only requirement for custom renderers is that they should respond to `present`. class UserPresenter include Decidim::SanitizeHelper + # Public: Initializes the presenter. # # user - An instance of Decidim::User diff --git a/decidim-core/db/migrate/20250819110800_convert_private_exports_id_to_uuid.rb b/decidim-core/db/migrate/20250819110800_convert_private_exports_id_to_uuid.rb index cc4a45ba43ef4..126336318e3b7 100644 --- a/decidim-core/db/migrate/20250819110800_convert_private_exports_id_to_uuid.rb +++ b/decidim-core/db/migrate/20250819110800_convert_private_exports_id_to_uuid.rb @@ -18,7 +18,7 @@ def up t.index [:uuid], name: "index_decidim_private_exports_on_uuid", unique: true end # Copy data from old table to new table - execute <<-SQL.squish + execute <<~SQL.squish INSERT INTO decidim_private_exports_new (uuid, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at) SELECT id, export_type, attached_to_type, attached_to_id, file, content_type, file_size, NOW(), metadata, created_at, updated_at FROM decidim_private_exports @@ -43,7 +43,7 @@ def down t.timestamps end - execute <<-SQL.squish + execute <<~SQL.squish INSERT INTO decidim_private_exports_new (id, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at) SELECT uuid, export_type, attached_to_type, attached_to_id, file, content_type, file_size, expires_at, metadata, created_at, updated_at FROM decidim_private_exports diff --git a/decidim-core/lib/decidim/acts_as_tree.rb b/decidim-core/lib/decidim/acts_as_tree.rb index ce300dd81bf86..f65d84ba69a23 100644 --- a/decidim-core/lib/decidim/acts_as_tree.rb +++ b/decidim-core/lib/decidim/acts_as_tree.rb @@ -33,18 +33,18 @@ def polymorphic_condition(item) end def tree_sql_for(item) - <<-SQL.squish - WITH RECURSIVE search_tree(id, path) AS ( - SELECT id, ARRAY[id] - FROM #{table_name} - WHERE id = #{item.id} - UNION ALL - SELECT #{table_name}.id, path || #{table_name}.id - FROM search_tree - JOIN #{table_name} ON #{table_name}.#{parent_item_foreign_key} = search_tree.id #{polymorphic_condition(item)} - WHERE NOT #{table_name}.id = ANY(path) - ) - SELECT id FROM search_tree ORDER BY path + <<~SQL.squish + WITH RECURSIVE search_tree(id, path) AS ( + SELECT id, ARRAY[id] + FROM #{table_name} + WHERE id = #{item.id} + UNION ALL + SELECT #{table_name}.id, path || #{table_name}.id + FROM search_tree + JOIN #{table_name} ON #{table_name}.#{parent_item_foreign_key} = search_tree.id #{polymorphic_condition(item)} + WHERE NOT #{table_name}.id = ANY(path) + ) + SELECT id FROM search_tree ORDER BY path SQL end end diff --git a/decidim-core/lib/decidim/api/functions/category_list.rb b/decidim-core/lib/decidim/api/functions/category_list.rb index 1a5e47f65761d..7ea84db32c8d8 100644 --- a/decidim-core/lib/decidim/api/functions/category_list.rb +++ b/decidim-core/lib/decidim/api/functions/category_list.rb @@ -15,6 +15,7 @@ module Core # searches. class CategoryList include NeedsApiFilterAndOrder + attr_reader :model_class def initialize diff --git a/decidim-core/lib/decidim/api/functions/component_list.rb b/decidim-core/lib/decidim/api/functions/component_list.rb index 23b9f298ca656..986a72f3dc287 100644 --- a/decidim-core/lib/decidim/api/functions/component_list.rb +++ b/decidim-core/lib/decidim/api/functions/component_list.rb @@ -17,6 +17,7 @@ module Core class ComponentList include NeedsApiFilterAndOrder include NeedsApiDefaultOrder + attr_reader :model_class def initialize diff --git a/decidim-core/lib/decidim/api/functions/component_list_base.rb b/decidim-core/lib/decidim/api/functions/component_list_base.rb index 7a913a7a7c82b..a13273e8b4391 100644 --- a/decidim-core/lib/decidim/api/functions/component_list_base.rb +++ b/decidim-core/lib/decidim/api/functions/component_list_base.rb @@ -28,6 +28,7 @@ module Core class ComponentListBase include NeedsApiFilterAndOrder include NeedsApiDefaultOrder + attr_reader :model_class def initialize(model_class:) diff --git a/decidim-core/lib/decidim/api/functions/participatory_space_list_base.rb b/decidim-core/lib/decidim/api/functions/participatory_space_list_base.rb index 291a93cf376a4..5a644dd403b68 100644 --- a/decidim-core/lib/decidim/api/functions/participatory_space_list_base.rb +++ b/decidim-core/lib/decidim/api/functions/participatory_space_list_base.rb @@ -10,6 +10,7 @@ module Core class ParticipatorySpaceListBase include NeedsApiFilterAndOrder include NeedsApiDefaultOrder + attr_reader :manifest def initialize(manifest:) diff --git a/decidim-core/lib/decidim/api/interfaces/amendable_entity_interface.rb b/decidim-core/lib/decidim/api/interfaces/amendable_entity_interface.rb index aaa97c1514c8d..22e6dad38f854 100644 --- a/decidim-core/lib/decidim/api/interfaces/amendable_entity_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/amendable_entity_interface.rb @@ -6,6 +6,7 @@ module Core # The only requirement is to have an ID and the Type name be the class.name + Type module AmendableEntityInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with amendments" field :id, ID, "ID of this entity", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/amendable_interface.rb b/decidim-core/lib/decidim/api/interfaces/amendable_interface.rb index e962390872371..4092b38be7657 100644 --- a/decidim-core/lib/decidim/api/interfaces/amendable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/amendable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an amendable object. module AmendableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with amendments" field :amendments, [Decidim::Core::AmendmentType, { null: true }], description: "This object's amendments", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/attachable_collection_interface.rb b/decidim-core/lib/decidim/api/interfaces/attachable_collection_interface.rb index 28b08a789f24b..67f2efb474c4c 100644 --- a/decidim-core/lib/decidim/api/interfaces/attachable_collection_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/attachable_collection_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an attachable object. module AttachableCollectionInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with attachments" field :attachment_collections, [Decidim::Core::AttachmentCollectionType, { null: true }], "This object's attachment collections", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/attachable_interface.rb b/decidim-core/lib/decidim/api/interfaces/attachable_interface.rb index 14e06ecf06c5d..e04eadd988dd0 100644 --- a/decidim-core/lib/decidim/api/interfaces/attachable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/attachable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a attachable object. module AttachableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with attachments" field :attachments, [Decidim::Core::AttachmentType, { null: true }], "This object's attachments", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/author_interface.rb b/decidim-core/lib/decidim/api/interfaces/author_interface.rb index a82fe64464cbe..691194f9091db 100644 --- a/decidim-core/lib/decidim/api/interfaces/author_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/author_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an author who owns a resource. module AuthorInterface include Decidim::Api::Types::BaseInterface + graphql_name "Author" description "An author" diff --git a/decidim-core/lib/decidim/api/interfaces/authorable_interface.rb b/decidim-core/lib/decidim/api/interfaces/authorable_interface.rb index 8980cbfc2d381..a688516d75625 100644 --- a/decidim-core/lib/decidim/api/interfaces/authorable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/authorable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a commentable object. module AuthorableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in authorable objects." field :author, Decidim::Core::AuthorInterface, "The resource author", null: true do diff --git a/decidim-core/lib/decidim/api/interfaces/categories_container_interface.rb b/decidim-core/lib/decidim/api/interfaces/categories_container_interface.rb index bac046bb47473..6076462930714 100644 --- a/decidim-core/lib/decidim/api/interfaces/categories_container_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/categories_container_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a resource that contains categories. module CategoriesContainerInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects that contain categories." field :categories, [Decidim::Core::CategoryType, { null: true }], "Categories for this space", null: false do diff --git a/decidim-core/lib/decidim/api/interfaces/categorizable_interface.rb b/decidim-core/lib/decidim/api/interfaces/categorizable_interface.rb index 396df3c7fb3ab..c3d9c06b1c5b8 100644 --- a/decidim-core/lib/decidim/api/interfaces/categorizable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/categorizable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a categorizable object. module CategorizableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in categorizable objects." field :category, Decidim::Core::CategoryType, "The object's category", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/coauthorable_interface.rb b/decidim-core/lib/decidim/api/interfaces/coauthorable_interface.rb index bbdfa5eab3676..7ddb3c1abc381 100644 --- a/decidim-core/lib/decidim/api/interfaces/coauthorable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/coauthorable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a coauthorable object. module CoauthorableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in coauthorable objects." field :authors_count, Integer, diff --git a/decidim-core/lib/decidim/api/interfaces/component_interface.rb b/decidim-core/lib/decidim/api/interfaces/component_interface.rb index 81ae0d58347e8..e0ba0a104d9b3 100644 --- a/decidim-core/lib/decidim/api/interfaces/component_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/component_interface.rb @@ -4,6 +4,7 @@ module Decidim module Core module ComponentInterface include Decidim::Api::Types::BaseInterface + description "This interface is implemented by all components that belong into a Participatory Space" implements Decidim::Core::TimestampsInterface diff --git a/decidim-core/lib/decidim/api/interfaces/fingerprint_interface.rb b/decidim-core/lib/decidim/api/interfaces/fingerprint_interface.rb index d000a785694a2..f6836a71f3c4d 100644 --- a/decidim-core/lib/decidim/api/interfaces/fingerprint_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/fingerprint_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a fingerprintable object. module FingerprintInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in fingerprintable objects." field :fingerprint, Decidim::Core::FingerprintType, "This object's fingerprint", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/followable_interface.rb b/decidim-core/lib/decidim/api/interfaces/followable_interface.rb index b3f210bb8a981..ac1ce4404fa55 100644 --- a/decidim-core/lib/decidim/api/interfaces/followable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/followable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a followable object. module FollowableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in followable objects." field :followers, [Decidim::Core::AuthorInterface, { null: true }], "The followers of this resource", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/likeable_interface.rb b/decidim-core/lib/decidim/api/interfaces/likeable_interface.rb index 5aa137525e5e3..488d1c2e3031d 100644 --- a/decidim-core/lib/decidim/api/interfaces/likeable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/likeable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an object capable of likes. module LikeableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with likes" field :likes, [Decidim::Core::AuthorInterface, { null: true }], "The likes of this object", null: false diff --git a/decidim-core/lib/decidim/api/interfaces/localizable_interface.rb b/decidim-core/lib/decidim/api/interfaces/localizable_interface.rb index a14a93722b7a6..03eb2c86099ab 100644 --- a/decidim-core/lib/decidim/api/interfaces/localizable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/localizable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a localizable (that has address, latitude and longitude) object. module LocalizableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in localizable objects." field :address, GraphQL::Types::String, "The physical address (location) of this result", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/participatory_space_interface.rb b/decidim-core/lib/decidim/api/interfaces/participatory_space_interface.rb index 7710747fec7ef..a4900d6fb4b3e 100644 --- a/decidim-core/lib/decidim/api/interfaces/participatory_space_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/participatory_space_interface.rb @@ -4,6 +4,7 @@ module Decidim module Core module ParticipatorySpaceInterface include Decidim::Api::Types::BaseInterface + graphql_name "ParticipatorySpaceInterface" description "The interface that all participatory spaces should implement." diff --git a/decidim-core/lib/decidim/api/interfaces/referable_interface.rb b/decidim-core/lib/decidim/api/interfaces/referable_interface.rb index 27161d0c7f8d9..d5cad3baf3753 100644 --- a/decidim-core/lib/decidim/api/interfaces/referable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/referable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an object that have a reference field. module ReferableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in display reference methods" field :reference, GraphQL::Types::String, "The reference for this record", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/scopable_interface.rb b/decidim-core/lib/decidim/api/interfaces/scopable_interface.rb index 90fdab6712e91..afb935a26f2a6 100644 --- a/decidim-core/lib/decidim/api/interfaces/scopable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/scopable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a scopable object. module ScopableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in scopable objects." field :scope, Decidim::Core::ScopeApiType, "The object's scope", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/taxonomizable_interface.rb b/decidim-core/lib/decidim/api/interfaces/taxonomizable_interface.rb index 7c0b20eadd3cf..83b106b7fac99 100644 --- a/decidim-core/lib/decidim/api/interfaces/taxonomizable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/taxonomizable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents a categorizable object. module TaxonomizableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in taxonomizable objects." field :taxonomies, [Decidim::Core::TaxonomyType], "The object's taxonomies", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/timestamps_interface.rb b/decidim-core/lib/decidim/api/interfaces/timestamps_interface.rb index 1c297ab5abd71..09b72ac5db8dc 100644 --- a/decidim-core/lib/decidim/api/interfaces/timestamps_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/timestamps_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an object with standard create_at and updated_at timestamps. module TimestampsInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with created_at and updated_at attributes" field :created_at, Decidim::Core::DateTimeType, description: "The date and time this object was created", null: true diff --git a/decidim-core/lib/decidim/api/interfaces/traceable_interface.rb b/decidim-core/lib/decidim/api/interfaces/traceable_interface.rb index cc3f80678be0e..23764b075c93c 100644 --- a/decidim-core/lib/decidim/api/interfaces/traceable_interface.rb +++ b/decidim-core/lib/decidim/api/interfaces/traceable_interface.rb @@ -5,6 +5,7 @@ module Core # This interface represents an traceable object. module TraceableInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with traceability (versions)" field :versions, [Decidim::Core::TraceVersionType, { null: true }], "This object's versions", null: false diff --git a/decidim-core/lib/decidim/command.rb b/decidim-core/lib/decidim/command.rb index 4fde094cd10fa..413dda633ade4 100644 --- a/decidim-core/lib/decidim/command.rb +++ b/decidim-core/lib/decidim/command.rb @@ -8,6 +8,7 @@ module Decidim class Command include ::Wisper::Publisher + delegate :locale, to: :I18n def self.call(*, **, &) diff --git a/decidim-core/lib/decidim/core.rb b/decidim-core/lib/decidim/core.rb index e59f148306552..e9711b587027d 100644 --- a/decidim-core/lib/decidim/core.rb +++ b/decidim-core/lib/decidim/core.rb @@ -149,6 +149,7 @@ module Commands end include ActiveSupport::Configurable + # Loads seeds from all engines. def self.seed! # After running the migrations, some records may have loaded their column diff --git a/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb b/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb index 10bcc8fed8949..ff4b995fc1d74 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb @@ -4,6 +4,7 @@ shared_context "when a simple event" do include Decidim::SanitizeHelper + subject { event_instance } let(:event_instance) do diff --git a/decidim-core/lib/decidim/exporters/csv.rb b/decidim-core/lib/decidim/exporters/csv.rb index a42da8d2888e1..c89c63bf69247 100644 --- a/decidim-core/lib/decidim/exporters/csv.rb +++ b/decidim-core/lib/decidim/exporters/csv.rb @@ -66,7 +66,7 @@ def flatten(object, key = nil) result.merge(flatten(value, new_key)) end when Array - { key.to_s => object.compact.map(&:to_s).join(", ") } + { key.to_s => object.compact.join(", ") } else { key.to_s => object } end diff --git a/decidim-core/lib/decidim/filter_form_builder.rb b/decidim-core/lib/decidim/filter_form_builder.rb index a10285a1710bb..bc7a8584c74a5 100644 --- a/decidim-core/lib/decidim/filter_form_builder.rb +++ b/decidim-core/lib/decidim/filter_form_builder.rb @@ -50,7 +50,7 @@ def dropdown_label(item, method, options = {}) private def check_boxes_tree_id(*args) - args.map(&:to_s).join("_") + args.join("_") end def default_form_type_for_collection(collection) diff --git a/decidim-core/lib/decidim/has_settings.rb b/decidim-core/lib/decidim/has_settings.rb index fbcdb938d143a..aa8a57bc1f4db 100644 --- a/decidim-core/lib/decidim/has_settings.rb +++ b/decidim-core/lib/decidim/has_settings.rb @@ -45,8 +45,8 @@ def default_step_settings=(data) def step_settings return {} unless participatory_space.allows_steps? - participatory_space.steps.each_with_object({}) do |step, result| - result[step.id.to_s] = new_settings_schema(:step, self[:settings].dig("steps", step.id.to_s)) + participatory_space.steps.to_h do |step| + [step.id.to_s, new_settings_schema(:step, self[:settings].dig("steps", step.id.to_s))] end end diff --git a/decidim-core/lib/decidim/legacy_form_builder.rb b/decidim-core/lib/decidim/legacy_form_builder.rb index c10791ed82f9f..2172d26547a0a 100644 --- a/decidim-core/lib/decidim/legacy_form_builder.rb +++ b/decidim-core/lib/decidim/legacy_form_builder.rb @@ -10,6 +10,7 @@ module Decidim class LegacyFormBuilder < ActionView::Helpers::FormBuilder include ActionView::Helpers::TagHelper include ActionView::Helpers::OutputSafetyHelper + %w(file_field email_field text_field url_field number_field search_field color_field) .each do |method_name| diff --git a/decidim-core/lib/decidim/moderation_tools.rb b/decidim-core/lib/decidim/moderation_tools.rb index 1db69a58958f8..82581fd3e188c 100644 --- a/decidim-core/lib/decidim/moderation_tools.rb +++ b/decidim-core/lib/decidim/moderation_tools.rb @@ -111,7 +111,7 @@ def hide! private def affected_users - @affected_users ||= (@reportable.try(:authors) || [@reportable.try(:author)]).select { |author| author.is_a?(Decidim::User) } + @affected_users ||= (@reportable.try(:authors) || [@reportable.try(:author)]).grep(Decidim::User) end def report_reasons diff --git a/decidim-core/lib/decidim/participatory_space/has_members.rb b/decidim-core/lib/decidim/participatory_space/has_members.rb index 03e87511cde67..19a3a505c7be6 100644 --- a/decidim-core/lib/decidim/participatory_space/has_members.rb +++ b/decidim-core/lib/decidim/participatory_space/has_members.rb @@ -26,8 +26,8 @@ def self.visible_for(user) where( id: public_spaces + private_spaces - .joins(:members) - .where(decidim_members: { decidim_user_id: user.id }) + .joins(:members) + .where(decidim_members: { decidim_user_id: user.id }) ) else public_spaces diff --git a/decidim-core/spec/controllers/active_storage/disk_controller_spec.rb b/decidim-core/spec/controllers/active_storage/disk_controller_spec.rb index 8feca373c2830..1203a0a2770ba 100644 --- a/decidim-core/spec/controllers/active_storage/disk_controller_spec.rb +++ b/decidim-core/spec/controllers/active_storage/disk_controller_spec.rb @@ -6,6 +6,7 @@ module ActiveStorage describe DiskController do describe "GET #show" do include Rails.application.routes.url_helpers + before do ActiveStorage::Current.url_options = { host: "example.com", protocol: "http" } end diff --git a/decidim-core/spec/lib/attribute_object/model_spec.rb b/decidim-core/spec/lib/attribute_object/model_spec.rb index 32c0a6c438082..26ab68eb2ed20 100644 --- a/decidim-core/spec/lib/attribute_object/model_spec.rb +++ b/decidim-core/spec/lib/attribute_object/model_spec.rb @@ -54,7 +54,7 @@ module Decidim expect(subject.int).to eq(1) expect(subject.flt).to eq(1.1) expect(subject.eng).to be(Decidim::Core::Engine.instance) - expect(subject.arr.all? { |i| i.is_a?(OpenStruct) }).to be(true) + expect(subject.arr.all?(OpenStruct)).to be(true) expect(subject.arr[0].foo).to eq("bar") expect(subject.arr[1].foo).to eq("baz") @@ -64,7 +64,7 @@ module Decidim expect(subject.sub.role).to eq("Dough") expect(subject.sar).to be_a(Array) - expect(subject.sar.all? { |i| i.is_a?(submodel) }).to be(true) + expect(subject.sar.all?(submodel)).to be(true) expect(subject.sar[0].id).to eq(1) expect(subject.sar[0].name).to eq("John") expect(subject.sar[0].role).to eq("Dough") diff --git a/decidim-core/spec/presenters/decidim/notification_presenter_spec.rb b/decidim-core/spec/presenters/decidim/notification_presenter_spec.rb index e7120af54c17a..0340fddc09a74 100644 --- a/decidim-core/spec/presenters/decidim/notification_presenter_spec.rb +++ b/decidim-core/spec/presenters/decidim/notification_presenter_spec.rb @@ -5,6 +5,7 @@ module Decidim describe NotificationPresenter, type: :presenter do include ActiveSupport::Testing::TimeHelpers + let(:creating_date) { Time.parse("Wed, 1 Sep 2021 21:00:00 UTC +00:00").in_time_zone } let(:notification) { create(:notification, created_at: creating_date) } diff --git a/decidim-core/spec/types/attachment_collection_type_spec.rb b/decidim-core/spec/types/attachment_collection_type_spec.rb index 4a814cbd6e741..1b9c6d22d962d 100644 --- a/decidim-core/spec/types/attachment_collection_type_spec.rb +++ b/decidim-core/spec/types/attachment_collection_type_spec.rb @@ -25,7 +25,7 @@ module Core let!(:attachment) { create(:attachment, :with_image, attachment_collection: model) } it "returns the attachment id field" do - expect(response["attachments"]).to eq(["id" => attachment.id.to_s]) + expect(response["attachments"]).to eq([{ "id" => attachment.id.to_s }]) end end diff --git a/decidim-core/spec/validators/translatable_presence_validator_spec.rb b/decidim-core/spec/validators/translatable_presence_validator_spec.rb index 012d14ac78926..d479f0e35ca7b 100644 --- a/decidim-core/spec/validators/translatable_presence_validator_spec.rb +++ b/decidim-core/spec/validators/translatable_presence_validator_spec.rb @@ -9,6 +9,7 @@ module Decidim let(:record) do Class.new(Decidim::Form) do include TranslatableAttributes + mimic :participatory_process attribute :current_organization, Decidim::Organization translatable_attribute :description, String diff --git a/decidim-core/spec/validators/url_validator_spec.rb b/decidim-core/spec/validators/url_validator_spec.rb index 6c8cf5412d1c7..934d480ae0086 100644 --- a/decidim-core/spec/validators/url_validator_spec.rb +++ b/decidim-core/spec/validators/url_validator_spec.rb @@ -9,6 +9,7 @@ module Decidim let(:record) do Class.new(Decidim::Form) do include TranslatableAttributes + mimic :participatory_process attribute :url, String end.from_params(url:) diff --git a/decidim-debates/app/cells/decidim/debates/debate_l_cell.rb b/decidim-debates/app/cells/decidim/debates/debate_l_cell.rb index d8247398b5f09..1716578c5e84a 100644 --- a/decidim-debates/app/cells/decidim/debates/debate_l_cell.rb +++ b/decidim-debates/app/cells/decidim/debates/debate_l_cell.rb @@ -8,6 +8,7 @@ module Debates # for a given instance of a Debate class DebateLCell < Decidim::CardLCell include Decidim::SanitizeHelper + delegate :component_settings, to: :controller alias debate model diff --git a/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb b/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb index 1e96c3d69739b..ce2bec525f247 100644 --- a/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb +++ b/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb @@ -6,6 +6,7 @@ module Admin # This controller allows an admin to manage debates from a Participatory Space class DebatesController < Decidim::Debates::Admin::ApplicationController include Decidim::Admin::HasTrashableResources + helper Decidim::ApplicationHelper helper_method :debates diff --git a/decidim-demographics/app/controllers/decidim/demographics/admin/responses_controller.rb b/decidim-demographics/app/controllers/decidim/demographics/admin/responses_controller.rb index 896949388c64f..612f490ac4eba 100644 --- a/decidim-demographics/app/controllers/decidim/demographics/admin/responses_controller.rb +++ b/decidim-demographics/app/controllers/decidim/demographics/admin/responses_controller.rb @@ -5,6 +5,7 @@ module Demographics module Admin class ResponsesController < Admin::ApplicationController include Decidim::Forms::Admin::Concerns::HasQuestionnaireResponses + helper_method :questionnaire_for, :questionnaire def index diff --git a/decidim-demographics/app/controllers/decidim/demographics/application_controller.rb b/decidim-demographics/app/controllers/decidim/demographics/application_controller.rb index 1895b59df5adf..a2c03183f7647 100644 --- a/decidim-demographics/app/controllers/decidim/demographics/application_controller.rb +++ b/decidim-demographics/app/controllers/decidim/demographics/application_controller.rb @@ -4,6 +4,7 @@ module Decidim module Demographics class ApplicationController < Decidim::ApplicationController include FormFactory + register_permissions(::Decidim::Demographics::ApplicationController, ::Decidim::Demographics::Permissions, ::Decidim::Admin::Permissions, diff --git a/decidim-dev/app/models/decidim/dev/nested_dummy_resource.rb b/decidim-dev/app/models/decidim/dev/nested_dummy_resource.rb index c5554b04d7dd7..2fc309aefbe09 100644 --- a/decidim-dev/app/models/decidim/dev/nested_dummy_resource.rb +++ b/decidim-dev/app/models/decidim/dev/nested_dummy_resource.rb @@ -4,6 +4,7 @@ module Decidim module Dev class NestedDummyResource < ApplicationRecord include Decidim::Resourceable + belongs_to :dummy_resource end end diff --git a/decidim-dev/config/rubocop/ruby/disabled.yml b/decidim-dev/config/rubocop/ruby/disabled.yml index 141f7b83a324a..39bbf79de094b 100644 --- a/decidim-dev/config/rubocop/ruby/disabled.yml +++ b/decidim-dev/config/rubocop/ruby/disabled.yml @@ -104,3 +104,12 @@ Style/EmptyStringInsideInterpolation: Style/ComparableBetween: Enabled: false + +Style/FileOpen: + Enabled: false + +Style/ModuleMemberExistenceCheck: + Enabled: false + +Style/OneClassPerFile: + Enabled: false diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index f4d0dc1a669b0..39e314200f5db 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -53,7 +53,7 @@ Gem::Specification.new do |s| s.add_dependency "rspec_junit_formatter", "~> 0.6.0" s.add_dependency "rspec-rails", ">= 6", "< 9" s.add_dependency "rspec-retry", "~> 0.6.2" - s.add_dependency "rubocop", "~> 1.78.0" + s.add_dependency "rubocop", ">= 1.78", "< 1.86" s.add_dependency "rubocop-capybara", "~> 2.22.0", ">= 2.22.1" s.add_dependency "rubocop-factory_bot", ">= 2.27", "< 2.29" s.add_dependency "rubocop-faker", "~> 1.3", ">= 1.3.0" diff --git a/decidim-dev/lib/decidim/dev.rb b/decidim-dev/lib/decidim/dev.rb index 3da87466c2f06..1124c364c66bb 100644 --- a/decidim-dev/lib/decidim/dev.rb +++ b/decidim-dev/lib/decidim/dev.rb @@ -23,6 +23,7 @@ module Decidim # them. module Dev include ActiveSupport::Configurable + autoload :DummyTranslator, "decidim/dev/dummy_translator" # Public: Finds an asset. diff --git a/decidim-elections/app/commands/decidim/elections/admin/update_election.rb b/decidim-elections/app/commands/decidim/elections/admin/update_election.rb index 442dcc20dd341..21bf0ea7fac47 100644 --- a/decidim-elections/app/commands/decidim/elections/admin/update_election.rb +++ b/decidim-elections/app/commands/decidim/elections/admin/update_election.rb @@ -5,6 +5,7 @@ module Elections module Admin class UpdateElection < Decidim::Commands::UpdateResource include ::Decidim::GalleryMethods + fetch_form_attributes :title, :description, :start_at, :end_at, :results_availability def initialize(form, election) diff --git a/decidim-elections/app/models/decidim/elections/vote.rb b/decidim-elections/app/models/decidim/elections/vote.rb index 0c161cfdd34ec..22666c3a8afec 100644 --- a/decidim-elections/app/models/decidim/elections/vote.rb +++ b/decidim-elections/app/models/decidim/elections/vote.rb @@ -4,6 +4,7 @@ module Decidim module Elections class Vote < Elections::ApplicationRecord include Decidim::Traceable + belongs_to :question, class_name: "Decidim::Elections::Question", counter_cache: true, inverse_of: :votes belongs_to :response_option, class_name: "Decidim::Elections::ResponseOption", counter_cache: true, inverse_of: :votes diff --git a/decidim-elections/app/models/decidim/elections/voter.rb b/decidim-elections/app/models/decidim/elections/voter.rb index 0c039bb8351fc..a761c3bcf9568 100644 --- a/decidim-elections/app/models/decidim/elections/voter.rb +++ b/decidim-elections/app/models/decidim/elections/voter.rb @@ -4,6 +4,7 @@ module Decidim module Elections class Voter < Elections::ApplicationRecord include Decidim::Traceable + belongs_to :election, class_name: "Decidim::Elections::Election" validates :data, presence: true diff --git a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb index aad4906d80b32..a9c71bfe18698 100644 --- a/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb +++ b/decidim-forms/app/controllers/decidim/forms/admin/concerns/has_questionnaire.rb @@ -22,6 +22,7 @@ module HasQuestionnaire if defined?(Decidim::Templates::Admin::Concerns::Templatable) include Decidim::Templates::Admin::Concerns::Templatable + helper Decidim::DatalistSelectHelper def templatable_type diff --git a/decidim-forms/lib/decidim/api/questionnaire_entity_interface.rb b/decidim-forms/lib/decidim/api/questionnaire_entity_interface.rb index 69acbc15ddf64..415754f02ee6b 100644 --- a/decidim-forms/lib/decidim/api/questionnaire_entity_interface.rb +++ b/decidim-forms/lib/decidim/api/questionnaire_entity_interface.rb @@ -6,6 +6,7 @@ module Forms # The only requirement is to have an ID and the Type name be the class.name + Type module QuestionnaireEntityInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in objects with questionnaires" field :id, GraphQL::Types::ID, "ID of this entity", null: false diff --git a/decidim-forms/lib/decidim/forms/download_your_data_user_responses_serializer.rb b/decidim-forms/lib/decidim/forms/download_your_data_user_responses_serializer.rb index 3d05a1dbadd76..efe9dc88cb219 100644 --- a/decidim-forms/lib/decidim/forms/download_your_data_user_responses_serializer.rb +++ b/decidim-forms/lib/decidim/forms/download_your_data_user_responses_serializer.rb @@ -4,6 +4,7 @@ module Decidim module Forms class DownloadYourDataUserResponsesSerializer < Decidim::Exporters::Serializer include Decidim::TranslationsHelper + # Serializes an user response for download your data def serialize { diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 197a336f0213c..6346af2d9c7ca 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -142,7 +142,7 @@ PATH rspec-rails (>= 6, < 9) rspec-retry (~> 0.6.2) rspec_junit_formatter (~> 0.6.0) - rubocop (~> 1.78.0) + rubocop (>= 1.78, < 1.86) rubocop-capybara (~> 2.22.0, >= 2.22.1) rubocop-factory_bot (>= 2.27, < 2.29) rubocop-faker (~> 1.3, >= 1.3.0) @@ -504,6 +504,9 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.18.1) + json-schema (6.1.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) jwt (3.1.2) base64 kaminari (1.2.2) @@ -547,6 +550,8 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) + mcp (0.7.1) + json-schema (>= 4.1) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) @@ -766,15 +771,16 @@ GEM rspec-support (3.13.7) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.78.0) + rubocop (1.85.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) + mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.45.1, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) diff --git a/decidim-generators/lib/decidim/generators/test/generator_examples.rb b/decidim-generators/lib/decidim/generators/test/generator_examples.rb index 9bb24238c5398..3a4e4a1fadc37 100644 --- a/decidim-generators/lib/decidim/generators/test/generator_examples.rb +++ b/decidim-generators/lib/decidim/generators/test/generator_examples.rb @@ -27,7 +27,6 @@ Bundler.with_original_env { Decidim::GemManager.capture(command, env:) } end - # rubocop:disable RSpec/BeforeAfterAll before(:all) do Bundler.with_original_env { Decidim::GemManager.install_all(out: File::NULL) } end @@ -35,7 +34,6 @@ after(:all) do Bundler.with_original_env { Decidim::GemManager.uninstall_all(out: File::NULL) } end - # rubocop:enable RSpec/BeforeAfterAll end shared_examples_for "a new production application" do diff --git a/decidim-initiatives/app/commands/decidim/initiatives/update_initiative.rb b/decidim-initiatives/app/commands/decidim/initiatives/update_initiative.rb index 0b83d0e5e314e..da7a304b75b0b 100644 --- a/decidim-initiatives/app/commands/decidim/initiatives/update_initiative.rb +++ b/decidim-initiatives/app/commands/decidim/initiatives/update_initiative.rb @@ -8,6 +8,7 @@ class UpdateInitiative < Decidim::Command include ::Decidim::MultipleAttachmentsMethods include ::Decidim::GalleryMethods include CurrentLocale + delegate :current_user, to: :form # Public: Initializes the command. diff --git a/decidim-initiatives/app/controllers/concerns/decidim/initiatives/admin/initiative_admin.rb b/decidim-initiatives/app/controllers/concerns/decidim/initiatives/admin/initiative_admin.rb index d8ca4ad3453d0..3fbfdac0920ec 100644 --- a/decidim-initiatives/app/controllers/concerns/decidim/initiatives/admin/initiative_admin.rb +++ b/decidim-initiatives/app/controllers/concerns/decidim/initiatives/admin/initiative_admin.rb @@ -16,6 +16,7 @@ module InitiativeAdmin include NeedsInitiative include Decidim::Admin::ParticipatorySpaceAdminContext + participatory_space_admin_layout alias_method :current_participatory_space, :current_initiative diff --git a/decidim-initiatives/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb b/decidim-initiatives/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb index 1e64e7787849a..2c3bb4d9ff1ac 100644 --- a/decidim-initiatives/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb +++ b/decidim-initiatives/app/controllers/decidim/initiatives/admin/initiatives_types_controller.rb @@ -7,6 +7,7 @@ module Admin # organization. class InitiativesTypesController < Decidim::Initiatives::Admin::ApplicationController include Decidim::TranslatableAttributes + before_action :set_controller_breadcrumb, except: [:index, :new, :create] add_breadcrumb_item_from_menu :admin_initiatives_menu diff --git a/decidim-initiatives/app/controllers/decidim/initiatives/application_controller.rb b/decidim-initiatives/app/controllers/decidim/initiatives/application_controller.rb index 1f10352f1d081..c03cae78845b4 100644 --- a/decidim-initiatives/app/controllers/decidim/initiatives/application_controller.rb +++ b/decidim-initiatives/app/controllers/decidim/initiatives/application_controller.rb @@ -8,6 +8,7 @@ module Initiatives # this engine inherit. class ApplicationController < Decidim::ApplicationController include NeedsPermission + register_permissions(::Decidim::Initiatives::ApplicationController, ::Decidim::Initiatives::Permissions, ::Decidim::Admin::Permissions, diff --git a/decidim-initiatives/app/controllers/decidim/initiatives/versions_controller.rb b/decidim-initiatives/app/controllers/decidim/initiatives/versions_controller.rb index 98edb7d1930f3..ecabc111d6e4b 100644 --- a/decidim-initiatives/app/controllers/decidim/initiatives/versions_controller.rb +++ b/decidim-initiatives/app/controllers/decidim/initiatives/versions_controller.rb @@ -6,6 +6,7 @@ module Initiatives # has been updated through time. class VersionsController < Decidim::Initiatives::ApplicationController include ParticipatorySpaceContext + helper InitiativeHelper include NeedsInitiative diff --git a/decidim-initiatives/app/forms/decidim/initiatives/admin/initiative_form.rb b/decidim-initiatives/app/forms/decidim/initiatives/admin/initiative_form.rb index 102152b8bf50e..603f62f85c7ad 100644 --- a/decidim-initiatives/app/forms/decidim/initiatives/admin/initiative_form.rb +++ b/decidim-initiatives/app/forms/decidim/initiatives/admin/initiative_form.rb @@ -72,8 +72,8 @@ def area # Private: set the in-person signatures to zero for every scope def zero_offline_votes_with_scopes_names(model) - model.votable_initiative_type_scopes.each_with_object({}) do |initiative_scope_type, all_votes| - all_votes[initiative_scope_type.decidim_scopes_id || "global"] = [0, initiative_scope_type.scope_name] + model.votable_initiative_type_scopes.to_h do |initiative_scope_type| + [initiative_scope_type.decidim_scopes_id || "global", [0, initiative_scope_type.scope_name]] end end diff --git a/decidim-initiatives/db/migrate/20171109132011_enable_pg_trgm_extension_for_initiatives.rb b/decidim-initiatives/db/migrate/20171109132011_enable_pg_trgm_extension_for_initiatives.rb index 5f9574b695f42..2b2cacb51e38f 100644 --- a/decidim-initiatives/db/migrate/20171109132011_enable_pg_trgm_extension_for_initiatives.rb +++ b/decidim-initiatives/db/migrate/20171109132011_enable_pg_trgm_extension_for_initiatives.rb @@ -8,7 +8,7 @@ def change # required so that test suite works in ci env enable_extension "pg_trgm" rescue StandardError - raise <<-MSG.squish + raise <<~MSG.squish Decidim requires the pg_trgm extension to be enabled in your PostgreSQL. You can do so by running `CREATE EXTENSION IF NOT EXISTS "pg_trgm";` on the current DB as a PostgreSQL super user. diff --git a/decidim-initiatives/lib/decidim/api/initiative_type_interface.rb b/decidim-initiatives/lib/decidim/api/initiative_type_interface.rb index 57eb1469aaed7..dddd987062e58 100644 --- a/decidim-initiatives/lib/decidim/api/initiative_type_interface.rb +++ b/decidim-initiatives/lib/decidim/api/initiative_type_interface.rb @@ -6,6 +6,7 @@ module Initiatives module InitiativeTypeInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used in Initiative objects." field :initiative_type, Decidim::Initiatives::InitiativeApiType, "The object's initiative type", null: true, method: :type diff --git a/decidim-initiatives/lib/decidim/initiatives/application_form_pdf.rb b/decidim-initiatives/lib/decidim/initiatives/application_form_pdf.rb index b6ef77eac5436..aa969e25b9ec4 100644 --- a/decidim-initiatives/lib/decidim/initiatives/application_form_pdf.rb +++ b/decidim-initiatives/lib/decidim/initiatives/application_form_pdf.rb @@ -6,6 +6,7 @@ module Decidim module Initiatives class ApplicationFormPDF include Decidim::OrganizationHelper + def initialize(initiative) @initiative = initiative end diff --git a/decidim-meetings/app/cells/decidim/meetings/question_responses_cell.rb b/decidim-meetings/app/cells/decidim/meetings/question_responses_cell.rb index 0daf3ae012b06..9af17cb018d3a 100644 --- a/decidim-meetings/app/cells/decidim/meetings/question_responses_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/question_responses_cell.rb @@ -22,7 +22,7 @@ def response_options_with_percentages # # This calculation is a bit complex because of multiple option responses question_responses_choices = Decidim::Meetings::ResponseOption.where(decidim_question_id: model.id) - .joins([choices: :response]) + .joins([{ choices: :response }]) .group(Arel.sql("#{responses_table_name}.id, #{response_options_table_name}.id")) .select(<<~SELECT #{response_options_table_name}.id AS id, diff --git a/decidim-meetings/app/serializers/decidim/meetings/registration_serializer.rb b/decidim-meetings/app/serializers/decidim/meetings/registration_serializer.rb index 2cdae411418c8..0fd6447761819 100644 --- a/decidim-meetings/app/serializers/decidim/meetings/registration_serializer.rb +++ b/decidim-meetings/app/serializers/decidim/meetings/registration_serializer.rb @@ -4,6 +4,7 @@ module Decidim module Meetings class RegistrationSerializer < Decidim::Exporters::Serializer include Decidim::TranslationsHelper + # Serializes a registration def serialize { diff --git a/decidim-meetings/lib/decidim/api/linked_resources_interface.rb b/decidim-meetings/lib/decidim/api/linked_resources_interface.rb index b8a00daf4bcf1..b985bef6395f3 100644 --- a/decidim-meetings/lib/decidim/api/linked_resources_interface.rb +++ b/decidim-meetings/lib/decidim/api/linked_resources_interface.rb @@ -5,6 +5,7 @@ module Meetings # This interface represents all linked resources available in the module meetings module LinkedResourcesInterface include Decidim::Api::Types::BaseInterface + graphql_name "MeetingsLinkedResourcesInterface" description "An interface that can be used with Resourceable models." diff --git a/decidim-meetings/lib/decidim/api/services_interface.rb b/decidim-meetings/lib/decidim/api/services_interface.rb index 756c8e660159b..dff8b7ccc02dd 100644 --- a/decidim-meetings/lib/decidim/api/services_interface.rb +++ b/decidim-meetings/lib/decidim/api/services_interface.rb @@ -5,6 +5,7 @@ module Meetings # This interface represents a categorizable object. module ServicesInterface include Decidim::Api::Types::BaseInterface + description "An interface that can be used with services." field :services, [Decidim::Meetings::ServiceType, { null: true }], "The object's services", null: false diff --git a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/concerns/participatory_process_admin.rb b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/concerns/participatory_process_admin.rb index 320dda5af3618..55920b67d2c26 100644 --- a/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/concerns/participatory_process_admin.rb +++ b/decidim-participatory_processes/app/controllers/decidim/participatory_processes/admin/concerns/participatory_process_admin.rb @@ -19,6 +19,7 @@ module ParticipatoryProcessAdmin included do include Decidim::Admin::ParticipatorySpaceAdminContext + helper_method :current_participatory_process add_breadcrumb_item_from_menu :admin_participatory_process_menu diff --git a/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/admin_users.rb b/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/admin_users.rb index d000946635e53..cefd91db5811a 100644 --- a/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/admin_users.rb +++ b/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/admin_users.rb @@ -43,7 +43,7 @@ def query def processes_user_admins Decidim::User.where( id: Decidim::ParticipatoryProcessUserRole.where(participatory_process: processes, role: :admin) - .select(:decidim_user_id) + .select(:decidim_user_id) ) end diff --git a/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/moderators.rb b/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/moderators.rb index dabc4b3dcba0a..5f573fb55d600 100644 --- a/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/moderators.rb +++ b/decidim-participatory_processes/app/queries/decidim/participatory_processes/admin/moderators.rb @@ -44,7 +44,7 @@ def query def processes_user_admins Decidim::User.where( id: Decidim::ParticipatoryProcessUserRole.where(participatory_process: processes) - .where.not(role: :collaborator) + .where.not(role: :collaborator) .select(:decidim_user_id) ) end diff --git a/decidim-proposals/app/commands/decidim/proposals/admin/merge_proposals.rb b/decidim-proposals/app/commands/decidim/proposals/admin/merge_proposals.rb index 5d70da589393c..faac2a8144ceb 100644 --- a/decidim-proposals/app/commands/decidim/proposals/admin/merge_proposals.rb +++ b/decidim-proposals/app/commands/decidim/proposals/admin/merge_proposals.rb @@ -7,6 +7,7 @@ module Admin # one component to another. class MergeProposals < Decidim::Command include ::Decidim::MultipleAttachmentsMethods + # Public: Initializes the command. # # form - A form object with the params. diff --git a/decidim-proposals/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb b/decidim-proposals/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb index 85322bbc9fa7f..0f436615ce1c2 100644 --- a/decidim-proposals/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb +++ b/decidim-proposals/app/commands/decidim/proposals/admin/update_proposal_taxonomies.rb @@ -6,6 +6,7 @@ module Admin # A command with all the business logic when an admin batch updates proposals taxonomies. class UpdateProposalTaxonomies < UpdateResourcesTaxonomies include TranslatableAttributes + # Public: Initializes the command. # # taxonomy_ids - the taxonomy ids to update diff --git a/decidim-proposals/app/forms/decidim/proposals/admin/proposals_import_form.rb b/decidim-proposals/app/forms/decidim/proposals/admin/proposals_import_form.rb index 47010b4acfc5c..a8612379e668c 100644 --- a/decidim-proposals/app/forms/decidim/proposals/admin/proposals_import_form.rb +++ b/decidim-proposals/app/forms/decidim/proposals/admin/proposals_import_form.rb @@ -7,6 +7,7 @@ module Admin # from another component. class ProposalsImportForm < Decidim::Form include TranslatableAttributes + mimic :proposals_import attribute :origin_component_id, Integer diff --git a/decidim-proposals/app/forms/decidim/proposals/admin/proposals_merge_form.rb b/decidim-proposals/app/forms/decidim/proposals/admin/proposals_merge_form.rb index e3ce715b6dd8a..e7ade3549d2e5 100644 --- a/decidim-proposals/app/forms/decidim/proposals/admin/proposals_merge_form.rb +++ b/decidim-proposals/app/forms/decidim/proposals/admin/proposals_merge_form.rb @@ -8,6 +8,7 @@ module Admin class ProposalsMergeForm < ProposalBaseForm include Decidim::HasUploadValidations include Decidim::AttachmentAttributes + translatable_attribute :title, String do |field, _locale| validates field, length: { in: 15..150 }, if: proc { |resource| resource.send(field).present? } end diff --git a/decidim-proposals/app/models/decidim/proposals/proposal.rb b/decidim-proposals/app/models/decidim/proposals/proposal.rb index 626515d42b113..f3eb362997f3b 100644 --- a/decidim-proposals/app/models/decidim/proposals/proposal.rb +++ b/decidim-proposals/app/models/decidim/proposals/proposal.rb @@ -398,13 +398,13 @@ def self.ransack(params = {}, options = {}) # method to filter by assigned evaluator role ID def self.evaluator_role_ids_has(value) - query = <<-SQL.squish - :value = any( - (SELECT decidim_proposals_evaluation_assignments.evaluator_role_id - FROM decidim_proposals_evaluation_assignments - WHERE decidim_proposals_evaluation_assignments.decidim_proposal_id = decidim_proposals_proposals.id + query = <<~SQL.squish + :value = any( + (SELECT decidim_proposals_evaluation_assignments.evaluator_role_id + FROM decidim_proposals_evaluation_assignments + WHERE decidim_proposals_evaluation_assignments.decidim_proposal_id = decidim_proposals_proposals.id + ) ) - ) SQL where(query, value:) end @@ -457,14 +457,14 @@ def self.sort_by_translated_title_desc end ransacker :is_emendation do |_parent| - query = <<-SQL.squish - ( - SELECT EXISTS ( - SELECT 1 FROM decidim_amendments - WHERE decidim_amendments.decidim_emendation_type = 'Decidim::Proposals::Proposal' - AND decidim_amendments.decidim_emendation_id = decidim_proposals_proposals.id + query = <<~SQL.squish + ( + SELECT EXISTS ( + SELECT 1 FROM decidim_amendments + WHERE decidim_amendments.decidim_emendation_type = 'Decidim::Proposals::Proposal' + AND decidim_amendments.decidim_emendation_id = decidim_proposals_proposals.id + ) ) - ) SQL Arel.sql(query) end diff --git a/decidim-proposals/db/migrate/20171212102250_enable_pg_trgm_extension_for_proposals.rb b/decidim-proposals/db/migrate/20171212102250_enable_pg_trgm_extension_for_proposals.rb index 82d3b9a4c9b4f..c9c6bcbfc31cd 100644 --- a/decidim-proposals/db/migrate/20171212102250_enable_pg_trgm_extension_for_proposals.rb +++ b/decidim-proposals/db/migrate/20171212102250_enable_pg_trgm_extension_for_proposals.rb @@ -8,7 +8,7 @@ def change # required so that test suite works in ci env enable_extension "pg_trgm" rescue StandardError - raise <<-MSG.squish + raise <<~MSG.squish Decidim requires the pg_trgm extension to be enabled in your PostgreSQL. You can do so by running `CREATE EXTENSION IF NOT EXISTS "pg_trgm";` on the current DB as a PostgreSQL super user. diff --git a/decidim-proposals/db/migrate/20200212120110_sync_proposals_state_with_amendments_state.rb b/decidim-proposals/db/migrate/20200212120110_sync_proposals_state_with_amendments_state.rb index 698f60311f54e..e61d2d68ce044 100644 --- a/decidim-proposals/db/migrate/20200212120110_sync_proposals_state_with_amendments_state.rb +++ b/decidim-proposals/db/migrate/20200212120110_sync_proposals_state_with_amendments_state.rb @@ -2,7 +2,7 @@ class SyncProposalsStateWithAmendmentsState < ActiveRecord::Migration[5.2] def up - execute <<-SQL.squish + execute <<~SQL.squish UPDATE decidim_proposals_proposals AS proposals SET state = amendments.state FROM decidim_amendments AS amendments @@ -15,7 +15,7 @@ def up end def down - execute <<-SQL.squish + execute <<~SQL.squish UPDATE decidim_proposals_proposals AS proposals SET state = NULL FROM decidim_amendments AS amendments diff --git a/decidim-proposals/db/migrate/20200306123652_publish_existing_proposals_state.rb b/decidim-proposals/db/migrate/20200306123652_publish_existing_proposals_state.rb index 0e6a37d0366c9..69b39bbb0f5df 100644 --- a/decidim-proposals/db/migrate/20200306123652_publish_existing_proposals_state.rb +++ b/decidim-proposals/db/migrate/20200306123652_publish_existing_proposals_state.rb @@ -2,13 +2,13 @@ class PublishExistingProposalsState < ActiveRecord::Migration[5.2] def up - execute <<-SQL.squish + execute <<~SQL.squish UPDATE decidim_proposals_proposals SET state_published_at = COALESCE(answered_at, published_at) WHERE state IS NOT NULL SQL end def down - execute <<-SQL.squish + execute <<~SQL.squish UPDATE decidim_proposals_proposals SET state_published_at = NULL SQL end diff --git a/decidim-proposals/spec/commands/decidim/proposals/admin/create_proposal_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/admin/create_proposal_spec.rb index f596535fdcff9..4146c45b5ef5d 100644 --- a/decidim-proposals/spec/commands/decidim/proposals/admin/create_proposal_spec.rb +++ b/decidim-proposals/spec/commands/decidim/proposals/admin/create_proposal_spec.rb @@ -203,7 +203,7 @@ module Admin let(:component) { create(:proposal_component, :with_attachments_allowed) } let(:uploaded_files) do [ - file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } ] end diff --git a/decidim-proposals/spec/commands/decidim/proposals/admin/update_proposal_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/admin/update_proposal_spec.rb index ee6bbcddcbe7b..b8bd738506565 100644 --- a/decidim-proposals/spec/commands/decidim/proposals/admin/update_proposal_spec.rb +++ b/decidim-proposals/spec/commands/decidim/proposals/admin/update_proposal_spec.rb @@ -116,7 +116,7 @@ let(:component) { create(:proposal_component, :with_attachments_allowed) } let(:uploaded_files) do [ - file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } ] end diff --git a/decidim-proposals/spec/commands/decidim/proposals/update_proposal_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/update_proposal_spec.rb index 1a0f380f525b2..6df0388520b10 100644 --- a/decidim-proposals/spec/commands/decidim/proposals/update_proposal_spec.rb +++ b/decidim-proposals/spec/commands/decidim/proposals/update_proposal_spec.rb @@ -146,7 +146,7 @@ module Proposals let(:component) { create(:proposal_component, :with_attachments_allowed) } let(:uploaded_files) do [ - file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } ] end diff --git a/decidim-proposals/spec/lib/decidim/proposals/download_your_data_proposal_serializer_spec.rb b/decidim-proposals/spec/lib/decidim/proposals/download_your_data_proposal_serializer_spec.rb index feab4c95e11d9..91133740bcd9e 100644 --- a/decidim-proposals/spec/lib/decidim/proposals/download_your_data_proposal_serializer_spec.rb +++ b/decidim-proposals/spec/lib/decidim/proposals/download_your_data_proposal_serializer_spec.rb @@ -27,12 +27,12 @@ module Proposals let(:expected_answer) do answer = proposal.answer - Decidim.available_locales.each_with_object({}) do |locale, result| - result[locale.to_s] = if answer.is_a?(Hash) - answer[locale.to_s] || "" - else - "" - end + Decidim.available_locales.to_h do |locale| + [locale.to_s, if answer.is_a?(Hash) + answer[locale.to_s] || "" + else + "" + end] end end diff --git a/decidim-proposals/spec/lib/decidim/proposals/proposal_serializer_spec.rb b/decidim-proposals/spec/lib/decidim/proposals/proposal_serializer_spec.rb index eb47bd9270931..faa033eb10cc2 100644 --- a/decidim-proposals/spec/lib/decidim/proposals/proposal_serializer_spec.rb +++ b/decidim-proposals/spec/lib/decidim/proposals/proposal_serializer_spec.rb @@ -27,12 +27,12 @@ module Proposals let(:expected_answer) do answer = proposal.answer - Decidim.available_locales.each_with_object({}) do |locale, result| - result[locale.to_s] = if answer.is_a?(Hash) - answer[locale.to_s] || "" - else - "" - end + Decidim.available_locales.to_h do |locale| + [locale.to_s, if answer.is_a?(Hash) + answer[locale.to_s] || "" + else + "" + end] end end diff --git a/decidim-proposals/spec/system/proposals_spec.rb b/decidim-proposals/spec/system/proposals_spec.rb index 2b41406dc7962..16c7da68b9687 100644 --- a/decidim-proposals/spec/system/proposals_spec.rb +++ b/decidim-proposals/spec/system/proposals_spec.rb @@ -4,6 +4,7 @@ describe "Proposals" do include ActionView::Helpers::TextHelper + include_context "with a component" let(:manifest_name) { "proposals" } diff --git a/decidim-surveys/app/mailers/decidim/surveys/survey_confirmation_mailer.rb b/decidim-surveys/app/mailers/decidim/surveys/survey_confirmation_mailer.rb index c44435e1879e6..eea57639d5767 100644 --- a/decidim-surveys/app/mailers/decidim/surveys/survey_confirmation_mailer.rb +++ b/decidim-surveys/app/mailers/decidim/surveys/survey_confirmation_mailer.rb @@ -4,6 +4,7 @@ module Decidim module Surveys class SurveyConfirmationMailer < ApplicationMailer include TranslatableAttributes + helper Decidim::SanitizeHelper def confirmation(user, questionnaire, responses) diff --git a/decidim-system/app/forms/decidim/system/register_organization_form.rb b/decidim-system/app/forms/decidim/system/register_organization_form.rb index ab099af92e6a3..7208d62b5fd02 100644 --- a/decidim-system/app/forms/decidim/system/register_organization_form.rb +++ b/decidim-system/app/forms/decidim/system/register_organization_form.rb @@ -8,6 +8,7 @@ module System # class RegisterOrganizationForm < BaseOrganizationForm include JsonbAttributes + mimic :organization attribute :name, String diff --git a/decidim-system/lib/decidim/system/menu.rb b/decidim-system/lib/decidim/system/menu.rb index 2e2a2a741e0ac..abfe75e58dd95 100644 --- a/decidim-system/lib/decidim/system/menu.rb +++ b/decidim-system/lib/decidim/system/menu.rb @@ -9,7 +9,7 @@ def self.register_system_menu! I18n.t("menu.dashboard", scope: "decidim.system"), decidim_system.root_path, position: 1, - active: ["decidim/system/dashboard" => :show] + active: [{ "decidim/system/dashboard" => :show }] if Decidim.module_installed?(:api) menu.add_item :api_credentials, I18n.t("menu.api_credentials", scope: "decidim.system"), diff --git a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb index 4fb6338131ed7..6de2ef845cd04 100644 --- a/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb +++ b/decidim-templates/app/controllers/decidim/templates/admin/questionnaire_templates_controller.rb @@ -8,6 +8,7 @@ module Admin class QuestionnaireTemplatesController < Decidim::Templates::Admin::ApplicationController include Decidim::TranslatableAttributes include Decidim::Forms::Admin::Concerns::HasQuestionnaire + helper Decidim::Forms::Admin::ApplicationHelper helper_method :template, :questionnaire diff --git a/decidim-verifications/app/controllers/concerns/decidim/verifications/renewable.rb b/decidim-verifications/app/controllers/concerns/decidim/verifications/renewable.rb index 9a30d62aa03e5..ac840e2cbaf1a 100644 --- a/decidim-verifications/app/controllers/concerns/decidim/verifications/renewable.rb +++ b/decidim-verifications/app/controllers/concerns/decidim/verifications/renewable.rb @@ -7,6 +7,7 @@ module Verifications # Common logic to renew authorizations module Renewable extend ActiveSupport::Concern + included do def renew enforce_permission_to(:renew, :authorization, authorization:) diff --git a/decidim-verifications/app/controllers/decidim/verifications/authorizations_controller.rb b/decidim-verifications/app/controllers/decidim/verifications/authorizations_controller.rb index 4992600476ba7..942d8617c9e40 100644 --- a/decidim-verifications/app/controllers/decidim/verifications/authorizations_controller.rb +++ b/decidim-verifications/app/controllers/decidim/verifications/authorizations_controller.rb @@ -14,6 +14,7 @@ class AuthorizationsController < Verifications::ApplicationController include Decidim::UserProfile include Decidim::HtmlSafeFlash include Decidim::Verifications::Renewable + helper Decidim::DecidimFormHelper helper Decidim::AuthorizationFormHelper helper Decidim::TranslationsHelper diff --git a/decidim-verifications/app/forms/decidim/verifications/csv_census/admin/census_data_form.rb b/decidim-verifications/app/forms/decidim/verifications/csv_census/admin/census_data_form.rb index ce5fc4e0149e2..a84c16a5b99a9 100644 --- a/decidim-verifications/app/forms/decidim/verifications/csv_census/admin/census_data_form.rb +++ b/decidim-verifications/app/forms/decidim/verifications/csv_census/admin/census_data_form.rb @@ -8,6 +8,7 @@ module Admin class CensusDataForm < Form include Decidim::HasUploadValidations include Decidim::ProcessesFileLocally + mimic :census_data attribute :file, Decidim::Attributes::Blob diff --git a/decidim-verifications/app/forms/decidim/verifications/id_documents/admin/config_form.rb b/decidim-verifications/app/forms/decidim/verifications/id_documents/admin/config_form.rb index ca7650af33a42..61676037af721 100644 --- a/decidim-verifications/app/forms/decidim/verifications/id_documents/admin/config_form.rb +++ b/decidim-verifications/app/forms/decidim/verifications/id_documents/admin/config_form.rb @@ -7,6 +7,7 @@ module IdDocuments module Admin class ConfigForm < Decidim::Form include TranslatableAttributes + mimic :config attribute :offline, Boolean diff --git a/decidim-verifications/lib/decidim/verifications.rb b/decidim-verifications/lib/decidim/verifications.rb index e77044817f288..8e09001ce910f 100644 --- a/decidim-verifications/lib/decidim/verifications.rb +++ b/decidim-verifications/lib/decidim/verifications.rb @@ -28,6 +28,7 @@ def self.authorization_handlers module Verifications include ActiveSupport::Configurable + config_accessor :document_types do Decidim::Env.new("VERIFICATIONS_DOCUMENT_TYPES", "identification_number,passport").to_array end From 8f8df45fb04890132af36465d3429cdc49dd6d86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:52:08 +0200 Subject: [PATCH 050/135] Bump to dependencies: Bump acts_as_list from 1.2.4 to 1.2.6 (#16285) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c0cb60bbc86fe..258ae7a8c3efb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -281,7 +281,7 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - acts_as_list (1.2.4) + acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) addressable (2.8.8) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 6346af2d9c7ca..eaf39e0ce2826 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -281,7 +281,7 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - acts_as_list (1.2.4) + acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) addressable (2.8.9) From bb2016dcab25cd21f30f7ffdbe48277c1f9cc671 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:35:42 +0200 Subject: [PATCH 051/135] Bump to dependencies: Bump selenium-webdriver from 4.27.0 to 4.41.0 (#16286) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-generators/Gemfile.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 258ae7a8c3efb..cff2773322d64 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -844,11 +844,11 @@ GEM sass-embedded (1.97.3-x86_64-linux-gnu) google-protobuf (~> 4.31) securerandom (0.4.1) - selenium-webdriver (4.27.0) + selenium-webdriver (4.41.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) selma (0.4.15-arm64-darwin) selma (0.4.15-x86_64-linux) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index eaf39e0ce2826..b560b56622928 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -834,11 +834,11 @@ GEM sass-embedded (1.97.3-x86_64-linux-gnu) google-protobuf (~> 4.31) securerandom (0.4.1) - selenium-webdriver (4.27.0) + selenium-webdriver (4.41.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) selma (0.4.15-x86_64-linux) semantic_range (3.1.0) From fe258032e9b3d16ff4c0867af934de78a0786eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 2 Mar 2026 20:51:49 +0100 Subject: [PATCH 052/135] Change header breadcrumb color to match primary color (#16281) --- decidim-core/app/packs/stylesheets/decidim/_header.scss | 6 +++--- decidim-core/app/packs/stylesheets/decidim/_layout.scss | 2 +- .../decidim/header/_follow_space_menu_bar_button.html.erb | 4 ++-- .../decidim/header/_menu_breadcrumb_desktop.html.erb | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_header.scss b/decidim-core/app/packs/stylesheets/decidim/_header.scss index 1e90c72c0fae2..c606f3eb57b27 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_header.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_header.scss @@ -323,11 +323,11 @@ header { @apply container h-full flex justify-between items-center sm:relative last-of-type:[&>svg]:hidden; &__container { - @apply bg-white relative h-14 flex justify-between; + @apply bg-primary relative h-14 flex justify-between; } &__breadcrumb-desktop { - @apply hidden lg:flex justify-between items-center [&>*]:text-md [&>*]:text-black gap-x-2; + @apply hidden lg:flex justify-between items-center [&>*]:text-md [&>*]:text-white gap-x-2; .no-interactive { @apply font-normal px-0; @@ -420,7 +420,7 @@ header { @apply block lg:hidden w-full z-20; &__dropdown-trigger { - @apply flex items-center justify-between text-black; + @apply flex items-center justify-between text-white; svg { @apply w-6 h-6 fill-current; diff --git a/decidim-core/app/packs/stylesheets/decidim/_layout.scss b/decidim-core/app/packs/stylesheets/decidim/_layout.scss index e5ad2e758dc3b..3968820e6ea5c 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_layout.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_layout.scss @@ -6,7 +6,7 @@ } [data-content] { - @apply relative flex flex-col flex-1 border-t-neutral-100 border-t; + @apply relative flex flex-col flex-1; } } diff --git a/decidim-core/app/views/layouts/decidim/header/_follow_space_menu_bar_button.html.erb b/decidim-core/app/views/layouts/decidim/header/_follow_space_menu_bar_button.html.erb index 797687e6efcde..24fa2648ac10d 100644 --- a/decidim-core/app/views/layouts/decidim/header/_follow_space_menu_bar_button.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_follow_space_menu_bar_button.html.erb @@ -1,7 +1,7 @@ <%= content_for :participatory_space_actions do %> - <%= cell("decidim/follow_button", participatory_space, button_classes: "button button__sm button__transparent-secondary") %> + <%= cell("decidim/follow_button", participatory_space, button_classes: "button button__sm button__transparent") %> <% end %> <%= content_for :participatory_space_mobile_actions do %> - <%= cell("decidim/follow_button", participatory_space, button_classes: "button button__sm button__transparent-secondary", mobile: true) %> + <%= cell("decidim/follow_button", participatory_space, button_classes: "button button__sm button__transparent", mobile: true) %> <% end %> diff --git a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb index f82029021eacb..bc0fdefd28e5b 100644 --- a/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +++ b/decidim-core/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb @@ -5,7 +5,7 @@ <% next if item.blank? %> <% item_label = translated_attribute(item[:label]) %> - + <%= link_to_if(item[:url].present? && !is_active_link?(item[:url], :exclusive), item_label, item[:url], class: "menu-bar__breadcrumb-desktop__dropdown-wrapper menu-bar__breadcrumb-desktop__dropdown-trigger", "aria-current": (item[:active] ? "page" : nil)) do %> <%# This block is executed if the condition is false %> <%= content_tag :span, item_label, class: "menu-bar__breadcrumb-desktop__dropdown-trigger no-interactive", tabindex: "0", "aria-current": (item[:active] ? "page" : nil) %> From 6eaab26cbc4738ffe318867d59c8c12bd79b0e3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:00:20 +0200 Subject: [PATCH 053/135] Bump to dependencies: Bump rubocop-rspec from 3.7.0 to 3.9.0 (#16287) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 6 +++--- .../decidim/debates/update_debate_spec.rb | 1 - decidim-generators/Gemfile.lock | 4 ++-- .../admin/admin_manages_initiatives_spec.rb | 19 +++++++------------ .../admin/admin_manages_meetings_spec.rb | 1 - .../spec/system/admin/filter_meetings_spec.rb | 6 ++---- .../system/admin/filter_proposals_spec.rb | 6 ++---- 7 files changed, 16 insertions(+), 27 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cff2773322d64..173fdb689ab94 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -284,7 +284,7 @@ GEM acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) - addressable (2.8.8) + addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) @@ -817,9 +817,9 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.7.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.81) rubocop-rspec_rails (2.32.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) diff --git a/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb b/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb index 7987add432acd..e697af9d1d449 100644 --- a/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb +++ b/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb @@ -125,7 +125,6 @@ expect do subject.call debate.reload - pp form.errors end.to change(debate.attachments, :count).by(2) debate_attachments = debate.attachments diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index b560b56622928..5c11f0d65b7a3 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -809,9 +809,9 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.7.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.81) rubocop-rspec_rails (2.32.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) diff --git a/decidim-initiatives/spec/system/admin/admin_manages_initiatives_spec.rb b/decidim-initiatives/spec/system/admin/admin_manages_initiatives_spec.rb index f15ed92ff69d0..252d917c43a09 100644 --- a/decidim-initiatives/spec/system/admin/admin_manages_initiatives_spec.rb +++ b/decidim-initiatives/spec/system/admin/admin_manages_initiatives_spec.rb @@ -58,10 +58,8 @@ def initiative_without_area(area) describe "listing initiatives" do STATES.each do |state| - i18n_state = I18n.t(state, scope: "decidim.admin.filters.initiatives.state_eq.values") - - context "when filtering collection by state: #{i18n_state}" do - it_behaves_like "a filtered collection", options: "State", filter: i18n_state do + context "when filtering collection by state: #{I18n.t(state, scope: "decidim.admin.filters.initiatives.state_eq.values")}" do + it_behaves_like "a filtered collection", options: "State", filter: I18n.t(state, scope: "decidim.admin.filters.initiatives.state_eq.values") do let(:in_filter) { translated(initiative_with_state(state).title) } let(:not_in_filter) { translated(initiative_without_state(state).title) } end @@ -69,16 +67,15 @@ def initiative_without_area(area) end Decidim::InitiativesTypeScope.all.each do |scoped_type| - type = scoped_type.type - i18n_type = type.title[I18n.locale.to_s] + let(:type) { scoped_type.type } - context "when filtering collection by type: #{i18n_type}" do + context "when filtering collection by type: #{scoped_type.type.title[I18n.locale.to_s]}" do before do create(:initiative, organization:, scoped_type: scoped_type1) create(:initiative, organization:, scoped_type: scoped_type2) end - it_behaves_like "a filtered collection", options: "Type", filter: i18n_type do + it_behaves_like "a filtered collection", options: "Type", filter: scoped_type.type.title[I18n.locale.to_s] do let(:in_filter) { translated(initiative_with_type(type).title) } let(:not_in_filter) { translated(initiative_without_type(type).title) } end @@ -92,15 +89,13 @@ def initiative_without_area(area) end Decidim::Area.all.each do |area| - i18n_area = area.name[I18n.locale.to_s] - - context "when filtering collection by area: #{i18n_area}" do + context "when filtering collection by area: #{area.name[I18n.locale.to_s]}" do before do create(:initiative, organization:, area: area1) create(:initiative, organization:, area: area2) end - it_behaves_like "a filtered collection", options: "Area", filter: i18n_area do + it_behaves_like "a filtered collection", options: "Area", filter: area.name[I18n.locale.to_s] do let(:in_filter) { translated(initiative_with_area(area).title) } let(:not_in_filter) { translated(initiative_without_area(area).title) } end diff --git a/decidim-meetings/spec/system/admin/admin_manages_meetings_spec.rb b/decidim-meetings/spec/system/admin/admin_manages_meetings_spec.rb index 306cdd8a53eca..9590085050add 100644 --- a/decidim-meetings/spec/system/admin/admin_manages_meetings_spec.rb +++ b/decidim-meetings/spec/system/admin/admin_manages_meetings_spec.rb @@ -410,7 +410,6 @@ expect(page).to have_callout("Meeting successfully created. Notice this is unpublished yet, you need to manually publish it.") new_meeting = Decidim::Meetings::Meeting.last - puts "Meeting location: #{new_meeting.location}" expect(new_meeting.location.values).to all(be_blank) expect(new_meeting.address).to be_empty end diff --git a/decidim-meetings/spec/system/admin/filter_meetings_spec.rb b/decidim-meetings/spec/system/admin/filter_meetings_spec.rb index 70abcb86e05a1..1b6e7a7cb7d23 100644 --- a/decidim-meetings/spec/system/admin/filter_meetings_spec.rb +++ b/decidim-meetings/spec/system/admin/filter_meetings_spec.rb @@ -38,10 +38,8 @@ def meeting_without_type(type) before { visit_component_admin } TYPES.each do |state| - i18n_state = I18n.t(state, scope: "decidim.admin.filters.meetings.with_any_type.values") - - context "when filtering meetings by type: #{i18n_state}" do - it_behaves_like "a filtered collection", options: "Type", filter: i18n_state do + context "when filtering meetings by type: #{I18n.t(state, scope: "decidim.admin.filters.meetings.with_any_type.values")}" do + it_behaves_like "a filtered collection", options: "Type", filter: I18n.t(state, scope: "decidim.admin.filters.meetings.with_any_type.values") do let(:in_filter) { translated(meeting_with_type(state).title) } let(:not_in_filter) { translated(meeting_without_type(state).title) } end diff --git a/decidim-proposals/spec/system/admin/filter_proposals_spec.rb b/decidim-proposals/spec/system/admin/filter_proposals_spec.rb index 2003cd3c4b8ed..8253c594555a6 100644 --- a/decidim-proposals/spec/system/admin/filter_proposals_spec.rb +++ b/decidim-proposals/spec/system/admin/filter_proposals_spec.rb @@ -56,10 +56,8 @@ def proposal_without_state(token) before { visit_component_admin } STATES.each do |state| - i18n_state = I18n.t(state, scope: "decidim.admin.filters.proposals.state_eq.values") - - context "when filtering proposals by state: #{i18n_state}" do - it_behaves_like "a filtered collection", options: "State", filter: i18n_state do + context "when filtering proposals by state: #{I18n.t(state, scope: "decidim.admin.filters.proposals.state_eq.values")}" do + it_behaves_like "a filtered collection", options: "State", filter: I18n.t(state, scope: "decidim.admin.filters.proposals.state_eq.values") do let(:in_filter) { translated(proposal_with_state(state).title) } let(:not_in_filter) { translated(proposal_without_state(state).title) } end From b46b9f5484fb004bd82c832b5119812a7b6111d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:47:53 +0200 Subject: [PATCH 054/135] Bump to dependencies: Bump rubocop-performance from 1.25.0 to 1.26.1 (#16290) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-generators/Gemfile.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 173fdb689ab94..f58260938e184 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -807,10 +807,10 @@ GEM rubocop-graphql (1.5.6) lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2) - rubocop-performance (1.25.0) + rubocop-performance (1.26.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) rubocop-rails (2.32.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 5c11f0d65b7a3..431d03df0522d 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -799,10 +799,10 @@ GEM rubocop-graphql (1.5.6) lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2) - rubocop-performance (1.25.0) + rubocop-performance (1.26.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) rubocop-rails (2.32.0) activesupport (>= 4.2.0) lint_roller (~> 1.1) From eec57d0edc46cf6aa5de03e5c13088caab0ede1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:03:35 +0200 Subject: [PATCH 055/135] Bump to dependencies: Bump omniauth-rails_csrf_protection from 1.0.2 to 2.0.1 (#16291) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f58260938e184..a6a2481c62bff 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,7 +88,7 @@ PATH omniauth (~> 2.0) omniauth-facebook (~> 5.0) omniauth-google-oauth2 (~> 1.0) - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (>= 1, < 3) omniauth-twitter (~> 1.4) paper_trail (~> 16.0) paranoia (~> 3.0.0) @@ -620,7 +620,7 @@ GEM omniauth-oauth2 (1.9.0) oauth2 (>= 2.0.2, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.2) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-twitter (1.4.0) diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 727e917c4d60f..68d0095fa9c53 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -61,7 +61,7 @@ Gem::Specification.new do |s| s.add_dependency "omniauth", "~> 2.0" s.add_dependency "omniauth-facebook", "~> 5.0" s.add_dependency "omniauth-google-oauth2", "~> 1.0" - s.add_dependency "omniauth-rails_csrf_protection", "~> 1.0" + s.add_dependency "omniauth-rails_csrf_protection", ">= 1", "< 3" s.add_dependency "omniauth-twitter", "~> 1.4" s.add_dependency "paper_trail", "~> 16.0" s.add_dependency "paranoia", "~> 3.0.0" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 431d03df0522d..d7fcf7a87a5b9 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -88,7 +88,7 @@ PATH omniauth (~> 2.0) omniauth-facebook (~> 5.0) omniauth-google-oauth2 (~> 1.0) - omniauth-rails_csrf_protection (~> 1.0) + omniauth-rails_csrf_protection (>= 1, < 3) omniauth-twitter (~> 1.4) paper_trail (~> 16.0) paranoia (~> 3.0.0) @@ -612,7 +612,7 @@ GEM omniauth-oauth2 (1.9.0) oauth2 (>= 2.0.2, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.2) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-twitter (1.4.0) From 77fe2e2a06315964fa7b478614f11b0d23dee422 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 4 Mar 2026 10:50:49 +0200 Subject: [PATCH 056/135] Fix for editing a meeting page from related process (#16251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Admin: Can't go in edit meeting page from related process * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update decidim-meetings/spec/system/admin/admin_manages_meetings_links_spec.rb Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .../meetings/admin/meetings/_meeting-tr.html.erb | 4 ++-- .../admin/admin_manages_meetings_links_spec.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/decidim-meetings/app/views/decidim/meetings/admin/meetings/_meeting-tr.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/meetings/_meeting-tr.html.erb index 7d1a58b7100b2..88b052cfe7d4c 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/meetings/_meeting-tr.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/meetings/_meeting-tr.html.erb @@ -3,7 +3,7 @@ "> <% if allowed_to? :update, :meeting, meeting: meeting %> - <%= link_to present(meeting).title(html_escape: true), edit_meeting_path(meeting) %> + <%= link_to present(meeting).title(html_escape: true), Decidim::ResourceLocatorPresenter.new(meeting).edit %> <% else %> <%= present(meeting).title(html_escape: true) %>
<% end %> @@ -42,7 +42,7 @@ "> <% if is_linked %> - <%= t("index.linked_meeting_warning_html", href: edit_meeting_path(meeting), name: present(meeting).space_title, scope: "decidim.meetings.admin.meetings") %> + <%= t("index.linked_meeting_warning_html", href: Decidim::ResourceLocatorPresenter.new(meeting).edit, name: present(meeting).space_title, scope: "decidim.meetings.admin.meetings") %> <% else %> <%= render partial: "decidim/meetings/admin/meetings/meeting_actions", locals: { meeting:, view: } %> <% end %> diff --git a/decidim-meetings/spec/system/admin/admin_manages_meetings_links_spec.rb b/decidim-meetings/spec/system/admin/admin_manages_meetings_links_spec.rb index 15311eed63e9b..c23444f7d3133 100644 --- a/decidim-meetings/spec/system/admin/admin_manages_meetings_links_spec.rb +++ b/decidim-meetings/spec/system/admin/admin_manages_meetings_links_spec.rb @@ -23,6 +23,20 @@ expect(page).to have_css("tbody tr:first-child", text: Decidim::Meetings::MeetingPresenter.new(other_meeting).title) expect(page).to have_css("tbody tr:last-child", text: Decidim::Meetings::MeetingPresenter.new(meeting).title) end + + it "redirects to the proper edit meeting page, outside the linked meeting" do + expect(resource_locator(meeting).admin_index).to include(current_path) + + within "tr", text: Decidim::Meetings::MeetingPresenter.new(other_meeting).title do + expect(page).to have_content("This meeting must be edited from") + click_on translated(other_participatory_space.title) + end + + expect(page).to have_current_path(resource_locator(other_meeting).edit) + click_on "Update" + + expect(page).to have_current_path(resource_locator(other_meeting).admin_index) + end end describe "linking a meeting" do @@ -44,6 +58,7 @@ expect do click_on "Update" + sleep 1 end.to change { meeting.meeting_links.count }.by(1) end end From e73176ce60c5ea7660ca7865bee2019b19a78b44 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 4 Mar 2026 15:47:09 +0200 Subject: [PATCH 057/135] Upgrade to Rails 8.0.4 (#16214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Upgrade to Rails 8.0.4 * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review * Patch demographics routes * Patch the Params in meetings * Update decidim-demographics/lib/decidim/demographics/admin_engine.rb Co-authored-by: Andrés Pereira de Lucena * Apply review recommendations * Revert RAILS_LOG_TO_STDOUT change * Removed the defaults * Add release notes * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- Gemfile.lock | 126 +++++++++--------- RELEASE_NOTES.md | 28 +++- .../admin/results_bulk_actions_controller.rb | 12 +- .../app/jobs/decidim/admin/newsletter_job.rb | 2 +- .../controllers/decidim/follows_controller.rb | 2 +- .../decidim/check_boxes_tree_helper.rb | 2 +- .../shared/filters/_dropdown_label.html.erb | 2 +- decidim-core/config/routes.rb | 2 +- decidim-core/decidim-core.gemspec | 4 +- decidim-core/lib/decidim/core/engine.rb | 15 --- decidim-core/lib/decidim/taxonomizable.rb | 2 +- decidim-core/lib/tasks/decidim_procfile.rake | 2 +- .../cells/decidim/upload_modal_cell_spec.rb | 4 +- .../decidim/check_boxes_tree_helper_spec.rb | 4 +- .../lib/decidim/demographics/admin_engine.rb | 2 +- .../lib/decidim/demographics/engine.rb | 2 +- decidim-generators/Gemfile.lock | 124 +++++++++-------- .../lib/decidim/generators/app_generator.rb | 21 ++- .../meetings/polls/responses_controller.rb | 2 +- .../lib/decidim/meetings/engine.rb | 2 +- 20 files changed, 178 insertions(+), 182 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a6a2481c62bff..85fe675a41f63 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,8 +97,8 @@ PATH premailer-rails (~> 1.10) rack (>= 3.2.4, < 4.0) rack-attack (~> 6.7.0) - rails (~> 7.2.0, >= 7.2.3) - rails-i18n (~> 7.0) + rails (~> 8.0.0, >= 8.0.4) + rails-i18n (~> 8.0.0, >= 8.0.2) ransack (~> 4.2.0) redis (~> 4.1) request_store (~> 1.7.0) @@ -205,71 +205,68 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (7.2.3) - actionpack (= 7.2.3) - activesupport (= 7.2.3) + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.3) - actionpack (= 7.2.3) - activejob (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) mail (>= 2.8.0) - actionmailer (7.2.3) - actionpack (= 7.2.3) - actionview (= 7.2.3) - activejob (= 7.2.3) - activesupport (= 7.2.3) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3) - actionview (= 7.2.3) - activesupport (= 7.2.3) - cgi + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.3) - actionpack (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3) - activesupport (= 7.2.3) + actionview (8.0.4) + activesupport (= 8.0.4) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_link_to (1.0.5) actionpack addressable - activejob (7.2.3) - activesupport (= 7.2.3) + activejob (8.0.4) + activesupport (= 8.0.4) globalid (>= 0.3.6) - activemodel (7.2.3) - activesupport (= 7.2.3) - activerecord (7.2.3) - activemodel (= 7.2.3) - activesupport (= 7.2.3) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) timeout (>= 0.4.0) - activestorage (7.2.3) - actionpack (= 7.2.3) - activejob (= 7.2.3) - activerecord (= 7.2.3) - activesupport (= 7.2.3) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) marcel (~> 1.0) - activesupport (7.2.3) + activesupport (8.0.4) base64 benchmark (>= 0.3) bigdecimal @@ -281,10 +278,11 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) - addressable (2.8.9) + addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) base64 (0.3.0) @@ -331,7 +329,6 @@ GEM cells-rails (0.1.6) actionpack (>= 5.0) cells (>= 4.1.6, < 5.0.0) - cgi (0.5.1) charlock_holmes (0.7.9) chartkick (5.2.1) childprocess (5.1.0) @@ -556,7 +553,7 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) - mcp (0.7.1) + mcp (0.8.0) json-schema (>= 4.1) mime-types (3.7.0) logger @@ -682,20 +679,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (7.2.3) - actioncable (= 7.2.3) - actionmailbox (= 7.2.3) - actionmailer (= 7.2.3) - actionpack (= 7.2.3) - actiontext (= 7.2.3) - actionview (= 7.2.3) - activejob (= 7.2.3) - activemodel (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) bundler (>= 1.15.0) - railties (= 7.2.3) + railties (= 8.0.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -707,13 +704,12 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (7.0.10) + rails-i18n (8.0.2) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.2.3) - actionpack (= 7.2.3) - activesupport (= 7.2.3) - cgi + railties (>= 8.0.0, < 9) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 619197596dbf2..e228ca19f86d7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -32,12 +32,34 @@ gem "decidim", github: "decidim/decidim" gem "decidim-dev", github: "decidim/decidim" ``` -### 1.3. Run these commands +### 1.3. Rails upgrade + +This particular release is deploying a new Rails version, 8.0. As a result you need to update your application configuration. Before that, you need to run the following commands: ```console sudo apt install libvips libvips-tools # or the alternative installation process for your operating system. See "3.5. Replace image processing with imagemagick to libvips" bundle update decidim bin/rails decidim:upgrade +``` + +Please edit your `config/application.rb` to use the new Rails defaults. + +```diff +module DevelopDevelopmentApp + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. +- config.load_defaults 7.2 ++ config.load_defaults 8.0 + # .... + end +end +``` + +You can read more about this change on PR [#16214](https://github.com/decidim/decidim/pull/16214). + +### 1.4. Run these commands + +```console bin/rails db:migrate bin/rails decidim:upgrade:encryption # skip this command if you have run it before: @@ -48,7 +70,7 @@ bin/rails decidim:upgrade:fix_deleted_private_follows bin/rails data:migrate ``` -### 1.4. AWS/Azure/Google Cloud assets storage +### 1.5. AWS/Azure/Google Cloud assets storage There is a bug related to the cache expiration using Active Storage (assets, such as images). For fixing this issue, the Rails team added an extra active storage parameter, `public: true` that you can add it to your storage configuration. If you followed the step `3.4. Deprecation of Rails.application.secrets` and changed your `config/storage.yml` file you don't need to do anything else. @@ -58,7 +80,7 @@ Apart of that, you also need to configure your preferred cloud service provider You can read more about this change on PR [#15005](https://github.com/decidim/decidim/pull/15005/). -### 1.5. Follow the steps and commands detailed in these notes +### 1.6. Follow the steps and commands detailed in these notes ## 2. General notes diff --git a/decidim-accountability/app/controllers/decidim/accountability/admin/results_bulk_actions_controller.rb b/decidim-accountability/app/controllers/decidim/accountability/admin/results_bulk_actions_controller.rb index a89efd03afd5c..20ef9ad67aec6 100644 --- a/decidim-accountability/app/controllers/decidim/accountability/admin/results_bulk_actions_controller.rb +++ b/decidim-accountability/app/controllers/decidim/accountability/admin/results_bulk_actions_controller.rb @@ -88,12 +88,12 @@ def result_ids end def result_params - @result_params ||= params.require(:result_bulk_actions).permit( - :decidim_accountability_status_id, - :start_date, - :end_date, - result_ids: [], - taxonomies: [] + @result_params ||= params.expect( + result_bulk_actions: [:decidim_accountability_status_id, + :start_date, + :end_date, + { result_ids: [], + taxonomies: [] }] ) end end diff --git a/decidim-admin/app/jobs/decidim/admin/newsletter_job.rb b/decidim-admin/app/jobs/decidim/admin/newsletter_job.rb index fda13b1410b60..28834cff4edd9 100644 --- a/decidim-admin/app/jobs/decidim/admin/newsletter_job.rb +++ b/decidim-admin/app/jobs/decidim/admin/newsletter_job.rb @@ -6,7 +6,7 @@ module Admin # class NewsletterJob < ApplicationJob queue_as :newsletter - self.enqueue_after_transaction_commit = :never + self.enqueue_after_transaction_commit = false def perform(newsletter, form, recipients_ids) @newsletter = newsletter diff --git a/decidim-core/app/controllers/decidim/follows_controller.rb b/decidim-core/app/controllers/decidim/follows_controller.rb index 056526d29bc91..59b4d6f35a6b2 100644 --- a/decidim-core/app/controllers/decidim/follows_controller.rb +++ b/decidim-core/app/controllers/decidim/follows_controller.rb @@ -44,7 +44,7 @@ def resource end def button_options - params.require(:follow).permit(:button_classes).to_h.symbolize_keys + params.expect(follow: [:button_classes]).to_h.symbolize_keys end def button_cell_mobile diff --git a/decidim-core/app/helpers/decidim/check_boxes_tree_helper.rb b/decidim-core/app/helpers/decidim/check_boxes_tree_helper.rb index 963f50e0efcb4..61c9840f903c0 100644 --- a/decidim-core/app/helpers/decidim/check_boxes_tree_helper.rb +++ b/decidim-core/app/helpers/decidim/check_boxes_tree_helper.rb @@ -18,7 +18,7 @@ def check_boxes_tree_options(value, label, **options) label_options: { "data-children-checkbox": parent_id, value:, - for: "#{options[:namespace]}_#{options[:id]}" + for: options[:id] } } options.merge!(checkbox_options) diff --git a/decidim-core/app/views/decidim/shared/filters/_dropdown_label.html.erb b/decidim-core/app/views/decidim/shared/filters/_dropdown_label.html.erb index 195d0db06d152..67e4db46adc07 100644 --- a/decidim-core/app/views/decidim/shared/filters/_dropdown_label.html.erb +++ b/decidim-core/app/views/decidim/shared/filters/_dropdown_label.html.erb @@ -39,7 +39,7 @@ <% if item.tree_node? && item.node.present? %> <% subitems_content = capture do %> <% item.node.each do |subitem| %> - <%= form.dropdown_label(subitem, method, name: "#{name}", check_boxes_tree_id:, parent_id: data_checkboxes_tree_id || "") %> + <%= form.dropdown_label(subitem, method, name:, check_boxes_tree_id:, parent_id: data_checkboxes_tree_id || "") %> <% end %> <% end %> diff --git a/decidim-core/config/routes.rb b/decidim-core/config/routes.rb index 3f68b5cad0be3..c6794f7ea714d 100644 --- a/decidim-core/config/routes.rb +++ b/decidim-core/config/routes.rb @@ -131,7 +131,7 @@ resource :report, only: [:create] resource :report_user, only: [:create] resources :likes, only: [:create, :destroy] - resources :amends, only: [:new, :reject, :accept], controller: :amendments do + resources :amends, only: [:new], controller: :amendments do collection do post :create end diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 68d0095fa9c53..0d7d5c1ecd777 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -70,8 +70,8 @@ Gem::Specification.new do |s| s.add_dependency "premailer-rails", "~> 1.10" s.add_dependency "rack", ">= 3.2.4", "< 4.0" s.add_dependency "rack-attack", "~> 6.7.0" - s.add_dependency "rails", "~> 7.2.0", ">= 7.2.3" - s.add_dependency "rails-i18n", "~> 7.0" + s.add_dependency "rails", "~> 8.0.0", ">= 8.0.4" + s.add_dependency "rails-i18n", "~> 8.0.0", ">= 8.0.2" s.add_dependency "ransack", "~> 4.2.0" s.add_dependency "redis", "~> 4.1" s.add_dependency "request_store", "~> 1.7.0" diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index abdcabc70de93..b1d8c27fdf65e 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -248,21 +248,6 @@ class Engine < ::Rails::Engine app.config.i18n.raise_on_missing_translations = Rails.env.local? end - initializer "decidim_core.active_storage_method_patch" do |_app| - if Rails::VERSION::MAJOR < 8 - # This is a manual bugfix of https://github.com/rails/rails/pull/51931 - module Attachment - def named_variants - record.attachment_reflections[name]&.named_variants || {} - end - end - - ActiveSupport.on_load(:active_storage_attachment) { prepend Attachment } - else - Decidim.deprecator.warn("Remove decidim_core.active_storage_method_patch initializer from #{__FILE__}") - end - end - initializer "decidim_core.action_controller" do |_app| config.to_prepare do ActiveSupport.on_load :action_controller do diff --git a/decidim-core/lib/decidim/taxonomizable.rb b/decidim-core/lib/decidim/taxonomizable.rb index 7b2b309736ba8..53622965ee81f 100644 --- a/decidim-core/lib/decidim/taxonomizable.rb +++ b/decidim-core/lib/decidim/taxonomizable.rb @@ -51,7 +51,7 @@ module Taxonomizable Arel::Nodes::Intersect.new(memo, query) end - @klass.from(Arel::Nodes::As.new(subquery, Arel.sql(@klass.arel_table.name))) + from(Arel::Nodes::As.new(subquery, Arel.sql(arel_table.name))) } private diff --git a/decidim-core/lib/tasks/decidim_procfile.rake b/decidim-core/lib/tasks/decidim_procfile.rake index a72f31066cdab..d8ecfbaacf5e3 100644 --- a/decidim-core/lib/tasks/decidim_procfile.rake +++ b/decidim-core/lib/tasks/decidim_procfile.rake @@ -30,7 +30,7 @@ if ! gem list foreman -i --silent; then gem install foreman fi -exec foreman start -f Procfile.dev "$@") +exec foreman start -f Procfile.dev "$@"), force: true actions :chmod, "bin/dev", 0o755 end diff --git a/decidim-core/spec/cells/decidim/upload_modal_cell_spec.rb b/decidim-core/spec/cells/decidim/upload_modal_cell_spec.rb index f52fb521c5942..4cd4a01579d25 100644 --- a/decidim-core/spec/cells/decidim/upload_modal_cell_spec.rb +++ b/decidim-core/spec/cells/decidim/upload_modal_cell_spec.rb @@ -124,11 +124,11 @@ def model_name end it "escapes the truncated filename" do - expect(my_cell.send(:truncated_file_name_for, attachments.first)).to eq("<svg onload=alert('ALERT')>.pdf") + expect(my_cell.send(:truncated_file_name_for, attachments.first)).to eq("-svg onload=alert('ALERT')-.pdf") end it "escapes the filename" do - expect(my_cell.send(:file_name_for, attachments.first)).to eq("<svg onload=alert('ALERT')>.pdf") + expect(my_cell.send(:file_name_for, attachments.first)).to eq("-svg onload=alert('ALERT')-.pdf") end end end diff --git a/decidim-core/spec/helpers/decidim/check_boxes_tree_helper_spec.rb b/decidim-core/spec/helpers/decidim/check_boxes_tree_helper_spec.rb index 08f21923d2fec..197069c4be6f6 100644 --- a/decidim-core/spec/helpers/decidim/check_boxes_tree_helper_spec.rb +++ b/decidim-core/spec/helpers/decidim/check_boxes_tree_helper_spec.rb @@ -39,7 +39,7 @@ module Decidim data: { checkboxes_tree: "with_any_whatever_" }, include_hidden: false, label: "All", - label_options: { "data-global-checkbox": "", value: "", for: "_" }, + label_options: { "data-global-checkbox": "", value: "", for: nil }, multiple: true, value: "" } @@ -69,7 +69,7 @@ module Decidim label: "An option", multiple: true, include_hidden: false, - label_options: { "data-children-checkbox": "with_any_whatever_", value: "an_option", for: "_" } + label_options: { "data-children-checkbox": "with_any_whatever_", value: "an_option", for: nil } } end diff --git a/decidim-demographics/lib/decidim/demographics/admin_engine.rb b/decidim-demographics/lib/decidim/demographics/admin_engine.rb index 1ea43b509bd6f..41d2436d6e5d4 100644 --- a/decidim-demographics/lib/decidim/demographics/admin_engine.rb +++ b/decidim-demographics/lib/decidim/demographics/admin_engine.rb @@ -15,7 +15,7 @@ class AdminEngine < ::Rails::Engine collection do resource :settings, only: [:show, :update] - resource :questions, only: [:edit_questions, :update_questions] do + resource :questions do collection do get :edit_questions patch :update_questions diff --git a/decidim-demographics/lib/decidim/demographics/engine.rb b/decidim-demographics/lib/decidim/demographics/engine.rb index adb1b01c304ed..21a0ff6efc6f1 100644 --- a/decidim-demographics/lib/decidim/demographics/engine.rb +++ b/decidim-demographics/lib/decidim/demographics/engine.rb @@ -8,7 +8,7 @@ class Engine < ::Rails::Engine isolate_namespace Decidim::Demographics routes do - resource :demographics, only: [:show, :respond, :destroy] do + resource :demographics, only: [:show, :destroy] do collection do post :respond end diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index d7fcf7a87a5b9..b5628495fec00 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -97,8 +97,8 @@ PATH premailer-rails (~> 1.10) rack (>= 3.2.4, < 4.0) rack-attack (~> 6.7.0) - rails (~> 7.2.0, >= 7.2.3) - rails-i18n (~> 7.0) + rails (~> 8.0.0, >= 8.0.4) + rails-i18n (~> 8.0.0, >= 8.0.2) ransack (~> 4.2.0) redis (~> 4.1) request_store (~> 1.7.0) @@ -205,71 +205,68 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (7.2.3) - actionpack (= 7.2.3) - activesupport (= 7.2.3) + actioncable (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.3) - actionpack (= 7.2.3) - activejob (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + actionmailbox (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) mail (>= 2.8.0) - actionmailer (7.2.3) - actionpack (= 7.2.3) - actionview (= 7.2.3) - activejob (= 7.2.3) - activesupport (= 7.2.3) + actionmailer (8.0.4) + actionpack (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activesupport (= 8.0.4) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3) - actionview (= 7.2.3) - activesupport (= 7.2.3) - cgi + actionpack (8.0.4) + actionview (= 8.0.4) + activesupport (= 8.0.4) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.3) - actionpack (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + actiontext (8.0.4) + actionpack (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3) - activesupport (= 7.2.3) + actionview (8.0.4) + activesupport (= 8.0.4) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_link_to (1.0.5) actionpack addressable - activejob (7.2.3) - activesupport (= 7.2.3) + activejob (8.0.4) + activesupport (= 8.0.4) globalid (>= 0.3.6) - activemodel (7.2.3) - activesupport (= 7.2.3) - activerecord (7.2.3) - activemodel (= 7.2.3) - activesupport (= 7.2.3) + activemodel (8.0.4) + activesupport (= 8.0.4) + activerecord (8.0.4) + activemodel (= 8.0.4) + activesupport (= 8.0.4) timeout (>= 0.4.0) - activestorage (7.2.3) - actionpack (= 7.2.3) - activejob (= 7.2.3) - activerecord (= 7.2.3) - activesupport (= 7.2.3) + activestorage (8.0.4) + actionpack (= 8.0.4) + activejob (= 8.0.4) + activerecord (= 8.0.4) + activesupport (= 8.0.4) marcel (~> 1.0) - activesupport (7.2.3) + activesupport (8.0.4) base64 benchmark (>= 0.3) bigdecimal @@ -281,6 +278,7 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) @@ -330,7 +328,6 @@ GEM cells-rails (0.1.6) actionpack (>= 5.0) cells (>= 4.1.6, < 5.0.0) - cgi (0.5.1) charlock_holmes (0.7.9) chartkick (5.2.1) childprocess (5.1.0) @@ -550,7 +547,7 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) - mcp (0.7.1) + mcp (0.8.0) json-schema (>= 4.1) mime-types (3.7.0) logger @@ -674,20 +671,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (7.2.3) - actioncable (= 7.2.3) - actionmailbox (= 7.2.3) - actionmailer (= 7.2.3) - actionpack (= 7.2.3) - actiontext (= 7.2.3) - actionview (= 7.2.3) - activejob (= 7.2.3) - activemodel (= 7.2.3) - activerecord (= 7.2.3) - activestorage (= 7.2.3) - activesupport (= 7.2.3) + rails (8.0.4) + actioncable (= 8.0.4) + actionmailbox (= 8.0.4) + actionmailer (= 8.0.4) + actionpack (= 8.0.4) + actiontext (= 8.0.4) + actionview (= 8.0.4) + activejob (= 8.0.4) + activemodel (= 8.0.4) + activerecord (= 8.0.4) + activestorage (= 8.0.4) + activesupport (= 8.0.4) bundler (>= 1.15.0) - railties (= 7.2.3) + railties (= 8.0.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -699,13 +696,12 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (7.0.10) + rails-i18n (8.0.2) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 8) - railties (7.2.3) - actionpack (= 7.2.3) - activesupport (= 7.2.3) - cgi + railties (>= 8.0.0, < 9) + railties (8.0.4) + actionpack (= 8.0.4) + activesupport (= 8.0.4) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) diff --git a/decidim-generators/lib/decidim/generators/app_generator.rb b/decidim-generators/lib/decidim/generators/app_generator.rb index 099742b7d3863..e5b6dfa61289d 100644 --- a/decidim-generators/lib/decidim/generators/app_generator.rb +++ b/decidim-generators/lib/decidim/generators/app_generator.rb @@ -119,9 +119,14 @@ def remove_sprockets_requirement gsub_file "config/environments/production.rb", /config\.assets.*$/, "" end + def patch_production_file + gsub_file "config/environments/production.rb", /config\.action_mailer\.default_url_options = { host: "example.com" }$/, + "# config.action_mailer.default_url_options = { host: \"example.com\" }" + end + def patch_test_file - gsub_file "config/environments/test.rb", /config\.action_mailer\.default_url_options = { host: "www.example.com" }$/, - "# config.action_mailer.default_url_options = { host: \"www.example.com\" }" + gsub_file "config/environments/test.rb", /config\.action_mailer\.default_url_options = { host: "example.com" }$/, + "# config.action_mailer.default_url_options = { host: \"example.com\" }" end def disable_annotate_rendered_view_on_development @@ -340,19 +345,11 @@ def remove_default_favicon end def production_environment - gsub_file "config/environments/production.rb", - /config.log_level = :info/, - "config.log_level = %w(debug info warn error fatal).include?(ENV['RAILS_LOG_LEVEL']) ? ENV['RAILS_LOG_LEVEL'] : :info" - gsub_file "config/environments/production.rb", %r{# config.asset_host = "http://assets.example.com"}, "config.asset_host = ENV['RAILS_ASSET_HOST'] if ENV['RAILS_ASSET_HOST'].present?" - gsub_file "config/environments/production.rb", /# Log to STDOUT by default\n((.*)\n){3}/, <<~CONFIG - if ENV["RAILS_LOG_TO_STDOUT"].present? - config.logger = ActiveSupport::Logger.new(STDOUT) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - end + gsub_file "config/environments/production.rb", /config\.logger\s*=\s*ActiveSupport::TaggedLogging\.logger\(STDOUT\)/, <<~CONFIG + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) if ENV["RAILS_LOG_TO_STDOUT"].present? CONFIG end diff --git a/decidim-meetings/app/controllers/decidim/meetings/polls/responses_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/polls/responses_controller.rb index 2f4ecbae7770a..c26570de0f629 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/polls/responses_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/polls/responses_controller.rb @@ -37,7 +37,7 @@ def question end def response_params - params.require(:response).permit(:question_id, choices: [:body, :response_option_id]) + params.expect(response: [:question_id, { choices: [[:body, :response_option_id]] }]) end end end diff --git a/decidim-meetings/lib/decidim/meetings/engine.rb b/decidim-meetings/lib/decidim/meetings/engine.rb index f90965d9f82d9..ed28a466f8916 100644 --- a/decidim-meetings/lib/decidim/meetings/engine.rb +++ b/decidim-meetings/lib/decidim/meetings/engine.rb @@ -13,7 +13,7 @@ class Engine < ::Rails::Engine isolate_namespace Decidim::Meetings routes do - resources :meetings, only: [:index, :show, :new, :create, :edit, :update, :withdraw] do + resources :meetings, only: [:index, :show, :new, :create, :edit, :update] do member do put :withdraw end From 3d0b819d7118bba9490f32fe919a5864ab327841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 4 Mar 2026 22:10:59 +0100 Subject: [PATCH 058/135] Fix grandchildren navigation in assemblies (#16299) --- .../assemblies/admin/assemblies/index.js.erb | 2 +- .../admin/admin_manages_assemblies_spec.rb | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) 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 index 41626d1357ee0..1434001b85107 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/admin/assemblies/index.js.erb @@ -6,7 +6,7 @@ $('[data-assembly-id="<%= parent_assembly_id %>"]').after( ); // Dispatch the `ajax:loaded` event with the newly inserted element -const insertedElement = $('[data-assembly-id="<%= parent_assembly_id %>"]').next()[0]; +var insertedElement = $('[data-assembly-id="<%= parent_assembly_id %>"]').next()[0]; document.dispatchEvent(new CustomEvent("ajax:loaded", { detail: insertedElement })); var component = new window.Decidim.AdminAssembliesListComponent(); 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 b0afda2e383d6..80bea12b830d7 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -203,4 +203,61 @@ end end end + + context "when navigating grandchild assemblies (3rd level)" do + let!(:grandmother_assembly) { create(:assembly, organization:) } + let!(:mother_assembly) { create(:assembly, organization:, parent: grandmother_assembly) } + let!(:child_assembly) { create(:assembly, :with_content_blocks, organization:, parent: mother_assembly) } + let(:assembly) { child_assembly } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_assemblies.assemblies_path + end + + describe "listing grandchild assemblies" do + it "expands both parent and grandparent assemblies to show child" do + expect(page).to have_no_content(translated(mother_assembly.title)) + expect(page).to have_no_content(translated(child_assembly.title)) + + # Opens grandmother (1st level) + within "tr", text: translated(grandmother_assembly.title) do + find("a[data-arrow-down]").click + end + + expect(page).to have_content(translated(mother_assembly.title)) + expect(page).to have_no_content(translated(child_assembly.title)) + + # Opens mother (2nd level) + within "tr", text: translated(mother_assembly.title) do + find("a[data-arrow-down]").click + end + + expect(page).to have_content(translated(child_assembly.title)) + + # Opens actions dropdown in child + find("button[data-target='actions-assembly-#{child_assembly.id}']").click + expect(page).to have_content("Edit") + expect(page).to have_content("Share link") + expect(page).to have_content("Export") + + # Collapse mother (2nd level) + within "tr", text: translated(mother_assembly.title) do + find("a[data-arrow-up]").click + end + + expect(page).to have_no_content(translated(child_assembly.title)) + expect(page).to have_content(translated(mother_assembly.title)) + + # Collapse grandmother (1st level) + within "tr", text: translated(grandmother_assembly.title) do + find("a[data-arrow-up]").click + end + + expect(page).to have_no_content(translated(mother_assembly.title)) + expect(page).to have_no_content(translated(child_assembly.title)) + end + end + end end From d314665cf5a8ee88e7264144cb67c794ef0ff93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 4 Mar 2026 22:43:00 +0100 Subject: [PATCH 059/135] Remove calendar from highlighted meetings content block (#16289) --- .../show.erb | 7 ---- ...highlighted_meetings_for_component_cell.rb | 8 ---- .../decidim/meetings/meeting_month/show.erb | 33 ---------------- .../highlighted_meetings_cell_spec.rb | 18 +++------ .../meetings/meeting_month_cell_spec.rb | 38 ------------------- 5 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 decidim-meetings/app/cells/decidim/meetings/meeting_month/show.erb delete mode 100644 decidim-meetings/spec/cells/decidim/meetings/meeting_month_cell_spec.rb diff --git a/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component/show.erb b/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component/show.erb index 455dccabe23eb..3fd745395c89e 100644 --- a/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component/show.erb @@ -20,13 +20,6 @@
<% end %>
- <% if show_calendar? %> -
- <% calendar_months.each do |start_date| %> - <%= cell "decidim/meetings/meeting_month", collection, start_date: %> - <% end %> -
- <% end %>
<%= title %> <% collection.includes(:component).each do |meeting| %> diff --git a/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb b/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb index a49c302ab6d63..8a4c3989b31b1 100644 --- a/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/highlighted_meetings_for_component_cell.rb @@ -72,14 +72,6 @@ def all_online_meetings? collection.collect(&:type_of_meeting).all?("online") end - def show_calendar? - @show_calendar ||= show_upcoming_meetings? && collection.minimum(:start_time).before?(2.months.from_now.beginning_of_month) - end - - def calendar_months - [Date.current, Date.current.next_month] - end - def past_meetings @past_meetings ||= base_relation.past.order(end_time: :desc, start_time: :desc) end diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_month/show.erb b/decidim-meetings/app/cells/decidim/meetings/meeting_month/show.erb deleted file mode 100644 index 5acfb5554ab59..0000000000000 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_month/show.erb +++ /dev/null @@ -1,33 +0,0 @@ - - - - <% abbr_day_names.each_with_index do |day, i| %> - - <% end %> - - - <% weeks.each do |days| %> - - <% is_first_day = first_day_of_month?(days.first) %> - - <% if is_first_day %> - <% (7 - days.length).times do %> - - <% end %> - <% end %> - - <% days.each do |day| %> - <%= content_tag :td, class: day_class(day) do %> - - <% end %> - <% end %> - - <% if !is_first_day %> - <% (7 - days.length).times do %> - - <% end %> - <% end %> - - <% end %> - -
<%= month_name %>
<%= day %>
diff --git a/decidim-meetings/spec/cells/decidim/meetings/content_blocks/highlighted_meetings_cell_spec.rb b/decidim-meetings/spec/cells/decidim/meetings/content_blocks/highlighted_meetings_cell_spec.rb index 4b979c3381ad8..fb90403d23c12 100644 --- a/decidim-meetings/spec/cells/decidim/meetings/content_blocks/highlighted_meetings_cell_spec.rb +++ b/decidim-meetings/spec/cells/decidim/meetings/content_blocks/highlighted_meetings_cell_spec.rb @@ -127,20 +127,12 @@ module ContentBlocks end context "with upcoming meetings in other month" do - context "when there are meetings in this month" do - context "and there are meetings in the next month" do - let!(:next_month_meeting) { create(:meeting, :published, component: meeting.component, start_time: meeting.start_time.advance(months: 1)) } - - it "renders the two months" do - expect(html).to have_css(".meeting-calendar__month time", count: 61) - end - end + let!(:second_meeting) do + create(:meeting, :published, start_time: 1.month.from_now, component: meeting.component) + end - context "and there are no meetings in the next month" do - it "renders only the current month" do - expect(html).to have_css(".meeting-calendar__month time", count: 31) - end - end + it "renders the meetings" do + expect(html).to have_css(".card__list", count: 2) end end end diff --git a/decidim-meetings/spec/cells/decidim/meetings/meeting_month_cell_spec.rb b/decidim-meetings/spec/cells/decidim/meetings/meeting_month_cell_spec.rb deleted file mode 100644 index b681b8a625d08..0000000000000 --- a/decidim-meetings/spec/cells/decidim/meetings/meeting_month_cell_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Meetings - describe MeetingMonthCell, type: :cell do - subject { my_cell.call } - - let!(:collection) { create_list(:meeting, 5, :published, start_time: Time.zone.local(2021, 5, 15)) } - let(:my_cell) { cell("decidim/meetings/meeting_month", collection, start_date:) } - - context "when the date is the same month" do - let(:start_date) { Time.zone.local(2021, 5, 1) } - - it "renders the date of the meetings" do - expect(subject).to have_css(".is-past-event") - end - - it "renders the month" do - expect(subject).to have_css("time", count: 31) - end - end - - context "when the date is a month without meetings" do - let(:start_date) { Time.zone.local(2021, 4, 1) } - - it "does not render any meeting" do - expect(subject).to have_no_css(".is-past-event") - end - - it "does not render the month" do - expect(subject).to have_css("time", count: 0) - end - end - end - end -end From cc54905389bade922e0a5f4d6d64914ed2366c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 4 Mar 2026 22:55:46 +0100 Subject: [PATCH 060/135] Fix showing end dates when meetings have multiple dates (#16288) --- .../app/packs/stylesheets/decidim/_cards.scss | 14 +++- .../decidim/meetings/dates_and_map/show.erb | 14 ++-- .../decidim/meetings/meeting_l/image.erb | 20 +++++- .../cells/decidim/meetings/meeting_l_cell.rb | 12 ++++ .../stylesheets/decidim/meetings/_item.scss | 4 +- .../decidim/meetings/meeting_l_cell_spec.rb | 64 +++++++++++++++++++ 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/decidim-core/app/packs/stylesheets/decidim/_cards.scss b/decidim-core/app/packs/stylesheets/decidim/_cards.scss index 18d43176030c0..0f717c986c397 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_cards.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_cards.scss @@ -116,7 +116,7 @@ } &__calendar { - @apply w-14 flex flex-col justify-start rounded overflow-hidden bg-background text-center; + @apply w-20 flex flex-col justify-start rounded overflow-hidden bg-background text-center; /* overwrite defaults */ &-list__reset { @@ -132,13 +132,21 @@ } &-year { - @apply text-black text-xs; + @apply text-black text-xs mb-0.5; } &-month, &-day, &-year { - @apply inline-flex items-center justify-evenly empty:[&>div]:hidden; + @apply inline-flex items-center justify-center empty:[&>div]:hidden; + } + + &-separator { + @apply mx-2 font-normal text-sm; + } + + .card__list-content { + @apply mt-0.5; } } diff --git a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb index c5fed64effe1e..f06069058dd6c 100644 --- a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb @@ -1,14 +1,18 @@
-

<%= l(start_time, format: !same_month? ? "%b" : "%B") %>

-

<%= "-" if !same_month? %>

-

<%= l(end_time, format: "%b") if !same_month? %>

+

<%= l(start_time, format: same_month? ? "%B" : "%b") %>

+ <% unless same_month? %> +

-

+

<%= l(end_time, format: "%b") %>

+ <% end %>

<%= l(start_time, format: "%d") %>

-

<%= "-" if !same_day? || !same_month? %>

-

<%= l(end_time, format: "%d") if !same_day? || !same_month? %>

+ <% unless same_day? && same_month? %> +

-

+

<%= l(end_time, format: "%d") %>

+ <% end %>

<%= year %>

diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb index 302347735899f..9b01dc40bc830 100644 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb +++ b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb @@ -1,5 +1,19 @@ diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb b/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb index f88db911871f1..c6133ac07db81 100644 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb @@ -36,6 +36,18 @@ def current_space @current_space ||= current_component.participatory_space end + def same_month? + return true if meeting.end_time.blank? + + meeting.start_time.year == meeting.end_time.year && meeting.start_time.month == meeting.end_time.month + end + + def same_day? + return true if meeting.end_time.blank? + + meeting.start_time.to_date == meeting.end_time.to_date + end + def metadata_cell "decidim/meetings/meeting_card_metadata" end diff --git a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss index 4b79ecbc454cd..57e474d6de0cc 100644 --- a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss +++ b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss @@ -35,11 +35,11 @@ &-month, &-day, &-year { - @apply inline-flex items-center justify-evenly empty:[&>p]:hidden; + @apply inline-flex items-center justify-center empty:[&>p]:hidden; } &-separator { - @apply mx-2 font-normal text-sm; + @apply mx-4 font-normal text-sm; } &__lg { diff --git a/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb b/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb index d6f16ebcc1009..1dd2bccbfa760 100644 --- a/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb +++ b/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb @@ -28,6 +28,70 @@ module Decidim::Meetings it "shows the start time's year" do expect(subject).to have_css(".card__calendar-year", text: "2020") end + + it "does not show separator" do + expect(subject).to have_no_css(".card__calendar-separator") + end + end + + context "when meeting spans multiple days in the same month" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: Time.new(2020, 10, 17, 12, 0, 0, 0)) } + + it "shows the start day" do + expect(subject).to have_css(".card__calendar-day", text: "15") + end + + it "shows the end day" do + expect(subject).to have_css(".card__calendar-day", text: "17") + end + + it "shows the separator" do + expect(subject).to have_css(".card__calendar-separator") + end + + it "does not show month separator" do + expect(subject).to have_css(".card__calendar-month", text: "October") + end + end + + context "when meeting spans multiple months" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: Time.new(2020, 11, 17, 12, 0, 0, 0)) } + + it "shows the start month" do + expect(subject).to have_css(".card__calendar-month", text: "Oct") + end + + it "shows the end month" do + expect(subject).to have_css(".card__calendar-month", text: "Nov") + end + + it "shows the start day" do + expect(subject).to have_css(".card__calendar-day", text: "15") + end + + it "shows the end day" do + expect(subject).to have_css(".card__calendar-day", text: "17") + end + + it "shows month separator" do + expect(subject).to have_css(".card__calendar-separator") + end + end + + context "when meeting has no end time" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + + it "shows the start time's month" do + expect(subject).to have_css(".card__calendar-month", text: "October") + end + + it "shows the start time's day" do + expect(subject).to have_css(".card__calendar-day", text: "15") + end + + it "does not show separator" do + expect(subject).to have_no_css(".meeting__calendar-separator") + end end context "when title contains special html entities" do From 98ab9c8b8f34874abf50ded138d431e9c3c6a75e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:28:55 +0200 Subject: [PATCH 061/135] Bump to dependencies: Bump redis from 4.8.1 to 5.4.1 (#16302) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 7 +++++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 85fe675a41f63..462225358a197 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,7 +100,7 @@ PATH rails (~> 8.0.0, >= 8.0.4) rails-i18n (~> 8.0.0, >= 8.0.2) ransack (~> 4.2.0) - redis (~> 4.1) + redis (>= 4.1, < 6.0) request_store (~> 1.7.0) rqrcode (~> 2.2.0) ruby-vips (~> 2.2) @@ -730,7 +730,10 @@ GEM psych (>= 4.0.0) tsort redcarpet (3.6.1) - redis (4.8.1) + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.26.4) + connection_pool regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 0d7d5c1ecd777..0217a8f0d6803 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -73,7 +73,7 @@ Gem::Specification.new do |s| s.add_dependency "rails", "~> 8.0.0", ">= 8.0.4" s.add_dependency "rails-i18n", "~> 8.0.0", ">= 8.0.2" s.add_dependency "ransack", "~> 4.2.0" - s.add_dependency "redis", "~> 4.1" + s.add_dependency "redis", ">= 4.1", "< 6.0" s.add_dependency "request_store", "~> 1.7.0" s.add_dependency "rqrcode", "~> 2.2.0" s.add_dependency "ruby-vips", "~> 2.2" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index b5628495fec00..03f4b6bccd086 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -100,7 +100,7 @@ PATH rails (~> 8.0.0, >= 8.0.4) rails-i18n (~> 8.0.0, >= 8.0.2) ransack (~> 4.2.0) - redis (~> 4.1) + redis (>= 4.1, < 6.0) request_store (~> 1.7.0) rqrcode (~> 2.2.0) ruby-vips (~> 2.2) @@ -722,7 +722,10 @@ GEM psych (>= 4.0.0) tsort redcarpet (3.6.1) - redis (4.8.1) + redis (5.4.1) + redis-client (>= 0.22.0) + redis-client (0.26.4) + connection_pool regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) From 7c9cd5f21a87cbe53451ec804cb49528e3e72394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 5 Mar 2026 13:08:17 +0100 Subject: [PATCH 062/135] Add docs for tile layer configuration in Maps (#16297) --- .github/actions/spelling/expect.txt | 1 + docs/modules/services/pages/maps.adoc | 42 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index f9315f4e9bcd8..4d2bf80cbcc13 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -880,6 +880,7 @@ timeouter timestamped tiptap Tmpname +tms TLDR todos Tomorrowland diff --git a/docs/modules/services/pages/maps.adoc b/docs/modules/services/pages/maps.adoc index ca50bb187a961..6fe1705bac0df 100644 --- a/docs/modules/services/pages/maps.adoc +++ b/docs/modules/services/pages/maps.adoc @@ -83,6 +83,48 @@ MAPS_API_KEY=your_api_key_here For further information, see the service provider's documentation or take a look at the <> section. +==== Tile layer configuration options + +Some tile servers use the https://en.wikipedia.org/wiki/Tile_Map_Service[TMS (Tile Map Service)] specification instead of the standard XYZ tile scheme. +If your tile server requires TMS, you can enable it by setting the `tms` option to `true`: + +[source,ruby] +---- +config.maps = { + provider: :osm, + api_key: ENV["MAPS_API_KEY"], + dynamic: { + tile_layer: { + url: "https://tiles.example.org/{z}/{x}/{y}.png", + tms: true, + attribution: %( + © OpenStreetMap contributors + ).strip + } + } +} +---- + +Alternatively, you can use the `MAPS_EXTRA_VARS` xref:configure:environment_variables.adoc[Environment Variable] to set the `tms` option: + +[source,bash] +---- +MAPS_EXTRA_VARS="tms=true" +---- + +All configuration options supported by https://leafletjs.com/reference.html#tilelayer[Leaflet's TileLayer] can be passed to the `tile_layer` configuration, including: + +* `minZoom` - Minimum zoom level +* `maxZoom` - Maximum zoom level +* `subdomains` - Subdomains for the tile server (e.g., `"abc"` or `["a", "b", "c"]`) +* `errorTileUrl` - URL for error tiles +* `zoomOffset` - Zoom offset +* `tms` - Set to `true` if your tile server uses TMS specification +* `zoomReverse` - Set to `true` to reverse zoom levels +* `detectRetina` - Set to `true` to request retina tiles +* `crossOrigin` - CrossOrigin attribute for tiles (e.g., `"anonymous"`) +* `referrerPolicy` - Referrer policy for tiles (e.g., `"no-referrer-when-downgrade"`) + === Combining multiple service providers It is also possible to combine multiple service providers for the different categories of map services. From 3ecf67ea38632f2a85818a4522369d1ea591b60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 5 Mar 2026 14:02:43 +0100 Subject: [PATCH 063/135] Fix focus guard related bugs (#16213) --- .../report_button/already_reported_modal.erb | 2 +- .../decidim/report_button/flag_modal.erb | 2 +- .../already_reported_modal.erb | 2 +- .../decidim/report_user_button/flag_modal.erb | 2 +- .../cells/decidim/share_text_widget/modal.erb | 2 +- decidim-core/app/packs/src/decidim/a11y.js | 29 +++++++ .../app/packs/src/decidim/a11y.test.js | 81 +++++++++++++++++++ .../src/decidim/refactor/moved/focus_guard.js | 8 +- 8 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 decidim-core/app/packs/src/decidim/a11y.test.js diff --git a/decidim-core/app/cells/decidim/report_button/already_reported_modal.erb b/decidim-core/app/cells/decidim/report_button/already_reported_modal.erb index 340b83a5e9024..e519754f8bd3c 100644 --- a/decidim-core/app/cells/decidim/report_button/already_reported_modal.erb +++ b/decidim-core/app/cells/decidim/report_button/already_reported_modal.erb @@ -1,7 +1,7 @@ <%= decidim_modal id: modal_id, class: "flag-modal" do %>
<%= icon "flag-line" %> -

<%= t("decidim.shared.flag_modal.title") %>

+

<%= t("decidim.shared.flag_modal.already_reported") %>

diff --git a/decidim-core/app/cells/decidim/report_button/flag_modal.erb b/decidim-core/app/cells/decidim/report_button/flag_modal.erb index db6c14a09a2a7..015902ff176b8 100644 --- a/decidim-core/app/cells/decidim/report_button/flag_modal.erb +++ b/decidim-core/app/cells/decidim/report_button/flag_modal.erb @@ -2,7 +2,7 @@ <%= decidim_form_for report_form, builder:, url: report_path, method: :post, html: { id: nil, data: { controller: "report-form" } } do |f| %>
<%= icon "flag-line" %> -

<%= t("decidim.shared.flag_modal.title") %>

+
diff --git a/decidim-core/app/cells/decidim/report_user_button/already_reported_modal.erb b/decidim-core/app/cells/decidim/report_user_button/already_reported_modal.erb index e41491b91f38f..92e6b641d4b60 100644 --- a/decidim-core/app/cells/decidim/report_user_button/already_reported_modal.erb +++ b/decidim-core/app/cells/decidim/report_user_button/already_reported_modal.erb @@ -1,7 +1,7 @@ <%= decidim_modal id: modal_id, class: "flag-user-modal" do %>
<%= icon "flag-line" %> -

<%= t("decidim.shared.flag_user_modal.title") %>

+

<%= t("decidim.shared.flag_user_modal.already_reported") %>

diff --git a/decidim-core/app/cells/decidim/report_user_button/flag_modal.erb b/decidim-core/app/cells/decidim/report_user_button/flag_modal.erb index 4e33169946d1a..faac61f37fe45 100644 --- a/decidim-core/app/cells/decidim/report_user_button/flag_modal.erb +++ b/decidim-core/app/cells/decidim/report_user_button/flag_modal.erb @@ -2,7 +2,7 @@ <%= decidim_form_for report_form, builder:, url: report_path, method: :post, html: { id: nil, data: { controller: "report-form" } } do |f| %>
<%= icon "flag-line" %> -

<%= t("decidim.shared.flag_user_modal.title") %>

+

<%= t("decidim.shared.flag_user_modal.description") %>

diff --git a/decidim-core/app/cells/decidim/share_text_widget/modal.erb b/decidim-core/app/cells/decidim/share_text_widget/modal.erb index 2068700f1dd8a..16cc81cc75caa 100644 --- a/decidim-core/app/cells/decidim/share_text_widget/modal.erb +++ b/decidim-core/app/cells/decidim/share_text_widget/modal.erb @@ -1,6 +1,6 @@ <%= decidim_modal id: "socialShare", class: "share-modal" do %>
-

<%= t("share", scope: "decidim.shared.share_modal") %>

+

<%= t("share", scope: "decidim.shared.share_modal") %>

diff --git a/decidim-core/app/packs/src/decidim/a11y.js b/decidim-core/app/packs/src/decidim/a11y.js index 7b83f498479fb..432dfd6e9fb7f 100644 --- a/decidim-core/app/packs/src/decidim/a11y.js +++ b/decidim-core/app/packs/src/decidim/a11y.js @@ -8,6 +8,13 @@ import Dialogs from "a11y-dialog-component"; * @return {void} */ const createDialog = (component) => { + const getFocusableElements = (container) => { + const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])"; + return Array.from(container.querySelectorAll(selectors)).filter( + (el) => el.offsetParent !== null + ); + }; + const { dataset: { dialog, ...attrs } } = component; @@ -29,11 +36,33 @@ const createDialog = (component) => { backdropSelector: `[data-dialog="${dialog}"]`, enableAutoFocus: false, onOpen: (params, trigger) => { + const keyHandler = (event) => { + if (event.key !== "Tab") { + return; + } + const focusable = getFocusableElements(params); + if (focusable.length === 0) { + return; + } + if (event.shiftKey && document.activeElement === focusable[0]) { + event.preventDefault(); + focusable[focusable.length - 1].focus({ preventScroll: true }); + } else if (!event.shiftKey && document.activeElement === focusable[focusable.length - 1]) { + event.preventDefault(); + focusable[0].focus({ preventScroll: true }); + } + }; + params._focusTrapHandler = keyHandler; + params.addEventListener("keydown", keyHandler); setFocusOnTitle(params); window.focusGuard.trap(params, trigger); params.dispatchEvent(new CustomEvent("open.dialog")); }, onClose: (params) => { + if (params._focusTrapHandler) { + params.removeEventListener("keydown", params._focusTrapHandler); + Reflect.deleteProperty(params, "_focusTrapHandler"); + } window.focusGuard.disable(); params.dispatchEvent(new CustomEvent("close.dialog")); }, diff --git a/decidim-core/app/packs/src/decidim/a11y.test.js b/decidim-core/app/packs/src/decidim/a11y.test.js new file mode 100644 index 0000000000000..7f2e2340808ff --- /dev/null +++ b/decidim-core/app/packs/src/decidim/a11y.test.js @@ -0,0 +1,81 @@ +/* global jest */ + +import { createDialog } from "src/decidim/a11y" + +describe("a11y dialog focus trap", () => { + const dialogHtml = ` + +
+
+

Test Dialog

+ Link 1 + + + + Link 2 +
+
+ +
+
+ `; + + beforeEach(() => { + document.body.innerHTML = dialogHtml; + window.Decidim = { + currentDialogs: {} + }; + window.focusGuard = { + trap: jest.fn(), + disable: jest.fn() + }; + }); + + describe("keydown handler", () => { + let dialogEl = null; + + beforeEach(() => { + const component = document.querySelector("[data-dialog]"); + createDialog(component); + dialogEl = document.querySelector("[data-dialog='testDialog']"); + // Get the dialog from Decidim.currentDialogs and open it + const dialog = window.Decidim.currentDialogs.testDialog; + dialog.open(); + }); + + it("adds keydown handler on open", () => { + expect(dialogEl._focusTrapHandler).toBeDefined(); + }); + + it("handles Tab key", () => { + const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])"; + const tabbableElements = Array.from(dialogEl.querySelectorAll(selectors)).filter( + (el) => el.offsetParent || el.offsetParent === null + ); + tabbableElements[tabbableElements.length - 1].focus(); + + const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true }); + const preventDefault = jest.fn(); + event.preventDefault = preventDefault; + + dialogEl.dispatchEvent(event); + + expect(preventDefault).toHaveBeenCalled(); + }); + }); + + describe("onClose cleanup", () => { + it("removes the keydown handler on close", () => { + const component = document.querySelector("[data-dialog]"); + createDialog(component); + const dialogEl = document.querySelector("[data-dialog='testDialog']"); + + const dialog = window.Decidim.currentDialogs.testDialog; + dialog.open(); + expect(dialogEl._focusTrapHandler).toBeDefined(); + + dialog.close(); + expect(dialogEl._focusTrapHandler).toBeUndefined(); + }); + }); +}); diff --git a/decidim-core/app/packs/src/decidim/refactor/moved/focus_guard.js b/decidim-core/app/packs/src/decidim/refactor/moved/focus_guard.js index 1a2ece848623f..5b7b0bdedbb7e 100644 --- a/decidim-core/app/packs/src/decidim/refactor/moved/focus_guard.js +++ b/decidim-core/app/packs/src/decidim/refactor/moved/focus_guard.js @@ -78,16 +78,16 @@ export default class FocusGuard { let target = null; if (guard.dataset.position === "start") { - // Focus at the start guard, so focus the first focusable element after that - for (let ind = 0; ind < visibleNodes.length; ind += 1) { + // Focus at the start guard, so focus the last focusable element (cycle forward to end) + for (let ind = visibleNodes.length - 1; ind >= 0; ind -= 1) { if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) { target = visibleNodes[ind]; break; } } } else { - // Focus at the end guard, so focus the first focusable element after that - for (let ind = visibleNodes.length - 1; ind >= 0; ind -= 1) { + // Focus at the end guard, so focus the first focusable element (cycle back to start) + for (let ind = 0; ind < visibleNodes.length; ind += 1) { if (!this.isFocusGuard(visibleNodes[ind]) && this.isFocusable(visibleNodes[ind])) { target = visibleNodes[ind]; break; From c8967e468b0652d85605950adb2dca304813a55f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:09:52 +0200 Subject: [PATCH 064/135] Bump to dependencies: Bump rails-i18n from 8.0.2 to 8.1.0 in the rails group (#16317) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-core/decidim-core.gemspec | 2 +- decidim-generators/Gemfile.lock | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 462225358a197..42406ab2942db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,7 +98,7 @@ PATH rack (>= 3.2.4, < 4.0) rack-attack (~> 6.7.0) rails (~> 8.0.0, >= 8.0.4) - rails-i18n (~> 8.0.0, >= 8.0.2) + rails-i18n (>= 8.0.2, < 8.2) ransack (~> 4.2.0) redis (>= 4.1, < 6.0) request_store (~> 1.7.0) @@ -704,7 +704,7 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (8.0.2) + rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) railties (8.0.4) diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 0217a8f0d6803..9e6fdfe702edb 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -71,7 +71,7 @@ Gem::Specification.new do |s| s.add_dependency "rack", ">= 3.2.4", "< 4.0" s.add_dependency "rack-attack", "~> 6.7.0" s.add_dependency "rails", "~> 8.0.0", ">= 8.0.4" - s.add_dependency "rails-i18n", "~> 8.0.0", ">= 8.0.2" + s.add_dependency "rails-i18n", ">= 8.0.2", "< 8.2" s.add_dependency "ransack", "~> 4.2.0" s.add_dependency "redis", ">= 4.1", "< 6.0" s.add_dependency "request_store", "~> 1.7.0" diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 03f4b6bccd086..a8649bfdb337e 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -98,7 +98,7 @@ PATH rack (>= 3.2.4, < 4.0) rack-attack (~> 6.7.0) rails (~> 8.0.0, >= 8.0.4) - rails-i18n (~> 8.0.0, >= 8.0.2) + rails-i18n (>= 8.0.2, < 8.2) ransack (~> 4.2.0) redis (>= 4.1, < 6.0) request_store (~> 1.7.0) @@ -696,7 +696,7 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (8.0.2) + rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) railties (8.0.4) From 6a36ccdb93c3dbf4ef6af08eddb6b689e58ef3dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:30:27 +0200 Subject: [PATCH 065/135] Bump to dependencies: Bump rack-cors from 1.1.1 to 3.0.0 (#16324) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 7 ++++--- decidim-api/decidim-api.gemspec | 2 +- decidim-generators/Gemfile.lock | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 42406ab2942db..716daf9297c85 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,7 +37,7 @@ PATH devise-jwt (>= 0.12.1, < 0.14.0) graphql (>= 2.4.17, < 2.6) graphql-docs (>= 5, < 7) - rack-cors (~> 1.0) + rack-cors (>= 1, < 4) decidim-assemblies (0.32.0.dev) decidim-core (= 0.32.0.dev) decidim-blogs (0.32.0.dev) @@ -664,8 +664,9 @@ GEM rack (3.2.5) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (1.1.1) - rack (>= 2.0.0) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) diff --git a/decidim-api/decidim-api.gemspec b/decidim-api/decidim-api.gemspec index 79a644a4bb458..3adeff8e3035b 100644 --- a/decidim-api/decidim-api.gemspec +++ b/decidim-api/decidim-api.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |s| s.add_dependency "devise-jwt", ">= 0.12.1", "< 0.14.0" s.add_dependency "graphql", ">= 2.4.17", "< 2.6" s.add_dependency "graphql-docs", ">= 5", "< 7" - s.add_dependency "rack-cors", "~> 1.0" + s.add_dependency "rack-cors", ">= 1", "< 4" s.add_development_dependency "decidim-assemblies", version s.add_development_dependency "decidim-comments", version diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index a8649bfdb337e..707688042cf61 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -37,7 +37,7 @@ PATH devise-jwt (>= 0.12.1, < 0.14.0) graphql (>= 2.4.17, < 2.6) graphql-docs (>= 5, < 7) - rack-cors (~> 1.0) + rack-cors (>= 1, < 4) decidim-assemblies (0.32.0.dev) decidim-core (= 0.32.0.dev) decidim-blogs (0.32.0.dev) @@ -656,8 +656,9 @@ GEM rack (3.2.5) rack-attack (6.7.0) rack (>= 1.0, < 4) - rack-cors (1.1.1) - rack (>= 2.0.0) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) From 6c6f29597dd519c626dfbfe23e44923cdedb9937 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 6 Mar 2026 16:54:10 +0200 Subject: [PATCH 066/135] Fix search indexing rules on component publication (#16140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrés Pereira de Lucena --- .../lib/decidim/accountability/component.rb | 2 - ...dmin_manages_component_publication_spec.rb | 52 ++++++ .../admin/admin_publishes_component_spec.rb | 11 -- .../app/models/decidim/blogs/post.rb | 2 +- ...dmin_manages_component_publication_spec.rb | 27 +++ .../system/admin_publishes_component_spec.rb | 11 -- ...dmin_manages_component_publication_spec.rb | 52 ++++++ .../system/admin_publishes_component_spec.rb | 22 --- .../decidim/collaborative_texts/document.rb | 13 +- ...dmin_manages_component_publication_spec.rb | 27 +++ .../admin/admin_publishes_component_spec.rb | 11 -- ...dmin_manages_component_publication_spec.rb | 27 +++ .../system/admin_publishes_component_spec.rb | 11 -- .../test/rspec_support/component_context.rb | 156 ++++++++---------- .../app/models/decidim/elections/election.rb | 12 +- ...dmin_manages_component_publication_spec.rb | 27 +++ .../admin/admin_publishes_component_spec.rb | 11 -- ...dmin_manages_component_publication_spec.rb | 27 +++ .../admin/admin_publishes_component_spec.rb | 11 -- ...dmin_manages_component_publication_spec.rb | 17 ++ .../system/admin_publishes_component_spec.rb | 10 -- ...dmin_manages_component_publication_spec.rb | 27 +++ .../admin/admin_publishes_component_spec.rb | 11 -- ...dmin_manages_component_publication_spec.rb | 17 ++ .../system/admin_publishes_component_spec.rb | 10 -- 25 files changed, 386 insertions(+), 218 deletions(-) create mode 100644 decidim-accountability/spec/system/admin/admin_manages_component_publication_spec.rb delete mode 100644 decidim-accountability/spec/system/admin/admin_publishes_component_spec.rb create mode 100644 decidim-blogs/spec/system/admin_manages_component_publication_spec.rb delete mode 100644 decidim-blogs/spec/system/admin_publishes_component_spec.rb create mode 100644 decidim-budgets/spec/system/admin_manages_component_publication_spec.rb delete mode 100644 decidim-budgets/spec/system/admin_publishes_component_spec.rb create mode 100644 decidim-collaborative_texts/spec/system/admin/admin_manages_component_publication_spec.rb delete mode 100644 decidim-collaborative_texts/spec/system/admin/admin_publishes_component_spec.rb create mode 100644 decidim-debates/spec/system/admin_manages_component_publication_spec.rb delete mode 100644 decidim-debates/spec/system/admin_publishes_component_spec.rb create mode 100644 decidim-elections/spec/system/admin/admin_manages_component_publication_spec.rb delete mode 100644 decidim-elections/spec/system/admin/admin_publishes_component_spec.rb create mode 100644 decidim-meetings/spec/system/admin/admin_manages_component_publication_spec.rb delete mode 100644 decidim-meetings/spec/system/admin/admin_publishes_component_spec.rb create mode 100644 decidim-pages/spec/system/admin_manages_component_publication_spec.rb delete mode 100644 decidim-pages/spec/system/admin_publishes_component_spec.rb create mode 100644 decidim-proposals/spec/system/admin/admin_manages_component_publication_spec.rb delete mode 100644 decidim-proposals/spec/system/admin/admin_publishes_component_spec.rb create mode 100644 decidim-surveys/spec/system/admin_manages_component_publication_spec.rb delete mode 100644 decidim-surveys/spec/system/admin_publishes_component_spec.rb diff --git a/decidim-accountability/lib/decidim/accountability/component.rb b/decidim-accountability/lib/decidim/accountability/component.rb index 92f3dc36949bd..0f6564f3b04d4 100644 --- a/decidim-accountability/lib/decidim/accountability/component.rb +++ b/decidim-accountability/lib/decidim/accountability/component.rb @@ -14,14 +14,12 @@ component.on(:publish) do |instance| Decidim::Accountability::Result.where(component: instance).find_each do |result| Decidim::UpdateSearchIndexesJob.perform_later([result]) - Decidim::UpdateSearchIndexesJob.perform_later(result.children.to_a) end end component.on(:unpublish) do |instance| Decidim::Accountability::Result.where(component: instance).find_each do |result| Decidim::RemoveSearchIndexesJob.perform_later([result]) - Decidim::UpdateSearchIndexesJob.perform_later(result.children.to_a) end end diff --git a/decidim-accountability/spec/system/admin/admin_manages_component_publication_spec.rb b/decidim-accountability/spec/system/admin/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..101cd561bc080 --- /dev/null +++ b/decidim-accountability/spec/system/admin/admin_manages_component_publication_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + context "when there are no children" do + let!(:resource) { create(:result, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:accountability_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:accountability_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:accountability_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end + + context "when there are children" do + let!(:resource) { create(:result, component:) } + let!(:children) { create(:result, component:, parent: resource) } + + context "when cycling through publication states" do + let!(:component) { create(:accountability_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:accountability_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:accountability_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end + end +end diff --git a/decidim-accountability/spec/system/admin/admin_publishes_component_spec.rb b/decidim-accountability/spec/system/admin/admin_publishes_component_spec.rb deleted file mode 100644 index df961d66c13d1..0000000000000 --- a/decidim-accountability/spec/system/admin/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "accountability" } - let!(:resource) { create(:result, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-blogs/app/models/decidim/blogs/post.rb b/decidim-blogs/app/models/decidim/blogs/post.rb index 38c74550bb540..c1c1801729431 100644 --- a/decidim-blogs/app/models/decidim/blogs/post.rb +++ b/decidim-blogs/app/models/decidim/blogs/post.rb @@ -38,7 +38,7 @@ class Post < Blogs::ApplicationRecord D: :body, datetime: :created_at }, - index_on_create: true, + index_on_create: ->(post) { post.visible? }, index_on_update: ->(post) { post.visible? }) class << self diff --git a/decidim-blogs/spec/system/admin_manages_component_publication_spec.rb b/decidim-blogs/spec/system/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..322bc8361847d --- /dev/null +++ b/decidim-blogs/spec/system/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:post, :published, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:post_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:post_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:post_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-blogs/spec/system/admin_publishes_component_spec.rb b/decidim-blogs/spec/system/admin_publishes_component_spec.rb deleted file mode 100644 index 734951b434d63..0000000000000 --- a/decidim-blogs/spec/system/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "blogs" } - let!(:resource) { create(:post, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-budgets/spec/system/admin_manages_component_publication_spec.rb b/decidim-budgets/spec/system/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..734701841007c --- /dev/null +++ b/decidim-budgets/spec/system/admin_manages_component_publication_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + context "with a budget" do + let!(:resource) { create(:budget, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:budgets_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:budgets_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:budgets_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end + + context "with a project" do + let!(:budget) { create(:budget, component:) } + let!(:resource) { create(:project, budget:) } + + context "when cycling through publication states" do + let!(:component) { create(:budgets_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:budgets_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:budgets_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end + end +end diff --git a/decidim-budgets/spec/system/admin_publishes_component_spec.rb b/decidim-budgets/spec/system/admin_publishes_component_spec.rb deleted file mode 100644 index e46c9057daffa..0000000000000 --- a/decidim-budgets/spec/system/admin_publishes_component_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "budgets" } - - context "with a budget" do - let!(:resource) { create(:budget, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" - end - - context "with a project" do - let!(:budget) { create(:budget, component:) } - let!(:resource) { create(:project, budget:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" - end -end diff --git a/decidim-collaborative_texts/app/models/decidim/collaborative_texts/document.rb b/decidim-collaborative_texts/app/models/decidim/collaborative_texts/document.rb index 9576892e5ec88..a7ff766482fd8 100644 --- a/decidim-collaborative_texts/app/models/decidim/collaborative_texts/document.rb +++ b/decidim-collaborative_texts/app/models/decidim/collaborative_texts/document.rb @@ -29,13 +29,12 @@ class Document < CollaborativeTexts::ApplicationRecord delegate :organization, :participatory_space, to: :component delegate :draft?, :draft, :draft=, :body, :body=, to: :current_version - searchable_fields( - participatory_space: { component: :participatory_space }, - A: :title, - D: :consolidated_body, - datetime: :published_at - ) - + searchable_fields({ participatory_space: { component: :participatory_space }, + A: :title, + D: :consolidated_body, + datetime: :published_at }, + index_on_create: ->(document) { document.visible? }, + index_on_update: ->(document) { document.visible? }) def self.log_presenter_class_for(_log) Decidim::CollaborativeTexts::AdminLog::DocumentPresenter end diff --git a/decidim-collaborative_texts/spec/system/admin/admin_manages_component_publication_spec.rb b/decidim-collaborative_texts/spec/system/admin/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..7fb2938a9f5d5 --- /dev/null +++ b/decidim-collaborative_texts/spec/system/admin/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:collaborative_text_document, :published, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:collaborative_text_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:collaborative_text_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:collaborative_text_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-collaborative_texts/spec/system/admin/admin_publishes_component_spec.rb b/decidim-collaborative_texts/spec/system/admin/admin_publishes_component_spec.rb deleted file mode 100644 index a2cc0e19629a9..0000000000000 --- a/decidim-collaborative_texts/spec/system/admin/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "collaborative_texts" } - let!(:resource) { create(:collaborative_text_document, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-debates/spec/system/admin_manages_component_publication_spec.rb b/decidim-debates/spec/system/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..9fa9ba116fb4d --- /dev/null +++ b/decidim-debates/spec/system/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:debate, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:debates_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:debates_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:debates_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-debates/spec/system/admin_publishes_component_spec.rb b/decidim-debates/spec/system/admin_publishes_component_spec.rb deleted file mode 100644 index ce291e28d6e18..0000000000000 --- a/decidim-debates/spec/system/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "debates" } - let!(:resource) { create(:debate, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-dev/lib/decidim/dev/test/rspec_support/component_context.rb b/decidim-dev/lib/decidim/dev/test/rspec_support/component_context.rb index dac5caad1cd6c..f2ca4f1a9d957 100644 --- a/decidim-dev/lib/decidim/dev/test/rspec_support/component_context.rb +++ b/decidim-dev/lib/decidim/dev/test/rspec_support/component_context.rb @@ -115,116 +115,104 @@ def edit_component_path(component) end end -shared_context "when publishing and unpublishing the component" do - let(:title) { translated(current_component.name) } - # When resources are being created or modified, the following jobs are enqueued among the ones that we need to wait for. - # When running in CI, we have situations when job processing takes longer, causing flaky tests. - # Adding a list of exceptions here, helps us to avoid those situations. - let(:job_exceptions) do - [ - Decidim::MachineTranslationResourceJob, - ActiveStorage::AnalyzeJob - ] - end - - context "when component is unpublished" do - before do - current_component.unpublish! - current_component.participatory_space.try_add_to_index_as_search_resource - - visit decidim_admin_participatory_processes.components_path(current_component.participatory_space) - end - - it "reindexes on publication" do - Decidim::SearchableResource.where(resource:).delete_all - expect(Decidim::SearchableResource.where(resource:).count).to be_zero - - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Publish" - end - - expect(page).to have_admin_callout("The component has been successfully published") - - perform_enqueued_jobs(except: job_exceptions) +shared_examples "add component resources to search index" do + before do + resource.reload.component.manifest.run_hooks(:unpublish, resource.reload.component) + visit decidim_admin_participatory_processes.components_path(resource.reload.component.participatory_space) + end - expect(Decidim::SearchableResource.where(resource:).count).to be_positive - expect(component.reload).to be_published - end + around do |example| + perform_enqueued_jobs { example.run } end - context "when component is published" do - before do - current_component.publish! - current_component.participatory_space.try_add_to_index_as_search_resource + it "adds records to index" do + expect(Decidim::SearchableResource.where(resource:).count).to be_zero - visit decidim_admin_participatory_processes.components_path(current_component.participatory_space) + within "tr", text: translated(current_component.name) do + find("button[data-controller='dropdown']").click + click_on "Publish" end - it "removes records from index" do - perform_enqueued_jobs(except: job_exceptions) + perform_enqueued_jobs - expect(Decidim::SearchableResource.where(resource:).count).to be_positive + expect(page).to have_admin_callout("The component has been successfully published") + + expect(component.reload).to be_published + expect(resource.reload).to be_visible + expect(Decidim::SearchableResource.where(resource:).count).to be_positive + end +end - within ".sidebar-menu" do - click_on "Components" - end +shared_examples "removes component resources from search index" do + before do + resource.reload.component.manifest.run_hooks(:publish, resource.reload.component) + visit decidim_admin_participatory_processes.components_path(resource.reload.component.participatory_space) + end - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Hide from menu" - end + around do |example| + perform_enqueued_jobs { example.run } + end - perform_enqueued_jobs(except: job_exceptions) + it "removes records from index" do + expect(resource.reload.component).to be_published + expect(resource.component.participatory_space).to be_visible + expect(resource).to be_visible + expect(resource).to be_resource_visible - expect(Decidim::SearchableResource.where(resource:).count).to be_positive + expect(Decidim::SearchableResource.where(resource:).count).to be_positive - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Unpublish" - end + within "tr", text: translated(current_component.name) do + find("button[data-controller='dropdown']").click + click_on "Hide from menu" + end - expect(page).to have_admin_callout("The component has been successfully unpublished") + expect(component.reload).to be_published + expect(resource.reload).to be_visible - perform_enqueued_jobs(except: job_exceptions) + expect(Decidim::SearchableResource.where(resource:).count).to be_positive - expect(Decidim::SearchableResource.where(resource:).count).to be_zero - expect(current_component.reload).not_to be_published + within "tr", text: translated(current_component.name) do + find("button[data-controller='dropdown']").click + click_on "Unpublish" end + + expect(page).to have_admin_callout("The component has been successfully unpublished") + + expect(current_component.reload).not_to be_published + expect(resource.reload).not_to be_visible + expect(Decidim::SearchableResource.where(resource:).count).to be_zero end end -shared_context "when cycling through publication states" do - include_context "when managing a component as an admin" do - let(:title) { translated(current_component.name) } - - it "cycles through unpublished and published states successfully" do - visit decidim_admin_participatory_processes.components_path(current_component.participatory_space) +shared_examples "cycling through publication states" do + let(:title) { translated(current_component.name) } - within ".sidebar-menu" do - click_on "Components" - end + it "works without raising errors" do + visit decidim_admin_participatory_processes.components_path(component.participatory_space) - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Hide from menu" - end + within ".sidebar-menu" do + click_on "Components" + end - expect(page).to have_admin_callout("The component has been successfully hidden from the menu.") + within "tr", text: title do + find("button[data-controller='dropdown']").click + click_on "Hide from menu" + end - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Unpublish" - end + expect(page).to have_admin_callout("The component has been successfully hidden from the menu.") - expect(page).to have_admin_callout("The component has been successfully unpublished") + within "tr", text: title do + find("button[data-controller='dropdown']").click + click_on "Unpublish" + end - within "tr", text: title do - find("button[data-controller='dropdown']").click - click_on "Publish" - end + expect(page).to have_admin_callout("The component has been successfully unpublished") - expect(page).to have_admin_callout("The component has been successfully published") + within "tr", text: title do + find("button[data-controller='dropdown']").click + click_on "Publish" end + + expect(page).to have_admin_callout("The component has been successfully published") end end diff --git a/decidim-elections/app/models/decidim/elections/election.rb b/decidim-elections/app/models/decidim/elections/election.rb index 8211bdfc2070b..dc414ed32ec41 100644 --- a/decidim-elections/app/models/decidim/elections/election.rb +++ b/decidim-elections/app/models/decidim/elections/election.rb @@ -42,11 +42,13 @@ class Election < Elections::ApplicationRecord scope :ongoing, -> { published.where(start_at: ..Time.current, end_at: Time.current..) } scope :finished, -> { published.where(end_at: ..Time.current) } - searchable_fields( - A: :title, - D: :description, - participatory_space: { component: :participatory_space } - ) + searchable_fields({ + A: :title, + D: :description, + participatory_space: { component: :participatory_space } + }, + index_on_create: ->(election) { election.visible? }, + index_on_update: ->(election) { election.visible? }) def presenter Decidim::Elections::ElectionPresenter.new(self) diff --git a/decidim-elections/spec/system/admin/admin_manages_component_publication_spec.rb b/decidim-elections/spec/system/admin/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..4b5cb46e37c3c --- /dev/null +++ b/decidim-elections/spec/system/admin/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:election, :with_token_csv_census, :published, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:elections_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:elections_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:elections_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-elections/spec/system/admin/admin_publishes_component_spec.rb b/decidim-elections/spec/system/admin/admin_publishes_component_spec.rb deleted file mode 100644 index cc4118e7ceebb..0000000000000 --- a/decidim-elections/spec/system/admin/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "elections" } - let!(:resource) { create(:election, :with_token_csv_census, :published, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-meetings/spec/system/admin/admin_manages_component_publication_spec.rb b/decidim-meetings/spec/system/admin/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..1ba5cfec32d69 --- /dev/null +++ b/decidim-meetings/spec/system/admin/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:meeting, :published, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:meeting_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:meeting_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:meeting_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-meetings/spec/system/admin/admin_publishes_component_spec.rb b/decidim-meetings/spec/system/admin/admin_publishes_component_spec.rb deleted file mode 100644 index 4495b5163d72f..0000000000000 --- a/decidim-meetings/spec/system/admin/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "meetings" } - let!(:resource) { create(:meeting, :published, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-pages/spec/system/admin_manages_component_publication_spec.rb b/decidim-pages/spec/system/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..eace8ac13d7a0 --- /dev/null +++ b/decidim-pages/spec/system/admin_manages_component_publication_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + let!(:resource) { create(:page, component:) } + + # Note: other components also handle search index updates (additions/removals) when publishing or + # unpublishing. This component is excluded from general search, so those operations are not implemented here. + include_context "when managing a component as an admin" do + context "when cycling through publication states" do + let!(:component) { create(:page_component, participatory_space:) } + + include_examples "cycling through publication states" + end + end +end diff --git a/decidim-pages/spec/system/admin_publishes_component_spec.rb b/decidim-pages/spec/system/admin_publishes_component_spec.rb deleted file mode 100644 index 9b2fc5870f6b5..0000000000000 --- a/decidim-pages/spec/system/admin_publishes_component_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "pages" } - let!(:resource) { create(:page, component:) } - - include_context "when cycling through publication states" -end diff --git a/decidim-proposals/spec/system/admin/admin_manages_component_publication_spec.rb b/decidim-proposals/spec/system/admin/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..98a6e09443d7b --- /dev/null +++ b/decidim-proposals/spec/system/admin/admin_manages_component_publication_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + include_context "when managing a component as an admin" do + let!(:resource) { create(:proposal, :published, component:) } + + context "when cycling through publication states" do + let!(:component) { create(:proposal_component, participatory_space:) } + + include_examples "cycling through publication states" + end + + context "when component is unpublished, and admin publishes" do + let!(:component) { create(:proposal_component, :unpublished, participatory_space:) } + + include_examples "add component resources to search index" + end + + context "when component is published, and admin unpublishes" do + let!(:component) { create(:proposal_component, :published, participatory_space:) } + + include_examples "removes component resources from search index" + end + end +end diff --git a/decidim-proposals/spec/system/admin/admin_publishes_component_spec.rb b/decidim-proposals/spec/system/admin/admin_publishes_component_spec.rb deleted file mode 100644 index dae37826efb0b..0000000000000 --- a/decidim-proposals/spec/system/admin/admin_publishes_component_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "proposals" } - let!(:resource) { create(:proposal, :official, component:) } - - include_context "when publishing and unpublishing the component" - include_context "when cycling through publication states" -end diff --git a/decidim-surveys/spec/system/admin_manages_component_publication_spec.rb b/decidim-surveys/spec/system/admin_manages_component_publication_spec.rb new file mode 100644 index 0000000000000..14fbc40fbaac1 --- /dev/null +++ b/decidim-surveys/spec/system/admin_manages_component_publication_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages component publication" do + let!(:resource) { create(:survey, :published, :clean_after_publish, component:) } + + # Note: other components also handle search index updates (additions/removals) when publishing or + # unpublishing. This component is excluded from general search, so those operations are not implemented here. + include_context "when managing a component as an admin" do + context "when cycling through publication states" do + let!(:component) { create(:surveys_component, participatory_space:) } + + include_examples "cycling through publication states" + end + end +end diff --git a/decidim-surveys/spec/system/admin_publishes_component_spec.rb b/decidim-surveys/spec/system/admin_publishes_component_spec.rb deleted file mode 100644 index fff8d77cc2862..0000000000000 --- a/decidim-surveys/spec/system/admin_publishes_component_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Admin publishes component" do - let(:manifest_name) { "surveys" } - let!(:resource) { create(:survey, :published, :clean_after_publish, component:) } - - include_context "when cycling through publication states" -end From 04655387051250cdf4ee7f518c05603b11ae5a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 6 Mar 2026 16:42:15 +0100 Subject: [PATCH 067/135] Add action log for conferences' duplicate action (#16330) * Add action log for conferences' duplicate action * Fix i18n-tasks normalize offense --- .../conferences/admin/duplicate_conference.rb | 10 ++++++---- .../admin_log/conference_presenter.rb | 2 +- decidim-conferences/config/locales/en.yml | 1 + .../spec/commands/duplicate_conference_spec.rb | 18 +++++++++++++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/decidim-conferences/app/commands/decidim/conferences/admin/duplicate_conference.rb b/decidim-conferences/app/commands/decidim/conferences/admin/duplicate_conference.rb index fe2fad0772222..c6e93b9737a5f 100644 --- a/decidim-conferences/app/commands/decidim/conferences/admin/duplicate_conference.rb +++ b/decidim-conferences/app/commands/decidim/conferences/admin/duplicate_conference.rb @@ -24,10 +24,12 @@ def initialize(form, conference) def call return broadcast(:invalid) if form.invalid? - Conference.transaction do - duplicate_conference - duplicate_conference_attachments - duplicate_conference_components if @form.duplicate_components? + Decidim.traceability.perform_action!("duplicate", @conference, form.current_user) do + Conference.transaction do + duplicate_conference + duplicate_conference_attachments + duplicate_conference_components if @form.duplicate_components? + end end broadcast(:ok, @duplicated_conference) diff --git a/decidim-conferences/app/presenters/decidim/conferences/admin_log/conference_presenter.rb b/decidim-conferences/app/presenters/decidim/conferences/admin_log/conference_presenter.rb index ddefa46ef72d4..291d0ce8061e8 100644 --- a/decidim-conferences/app/presenters/decidim/conferences/admin_log/conference_presenter.rb +++ b/decidim-conferences/app/presenters/decidim/conferences/admin_log/conference_presenter.rb @@ -40,7 +40,7 @@ def i18n_labels_scope def action_string case action - when "create", "publish", "unpublish", "update", "update_diploma", "soft_delete", "restore" + when "create", "duplicate", "publish", "unpublish", "update", "update_diploma", "soft_delete", "restore" "decidim.admin_log.conference.#{action}" else super diff --git a/decidim-conferences/config/locales/en.yml b/decidim-conferences/config/locales/en.yml index 8a4ebf70fa971..b8abc5929cadd 100644 --- a/decidim-conferences/config/locales/en.yml +++ b/decidim-conferences/config/locales/en.yml @@ -322,6 +322,7 @@ en: admin_log: conference: create: "%{user_name} created the %{resource_name} conference" + duplicate: "%{user_name} duplicated the %{resource_name} conference" publish: "%{user_name} published the %{resource_name} conference" restore: "%{user_name} restored the %{resource_name} conference" send_conference_diplomas: "%{user_name} sent certificates of attendance to the %{resource_name} conference atendees" diff --git a/decidim-conferences/spec/commands/duplicate_conference_spec.rb b/decidim-conferences/spec/commands/duplicate_conference_spec.rb index 2878fea562690..940c0c2afeb78 100644 --- a/decidim-conferences/spec/commands/duplicate_conference_spec.rb +++ b/decidim-conferences/spec/commands/duplicate_conference_spec.rb @@ -7,6 +7,7 @@ module Decidim::Conferences subject { described_class.new(form, conference) } let(:organization) { create(:organization) } + let(:current_user) { create(:user, organization:) } let(:errors) { double.as_null_object } let!(:conference) { create(:conference, organization:, taxonomies: [taxonomy]) } let(:taxonomy) { create(:taxonomy, :with_parent, organization:) } @@ -17,7 +18,8 @@ module Decidim::Conferences invalid?: invalid, title: { en: "title" }, slug: "duplicated-slug", - duplicate_components?: duplicate_components + duplicate_components?: duplicate_components, + current_user: ) end @@ -56,6 +58,20 @@ module Decidim::Conferences it "broadcasts ok" do expect { subject.call }.to broadcast(:ok) end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:perform_action!) + .with("duplicate", conference, current_user) + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + + expect(action_log.action).to eq("duplicate") + expect(action_log.resource).to eq(conference) + expect(action_log.version).to be_present + end end context "when duplicate_components exists" do From a5f24659aaf70477c5303bf695def12d7d67b29a Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 6 Mar 2026 22:09:30 +0200 Subject: [PATCH 068/135] Upgrade to Rails 8.1.2 (#16310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Upgrade to Rails 8.1.2 * Add release notes about Azure * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Lint README * Misc improvements * Apply review recommendations * Update RELEASE_NOTES.md Co-authored-by: Andrés Pereira de Lucena * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Andrés Pereira de Lucena --- .github/actions/spelling/expect.txt | 1 + Gemfile.lock | 137 +++++++++--------- RELEASE_NOTES.md | 62 ++++---- decidim-core/decidim-core.gemspec | 8 +- decidim-core/lib/decidim/amendable.rb | 6 +- decidim-core/spec/lib/form_builder_spec.rb | 4 +- decidim-dev/decidim-dev.gemspec | 2 +- .../elections/admin/update_election_status.rb | 2 +- decidim-generators/Gemfile.lock | 137 +++++++++--------- decidim-generators/exe/decidim | 2 +- .../lib/decidim/generators/app_generator.rb | 9 +- .../generators/app_templates/storage.yml | 15 +- .../generators/test/generator_examples.rb | 8 +- .../spec/runtime/storage_flag_spec.rb | 2 +- decidim-meetings/spec/models/meeting_spec.rb | 2 +- .../lib/decidim/proposals/test/factories.rb | 8 +- .../pages/environment_variables.adoc | 22 --- 17 files changed, 203 insertions(+), 224 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 4d2bf80cbcc13..1890c2169ce4e 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -409,6 +409,7 @@ Ijkl ilike illgotten impersonatable +importmap inbox includeurl Indicacions diff --git a/Gemfile.lock b/Gemfile.lock index 716daf9297c85..24fd088c8b121 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,15 +90,15 @@ PATH omniauth-google-oauth2 (~> 1.0) omniauth-rails_csrf_protection (>= 1, < 3) omniauth-twitter (~> 1.4) - paper_trail (~> 16.0) - paranoia (~> 3.0.0) + paper_trail (~> 17.0) + paranoia (~> 3.1.0) pg (~> 1.5.0, < 2) pg_search (~> 2.2) premailer-rails (~> 1.10) rack (>= 3.2.4, < 4.0) rack-attack (~> 6.7.0) - rails (~> 8.0.0, >= 8.0.4) - rails-i18n (>= 8.0.2, < 8.2) + rails (~> 8.1.0) + rails-i18n (~> 8.1.0, < 8.2) ransack (~> 4.2.0) redis (>= 4.1, < 6.0) request_store (~> 1.7.0) @@ -119,7 +119,7 @@ PATH decidim-design (0.32.0.dev) decidim-core (= 0.32.0.dev) decidim-dev (0.32.0.dev) - bullet (~> 8.0.0) + bullet (~> 8.1.0) byebug (>= 11, < 14) capybara (~> 3.39) decidim-admin (= 0.32.0.dev) @@ -205,29 +205,31 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (8.0.4) - actionpack (= 8.0.4) - activesupport (= 8.0.4) + action_text-trix (2.1.16) + railties + actioncable (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.4) - actionpack (= 8.0.4) - activejob (= 8.0.4) - activerecord (= 8.0.4) - activestorage (= 8.0.4) - activesupport (= 8.0.4) + actionmailbox (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) - actionmailer (8.0.4) - actionpack (= 8.0.4) - actionview (= 8.0.4) - activejob (= 8.0.4) - activesupport (= 8.0.4) + actionmailer (8.1.2) + actionpack (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activesupport (= 8.1.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.4) - actionview (= 8.0.4) - activesupport (= 8.0.4) + actionpack (8.1.2) + actionview (= 8.1.2) + activesupport (= 8.1.2) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -235,15 +237,16 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.4) - actionpack (= 8.0.4) - activerecord (= 8.0.4) - activestorage (= 8.0.4) - activesupport (= 8.0.4) + actiontext (8.1.2) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.4) - activesupport (= 8.0.4) + actionview (8.1.2) + activesupport (= 8.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -251,29 +254,29 @@ GEM active_link_to (1.0.5) actionpack addressable - activejob (8.0.4) - activesupport (= 8.0.4) + activejob (8.1.2) + activesupport (= 8.1.2) globalid (>= 0.3.6) - activemodel (8.0.4) - activesupport (= 8.0.4) - activerecord (8.0.4) - activemodel (= 8.0.4) - activesupport (= 8.0.4) + activemodel (8.1.2) + activesupport (= 8.1.2) + activerecord (8.1.2) + activemodel (= 8.1.2) + activesupport (= 8.1.2) timeout (>= 0.4.0) - activestorage (8.0.4) - actionpack (= 8.0.4) - activejob (= 8.0.4) - activerecord (= 8.0.4) - activesupport (= 8.0.4) + activestorage (8.1.2) + actionpack (= 8.1.2) + activejob (= 8.1.2) + activerecord (= 8.1.2) + activesupport (= 8.1.2) marcel (~> 1.0) - activesupport (8.0.4) + activesupport (8.1.2) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -288,7 +291,6 @@ GEM base64 (0.3.0) batch-loader (2.0.6) bcrypt (3.1.21) - benchmark (0.5.0) better_html (2.2.0) actionview (>= 7.0) activesupport (>= 7.0) @@ -304,7 +306,7 @@ GEM racc browser (6.2.0) builder (3.3.0) - bullet (8.0.8) + bullet (8.1.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (13.0.0) @@ -627,14 +629,14 @@ GEM orm_adapter (0.5.0) ostruct (0.6.3) package_json (0.1.0) - paper_trail (16.0.0) - activerecord (>= 6.1) + paper_trail (17.0.0) + activerecord (>= 7.1) request_store (~> 1.4) parallel (1.27.0) parallel_tests (4.9.0) parallel - paranoia (3.0.1) - activerecord (>= 6, < 8.1) + paranoia (3.1.0) + activerecord (>= 7, < 8.2) parser (3.3.10.2) ast (~> 2.4.1) racc @@ -680,20 +682,20 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.0.4) - actioncable (= 8.0.4) - actionmailbox (= 8.0.4) - actionmailer (= 8.0.4) - actionpack (= 8.0.4) - actiontext (= 8.0.4) - actionview (= 8.0.4) - activejob (= 8.0.4) - activemodel (= 8.0.4) - activerecord (= 8.0.4) - activestorage (= 8.0.4) - activesupport (= 8.0.4) + rails (8.1.2) + actioncable (= 8.1.2) + actionmailbox (= 8.1.2) + actionmailer (= 8.1.2) + actionpack (= 8.1.2) + actiontext (= 8.1.2) + actionview (= 8.1.2) + activejob (= 8.1.2) + activemodel (= 8.1.2) + activerecord (= 8.1.2) + activestorage (= 8.1.2) + activesupport (= 8.1.2) bundler (>= 1.15.0) - railties (= 8.0.4) + railties (= 8.1.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -708,9 +710,9 @@ GEM rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - railties (8.0.4) - actionpack (= 8.0.4) - activesupport (= 8.0.4) + railties (8.1.2) + actionpack (= 8.1.2) + activesupport (= 8.1.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -731,10 +733,7 @@ GEM psych (>= 4.0.0) tsort redcarpet (3.6.1) - redis (5.4.1) - redis-client (>= 0.22.0) - redis-client (0.26.4) - connection_pool + redis (4.8.1) regexp_parser (2.11.3) reline (0.6.3) io-console (~> 0.5) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e228ca19f86d7..1b677ae56ecc3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -7,7 +7,7 @@ checking out the last version of this document in the [GitHub page for the relea As usual, we recommend that you have a full backup, of the database, application code and static files. -NOTE: Please note this release is updating Rails version from 7.2.2 to 7.2.3. Please ensure you back up your `SECRET_KEY_BASE` env variable and also `tmp/local_secret.txt` if you have it. +NOTE: Please note this release is updating Rails version from 7.2.2 to 8.1.2. Ensure you back up your `SECRET_KEY_BASE` env variable and also `tmp/local_secret.txt` if you have it. On your local development environment, you may need to set your `SECRET_KEY_BASE` env variable to the same value as the one present in your `tmp/local_secret.txt`. To update, follow these steps: @@ -32,34 +32,13 @@ gem "decidim", github: "decidim/decidim" gem "decidim-dev", github: "decidim/decidim" ``` -### 1.3. Rails upgrade - -This particular release is deploying a new Rails version, 8.0. As a result you need to update your application configuration. Before that, you need to run the following commands: +### 1.3. Run these commands ```console sudo apt install libvips libvips-tools # or the alternative installation process for your operating system. See "3.5. Replace image processing with imagemagick to libvips" bundle update decidim bin/rails decidim:upgrade -``` - -Please edit your `config/application.rb` to use the new Rails defaults. - -```diff -module DevelopDevelopmentApp - class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. -- config.load_defaults 7.2 -+ config.load_defaults 8.0 - # .... - end -end -``` - -You can read more about this change on PR [#16214](https://github.com/decidim/decidim/pull/16214). - -### 1.4. Run these commands - -```console +sed -i "s/config\.load_defaults 7\.2/config\.load_defaults 8.1/g" config/application.rb # see "2.1. Ruby on Rails update to 8.1" bin/rails db:migrate bin/rails decidim:upgrade:encryption # skip this command if you have run it before: @@ -70,7 +49,7 @@ bin/rails decidim:upgrade:fix_deleted_private_follows bin/rails data:migrate ``` -### 1.5. AWS/Azure/Google Cloud assets storage +### 1.4. AWS/Azure/Google Cloud assets storage There is a bug related to the cache expiration using Active Storage (assets, such as images). For fixing this issue, the Rails team added an extra active storage parameter, `public: true` that you can add it to your storage configuration. If you followed the step `3.4. Deprecation of Rails.application.secrets` and changed your `config/storage.yml` file you don't need to do anything else. @@ -80,11 +59,28 @@ Apart of that, you also need to configure your preferred cloud service provider You can read more about this change on PR [#15005](https://github.com/decidim/decidim/pull/15005/). -### 1.6. Follow the steps and commands detailed in these notes +### 1.5. Follow the steps and commands detailed in these notes ## 2. General notes -### 2.1. Module deprecations +### 2.1. Ruby on Rails update to 8.1 + +This particular release is deploying a new Rails version, 8.1. As a result you need to update your application configuration. Before that, you need to run the following commands: + +```console +sed -i "s/config\.load_defaults 7\.2/config\.load_defaults 8.1/g" config/application.rb # see "2.1. Ruby on Rails update to 8.1" +``` + +#### Removal of official Azure support from Active Storage + +Rails core team decided to remove the Azure Active Storage support from Rails 8.1, as the official Azure libraries are not maintained since September 2024. If you are using Azure for your Active Storage, support, you could use the unofficial Azure Active Storage gem [Azure Blob](https://github.com/testdouble/azure-blob) + +You can read more about this change on PR: + +- [Upgrade to Rails 8.0.4](https://github.com/decidim/decidim/pull/16214) +- [Upgrade to Rails 8.1.2](https://github.com/decidim/decidim/pull/16310). + +### 2.2. Module deprecations As part of our ongoing efforts to improve and make simpler Decidim, the following modules will be **deprecated** in this version (v0.31) and **removed** in the next major version (v0.32): @@ -100,7 +96,7 @@ The Sortitions module (`decidim-sortitions`) is removed in v0.32. This module pr The Polls feature within the Meetings module (`decidim-meetings`) will be removed in a future version (to be determined). This feature allowed meeting organizers to create polls during meetings. Organizations using meeting polls should plan to use external polling tools (for instance, through Jitsi) or migrate to other voting mechanisms available in Decidim, such as the new Elections module (`decidim-elections`). -### 2.2. Old private exports are now expired +### 2.3. Old private exports are now expired Due to some data consistency issues with the private exports, we have decided to expire all the previously generated files. Users are able to request and receive a new private export file. @@ -114,13 +110,13 @@ bin/rails decidim:upgrade:clean:remove_private_exports_attachments You can read more about this change on PR [#15020](https://github.com/decidim/decidim/pull/15020). -### 2.3. Add data migrations +### 2.4. Add data migrations At the moment we are adding this gem so we can start doing data migrations for fixes when v0.33.0 is released. You can read more about this at [Data migrations doc](https://docs.decidim.org/en/develop/develop/guide_data_migrations.html). You can read more about this change on PR [#15501](https://github.com/decidim/decidim/pull/15501). -### 2.4. Fix gitignore for ServiceWorker related files +### 2.5. Fix gitignore for ServiceWorker related files We detected a bug where some dynamic files are not added to the gitignore, so they could be committed to the repository. For fixing it, you need to add them to your gitignore file: @@ -130,7 +126,7 @@ echo "/public/sw.js*" >> .gitignore You can read more about this change on PR [#15601](https://github.com/decidim/decidim/pull/15601). -### 2.5. Data migration for organization short_name +### 2.6. Data migration for organization short_name A new data migration has been added to populate the `short_name` field for existing organizations. This field is required for the PWA (Progressive Web App) manifest to properly display the application name on mobile devices' home screens. @@ -140,7 +136,7 @@ This migration runs automatically when executing `bin/rails data:migrate` as par You can read more about this change on PR [#15729](https://github.com/decidim/decidim/pull/15729). -### 2.6. Add locale to the url +### 2.7. Add locale to the url For a long time Decidim has been using internally the user browser to detect the language of the user. This has been changed to use the locale of the url instead. @@ -163,7 +159,7 @@ We are also removing the `decidim_user_group_memberships` tables. You can read more about this change on PR [#16022](https://github.com/decidim/decidim/pull/16022). -### 2.8. [[TITLE OF THE ACTION]] +### 2.9. [[TITLE OF THE ACTION]] You can read more about this change on PR [#XXXX](https://github.com/decidim/decidim/pull/XXXX). diff --git a/decidim-core/decidim-core.gemspec b/decidim-core/decidim-core.gemspec index 9e6fdfe702edb..ff7dc9f6cd540 100644 --- a/decidim-core/decidim-core.gemspec +++ b/decidim-core/decidim-core.gemspec @@ -63,15 +63,15 @@ Gem::Specification.new do |s| s.add_dependency "omniauth-google-oauth2", "~> 1.0" s.add_dependency "omniauth-rails_csrf_protection", ">= 1", "< 3" s.add_dependency "omniauth-twitter", "~> 1.4" - s.add_dependency "paper_trail", "~> 16.0" - s.add_dependency "paranoia", "~> 3.0.0" + s.add_dependency "paper_trail", "~> 17.0" + s.add_dependency "paranoia", "~> 3.1.0" s.add_dependency "pg", "~> 1.5.0", "< 2" s.add_dependency "pg_search", "~> 2.2" s.add_dependency "premailer-rails", "~> 1.10" s.add_dependency "rack", ">= 3.2.4", "< 4.0" s.add_dependency "rack-attack", "~> 6.7.0" - s.add_dependency "rails", "~> 8.0.0", ">= 8.0.4" - s.add_dependency "rails-i18n", ">= 8.0.2", "< 8.2" + s.add_dependency "rails", "~> 8.1.0" + s.add_dependency "rails-i18n", "~> 8.1.0", "< 8.2" s.add_dependency "ransack", "~> 4.2.0" s.add_dependency "redis", ">= 4.1", "< 6.0" s.add_dependency "request_store", "~> 1.7.0" diff --git a/decidim-core/lib/decidim/amendable.rb b/decidim-core/lib/decidim/amendable.rb index b0911f9758ec5..34e6bfff5261c 100644 --- a/decidim-core/lib/decidim/amendable.rb +++ b/decidim-core/lib/decidim/amendable.rb @@ -155,7 +155,11 @@ def add_author(author) self.author = author end else # Assume is_a?(Decidim::Coauthorable) - coauthorships.clear + if persisted? + coauthorships.clear + else + coauthorships.target.clear + end add_coauthor(author) end end diff --git a/decidim-core/spec/lib/form_builder_spec.rb b/decidim-core/spec/lib/form_builder_spec.rb index 7d3614cd038dc..370aba0433b59 100644 --- a/decidim-core/spec/lib/form_builder_spec.rb +++ b/decidim-core/spec/lib/form_builder_spec.rb @@ -486,7 +486,7 @@ def organization it "renders the checkbox before the label text" do expect(output).to eq( - '
-
- <% if depth.zero? && has_replies_in_children? %> - - <% end %> -
<% if can_reply? %>
<%== cell("decidim/comments/comment_form", model, root_depth:, order:) %>
<% end %> -
"> - <% if has_replies_in_children? %> - <%= render :replies %> - <% end %> -
+ <% if has_replies_in_children? %> + <%= render :replies %> + <% elsif can_reply? %> +
+ <% end %> <% end %> <% if current_user.present? %> <%= cell("decidim/report_button", model, modal_id: "flagModalComment#{model.id}").flag_modal %> diff --git a/decidim-comments/app/cells/decidim/comments/comment_cell.rb b/decidim-comments/app/cells/decidim/comments/comment_cell.rb index 4e0c60d16d97c..3c792e8a940b6 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comment_cell.rb @@ -86,10 +86,6 @@ def comment_body formatted_body end - def replies - SortedComments.for(model, order_by: order) - end - def order options[:order] || "older" end @@ -234,7 +230,11 @@ def has_replies? end def has_replies_in_children? - model.descendants.where(decidim_commentable_type: "Decidim::Comments::Comment").not_hidden.not_deleted.exists? + replies_count.positive? + end + + def replies_count + @replies_count ||= model.replies.count end # action_authorization_button expects current_component to be available diff --git a/decidim-comments/app/cells/decidim/comments/comment_form/show.erb b/decidim-comments/app/cells/decidim/comments/comment_form/show.erb index 9670e0ca5ee7c..78bbcedd57d47 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_form/show.erb +++ b/decidim-comments/app/cells/decidim/comments/comment_form/show.erb @@ -31,7 +31,7 @@ <% end %>
"> + <% end %> + <%= add_comment %> + <%= user_comments_blocked_warning %> +

<% if single_comment? %> <%= t("decidim.components.comments.comment_details_title") %> @@ -11,18 +18,14 @@ <% end %>

- <%= render :order_control unless two_columns_layout? %> + <% if two_columns_layout? %> +
<%= render :order_control %>
+ <% else %> + <%= render :order_control %> + <% end %>
<%= single_comment_warning %> <%= blocked_comments_warning %> <%= render_comments %> - <% if user_signed_in? %> - - <% end %> - <%= add_comment %> - <%= user_comments_blocked_warning %>
- <%= cell("decidim/announcement", t("decidim.components.comments.loading"), callout_class: "primary loading-comments hidden") %>
diff --git a/decidim-comments/app/cells/decidim/comments/comments_cell.rb b/decidim-comments/app/cells/decidim/comments/comments_cell.rb index 8df08b549a893..011e629698288 100644 --- a/decidim-comments/app/cells/decidim/comments/comments_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comments_cell.rb @@ -109,7 +109,7 @@ def available_orders end def order - options[:order] || "older" + options[:order] || (two_columns_layout? ? "recent" : "older") end def decidim diff --git a/decidim-comments/app/cells/decidim/comments/two_columns_comments/column.erb b/decidim-comments/app/cells/decidim/comments/two_columns_comments/column.erb index 7804433020591..0dbc0e6dbfb45 100644 --- a/decidim-comments/app/cells/decidim/comments/two_columns_comments/column.erb +++ b/decidim-comments/app/cells/decidim/comments/two_columns_comments/column.erb @@ -14,7 +14,12 @@ <% @comments.each do |comment| %> <%= cell("decidim/comments/comment_thread", comment, order:) %> <% end %> - <% else %> -

<%= @no_comments_message %>

+ <% elsif @top_comment.blank? %> +

<%= t("decidim.components.comments.no_comments_yet") %>

+ <% end %> + + <% if @has_more %> + <% offset = @comments.size + (@top_comment.present? ? 1 : 0) %> + <%= controller.view_context.render partial: "decidim/comments/comments/load_more_comments", locals: { commentable: model, order:, offset:, alignment: @alignment } %> <% end %>
diff --git a/decidim-comments/app/cells/decidim/comments/two_columns_comments/show.erb b/decidim-comments/app/cells/decidim/comments/two_columns_comments/show.erb index 39dd70cfe85a0..615851a2f6bae 100644 --- a/decidim-comments/app/cells/decidim/comments/two_columns_comments/show.erb +++ b/decidim-comments/app/cells/decidim/comments/two_columns_comments/show.erb @@ -1,11 +1,14 @@
<%= comments_loading %> - <% @interleaved_comments.each do |comment| %> - <%= cell("decidim/comments/comment_thread", comment, order:, top_comment: (comment == @top_comment_in_favor || comment == @top_comment_against)) %> + <% @mobile_comments.each do |comment| %> + <%= cell("decidim/comments/comment_thread", comment, order:) %> + <% end %> + <% if @has_more_mobile %> + <%= controller.view_context.render partial: "decidim/comments/comments/load_more_comments", locals: { commentable: model, order:, offset: @mobile_comments.size } %> <% end %>
diff --git a/decidim-comments/app/cells/decidim/comments/two_columns_comments_cell.rb b/decidim-comments/app/cells/decidim/comments/two_columns_comments_cell.rb index c47c009950d4e..7f31dfb186026 100644 --- a/decidim-comments/app/cells/decidim/comments/two_columns_comments_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/two_columns_comments_cell.rb @@ -6,14 +6,15 @@ module Comments class TwoColumnsCommentsCell < Decidim::Comments::CommentsCell def call initialize_comments - @interleaved_comments = interleave_comments(@sorted_comments_in_favor, @sorted_comments_against) render :show end - def render_column(top_comment, comments, icon_name, title) - set_column_variables(top_comment, comments, icon_name, title) + # rubocop:disable Metrics/ParameterLists + def render_column(top_comment, comments, icon_name, title, alignment, has_more) + set_column_variables(top_comment, comments, icon_name, title, alignment, has_more) render :column end + # rubocop:enable Metrics/ParameterLists private @@ -21,19 +22,25 @@ def initialize_comments if model.closed? load_closed_comments else - @sorted_comments_in_favor = comments_in_favor - @sorted_comments_against = comments_against + @sorted_comments_in_favor = comments_in_favor_query.query + @sorted_comments_against = comments_against_query.query end + + counts = comments_count_by_alignment + @has_more_in_favor = (counts[1] || 0) > comments_in_favor_query.offset + comments_in_favor_query.limit + @has_more_against = (counts[-1] || 0) > comments_against_query.offset + comments_against_query.limit + + load_mobile_comments(counts.values.sum) end def load_closed_comments - @top_comment_in_favor, @sorted_comments_in_favor = sorted_comments(comments_in_favor) - @top_comment_against, @sorted_comments_against = sorted_comments(comments_against) + @top_comment_in_favor, @sorted_comments_in_favor = sorted_comments(comments_in_favor_query.query) + @top_comment_against, @sorted_comments_against = sorted_comments(comments_against_query.query) end def sorted_comments(comments) top_comment = find_top_comment(comments) - sorted_comments = comments.where.not(id: top_comment&.id).order(created_at: :asc) + sorted_comments = comments.where.not(id: top_comment&.id) [top_comment, sorted_comments] end @@ -45,42 +52,34 @@ def find_top_comment(comments) .first end - def interleave_comments(comments_in_favor, comments_against) - interleave_top_comments + interleave_remaining_comments(comments_in_favor, comments_against) - end - - def interleave_top_comments - return [] unless model.closed? - - Array(@top_comment_in_favor) + Array(@top_comment_against) + def comments_in_favor_query + @comments_in_favor_query ||= SortedComments.new(model, order_by: order, alignment: 1, offset: 0) end - def interleave_remaining_comments(comments_in_favor, comments_against) - interleaved = [] - max_length = [comments_in_favor.size, comments_against.size].max - - max_length.times do |i| - interleaved << comments_in_favor[i] if comments_in_favor[i] - interleaved << comments_against[i] if comments_against[i] - end - - interleaved + def comments_against_query + @comments_against_query ||= SortedComments.new(model, order_by: order, alignment: -1, offset: 0) end - def comments_in_favor - @comments_in_favor ||= model.comments.positive.order(:created_at) + def load_mobile_comments(total_count) + @sorted_comments_query = SortedComments.new(model, order_by: order, offset: 0) + @mobile_comments = @sorted_comments_query.query + @has_more_mobile = total_count > @sorted_comments_query.offset + @sorted_comments_query.limit end - def comments_against - @comments_against ||= model.comments.negative.order(:created_at) + def comments_count_by_alignment + @comments_count_by_alignment ||= Decidim::Comments::Comment.where(commentable: model).group(:alignment).count end - def set_column_variables(top_comment, comments, icon_name, title) + # rubocop:disable Metrics/ParameterLists + def set_column_variables(top_comment, comments, icon_name, title, alignment, has_more) @top_comment = top_comment @comments = comments @icon_name = icon_name @title = title + @alignment = alignment + @has_more = has_more end + # rubocop:enable Metrics/ParameterLists end end end diff --git a/decidim-comments/app/controllers/decidim/comments/comments_controller.rb b/decidim-comments/app/controllers/decidim/comments/comments_controller.rb index ae3ec488d8e6d..da1ec98d3405f 100644 --- a/decidim-comments/app/controllers/decidim/comments/comments_controller.rb +++ b/decidim-comments/app/controllers/decidim/comments/comments_controller.rb @@ -13,21 +13,29 @@ class CommentsController < Decidim::Comments::ApplicationController before_action :set_commentable, except: [:destroy, :update] before_action :ensure_commentable!, except: [:destroy, :update] - helper_method :root_depth, :commentable, :order, :reply?, :reload?, :root_comment + helper_method :root_depth, :commentable, :order, :reply?, :reload?, :root_comment, :load_more?, :comments_offset, :alignment def index enforce_permission_to(:read, :comment, commentable:) - @comments = SortedComments.for( - commentable, - order_by: order, - after: params.fetch(:after, 0).to_i - ) + if commentable.is_a?(Decidim::Comments::Comment) + @comments = commentable.descendants.includes(:author, :up_votes, :down_votes).to_a + @has_more_comments = false + else + @sorted_comments_query = SortedComments.new( + commentable, + order_by: order, + offset: comments_offset, + alignment: + ) + @comments = @sorted_comments_query.query + @has_more_comments = @sorted_comments_query.has_more? + end @comments = @comments.reject do |comment| next if comment.depth < 1 next if !comment.deleted? && !comment.hidden? - comment.commentable.descendants.where(decidim_commentable_type: "Decidim::Comments::Comment").not_hidden.not_deleted.blank? + comment.commentable.replies.blank? end @comments_count = commentable.comments_count @@ -35,6 +43,8 @@ def index format.js do if reload? render :reload + elsif load_more? + render :load_more_comments else render :index end @@ -187,10 +197,25 @@ def reload? params.fetch(:reload, 0).to_i == 1 end + def load_more? + params.fetch(:load_more, 0).to_i == 1 + end + + def comments_offset + params.fetch(:offset, 0).to_i + end + def root_depth params.fetch(:root_depth, 0).to_i end + def alignment + value = params.fetch(:alignment, nil) + return nil if value.blank? + + value.to_i + end + def commentable_path return commentable.polymorphic_resource_path({}) if commentable.respond_to?(:polymorphic_resource_path) diff --git a/decidim-comments/app/packs/entrypoints/decidim_comments.js b/decidim-comments/app/packs/entrypoints/decidim_comments.js index 0e86ff6387e49..52c4eb85d5151 100644 --- a/decidim-comments/app/packs/entrypoints/decidim_comments.js +++ b/decidim-comments/app/packs/entrypoints/decidim_comments.js @@ -4,3 +4,9 @@ import "stylesheets/comments.scss" // JavaScript import "src/decidim/comments/comments" import "src/decidim/comments/comments_mobile_modal" + +// Stimulus controllers +import { definitionsFromContext } from "src/decidim/refactor/support/stimulus" + +const context = require.context("src/decidim/comments/controllers", true, /controller\.js$/) +window.Stimulus.load(definitionsFromContext(context)) diff --git a/decidim-comments/app/packs/src/decidim/comments/comments.component.js b/decidim-comments/app/packs/src/decidim/comments/comments.component.js index e9d6bc250988d..1004ed2db4319 100644 --- a/decidim-comments/app/packs/src/decidim/comments/comments.component.js +++ b/decidim-comments/app/packs/src/decidim/comments/comments.component.js @@ -22,7 +22,6 @@ export default class CommentsComponent { this.rootDepth = config.rootDepth; this.order = config.order; this.lastCommentId = config.lastCommentId; - this.pollingInterval = config.pollingInterval || 15000; this.singleComment = config.singleComment; this.toggleTranslations = config.toggleTranslations; this.id = this.$element.attr("id") || this._getUID(); @@ -59,7 +58,6 @@ export default class CommentsComponent { unmountComponent() { if (this.mounted) { this.mounted = false; - this._stopPolling(); this.lastCommentId = null; $(".add-comment [data-opinion-toggle] button", this.$element).off("click.decidim-comments"); @@ -161,7 +159,6 @@ export default class CommentsComponent { const $submit = $("button[type='submit']", $form); $submit.attr("disabled", "disabled"); - this._stopPolling(); }); const $dropdown = $add.find("[data-comments-dropdown]"); @@ -225,22 +222,6 @@ export default class CommentsComponent { } }); } - - // Restart the polling - this._pollComments(); - } - - /** - * Sets a timeout to poll new comments. - * @private - * @returns {Void} - Returns nothing - */ - _pollComments() { - this._stopPolling(); - - this.pollTimeout = setTimeout(() => { - this._fetchComments(); - }, this.pollingInterval); } reloadAllComments() { @@ -265,29 +246,16 @@ export default class CommentsComponent { "root_depth": this.rootDepth, "order": this.order, // From here, the rest of properties are optional - ...(this.toggleTranslations && { "toggle_translations": this.toggleTranslations }), - ...(this.lastCommentId && { "after": this.lastCommentId }) + ...(this.toggleTranslations && { "toggle_translations": this.toggleTranslations }) }), success: () => { if (successCallback) { successCallback(); } - this._pollComments(); } }); } - /** - * Stops polling for new comments. - * @private - * @returns {Void} - Returns nothing - */ - _stopPolling() { - if (this.pollTimeout) { - clearTimeout(this.pollTimeout); - } - } - /** * Sets the loading comments element visible in the view. * @private @@ -299,16 +267,6 @@ export default class CommentsComponent { $("> .loading-comments", $container).removeClass("hidden"); } - /** - * Event listener for the ordering links. - * @private - * @returns {Void} - Returns nothing - */ - _onInitOrder() { - this._stopPolling(); - this._setLoading(); - } - /** * Updates the state of the submit button based on input text and opinion selection. * diff --git a/decidim-comments/app/packs/src/decidim/comments/comments.component.test.js b/decidim-comments/app/packs/src/decidim/comments/comments.component.test.js index fb78065410eb5..2e0fbd39f9f06 100644 --- a/decidim-comments/app/packs/src/decidim/comments/comments.component.test.js +++ b/decidim-comments/app/packs/src/decidim/comments/comments.component.test.js @@ -10,14 +10,11 @@ window.$ = jest.fn().mockImplementation((...args) => $(...args)); window.$.ajax = jest.fn().mockImplementation((...args) => $.ajax(...args)); window.$.extend = jest.fn().mockImplementation((...args) => $.extend(...args)); -// Rails.ajax is used by the fetching/polling of the comments +// Rails.ajax is used by the fetching of the comments import Rails from "@rails/ujs"; jest.mock("@rails/ujs"); window.Rails = Rails; -// Fake timers for testing polling -jest.useFakeTimers(); - import Configuration from "src/decidim/refactor/implementation/configuration"; // Component is loaded with require because using import loads it before $ has been mocked // so tests are not able to check the spied behaviours @@ -196,6 +193,36 @@ describe("CommentsComponent", () => { } const generateSingleComment = (commentId, content, replies = "") => { + const hasReplies = replies.trim().length > 0; + const repliesStructure = hasReplies + ? ` +
+
+ + +
+ +
+ ` + : `
`; + return `
@@ -232,40 +259,42 @@ describe("CommentsComponent", () => {

${content}

-
+
- -
${replies}
+ ${repliesStructure}
`; } @@ -359,8 +388,7 @@ describe("CommentsComponent", () => { commentsUrl: "/comments", rootDepth: 0, order: "older", - lastCommentId: 456, - pollingInterval: 1000 + lastCommentId: 456 }); $doc = $(document); @@ -393,8 +421,7 @@ describe("CommentsComponent", () => { data: new URLSearchParams({ "commentable_gid": "commentable-gid", "root_depth": 0, - order: "older", - after: 456 + order: "older" }), success: expect.any(Function) }); @@ -414,30 +441,6 @@ describe("CommentsComponent", () => { expect($(`${selector} .add-comment textarea`).prop("disabled")).toBeFalsy(); }); - it("starts polling for new comments", () => { - jest.spyOn(window, "setTimeout"); - Rails.ajax.mockImplementationOnce((options) => options.success()); - - subject.mountComponent(); - - expect(window.setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); - }); - - it("does not disable the textarea when polling comments normally", () => { - Rails.ajax.mockImplementationOnce((options) => options.success()); - - subject.mountComponent(); - - // Delay the success call 2s after the polling has happened to test that - // the textarea is still enabled when the polling is happening. - Rails.ajax.mockImplementationOnce((options) => { - setTimeout(() => options.success(), 2000); - }); - jest.advanceTimersByTime(1500); - - expect($(`${selector} .add-comment textarea`).prop("disabled")).toBeFalsy(); - }); - describe("when mounted", () => { beforeEach(() => { spyOnAddComment("on"); @@ -508,7 +511,7 @@ describe("CommentsComponent", () => { ).toBeTruthy(); }); - it("disables the submit button on submit and stops polling", () => { + it("disables the submit button on submit", () => { jest.spyOn(window, "clearTimeout"); commentText.html("This is a test comment") @@ -518,8 +521,6 @@ describe("CommentsComponent", () => { expect( $("button[type='submit']", commentSection.commentForm).is(":disabled") ).toBeTruthy(); - - expect(window.clearTimeout).toHaveBeenCalledWith(subject.pollTimeout); }); }); diff --git a/decidim-comments/app/packs/src/decidim/comments/controllers/load_more_comments/controller.js b/decidim-comments/app/packs/src/decidim/comments/controllers/load_more_comments/controller.js new file mode 100644 index 0000000000000..d6797f6a5a8b7 --- /dev/null +++ b/decidim-comments/app/packs/src/decidim/comments/controllers/load_more_comments/controller.js @@ -0,0 +1,196 @@ +import { Controller } from "@hotwired/stimulus" + +/** + * Load More Comments Stimulus Controller + * + * Handles paginated loading of additional comments via AJAX requests. + * When the user clicks the "Load more" button, it fetches the next page of comments + * from the server and appends them to the existing comment list. + * + * Usage: + *
+ * + * + *
+ */ +export default class extends Controller { + static get values() { + return { + url: String, + commentableGid: String, + order: String, + offset: Number, + perPage: Number, + alignment: Number + } + } + + static get targets() { + return ["button", "spinner"] + } + + connect() { + this.loading = false; + } + + /** + * Load more comments when the button is clicked + * @param {Event} event - The click event from the button + * @returns {void} + */ + async loadMore(event) { + event.preventDefault(); + + if (this.loading) { + return; + } + + this.loading = true; + this.showLoadingState(); + + try { + const url = this.buildUrl(); + const response = await this.makeRequest(url); + + if (response.ok) { + const script = await response.text(); + this.executeScript(script); + } else { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + this.handleError(error); + } finally { + this.loading = false; + this.hideLoadingState(); + } + } + + /** + * Build the URL with query parameters for the AJAX request + * @private + * @returns {string} The URL with query parameters + */ + buildUrl() { + const params = new URLSearchParams({ + "commentable_gid": this.commentableGidValue, + "order": this.orderValue, + "offset": this.offsetValue, + "load_more": 1 + }); + + if (this.hasAlignmentValue && typeof this.alignmentValue !== "undefined") { + params.append("alignment", this.alignmentValue); + } + + return `${this.urlValue}?${params.toString()}`; + } + + /** + * Make the HTTP request using fetch + * @private + * @param {string} url - The URL to request + * @returns {Promise} The fetch response + */ + async makeRequest(url) { + const csrfToken = this.getCSRFToken(); + + return fetch(url, { + method: "GET", + headers: { + "Accept": "text/javascript", + "X-Requested-With": "XMLHttpRequest", + ...(csrfToken && { "X-CSRF-Token": csrfToken }) + }, + credentials: "same-origin" + }); + } + + /** + * Get CSRF token from meta tag + * @private + * @returns {string|null} The CSRF token or null if not found + */ + getCSRFToken() { + const tokenElement = document.querySelector('meta[name="csrf-token"]'); + return tokenElement + ? tokenElement.getAttribute("content") + : null; + } + + /** + * Execute the JavaScript response from the server + * @private + * @param {string} script - The JavaScript code to execute + * @returns {void} + */ + executeScript(script) { + const scriptElement = document.createElement("script"); + scriptElement.textContent = script; + document.body.appendChild(scriptElement); + document.body.removeChild(scriptElement); + } + + /** + * Show loading state on the button + * @private + * @returns {void} + */ + showLoadingState() { + if (this.hasButtonTarget) { + this.buttonTarget.disabled = true; + this.buttonTarget.classList.add("loading"); + } + if (this.hasSpinnerTarget) { + this.spinnerTarget.classList.remove("hidden"); + } + } + + /** + * Hide loading state on the button + * @private + * @returns {void} + */ + hideLoadingState() { + if (this.hasButtonTarget) { + this.buttonTarget.disabled = false; + this.buttonTarget.classList.remove("loading"); + } + if (this.hasSpinnerTarget) { + this.spinnerTarget.classList.add("hidden"); + } + } + + /** + * Handle error response + * @private + * @param {Error} error - The error that occurred + * @returns {void} + */ + handleError(error) { + console.error("Error loading more comments:", error); + + this.dispatch("error", { + detail: { + error: error.message, + element: this.element + } + }); + } + + /** + * Hide the load more button when no more comments are available + * @public + * @returns {void} + */ + hideButton() { + this.element.remove(); + } +} diff --git a/decidim-comments/app/packs/src/decidim/comments/controllers/show_replies/controller.js b/decidim-comments/app/packs/src/decidim/comments/controllers/show_replies/controller.js new file mode 100644 index 0000000000000..8b65647ba8ccf --- /dev/null +++ b/decidim-comments/app/packs/src/decidim/comments/controllers/show_replies/controller.js @@ -0,0 +1,235 @@ +import { Controller } from "@hotwired/stimulus" + +/** + * Show Replies Stimulus Controller + * + * Handles lazy loading and toggling visibility of comment replies. + * On first click, it fetches replies via AJAX. Subsequent clicks toggle visibility. + * + * Usage: + *
+ * + * + * + *
+ */ +export default class extends Controller { + static get values() { + return { + url: String, + commentGid: String, + order: String, + loaded: Boolean + } + } + + static get targets() { + return ["container", "button", "spinner"] + } + + connect() { + this.loading = false; + } + + /** + * Toggle replies visibility - loads on first click, then toggles + * @param {Event} event - The click event from the button + * @returns {void} + */ + async toggle(event) { + event.preventDefault(); + + if (this.loading) { + return; + } + + if (this.loadedValue) { + // Already loaded - just toggle visibility + this.toggleVisibility(); + } else { + // First time - load replies via AJAX + await this.loadReplies(); + } + } + + /** + * Load replies via AJAX + * @private + * @returns {Promise} A promise that resolves when replies are loaded + */ + async loadReplies() { + this.loading = true; + this.showLoadingState(); + + try { + const url = this.buildUrl(); + const response = await this.makeRequest(url); + + if (response.ok) { + const script = await response.text(); + this.executeScript(script); + this.loadedValue = true; + this.showReplies(); + } else { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + this.handleError(error); + } finally { + this.loading = false; + this.hideLoadingState(); + } + } + + /** + * Build the URL with query parameters for the AJAX request + * @private + * @returns {string} The URL with query parameters + */ + buildUrl() { + const params = new URLSearchParams({ + "commentable_gid": this.commentGidValue, + "order": this.orderValue, + "offset": 0, + "load_more": 1 + }); + + return `${this.urlValue}?${params.toString()}`; + } + + /** + * Make the HTTP request using fetch + * @private + * @param {string} url - The URL to request + * @returns {Promise} The fetch response + */ + async makeRequest(url) { + const csrfToken = this.getCSRFToken(); + + return fetch(url, { + method: "GET", + headers: { + "Accept": "text/javascript", + "X-Requested-With": "XMLHttpRequest", + ...(csrfToken && { "X-CSRF-Token": csrfToken }) + }, + credentials: "same-origin" + }); + } + + /** + * Get CSRF token from meta tag + * @private + * @returns {string|null} The CSRF token or null if not found + */ + getCSRFToken() { + const tokenElement = document.querySelector('meta[name="csrf-token"]'); + return tokenElement + ? tokenElement.getAttribute("content") + : null; + } + + /** + * Execute the JavaScript response from the server + * @private + * @param {string} script - The JavaScript code to execute + * @returns {void} + */ + executeScript(script) { + const scriptElement = document.createElement("script"); + scriptElement.textContent = script; + document.body.appendChild(scriptElement); + document.body.removeChild(scriptElement); + } + + /** + * Show loading state + * @private + * @returns {void} + */ + showLoadingState() { + if (this.hasSpinnerTarget) { + this.spinnerTarget.classList.remove("hidden"); + } + if (this.hasButtonTarget) { + this.buttonTarget.disabled = true; + } + } + + /** + * Hide loading state + * @private + * @returns {void} + */ + hideLoadingState() { + if (this.hasSpinnerTarget) { + this.spinnerTarget.classList.add("hidden"); + } + if (this.hasButtonTarget) { + this.buttonTarget.disabled = false; + } + } + + /** + * Show replies (after loading or toggling) + * @private + * @returns {void} + */ + showReplies() { + if (this.hasContainerTarget) { + this.containerTarget.classList.remove("hidden"); + } + if (this.hasButtonTarget) { + this.buttonTarget.setAttribute("aria-expanded", "true"); + } + } + + /** + * Hide replies + * @private + * @returns {void} + */ + hideReplies() { + if (this.hasContainerTarget) { + this.containerTarget.classList.add("hidden"); + } + if (this.hasButtonTarget) { + this.buttonTarget.setAttribute("aria-expanded", "false"); + } + } + + /** + * Toggle visibility of replies + * @private + * @returns {void} + */ + toggleVisibility() { + if (this.hasContainerTarget && this.containerTarget.classList.contains("hidden")) { + this.showReplies(); + } else { + this.hideReplies(); + } + } + + /** + * Handle error response + * @private + * @param {Error} error - The error that occurred + * @returns {void} + */ + handleError(error) { + console.error("Error loading replies:", error); + + this.dispatch("error", { + detail: { + error: error.message, + element: this.element + } + }); + } +} diff --git a/decidim-comments/app/packs/stylesheets/comments.scss b/decidim-comments/app/packs/stylesheets/comments.scss index e04fff05c3888..6655cad0cffa9 100644 --- a/decidim-comments/app/packs/stylesheets/comments.scss +++ b/decidim-comments/app/packs/stylesheets/comments.scss @@ -6,7 +6,7 @@ } .add-comment { - @apply mt-12 md:bg-gray-5 p-0 md:px-4 md:pb-6 md:pt-1 rounded-lg w-full; + @apply mt-2 md:bg-gray-5 p-0 md:px-4 md:pb-6 md:pt-1 rounded-lg w-full; .new_comment { @apply w-full; @@ -461,10 +461,71 @@ } } +.show-replies-button, +.load-more-comments { + @apply flex items-center justify-center my-2; + + .button { + @apply border-0 rounded px-2 py-1 transition-colors; + + &:hover:not(:disabled) { + @apply bg-secondary/10 no-underline; + } + + &:disabled { + @apply opacity-100 bg-transparent text-secondary no-underline; + } + } +} + +.show-replies-button { + @apply my-0 relative z-10 h-0 overflow-visible; + + transform: translateY(calc(0.75rem + 1px)); + + .button { + @apply bg-white border-0; + + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12); + + &:hover:not(:disabled) { + @apply bg-white no-underline; + } + + &:disabled { + @apply bg-white opacity-100; + + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12); + } + + svg:first-of-type { + @apply block; + } + + svg:last-of-type { + @apply hidden; + } + + &[aria-expanded="true"] { + svg:first-of-type { + @apply hidden; + } + + svg:last-of-type { + @apply block; + } + } + } +} + .publish-comment-button { @apply ml-auto md:relative fixed inset-x-0 bottom-0 z-30 md:z-0 flex flex-row items-center md:items-end md:justify-end font-semibold justify-around bg-white md:bg-transparent gap-4 md:gap-0 p-4 md:p-0 shadow-inner md:shadow-none first:[&>*]:grow md:first:[&>*]:grow-0 h-14 md:h-fit first:[&>button]:py-3; } +[data-additional-reply] .comment__form-submit { + @apply flex justify-end; +} + .fullscreen { @apply fixed md:relative inset-0 w-screen md:w-full h-screen bg-white z-50 p-0 overflow-auto md:overflow-hidden rounded-none; } diff --git a/decidim-comments/app/queries/decidim/comments/sorted_comments.rb b/decidim-comments/app/queries/decidim/comments/sorted_comments.rb index e3a0e63f7a8ab..840c146880645 100644 --- a/decidim-comments/app/queries/decidim/comments/sorted_comments.rb +++ b/decidim-comments/app/queries/decidim/comments/sorted_comments.rb @@ -4,6 +4,8 @@ module Decidim module Comments # A class used to find comments for a commentable resource class SortedComments < Decidim::Query + DEFAULT_COMMENTS_LIMIT = 20 + attr_reader :commentable # Syntactic sugar to initialize the class and return the queried objects. @@ -11,6 +13,8 @@ class SortedComments < Decidim::Query # commentable - a resource that can have comments # options - The Hash options is used to refine the selection ( default: {}): # :order_by - The string order_by to sort by ( optional ) + # :limit - The number of items to load ( optional ) + # :offset - The number of items to skip ( optional ) def self.for(commentable, options = {}) new(commentable, options).query end @@ -20,6 +24,9 @@ def self.for(commentable, options = {}) # commentable = a resource that can have comments # options - The Hash options is used to refine the selection ( default: {}): # :order_by - The string order_by to sort by ( optional ) + # :limit - The number of items to load ( optional ) + # :offset - The number of items to skip ( optional ) + # :alignment - Filter by alignment: 1 (in_favor), -1 (against), 0 (neutral) ( optional ) def initialize(commentable, options = {}) options[:order_by] ||= "older" @commentable = commentable @@ -31,43 +38,63 @@ def initialize(commentable, options = {}) # level of nested replies. def query scope = base_scope - .includes(:author, :up_votes, :down_votes) - - case @options[:order_by] - when "recent" - order_by_recent(scope) - when "best_rated" - order_by_best_rated(scope) - when "most_discussed" - order_by_most_discussed(scope) - else - order_by_older(scope) - end + .includes(:author) + + sorted_scope = case @options[:order_by] + when "recent" + order_by_recent(scope) + when "best_rated" + order_by_best_rated(scope) + when "most_discussed" + order_by_most_discussed(scope) + else + order_by_older(scope) + end + + apply_limit(sorted_scope) + end + + def total_count + base_scope.count + end + + def has_more? + return false unless limited? + + total_count > offset + limit + end + + def offset + @options[:offset].to_i end - def count_replies(comment) - if comment.comment_threads.size.positive? - comment.comment_threads.size + comment.comment_threads.sum { |reply| count_replies(reply) } - else - 0 - end + def limit + @options[:limit]&.to_i || default_limit end private + def limited? + @options[:limit].present? || @options[:offset].present? + end + + def default_limit + DEFAULT_COMMENTS_LIMIT + end + + def apply_limit(scope) + return scope unless limited? + + scope.limit(limit).offset(offset) + end + def base_scope id = @options[:id] return Comment.where(root_commentable: commentable, id:) if id.present? - after = @options[:after] - if after.present? - return Comment.where(root_commentable: commentable).where( - "decidim_comments_comments.id > ?", - after - ) - end - - Comment.where(commentable:) + scope = Comment.where(commentable:) + scope = scope.where(alignment: @options[:alignment]) if @options[:alignment].present? + scope end def order_by_older(scope) @@ -79,15 +106,32 @@ def order_by_recent(scope) end def order_by_best_rated(scope) - scope.sort_by do |comment| - comment.up_votes.size - comment.down_votes.size - end.reverse + scope.order(Arel.sql("up_votes_count - down_votes_count DESC, created_at DESC")) end def order_by_most_discussed(scope) - scope.sort_by do |comment| - count_replies(comment) - end.reverse + scope + .select("decidim_comments_comments.*, COALESCE(descendants.total, 0) as descendants_count") + .joins(<<~SQL.squish) + LEFT JOIN LATERAL ( + WITH RECURSIVE comment_tree AS ( + SELECT id, decidim_commentable_id + FROM decidim_comments_comments AS replies + WHERE replies.decidim_commentable_id = decidim_comments_comments.id + AND replies.decidim_commentable_type = 'Decidim::Comments::Comment' + + UNION ALL + + SELECT r.id, r.decidim_commentable_id + FROM decidim_comments_comments AS r + INNER JOIN comment_tree ct ON r.decidim_commentable_id = ct.id + AND r.decidim_commentable_type = 'Decidim::Comments::Comment' + ) + SELECT COUNT(*) as total + FROM comment_tree + ) descendants ON true + SQL + .order(Arel.sql("descendants_count DESC, decidim_comments_comments.created_at DESC")) end end end diff --git a/decidim-comments/app/views/decidim/comments/comments/_load_more_comments.html.erb b/decidim-comments/app/views/decidim/comments/comments/_load_more_comments.html.erb new file mode 100644 index 0000000000000..134f605e2da9b --- /dev/null +++ b/decidim-comments/app/views/decidim/comments/comments/_load_more_comments.html.erb @@ -0,0 +1,22 @@ +
+ data-load-more-comments-alignment-value="<%= alignment %>" + <% end %> + class="load-more-comments"> + + +
diff --git a/decidim-comments/app/views/decidim/comments/comments/create.js.erb b/decidim-comments/app/views/decidim/comments/comments/create.js.erb index 01e36eb14f4fd..05236b3d53e93 100644 --- a/decidim-comments/app/views/decidim/comments/comments/create.js.erb +++ b/decidim-comments/app/views/decidim/comments/comments/create.js.erb @@ -13,12 +13,46 @@ hideButton.find("[data-show-comment-reply]").first().html('<%= t("decidim.components.comment.show_replies", count: Decidim::Comments::SortedComments.for(root_comment.reload).size) %>'); } else { - component.addThread(commentHtml, alignment, true); + var params = new URLSearchParams({ + "commentable_gid": component.commentableGid, + "root_depth": component.rootDepth, + "order": component.order, + "reload": 1 + }); + + var csrfToken = document.querySelector('meta[name="csrf-token"]'); + var headers = { + "Accept": "text/javascript", + "X-Requested-With": "XMLHttpRequest" + }; + if (csrfToken) { headers["X-CSRF-Token"] = csrfToken.getAttribute("content"); } + + fetch(component.commentsUrl + "?" + params.toString(), { + method: "GET", + headers: headers, + credentials: "same-origin" + }).then(function(response) { + if (!response.ok) { throw new Error("Network response was not ok"); } + return response.text(); + }).then(function(script) { + var scriptElement = document.createElement("script"); + scriptElement.textContent = script; + document.body.appendChild(scriptElement); + document.body.removeChild(scriptElement); + component._finalizeCommentCreation(null, true); + }).catch(function() { + var commentsContainer = document.getElementById(rootCommentableId); + var loadingElement = commentsContainer.querySelector(".loading-comments"); + var commentsElement = commentsContainer.querySelector(".comments"); + if (loadingElement) { loadingElement.classList.add("hidden"); } + if (commentsElement) { commentsElement.classList.remove("hidden"); } + component._finalizeCommentCreation(null, true); + }); } - Rails.fire(document, "comments:loaded", { - commentsIds: [<%= @comment.id %>, <%= @comment.commentable.id %>] - }); + document.dispatchEvent(new CustomEvent("comments:loaded", { + detail: { commentsIds: [<%= @comment.id %>, <%= @comment.commentable.id %>] } + })); // Update the comments count $(".comments-count", $comments).text(<%== t("decidim.components.comments.title", count: @comments_count).to_json %>); diff --git a/decidim-comments/app/views/decidim/comments/comments/index.js.erb b/decidim-comments/app/views/decidim/comments/comments/index.js.erb index cc64df3a31454..e6a0fc1e56c3c 100644 --- a/decidim-comments/app/views/decidim/comments/comments/index.js.erb +++ b/decidim-comments/app/views/decidim/comments/comments/index.js.erb @@ -1,35 +1,70 @@ (function() { - var rootCommentableId = <%== "comments-for-#{commentable.commentable_type.demodulize}-#{commentable.id}".to_json %>; + <% root_resource = commentable.is_a?(Decidim::Comments::Comment) ? commentable.root_commentable : commentable %> - var $comments = $("#" + rootCommentableId); + var commentsContainer = document.getElementById(<%== "comments-for-#{root_resource.commentable_type.demodulize}-#{root_resource.id}".to_json %>); + var $comments = $(commentsContainer); var component = $comments.data("comments"); - $(".loading-comments").addClass("hidden"); + // Find target container: desktop column, mobile container, or standard single-column + <% if alignment.present? %> + var targetContainer = commentsContainer.querySelector(<%== ".comments-section__#{alignment == 1 ? "in-favor" : "against"}".to_json %>); + <% else %> + var targetContainer = commentsContainer.querySelector("#mobileContainer") || commentsContainer.querySelector(".comment-threads"); + <% end %> - var commentsIds = []; -<% @comments.each do |comment| %> - var commentId = <%= comment.id.to_json %>; - commentsIds.push(commentId); - var commentHtml = '<%== j(render comment).strip %>'; - var inReplyTo = <%== (reply?(comment) ? comment.commentable.id : nil).to_json %>; - - var $comment = $("#comment_" + commentId); - if (1 > $comment.length) { - if (inReplyTo) { - component.addReply(inReplyTo, commentHtml); - } else { - component.addThread(commentHtml); + var loadingElement = commentsContainer.querySelector(".loading-comments"); + var commentsElement = commentsContainer.querySelector(".comments"); + if (loadingElement) { loadingElement.classList.add("hidden"); } + if (commentsElement) { commentsElement.classList.remove("hidden"); } + + <% unless alignment.present? %> + if (targetContainer) { + targetContainer.querySelectorAll(".comment-thread").forEach(function(el) { el.remove(); }); + var existingLoadMore = targetContainer.querySelector("[data-load-more-comments-wrapper]"); + if (existingLoadMore) { existingLoadMore.remove(); } } - } -<% end %> + <% end %> + + // Insert comments + var commentsIds = []; + <% @comments.each do |comment| %> + (function() { + var commentId = <%= comment.id.to_json %>; + var commentHtml = '<%== j(render comment).strip %>'; + var inReplyTo = <%== (reply?(comment) ? comment.commentable.id : nil).to_json %>; + + commentsIds.push(commentId); + if (targetContainer && !targetContainer.querySelector("#comment_" + commentId)) { + if (inReplyTo) { + component.addReply(inReplyTo, commentHtml); + } else { + component.addThread(commentHtml, <%= alignment.presence || "null" %>); + } + } + })(); + <% end %> + if (commentsIds.length) { component.lastCommentId = <%= @comments.last&.id || 0 %>; + Rails.fire(document, "comments:loaded", { commentsIds: commentsIds }); + } - Rails.fire(document, "comments:loaded", { - commentsIds: commentsIds - }); + // Update comments count + var countElement = commentsContainer.querySelector(".comments-count"); + if (countElement) { + countElement.textContent = <%== t("decidim.components.comments.title", count: @comments_count).to_json %>; } - // Update the comments count - $(".comments-count", $comments).text(<%== t("decidim.components.comments.title", count: @comments_count).to_json %>); + // Handle load more button + <% if @has_more_comments %> + if (targetContainer && !targetContainer.querySelector("[data-load-more-comments-wrapper]")) { + var loadMoreHtml = '<%== j(render partial: "decidim/comments/comments/load_more_comments", locals: { commentable:, order:, offset: @comments.size, alignment: alignment.presence }).strip %>'; + targetContainer.insertAdjacentHTML("beforeend", loadMoreHtml); + } + <% else %> + if (targetContainer) { + var loadMoreToRemove = targetContainer.querySelector("[data-load-more-comments-wrapper]"); + if (loadMoreToRemove) { loadMoreToRemove.remove(); } + } + <% end %> }()); diff --git a/decidim-comments/app/views/decidim/comments/comments/load_more_comments.js.erb b/decidim-comments/app/views/decidim/comments/comments/load_more_comments.js.erb new file mode 100644 index 0000000000000..0592e7bfc128b --- /dev/null +++ b/decidim-comments/app/views/decidim/comments/comments/load_more_comments.js.erb @@ -0,0 +1,73 @@ +(function() { + <% root_resource = commentable.is_a?(Decidim::Comments::Comment) ? commentable.root_commentable : commentable %> + <% is_loading_replies = commentable.is_a?(Decidim::Comments::Comment) %> + <% new_offset = comments_offset + @comments.size %> + + var commentsContainer = document.getElementById(<%== "comments-for-#{root_resource.commentable_type.demodulize}-#{root_resource.id}".to_json %>); + var $comments = $(commentsContainer); + var component = $comments.data("comments"); + + <% if is_loading_replies %> + var targetContainer = document.getElementById("comment-<%= commentable.id %>-replies"); + var loadMoreWrapper = null; + <% else %> + <% if alignment.present? %> + var targetContainer = commentsContainer.querySelector(<%== ".comments-section__#{alignment == 1 ? "in-favor" : "against"}".to_json %>); + <% else %> + var targetContainer = commentsContainer.querySelector("#mobileContainer") || commentsContainer.querySelector(".comment-threads"); + <% end %> + var loadMoreWrapper = targetContainer ? targetContainer.querySelector("[data-load-more-comments-wrapper]") : null; + <% end %> + + // Insert comments + var commentsIds = []; + <% @comments.each do |comment| %> + (function() { + var commentId = <%= comment.id.to_json %>; + var commentHtml = '<%== j(render comment).strip %>'; + var inReplyTo = <%== (reply?(comment) ? comment.commentable.id : nil).to_json %>; + + commentsIds.push(commentId); + var existingComment = targetContainer ? targetContainer.querySelector("#comment_" + commentId) : null; + if (!existingComment) { + if (inReplyTo) { + component.addReply(inReplyTo, commentHtml); + } else if (loadMoreWrapper) { + loadMoreWrapper.insertAdjacentHTML("beforebegin", commentHtml); + } else { + component.addThread(commentHtml, <%= alignment.presence || "null" %>); + } + } + })(); + <% end %> + + if (commentsIds.length) { + component.lastCommentId = <%= @comments.last&.id || 0 %>; + Rails.fire(document, "comments:loaded", { commentsIds: commentsIds }); + } + + <% if is_loading_replies %> + if (targetContainer) { + targetContainer.querySelectorAll(".comment-reply.hidden").forEach(function(el) { + el.classList.remove("hidden"); + }); + targetContainer.querySelectorAll(".show-replies-button").forEach(function(el) { + el.remove(); + }); + } + <% end %> + + <% unless is_loading_replies %> + <% if @has_more_comments %> + if (loadMoreWrapper) { + loadMoreWrapper.setAttribute("data-load-more-comments-offset-value", <%= new_offset %>); + var btn = loadMoreWrapper.querySelector("[data-load-more-comments-target='button']"); + var spin = loadMoreWrapper.querySelector("[data-load-more-comments-target='spinner']"); + if (btn) { btn.disabled = false; btn.classList.remove("loading"); } + if (spin) { spin.classList.add("hidden"); } + } + <% else %> + if (loadMoreWrapper) { loadMoreWrapper.remove(); } + <% end %> + <% end %> +}()); diff --git a/decidim-comments/app/views/decidim/comments/comments/update.js.erb b/decidim-comments/app/views/decidim/comments/comments/update.js.erb index 72d07565f36b4..77c035867ed49 100644 --- a/decidim-comments/app/views/decidim/comments/comments/update.js.erb +++ b/decidim-comments/app/views/decidim/comments/comments/update.js.erb @@ -1,26 +1,16 @@ document.addEventListener("turbo:load", () => { - var rootCommentableId = <%== "comments-for-#{@commentable.class.name.demodulize}-#{@commentable.id}".to_json %>; - var $comments = $("#" + rootCommentableId); - var config = $comments.data("decidim-comments"); - var component = $comments.data("comments"); - - component.unmountComponent(); - var commentHtml = '<%== j(render partial: "edited_comment", locals: { comment: @comment }).strip %>'; var commentId = <%= @comment.id.to_json %>; var $comment = $("#comment_<%= @comment.id %>"); - var $edit_modal = document.getElementById(`editCommentModal${commentId}`) + var $edit_modal = document.getElementById(`editCommentModal${commentId}`); - if ($edit_modal !== undefined) { - $edit_modal.remove() + if ($edit_modal) { + $edit_modal.remove(); } $comment.replaceWith(commentHtml); - // Re-create the component - component.mountComponent(); - Rails.fire(document, "comments:loaded", { commentsIds: [commentId] }); diff --git a/decidim-comments/config/locales/en.yml b/decidim-comments/config/locales/en.yml index c3149702743ef..b5efdfa4a168f 100644 --- a/decidim-comments/config/locales/en.yml +++ b/decidim-comments/config/locales/en.yml @@ -68,9 +68,6 @@ en: alignment: against: Against in_favor: In favor - answers: - one: "%{count} answer" - other: "%{count} answers" cancel_reply: Cancel reply comment_label: Comment %{comment_id} comment_label_reply: Comment %{comment_id} (reply to comment %{parent_comment_id}) @@ -84,7 +81,11 @@ en: hide_replies: one: Hide reply other: Hide %{count} replies + load_replies: Load replies moderated_at: Comment moderated on %{date} + replies_count: + one: "%{count} reply" + other: "%{count} replies" reply: Reply report: action: Report @@ -116,7 +117,9 @@ en: blocked_comments_warning: Comments are currently disabled, only administrators can reply or post new ones. comment_details_title: Comment details in_favor: In Favor + load_more_comments: Load more comments loading: Loading comments ... + no_comments_yet: No comments yet single_comment_warning: View all comments single_comment_warning_title: You are seeing a single comment title: diff --git a/decidim-comments/lib/decidim/comments/commentable.rb b/decidim-comments/lib/decidim/comments/commentable.rb index f41d7cc5ab0c1..57467ef3782e5 100644 --- a/decidim-comments/lib/decidim/comments/commentable.rb +++ b/decidim-comments/lib/decidim/comments/commentable.rb @@ -82,6 +82,11 @@ def update_comments_count def actions_for_comment(_comment, _current_user) [] end + + # Public: Returns the visible (not hidden, not deleted) descendant replies for this comment. + def replies + descendants.where(decidim_commentable_type: "Decidim::Comments::Comment").not_hidden.not_deleted + end end end end diff --git a/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb index 129215e3f2323..c3ff0a9db3d61 100644 --- a/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb @@ -20,8 +20,6 @@ module Decidim::Comments context "when rendering" do it "renders the card" do expect(subject).to have_css("#comment_#{comment.id}") - # An empty replies element is needed when dynamically adding replies - expect(subject).to have_css("#comment-#{comment.id}-replies", text: "") expect(subject).to have_css(".comment__content") expect(subject).to have_css("button[data-dialog-open='loginModal'][title='#{I18n.t("decidim.components.comment.report.action")}']") expect(subject).to have_css("a[href='/en/processes/#{participatory_process.slug}/f/#{component.id}/dummy_resources/#{commentable.id}?commentId=#{comment.id}#comment_#{comment.id}']") @@ -33,6 +31,7 @@ module Decidim::Comments expect(subject).to have_no_css(".comment-reply") expect(subject).to have_no_css("#flagModalComment#{comment.id}") expect(subject).to have_no_css(".label.alignment") + expect(subject).to have_no_css("#comment-#{comment.id}-replies") end context "when deleted" do @@ -81,7 +80,6 @@ module Decidim::Comments it "renders the card with an Edited message" do expect(subject).to have_css("#comment_#{comment.id}") - expect(subject).to have_css("#comment-#{comment.id}-replies", text: "") expect(subject).to have_css(".comment__content") expect(subject).to have_css("button[data-dialog-open='loginModal'][title='#{I18n.t("decidim.components.comment.report.action")}']") expect(subject).to have_css("a[href='/en/processes/#{participatory_process.slug}/f/#{component.id}/dummy_resources/#{commentable.id}?commentId=#{comment.id}#comment_#{comment.id}']") @@ -93,6 +91,7 @@ module Decidim::Comments expect(subject).to have_no_css(".add-comment") expect(subject).to have_no_css(".comment-reply") expect(subject).to have_no_css(".label.alignment") + expect(subject).to have_no_css("#comment-#{comment.id}-replies") end end @@ -149,12 +148,10 @@ module Decidim::Comments allow(resource_locator).to receive(:path).and_return("/dummies") end - it "renders the replies" do - element = subject.find("#comment-#{comment.id}-replies") - replies.each do |reply| - expect(element).to have_css("#comment_#{reply.id}") - expect(element).to have_content(reply.body.values.first) - end + it "renders the load replies button" do + expect(subject).to have_css("button[data-action='click->show-replies#toggle']") + expect(subject).to have_content(I18n.t("decidim.components.comment.replies_count", count: replies.size)) + expect(subject).to have_css("#comment-#{comment.id}-replies.hidden") end end @@ -170,6 +167,7 @@ module Decidim::Comments expect(subject).to have_css(".comment__actions button") expect(subject).to have_css("button[data-dialog-open='flagModalComment#{comment.id}']") expect(subject).to have_css("#flagModalComment#{comment.id}") + expect(subject).to have_css("#comment-#{comment.id}-replies") end context "with votes" do diff --git a/decidim-comments/spec/cells/decidim/comments/comment_thread_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/comment_thread_cell_spec.rb index c0dbad7b0dc40..3b3f7eebc7657 100644 --- a/decidim-comments/spec/cells/decidim/comments/comment_thread_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/comment_thread_cell_spec.rb @@ -36,8 +36,9 @@ module Decidim::Comments allow(resource_locator).to receive(:path).and_return("/dummies") end - it "renders the reply" do - expect(subject).to have_css(".comment-reply .comment", count: 10) + it "renders the load replies button and container for lazy-loaded replies" do + expect(subject).to have_css(".comment-reply") + expect(subject).to have_css("[data-action='click->show-replies#toggle']") expect(subject).to have_css("[aria-label='Comment thread started by #{comment.author.name} on 01/02/2018 12:30']") end diff --git a/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb index 4f0baf410b6f0..425ee514a2d16 100644 --- a/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb @@ -61,9 +61,9 @@ module Decidim::Comments end it "renders the thread" do - expect(subject).to have_css(".flash.primary.loading-comments", text: "Loading comments ...") expect(subject).to have_no_content(comment.body.values.first) expect(subject).to have_no_css(".add-comment") + expect(subject).to have_css(".comment__moderated") end it "renders the single comment warning" do diff --git a/decidim-comments/spec/cells/decidim/comments/two_columns_comments_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/two_columns_comments_cell_spec.rb index d6cfd391968f8..15471dc8e9a38 100644 --- a/decidim-comments/spec/cells/decidim/comments/two_columns_comments_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/two_columns_comments_cell_spec.rb @@ -19,47 +19,6 @@ module Comments allow(commentable).to receive(:closed?).and_return(false) end - describe "#interleave_comments" do - let!(:comments_in_favor) { create_list(:comment, 2, :in_favor, commentable:) } - let!(:comments_against) { create_list(:comment, 3, :against, commentable:) } - - it "returns interleaved comments" do - interleaved_comments = my_cell.send(:interleave_comments, comments_in_favor, comments_against) - - expected_result = [ - comments_in_favor[0], comments_against[0], - comments_in_favor[1], comments_against[1], - comments_against[2] - ] - - expect(interleaved_comments).to eq(expected_result) - end - end - - describe "#interleave_top_comments" do - let!(:top_comment_in_favor) { create(:comment, :in_favor, commentable:, up_votes_count: 10) } - let!(:top_comment_against) { create(:comment, :against, commentable:, up_votes_count: 15) } - - before do - allow(commentable).to receive(:closed?).and_return(true) - my_cell.instance_variable_set(:@top_comment_in_favor, top_comment_in_favor) - my_cell.instance_variable_set(:@top_comment_against, top_comment_against) - end - - it "includes top comments in the beginning for closed models" do - result = my_cell.send(:interleave_top_comments) - - expect(result).to eq([top_comment_in_favor, top_comment_against]) - end - - it "returns an empty array if the model is not closed" do - allow(commentable).to receive(:closed?).and_return(false) - - result = my_cell.send(:interleave_top_comments) - expect(result).to eq([]) - end - end - describe "#sorted_comments" do let!(:comments_in_favor) { create_list(:comment, 2, :in_favor, commentable:) } let!(:top_comment) { create(:comment, :in_favor, commentable:, up_votes_count: 20) } @@ -89,14 +48,6 @@ module Comments expect(subject).to have_content(top_comment_against.body.values.first) end end - - it "renders remaining comments interleaved" do - interleaved_comments = [comments_in_favor[0], comments_against[0], comments_in_favor[1], comments_against[1]] - - interleaved_comments.each do |comment| - expect(subject).to have_content(comment.body.values.first) - end - end end context "when the model is open" do @@ -110,14 +61,6 @@ module Comments it "does not render top comments separately" do expect(subject).to have_no_css(".most-upvoted-label") end - - it "renders comments interleaved without prioritizing top comments" do - interleaved_comments = [comments_in_favor[0], comments_against[0], comments_in_favor[1], comments_against[1]] - - interleaved_comments.each do |comment| - expect(subject).to have_content(comment.body.values.first) - end - end end end end diff --git a/decidim-comments/spec/queries/sorted_comments_spec.rb b/decidim-comments/spec/queries/sorted_comments_spec.rb index 120c6b2ed3555..067b9b834e35c 100644 --- a/decidim-comments/spec/queries/sorted_comments_spec.rb +++ b/decidim-comments/spec/queries/sorted_comments_spec.rb @@ -9,12 +9,10 @@ module Decidim::Comments let(:options) do { order_by:, - id:, - after: + id: } end let(:id) { nil } - let(:after) { nil } let!(:organization) { create(:organization) } let!(:participatory_process) { create(:participatory_process, organization:) } let!(:component) { create(:component, participatory_space: participatory_process) } @@ -24,10 +22,10 @@ module Decidim::Comments let!(:order_by) { nil } it "returns the commentable's comments" do - expect(subject.query).to eq [comment] + expect(subject.query.to_a).to eq [comment] end - it "eager loads comment's author, up_votes and down_votes" do + it "eager loads comment's author" do comment = subject.query[0] begin subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, data| @@ -35,8 +33,6 @@ module Decidim::Comments end expect(comment.author.name).to be_present - expect(comment.up_votes.size).to eq(0) - expect(comment.down_votes.size).to eq(0) rescue RSpec::Expectations::ExpectationNotMetError => e ActiveSupport::Notifications.unsubscribe(subscriber) raise e @@ -48,7 +44,7 @@ module Decidim::Comments it "return the comments ordered by created_at asc by default" do previous_comment = create(:comment, commentable:, author:, created_at: 1.week.ago, updated_at: 1.week.ago) future_comment = create(:comment, commentable:, author:, created_at: 1.week.from_now, updated_at: 1.week.from_now) - expect(subject.query).to eq [previous_comment, comment, future_comment] + expect(subject.query.to_a).to eq [previous_comment, comment, future_comment] end context "when filtering by id" do @@ -56,36 +52,70 @@ module Decidim::Comments let(:id) { comment.id } it "only returns the requested comment" do - expect(subject.query).to eq [comment] + expect(subject.query.to_a).to eq [comment] end end - context "when filtering comments after id" do - let!(:comments) { create_list(:comment, 10, commentable:, author:) } - let(:after) { comments.first.id } + context "when the comment is hidden" do + before do + moderation = create(:moderation, reportable: comment, participatory_space: comment.component.participatory_space, report_count: 1, hidden_at: Time.current) + create(:report, moderation:) + end - it "only returns the comments after the specified id" do - expect(subject.query).to eq(comments[1..-1]) + it "is included in the query" do + expect(subject.query).not_to be_empty end + end - context "when the after comments contain replies" do - let(:replies) { create_list(:comment, 5, commentable: comment, root_commentable: commentable, author:) } - let(:after) { comments.last.id } + context "when using pagination with limit" do + let!(:extra_comments) { create_list(:comment, 15, commentable:, author:) } + let(:options) { { order_by:, id:, limit: 5 } } - it "returns the replies" do - expect(subject.query).to eq(replies) - end + it "returns only the limited number of comments" do + expect(subject.query.size).to eq(5) + end + + it "returns total_count with all comments" do + expect(subject.total_count).to eq(16) end end - context "when the comment is hidden" do - before do - moderation = create(:moderation, reportable: comment, participatory_space: comment.component.participatory_space, report_count: 1, hidden_at: Time.current) - create(:report, moderation:) + context "when using pagination with offset and limit" do + let!(:extra_comments) { create_list(:comment, 15, commentable:, author:) } + let(:options) { { order_by:, id:, offset: 5, limit: 5 } } + + it "skips the first comments and returns the next batch" do + expect(subject.query.size).to eq(5) + expect(subject.query.to_a.first).to eq(extra_comments[4]) end + end - it "is included in the query" do - expect(subject.query).not_to be_empty + context "when filtering by alignment" do + let!(:comments_in_favor) { create_list(:comment, 3, :in_favor, commentable:, author:) } + let!(:comments_against) { create_list(:comment, 2, :against, commentable:, author:) } + let(:options) { { order_by:, id:, alignment: 1 } } + + it "returns only in_favor comments" do + expect(subject.query.to_a).to match_array(comments_in_favor) + expect(subject.total_count).to eq(3) + end + + context "when filtering against comments" do + let(:options) { { order_by:, id:, alignment: -1 } } + + it "returns only against comments" do + expect(subject.query.to_a).to match_array(comments_against) + expect(subject.total_count).to eq(2) + end + end + + context "when combining alignment with pagination" do + let(:options) { { order_by:, id:, alignment: 1, limit: 2 } } + + it "applies both filters correctly" do + expect(subject.query.size).to eq(2) + expect(subject.query.to_a).to all(satisfy { |c| c.alignment == 1 }) + end end end @@ -96,7 +126,7 @@ module Decidim::Comments it "return the comments ordered by recent" do previous_comment = create(:comment, commentable:, author:, created_at: 1.week.ago, updated_at: 1.week.ago) future_comment = create(:comment, commentable:, author:, created_at: 1.week.from_now, updated_at: 1.week.from_now) - expect(subject.query).to eq [previous_comment, comment, future_comment].reverse + expect(subject.query.to_a).to eq [previous_comment, comment, future_comment].reverse end end @@ -108,7 +138,7 @@ module Decidim::Comments less_voted_comment = create(:comment, commentable:, author:, created_at: 1.week.from_now, updated_at: 1.week.from_now) create(:comment_vote, comment: most_voted_comment, author:, weight: 1) create(:comment_vote, comment: less_voted_comment, author:, weight: -1) - expect(subject.query).to eq [most_voted_comment, comment, less_voted_comment] + expect(subject.query.to_a).to eq [most_voted_comment, comment, less_voted_comment] end end @@ -120,7 +150,7 @@ module Decidim::Comments less_commented = create(:comment, commentable:, author:, created_at: 1.week.from_now, updated_at: 1.week.from_now) create(:comment, commentable: comment) create_list(:comment, 3, commentable: most_commented) - expect(subject.query).to eq [most_commented, comment, less_commented] + expect(subject.query.to_a).to eq [most_commented, comment, less_commented] end end end diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index 518558417ef14..b458977b86ad8 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -67,7 +67,8 @@ select "Best rated", from: "order" end - expect(page).to have_css(".comments > div:nth-child(2)", text: "Most Rated Comment") + expect(page).to have_no_css(".loading-comments", visible: :visible) + expect(page).to have_css(".comment-threads .comment-thread", text: "Most Rated Comment") end context "when there are comments and replies" do @@ -79,8 +80,8 @@ expect(page).to have_no_content("Comments are disabled at this time") expect(page).to have_css(".comment", minimum: 1) - within("#accordion-#{single_comment.id}") do - expect(page).to have_content "1 answer" + within("#comment_#{single_comment.id}") do + expect(page).to have_content "1 reply" end end @@ -132,6 +133,7 @@ visit resource_path within "#comment_#{deleted_comment.id}" do + click_on "1 reply" expect(page).to have_css("#comment-#{deleted_comment.id}-replies") expect(page).to have_content(reply.author.name) expect(page).to have_content(reply.body.values.first) @@ -140,6 +142,30 @@ end end + context "when there are more comments than the default per page" do + let(:per_page) { Decidim::Comments::SortedComments::DEFAULT_COMMENTS_LIMIT } + let!(:extra_comments) { create_list(:comment, per_page - comments.size + 1, commentable:) } + let(:all_comments) { comments + extra_comments } + + it "shows a load more button and loads the next page" do + visit resource_path + + visible_comments = all_comments.sort_by(&:created_at).first(per_page) + hidden_comment = (all_comments.sort_by(&:created_at) - visible_comments).first + + visible_comments.each do |comment| + expect(page).to have_css("#comment_#{comment.id}") + end + expect(page).to have_no_css("#comment_#{hidden_comment.id}") + expect(page).to have_button("Load more comments") + + click_on "Load more comments" + + expect(page).to have_css("#comment_#{hidden_comment.id}") + expect(page).to have_no_button("Load more comments") + end + end + context "when not authenticated" do it "does not show form to add comments to user" do visit resource_path @@ -649,96 +675,48 @@ end end - context "when the user is writing a new comment while someone else comments" do - let(:new_comment_body) { "Hey, I just jumped in the conversation!" } - let(:new_comment) { build(:comment, commentable:, body: new_comment_body) } - let(:content) { "This is a new comment" } + context "when user can show and hide replies on a thread" do + let(:thread) { comments.first } + let(:new_reply_body) { "Hey, I just jumped inside the thread!" } + let!(:new_reply) { create(:comment, commentable: thread, root_commentable: commentable, body: new_reply_body) } - before do - within "form#new_comment_for_#{commentable.commentable_type.demodulize}_#{commentable.id}" do - field = find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}") - field.set " " - field.native.send_keys content + it "displays a way to display content" do + visit resource_path + within "#comment_#{thread.id}" do + expect(page).to have_content("1 reply") + click_on "1 reply" + expect(page).to have_content(new_reply_body) + click_on "Reply", match: :first + expect(page).to have_content("Publish reply") + find("textarea[name='comment[body]']").set("Test reply comments.") + click_on "Publish reply" + expect(page).to have_content("Test reply comments.") end - new_comment.save! end - it "does not clear the current user's comment" do - expect(page).to have_content(new_comment.body.values.first, wait: 20) - expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to include(content) + it "displays a way to hide content" do + visit resource_path + within "#comment_#{thread.id}" do + expect(page).to have_content("1 reply") + click_on "1 reply" + expect(page).to have_content(new_reply_body) + click_on "1 reply" + expect(page).to have_no_content(new_reply_body) + end end - context "when user can hide replies on a thread" do - let(:thread) { comments.first } - let(:new_reply_body) { "Hey, I just jumped inside the thread!" } - let!(:new_reply) { create(:comment, commentable: thread, root_commentable: commentable, body: new_reply_body) } + context "when there are more replies" do + let!(:new_replies) { create_list(:comment, 2, commentable: thread, root_commentable: commentable, body: new_reply_body) } - it "displays a way to to display content" do - visit current_path - within "#comment_#{thread.id}" do - expect(page).to have_content("1 answer") - click_on "1 answer" - expect(page).to have_content(new_reply_body) - click_on "Reply", match: :first - expect(page).to have_content("Publish reply") - find("textarea[name='comment[body]']").set("Test reply comments.") - click_on "Publish reply" - expect(page).to have_content("Show 2 replies") - click_on "Show 2 replies" - expect(page).to have_content("Test reply comments.") - end - end - - it "displays a way hide content" do - visit current_path + it "displays the load replies button" do + visit resource_path within "#comment_#{thread.id}" do - expect(page).to have_content("1 answer") - click_on "1 answer" - expect(page).to have_content("1 answer") - click_on "1 answer" + expect(page).to have_content("3 replies") expect(page).to have_no_content(new_reply_body) + click_on "3 replies" + expect(page).to have_content(new_reply_body) end end - - context "when are more replies" do - let!(:new_replies) { create_list(:comment, 2, commentable: thread, root_commentable: commentable, body: new_reply_body) } - - it "displays the show button" do - visit current_path - within "#comment_#{thread.id}" do - expect(page).to have_content("3 answers") - expect(page).to have_no_content(new_reply_body) - click_on "3 answers" - expect(page).to have_content(new_reply_body) - end - end - end - end - - context "when inside a thread reply form" do - let(:thread) { comments.first } - let(:new_reply_body) { "Hey, I just jumped inside the thread!" } - let(:new_reply) { build(:comment, commentable: thread, root_commentable: commentable, body: new_reply_body) } - let(:reply_content) { "This is a new reply" } - - before do - within "div#comment_#{thread.id}" do - find("span", text: "Reply").click - end - - within "form#new_comment_for_#{thread.commentable_type.demodulize}_#{thread.id}" do - field = find("#add-comment-#{thread.commentable_type.demodulize}-#{thread.id}") - field.set " " - field.native.send_keys reply_content - end - new_reply.save! - end - - it "does not clear the current user's comment" do - expect(page).to have_content(new_reply.body.values.first, wait: 20) - expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to include(content) - expect(page.find("#add-comment-#{thread.commentable_type.demodulize}-#{thread.id}").value).to include(reply_content) - end end end @@ -903,8 +881,8 @@ visit current_path within "#comments #comment_#{parent.id}" do - expect(page).to have_css("#comment-#{parent.id}-replies") - expect(page.find("#comment-#{parent.id}-replies").text).to be_blank + expect(page).to have_no_css(".show-replies-button") + expect(page).to have_no_css("#comment-#{parent.id}-replies") end end @@ -968,6 +946,10 @@ skip "Commentable comments has no votes" unless commentable.comments_have_votes? visit current_path + + within "#comment_#{comments[0].id}" do + click_on "1 reply" + end expect(page).to have_css("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up", text: /0/, visible: :all) page.find("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up").click expect(page).to have_css("#comment_#{comments[0].id} > [data-comment-footer] > .comment__footer-grid .comment__votes .js-comment__votes--up", text: /1/, visible: :all) @@ -1203,6 +1185,7 @@ let!(:user) { create(:user, :confirmed, organization:) } before do + switch_to_host(organization.host) login_as user, scope: :user end @@ -1217,7 +1200,7 @@ expect(page).to have_css(".comment", count: comments.length) within(".comments-two-columns") do - check_comments_order(".comments-section__in-favor", comments_in_favor) + check_comments_order(".comments-section__in-favor", comments_in_favor.reverse) check_comments_order(".comments-section__against", comments_against) end end @@ -1238,6 +1221,7 @@ context "when commentable is closed" do let!(:commentable) { closed_commentable } + let!(:comments) { [] } let!(:highest_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago, up_votes_count: 15) } let!(:high_voted_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 4.days.ago, up_votes_count: 10) } let!(:older_comment_in_favor) { create(:comment, :in_favor, commentable:, created_at: 3.days.ago, up_votes_count: 5) } @@ -1246,27 +1230,26 @@ let!(:high_voted_comment_against) { create(:comment, :against, commentable:, created_at: 5.days.ago, up_votes_count: 8) } let!(:older_comment_against) { create(:comment, :against, commentable:, created_at: 3.days.ago, up_votes_count: 4) } - it "shows comments with top comments at the beginning and interleaved order after" do + it "shows comments sorted by the selected filter in mobile view" do resize_window_to_mobile visit resource_path + sleep 1 within(".comment-threads") do - interleaved_comments = [ - highest_voted_comment_in_favor, + expected_order = [ highest_voted_comment_against, - high_voted_comment_in_favor, - high_voted_comment_against, + highest_voted_comment_in_favor, + older_comment_against, older_comment_in_favor, - older_comment_against + high_voted_comment_in_favor, + high_voted_comment_against ] all_comments = all(".comment-thread") - interleaved_comments.each_with_index do |comment, index| + expected_order.each_with_index do |comment, index| expect(all_comments[index]).to have_content(comment.body["en"]) end - - expect(page).to have_css(".most-upvoted-label", text: "Most upvoted", count: 2) end resize_window_to_desktop @@ -1293,31 +1276,36 @@ end context "when commentable is not closed" do + let!(:comments) { [] } let!(:oldest_in_favor_comment) { create(:comment, :in_favor, commentable:, created_at: 3.days.ago) } let!(:older_in_favor_comment) { create(:comment, :in_favor, commentable:, created_at: 2.days.ago) } let!(:oldest_against_comment) { create(:comment, :against, commentable:, created_at: 4.days.ago) } let!(:newer_against_comment) { create(:comment, :against, commentable:, created_at: 1.day.ago) } - it "shows the comments in two columns sorted by creation date in ascending order" do + it "shows the comments in two columns sorted by creation date in descending order" do + resize_window_to_desktop visit resource_path within(".comments-two-columns") do - check_comments_order(".comments-section__in-favor", [oldest_in_favor_comment, older_in_favor_comment]) - check_comments_order(".comments-section__against", [oldest_against_comment, newer_against_comment]) + check_comments_order(".comments-section__in-favor", [older_in_favor_comment, oldest_in_favor_comment]) + check_comments_order(".comments-section__against", [newer_against_comment, oldest_against_comment]) end end - it "allows the user to add a new comment at the end of the respective column" do + it "allows the user to add a new comment at the top of the respective column" do + resize_window_to_desktop visit resource_path add_new_comment("In favor", "This is a new comment in favor") within(".comments-section__in-favor") do expect(page).to have_content("This is a new comment in favor") + expect(first(".comment-thread")).to have_content("This is a new comment in favor") end end it "disables the publish button until 'in favor' or 'against' is selected" do + resize_window_to_desktop visit resource_path expect(page).to have_button("Publish comment", disabled: true) @@ -1335,14 +1323,21 @@ it "shows comments sorted by creation date when viewed on a small screen" do resize_window_to_mobile visit resource_path + sleep 1 within(".comment-threads") do - comments = all(".comment-thread") + expect(page).to have_css(".comment-thread", minimum: 4) - expect(comments[0]).to have_content(oldest_in_favor_comment.body["en"]) - expect(comments[1]).to have_content(oldest_against_comment.body["en"]) - expect(comments[2]).to have_content(older_in_favor_comment.body["en"]) - expect(comments[3]).to have_content(newer_against_comment.body["en"]) + expected_order = [ + newer_against_comment, + older_in_favor_comment, + oldest_in_favor_comment, + oldest_against_comment + ] + + expected_order.each_with_index do |comment, index| + expect(all(".comment-thread")[index]).to have_content(comment.body["en"]) + end end resize_window_to_desktop @@ -1364,25 +1359,26 @@ let!(:latest_comment_against) { create(:comment, :against, commentable:, created_at: 1.day.ago, up_votes_count: 1) } before do + resize_window_to_desktop visit resource_path end - it "shows the top voted comments at the top of each column, followed by comments in ascending chronological order" do + it "shows the top voted comments at the top of each column, followed by comments in descending chronological order" do within(".comments-two-columns") do check_comments_order(".comments-section__in-favor", [ highest_voted_comment_in_favor, - high_voted_comment_in_favor, - older_comment_in_favor, + latest_comment_in_favor, recent_comment_in_favor, - latest_comment_in_favor + older_comment_in_favor, + high_voted_comment_in_favor ]) check_comments_order(".comments-section__against", [ highest_voted_comment_against, - high_voted_comment_against, - older_comment_against, + latest_comment_against, recent_comment_against, - latest_comment_against + older_comment_against, + high_voted_comment_against ]) end end diff --git a/decidim-meetings/spec/system/meeting_spec.rb b/decidim-meetings/spec/system/meeting_spec.rb index f798a7f8660e9..a51dae4ed4a25 100644 --- a/decidim-meetings/spec/system/meeting_spec.rb +++ b/decidim-meetings/spec/system/meeting_spec.rb @@ -197,22 +197,6 @@ def visit_meeting travel 1.minute expect(page).to have_content("You were inactive for too long") end - - context "when comments are enabled" do - let(:comment) { create(:comment, commentable: meeting) } - - before do - component.settings[:comments_enabled] = true - end - - it "fetching comments does not prevent timeout" do - visit_meeting - comment - expect(page).to have_content(translated(comment.body), wait: 30) - expect(page).to have_content("If you continue being inactive", wait: 30) - expect(page).to have_content("You were inactive for too long", wait: 30) - end - end end end end From 0dd8ee2b237d9c50a750ade7c4cd4a3cde5201bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 10 Mar 2026 18:53:32 +0100 Subject: [PATCH 096/135] Be smart with the 'most liked' sorting option (#16300) Co-authored-by: Alexandru Emil Lupu --- .../concerns/decidim/proposals/orderable.rb | 8 +- .../app/models/decidim/proposals/proposal.rb | 9 +++ .../lib/decidim/proposals/component.rb | 2 + .../controllers/concerns/orderable_spec.rb | 79 ++++++++++++++++++- .../models/decidim/proposals/proposal_spec.rb | 52 ++++++++++++ .../spec/system/proposals_spec.rb | 16 ++++ 6 files changed, 162 insertions(+), 4 deletions(-) diff --git a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb index 785bf2b0a3ade..1295715fcd39b 100644 --- a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb +++ b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb @@ -22,7 +22,7 @@ def possible_orders @possible_orders ||= begin possible_orders = %w(random recent) possible_orders << "most_voted" if most_voted_order_available? - possible_orders << "most_liked" if current_settings.likes_enabled? + possible_orders << "most_liked" if most_liked_order_available? possible_orders << "most_commented" if most_commented_order_available? possible_orders << "most_followed" possible_orders << "with_more_authors" if with_more_authors_order_available? @@ -65,6 +65,12 @@ def most_commented_order_available? @most_commented_order_available = Decidim::Proposals::Proposal.most_commented_available?(current_component) end + def most_liked_order_available? + return @most_liked_order_available if defined?(@most_liked_order_available) + + @most_liked_order_available = Decidim::Proposals::Proposal.most_liked_available?(current_component) + end + def order_by_votes? most_voted_order_available? && current_settings.votes_blocked? end diff --git a/decidim-proposals/app/models/decidim/proposals/proposal.rb b/decidim-proposals/app/models/decidim/proposals/proposal.rb index f3eb362997f3b..2048ef5d29efe 100644 --- a/decidim-proposals/app/models/decidim/proposals/proposal.rb +++ b/decidim-proposals/app/models/decidim/proposals/proposal.rb @@ -185,6 +185,15 @@ def self.most_commented_available?(component) .exists? end + def self.most_liked_available?(component) + where(component:) + .published + .not_hidden + .not_withdrawn + .where("likes_count > 0") + .exists? + end + acts_as_list scope: :decidim_component_id searchable_fields({ diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index efae9f51a2998..1331709bbd082 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -37,6 +37,7 @@ POSSIBLE_SORT_ORDERS = %w(automatic random recent most_liked most_voted most_commented most_followed with_more_authors).freeze WITH_MORE_AUTHORS_ORDER = "with_more_authors" MOST_COMMENTED_ORDER = "most_commented" + MOST_LIKED_ORDER = "most_liked" sort_order_choices = lambda do |context| component = context[:component] @@ -44,6 +45,7 @@ orders = orders.excluding(WITH_MORE_AUTHORS_ORDER) unless component && Decidim::Proposals::Proposal.with_more_authors_available?(component) orders = orders.excluding(MOST_COMMENTED_ORDER) unless component && Decidim::Proposals::Proposal.most_commented_available?(component) + orders = orders.excluding(MOST_LIKED_ORDER) unless component && Decidim::Proposals::Proposal.most_liked_available?(component) orders end diff --git a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb index e1f625480bb9a..26138944e3379 100644 --- a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb +++ b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb @@ -24,7 +24,6 @@ class OrderableFakeController < Decidim::ApplicationController votes_enabled?: votes_enabled, votes_blocked?: votes_blocked, votes_hidden?: votes_hidden, - likes_enabled?: likes_enabled, comments_enabled?: comments_enabled) end let(:component_default_sort_order) { "automatic" } @@ -111,8 +110,18 @@ class OrderableFakeController < Decidim::ApplicationController let(:default_sort_order) { "most_liked" } let(:likes_enabled) { true } - it "default_order is most_liked" do - expect(controller.send(:default_order)).to eq(default_sort_order) + context "when there are no proposals with likes" do + it "defaults to random" do + expect(controller.send(:default_order)).to eq("random") + end + end + + context "when there are proposals with likes" do + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + + it "default_order is most_liked" do + expect(controller.send(:default_order)).to eq(default_sort_order) + end end end @@ -195,6 +204,7 @@ class OrderableFakeController < Decidim::ApplicationController context "with likes enabled" do let(:likes_enabled) { true } + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } it "shows most_liked option to sort" do expect(view.available_orders).to include("most_liked") @@ -209,6 +219,28 @@ class OrderableFakeController < Decidim::ApplicationController end end + context "with or without likes and most_liked availability" do + let!(:proposal_without_likes) { create(:proposal, component:) } + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + let(:likes_enabled) { true } + + context "when there are no proposals with likes" do + before do + proposal_with_likes.update!(likes_count: 0) + end + + it "does not show most_liked option to sort" do + expect(view.available_orders).not_to include("most_liked") + end + end + + context "when there are proposals with likes" do + it "shows most_liked option to sort" do + expect(view.available_orders).to include("most_liked") + end + end + end + context "with comments enabled" do let(:comments_enabled) { true } let!(:proposal_with_comments) { create(:proposal, component:, comments_count: 5) } @@ -363,6 +395,47 @@ class OrderableFakeController < Decidim::ApplicationController end end end + + describe "#most_liked_order_available?" do + let!(:proposal_without_likes) { create(:proposal, component:) } + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + + context "when there are proposals with only zero likes" do + before do + proposal_with_likes.update!(likes_count: 0) + end + + it "returns false" do + expect(controller.send(:most_liked_order_available?)).to be false + end + end + + context "when there are proposals with likes" do + it "returns true" do + expect(controller.send(:most_liked_order_available?)).to be true + end + end + + context "when proposals are not published" do + before do + proposal_with_likes.update!(published_at: nil) + end + + it "returns false" do + expect(controller.send(:most_liked_order_available?)).to be false + end + end + + context "when proposals are hidden" do + before do + create(:moderation, reportable: proposal_with_likes, hidden_at: Time.current) + end + + it "returns false" do + expect(controller.send(:most_liked_order_available?)).to be false + end + end + end end end end diff --git a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb index ce3ba168b0d34..3ea783b316c32 100644 --- a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb +++ b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb @@ -137,6 +137,58 @@ module Proposals end end + describe ".most_liked_available?" do + let(:component) { create(:proposal_component) } + + context "when there are no proposals with likes" do + let!(:proposal_without_likes) { create(:proposal, component:) } + + it "returns false" do + expect(described_class.most_liked_available?(component)).to be false + end + end + + context "when there are proposals with likes" do + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + + it "returns true" do + expect(described_class.most_liked_available?(component)).to be true + end + + context "when proposals are not published" do + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + + before do + proposal_with_likes.update!(published_at: nil) + end + + it "returns false" do + expect(described_class.most_liked_available?(component)).to be false + end + end + + context "when proposals are hidden" do + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5) } + + before do + create(:moderation, reportable: proposal_with_likes, hidden_at: Time.current) + end + + it "returns false" do + expect(described_class.most_liked_available?(component)).to be false + end + end + + context "when proposals are withdrawn" do + let!(:proposal_with_likes) { create(:proposal, component:, likes_count: 5, withdrawn_at: Time.current) } + + it "returns false" do + expect(described_class.most_liked_available?(component)).to be false + end + end + end + end + it "has a votes association returning proposal votes" do expect(subject.votes.count).to eq(0) end diff --git a/decidim-proposals/spec/system/proposals_spec.rb b/decidim-proposals/spec/system/proposals_spec.rb index 16c7da68b9687..9cfa60bea0e6d 100644 --- a/decidim-proposals/spec/system/proposals_spec.rb +++ b/decidim-proposals/spec/system/proposals_spec.rb @@ -681,6 +681,22 @@ end end + context "when there are no proposals with likes" do + let!(:proposals) { create_list(:proposal, 3, component:) } + + before do + visit_component + end + + it "does not show 'most_liked' ordering option" do + within ".order-by" do + expect(page).to have_css("div.order-by a", text: "Random") + page.find("a", text: "Random").click + expect(page).to have_no_content("Most liked") + end + end + end + context "when searching proposals" do let!(:proposals) do [ From 4b8367fb18320bc090496df60c16d21b7864c793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 10 Mar 2026 19:29:38 +0100 Subject: [PATCH 097/135] Remove references to the `banner_image` field in assemblies (#16357) --- .../assemblies/admin/create_assembly.rb | 2 +- .../assemblies/admin/duplicate_assembly.rb | 2 +- .../assemblies/admin/update_assembly.rb | 2 +- .../decidim/assemblies/admin/assembly_form.rb | 3 - .../app/models/decidim/assembly.rb | 3 - .../assembly_admin/assembly_admin.test.js | 9 -- .../decidim/assemblies/assembly_presenter.rb | 4 - .../decidim/assemblies/assembly_importer.rb | 10 -- .../open_data_assembly_serializer.rb | 1 - .../admin/assemblies/_form.html.erb | 4 - .../decidim/assemblies/members/index.html.erb | 21 +-- decidim-assemblies/config/locales/en.yml | 7 - .../lib/decidim/api/assembly_type.rb | 5 - .../lib/decidim/assemblies/seeds.rb | 1 - .../lib/decidim/assemblies/test/factories.rb | 1 - .../spec/commands/create_assembly_spec.rb | 6 - .../assemblies/admin/import_assembly_spec.rb | 4 - .../spec/commands/update_assembly_spec.rb | 32 +---- .../spec/forms/assembly_form_spec.rb | 5 +- .../assemblies/assembly_presenter_spec.rb | 9 -- .../assemblies/assembly_importer_spec.rb | 77 +--------- .../assemblies/assembly_serializer_spec.rb | 1 - .../open_data_assembly_serializer_spec.rb | 1 - .../spec/shared/manage_assemblies_examples.rb | 8 +- .../admin/admin_imports_assembly_spec.rb | 131 +----------------- .../admin/admin_manages_assemblies_spec.rb | 1 - .../system/assemblies_social_share_spec.rb | 13 +- .../spec/types/assembly_type_spec.rb | 8 -- .../spec/types/integration_schema_spec.rb | 4 - .../participatory_space_hero_cell.rb | 6 +- decidim-core/config/locales/en.yml | 3 + ...icipatory_space_members_shared_examples.rb | 2 +- .../decidim/meta_image_url_resolver_spec.rb | 7 +- .../members/index.html.erb | 20 +-- .../config/locales/en.yml | 3 - 35 files changed, 42 insertions(+), 374 deletions(-) diff --git a/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb b/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb index 8a45ea3178273..af78fe6474a65 100644 --- a/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb +++ b/decidim-assemblies/app/commands/decidim/assemblies/admin/create_assembly.rb @@ -6,7 +6,7 @@ module Admin # A command with all the business logic when creating a new assembly # in the system. class CreateAssembly < Decidim::Commands::CreateResource - fetch_file_attributes :hero_image, :banner_image + fetch_file_attributes :hero_image fetch_form_attributes :title, :subtitle, :weight, :slug, :description, :short_description, :promoted, :taxonomizations, :parent, :organization, diff --git a/decidim-assemblies/app/commands/decidim/assemblies/admin/duplicate_assembly.rb b/decidim-assemblies/app/commands/decidim/assemblies/admin/duplicate_assembly.rb index 3042adeeded07..901f911460d51 100644 --- a/decidim-assemblies/app/commands/decidim/assemblies/admin/duplicate_assembly.rb +++ b/decidim-assemblies/app/commands/decidim/assemblies/admin/duplicate_assembly.rb @@ -63,7 +63,7 @@ def duplicate_assembly end def duplicate_assembly_attachments - [:hero_image, :banner_image].each do |attribute| + [:hero_image].each do |attribute| next unless @assembly.attached_uploader(attribute).attached? @duplicated_assembly.send(attribute).attach(@assembly.send(attribute).blob) diff --git a/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb b/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb index e91d4df267451..5747fa541ca5e 100644 --- a/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb +++ b/decidim-assemblies/app/commands/decidim/assemblies/admin/update_assembly.rb @@ -6,7 +6,7 @@ module Admin # A command with all the business logic when updating a new assembly # in the system. class UpdateAssembly < Decidim::Commands::UpdateResource - fetch_file_attributes :hero_image, :banner_image + fetch_file_attributes :hero_image fetch_form_attributes :title, :subtitle, :slug, :promoted, :description, :short_description, :taxonomizations, :parent, :private_space, :developer_group, :local_area, diff --git a/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb index 5ab89729e7c4a..cbbfd81cf55d5 100644 --- a/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb +++ b/decidim-assemblies/app/forms/decidim/assemblies/admin/assembly_form.rb @@ -54,9 +54,7 @@ class AssemblyForm < Form attribute :duration, Decidim::Attributes::LocalizedDate attribute :included_at, Decidim::Attributes::LocalizedDate - attribute :banner_image attribute :hero_image - attribute :remove_banner_image, Boolean, default: false attribute :remove_hero_image, Boolean, default: false validates :parent, presence: true, if: ->(form) { form.parent.present? } @@ -69,7 +67,6 @@ class AssemblyForm < Form validates :created_by_other, translatable_presence: true, if: ->(form) { form.created_by == "others" } validates :title, :subtitle, :description, :short_description, translatable_presence: true - validates :banner_image, passthru: { to: Decidim::Assembly } validates :hero_image, passthru: { to: Decidim::Assembly } validates :weight, presence: true diff --git a/decidim-assemblies/app/models/decidim/assembly.rb b/decidim-assemblies/app/models/decidim/assembly.rb index 208cc845bae6b..eaacc4dec64a8 100644 --- a/decidim-assemblies/app/models/decidim/assembly.rb +++ b/decidim-assemblies/app/models/decidim/assembly.rb @@ -71,9 +71,6 @@ class Assembly < ApplicationRecord has_one_attached :hero_image validates_upload :hero_image, uploader: Decidim::HeroImageUploader - has_one_attached :banner_image - validates_upload :banner_image, uploader: Decidim::BannerImageUploader - validates :slug, uniqueness: { scope: :organization } validates :slug, presence: true, format: { with: Decidim::Assembly.slug_format } diff --git a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js index 0629e11cc3264..833fd91e73e64 100644 --- a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js +++ b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js @@ -494,15 +494,6 @@ describe("AssemblyAdminController", () => {
-
-
-
- -
-
- -
-
diff --git a/decidim-assemblies/app/presenters/decidim/assemblies/assembly_presenter.rb b/decidim-assemblies/app/presenters/decidim/assemblies/assembly_presenter.rb index 024b0b10455f1..ed8f5f43512c5 100644 --- a/decidim-assemblies/app/presenters/decidim/assemblies/assembly_presenter.rb +++ b/decidim-assemblies/app/presenters/decidim/assemblies/assembly_presenter.rb @@ -9,10 +9,6 @@ def hero_image_url assembly.attached_uploader(:hero_image).url end - def banner_image_url - assembly.attached_uploader(:banner_image).url - end - def area_name return if assembly.area.blank? diff --git a/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb b/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb index 8590c8b67b452..28900d7a89b9c 100644 --- a/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb +++ b/decidim-assemblies/app/serializers/decidim/assemblies/assembly_importer.rb @@ -60,8 +60,6 @@ def import(attributes, _user, opts) meta_scope: attributes["meta_scope"] ) import_hero_image(attributes["remote_hero_image_url"]) - import_banner_image(attributes["remote_banner_image_url"]) - @imported_assembly.save! @imported_assembly end @@ -177,14 +175,6 @@ def import_hero_image(url) @warnings << I18n.t("decidim.assemblies.admin.imports.hero_image_error", error: format_error(e)) end - def import_banner_image(url) - return if url.blank? - - @imported_assembly.attached_uploader(:banner_image).remote_url = url - rescue OpenURI::HTTPError, Errno::ENOENT, Errno::ECONNREFUSED, SocketError, Net::OpenTimeout, Net::ReadTimeout => e - @warnings << I18n.t("decidim.assemblies.admin.imports.banner_image_error", error: format_error(e)) - end - def format_error(error) return error.message unless error.respond_to?(:io) && error.io.respond_to?(:status) diff --git a/decidim-assemblies/app/serializers/decidim/assemblies/open_data_assembly_serializer.rb b/decidim-assemblies/app/serializers/decidim/assemblies/open_data_assembly_serializer.rb index a6dcfb2f4c0f5..4a5b77bb6aad2 100644 --- a/decidim-assemblies/app/serializers/decidim/assemblies/open_data_assembly_serializer.rb +++ b/decidim-assemblies/app/serializers/decidim/assemblies/open_data_assembly_serializer.rb @@ -12,7 +12,6 @@ def serialize url: EngineRouter.main_proxy(resource).assembly_url(resource), subtitle: resource.subtitle, remote_hero_image_url: Decidim::ParticipatoryProcesses::ParticipatoryProcessPresenter.new(resource).hero_image_url, - remote_banner_image_url: Decidim::Assemblies::AssemblyPresenter.new(resource).banner_image_url, developer_group: resource.developer_group, local_area: resource.local_area, meta_scope: resource.meta_scope, 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 861ca7914d867..e176aa7db01cd 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 @@ -98,10 +98,6 @@
<%= form.upload :hero_image, button_class: "button button__sm button__transparent-secondary" %>
- -
- <%= form.upload :banner_image, button_class: "button button__sm button__transparent-secondary" %> -
diff --git a/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb b/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb index 3fb3b7d068099..6bafa56d3e0ce 100644 --- a/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb +++ b/decidim-assemblies/app/views/decidim/assemblies/members/index.html.erb @@ -1,4 +1,4 @@ -<% add_decidim_page_title(t("assembly_members.index.title", scope: "decidim")) %> +<% add_decidim_page_title(t("members", scope: "decidim.participatory_space_members.index")) %> <% add_decidim_meta_tags( title: translated_attribute(current_participatory_space.title), resource: current_participatory_space) %> @@ -11,13 +11,14 @@ edit_link( ) %> -<%= cell "decidim/content_blocks/participatory_space_hero", nil, resource: current_participatory_space %> -
-

- <%= t("members", scope: "decidim.assemblies.assembly_members.index") %> - <%= collection.size %> -

-
+<% content_for :aside do %> +

+ <%= t("members", scope: "decidim.participatory_space_members.index") %> +

+<% end %> + +<%= render layout: "layouts/decidim/shared/layout_two_col" do %> +
<%= render(collection) %> -
-
+ +<% end %> diff --git a/decidim-assemblies/config/locales/en.yml b/decidim-assemblies/config/locales/en.yml index 9e01c707a6196..01813e60d6118 100644 --- a/decidim-assemblies/config/locales/en.yml +++ b/decidim-assemblies/config/locales/en.yml @@ -282,7 +282,6 @@ en: max_results: Maximum amount of elements to show imports: attachment_error: The attachment "%{title}" could not be imported (%{error}). - banner_image_error: The banner image could not be imported (%{error}). hero_image_error: The hero image could not be imported (%{error}). new_import: accepted_types: @@ -311,9 +310,6 @@ en: type: Type show: title: About this assembly - assembly_members: - index: - members: Members content_blocks: children_assemblies: name: Assemblies @@ -355,9 +351,6 @@ en: duration: Duration private_space: This is a private assembly social_networks_title: Visit assembly on - assembly_members: - index: - title: Members download_your_data: show: assemblies: Assemblies export diff --git a/decidim-assemblies/lib/decidim/api/assembly_type.rb b/decidim-assemblies/lib/decidim/api/assembly_type.rb index 1405d7e2aec10..5205f6f7cecab 100644 --- a/decidim-assemblies/lib/decidim/api/assembly_type.rb +++ b/decidim-assemblies/lib/decidim/api/assembly_type.rb @@ -17,7 +17,6 @@ class AssemblyType < Decidim::Api::Types::BaseObject description "An assembly" - field :banner_image, String, "The banner image for this assembly", null: true field :children, [Decidim::Assemblies::AssemblyType, { null: true }], "Children of this assembly", null: false field :children_count, Integer, "Number of children assemblies", null: true field :closing_date, Decidim::Core::DateType, "Closing date of the assembly", null: true @@ -64,10 +63,6 @@ def url def hero_image object.attached_uploader(:hero_image).url end - - def banner_image - object.attached_uploader(:banner_image).url - end end end end diff --git a/decidim-assemblies/lib/decidim/assemblies/seeds.rb b/decidim-assemblies/lib/decidim/assemblies/seeds.rb index 4667312ad2f4a..4900aa839bd20 100644 --- a/decidim-assemblies/lib/decidim/assemblies/seeds.rb +++ b/decidim-assemblies/lib/decidim/assemblies/seeds.rb @@ -60,7 +60,6 @@ def create_assembly!(parent: nil) end, organization:, hero_image: ::Faker::Boolean.boolean(true_ratio: 0.5) ? hero_image : nil, # Keep after organization - banner_image: ::Faker::Boolean.boolean(true_ratio: 0.5) ? banner_image : nil, # Keep after organization promoted: true, published_at: 2.weeks.ago, meta_scope: Decidim::Faker::Localized.word, diff --git a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb index cefe5ad3d9a55..b46807b899ffc 100644 --- a/decidim-assemblies/lib/decidim/assemblies/test/factories.rb +++ b/decidim-assemblies/lib/decidim/assemblies/test/factories.rb @@ -27,7 +27,6 @@ description { generate_localized_description(:assembly_description, skip_injection:) } organization hero_image { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } # Keep after organization - banner_image { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") } # Keep after organization published_at { Time.current } deleted_at { nil } meta_scope { generate_localized_word(:assembly_meta_scope, skip_injection:) } diff --git a/decidim-assemblies/spec/commands/create_assembly_spec.rb b/decidim-assemblies/spec/commands/create_assembly_spec.rb index 5b175f3ee64f9..e4952a91aa401 100644 --- a/decidim-assemblies/spec/commands/create_assembly_spec.rb +++ b/decidim-assemblies/spec/commands/create_assembly_spec.rb @@ -18,7 +18,6 @@ module Decidim::Assemblies end let(:related_process_ids) { [participatory_processes.map(&:id)] } let(:hero_image) { nil } - let(:banner_image) { nil } let(:taxonomizations) do 2.times.map { build(:taxonomization, taxonomy: create(:taxonomy, :with_parent, organization:), taxonomizable: nil) } end @@ -34,7 +33,6 @@ module Decidim::Assemblies slug: "slug", meta_scope: { en: "meta scope" }, hero_image:, - banner_image:, promoted: nil, developer_group: { en: "developer group" }, local_area: { en: "local" }, @@ -87,7 +85,6 @@ module Decidim::Assemblies content_type: "image/jpeg" ) end - let(:banner_image) { hero_image } before do allow(Decidim::ActionLogger).to receive(:log).and_return(true) @@ -99,7 +96,6 @@ module Decidim::Assemblies it "adds errors to the form" do expect(errors).to receive(:add).with(:hero_image, "File resolution is too large") - expect(errors).to receive(:add).with(:banner_image, "File resolution is too large") subject.call end end @@ -112,14 +108,12 @@ module Decidim::Assemblies content_type: "image/png" ) end - let(:banner_image) { nil } let(:form) do Admin::AssemblyForm.from_params( title: { en: "title" }, subtitle: { en: "subtitle" }, slug: "slug", hero_image:, - banner_image:, description: { en: "description" }, short_description: { en: "short_description" }, organization: diff --git a/decidim-assemblies/spec/commands/decidim/assemblies/admin/import_assembly_spec.rb b/decidim-assemblies/spec/commands/decidim/assemblies/admin/import_assembly_spec.rb index e12aba9a3f132..ee9b46ea875d5 100644 --- a/decidim-assemblies/spec/commands/decidim/assemblies/admin/import_assembly_spec.rb +++ b/decidim-assemblies/spec/commands/decidim/assemblies/admin/import_assembly_spec.rb @@ -44,10 +44,6 @@ def stub_calls_to_external_files "http://localhost:3000/uploads/decidim/assembly/hero_image/1/city.jpeg", "image/jpeg" ) - stub_get_request_with_format( - "http://localhost:3000/uploads/decidim/assembly/banner_image/1/city2.jpeg", - "image/jpeg" - ) stub_get_request_with_format( "http://localhost:3000/uploads/decidim/attachment/file/31/Exampledocument.pdf", "application/pdf" diff --git a/decidim-assemblies/spec/commands/update_assembly_spec.rb b/decidim-assemblies/spec/commands/update_assembly_spec.rb index 384d61f6e9732..0af3499198faa 100644 --- a/decidim-assemblies/spec/commands/update_assembly_spec.rb +++ b/decidim-assemblies/spec/commands/update_assembly_spec.rb @@ -22,7 +22,6 @@ module Decidim::Assemblies 2.times.map { create(:taxonomization, taxonomy: create(:taxonomy, :with_parent, organization:), taxonomizable: my_assembly) } end let(:taxonomy) { create(:taxonomy, :with_parent, organization:) } - let(:banner_image) { my_assembly.banner_image } let(:params) do { assembly: { @@ -69,8 +68,7 @@ module Decidim::Assemblies end let(:attachment_params) do { - hero_image: hero_image.blob, - banner_image: banner_image.blob + hero_image: hero_image.blob } end let(:context) do @@ -105,7 +103,6 @@ module Decidim::Assemblies context "when the uploaded hero image has too large dimensions" do let(:attachment_params) do { - banner_image: banner_image.blob, hero_image: ActiveStorage::Blob.create_and_upload!( io: File.open(Decidim::Dev.asset("5000x5000.png")), filename: "5000x5000.png", @@ -125,7 +122,6 @@ module Decidim::Assemblies allow(form).to receive(:invalid?).and_return(false) expect(my_assembly).to receive(:valid?).at_least(:once).and_return(false) my_assembly.errors.add(:hero_image, "File resolution is too large") - my_assembly.errors.add(:banner_image, "File resolution is too large") end it "broadcasts invalid" do @@ -136,7 +132,6 @@ module Decidim::Assemblies command.call expect(form.errors[:hero_image]).not_to be_empty - expect(form.errors[:banner_image]).not_to be_empty end end @@ -176,14 +171,12 @@ module Decidim::Assemblies expect(linked_participatory_processes).to match_array(participatory_processes) end - context "when homepage image is not updated" do + context "when hero image is not updated" do let(:attachment_params) do - { - banner_image: banner_image.blob - } + {} end - it "does not replace the homepage image" do + it "does not replace the hero image" do expect(my_assembly).not_to receive(:hero_image=) command.call @@ -193,23 +186,6 @@ module Decidim::Assemblies end end - context "when banner image is not updated" do - let(:attachment_params) do - { - hero_image: hero_image.blob - } - end - - it "does not replace the banner image" do - expect(my_assembly).not_to receive(:banner_image=) - - command.call - my_assembly.reload - - expect(my_assembly.banner_image).to be_present - end - end - context "when updating the parent assembly" do let!(:parent_assembly) { create(:assembly, organization:) } diff --git a/decidim-assemblies/spec/forms/assembly_form_spec.rb b/decidim-assemblies/spec/forms/assembly_form_spec.rb index cb707515ee6db..6b37e015f9025 100644 --- a/decidim-assemblies/spec/forms/assembly_form_spec.rb +++ b/decidim-assemblies/spec/forms/assembly_form_spec.rb @@ -120,7 +120,6 @@ module Admin "short_description_es" => short_description[:es], "short_description_ca" => short_description[:ca], "hero_image" => attachment, - "banner_image" => attachment, "slug" => slug, "private_space" => private_space, "has_members" => has_members, @@ -185,7 +184,7 @@ module Admin it { is_expected.not_to be_valid } end - context "when attachment (hero_image or banner_image) is too big" do + context "when attachment (hero_image) is too big" do before do organization.settings.tap do |settings| settings.upload.maximum_file_size.default = 5 @@ -298,7 +297,6 @@ module Admin slug: "another-slug", meta_scope: assembly.meta_scope, hero_image: nil, - banner_image: nil, promoted: assembly.promoted, description_en: assembly.description, description_ca: assembly.description, @@ -351,7 +349,6 @@ module Admin slug: "another-slug", meta_scope: assembly.meta_scope, hero_image: nil, - banner_image: nil, promoted: assembly.promoted, description_en: assembly.description, description_ca: assembly.description, diff --git a/decidim-assemblies/spec/presenters/decidim/assemblies/assembly_presenter_spec.rb b/decidim-assemblies/spec/presenters/decidim/assemblies/assembly_presenter_spec.rb index e4e50af28d4ad..dc43eac7cab3d 100644 --- a/decidim-assemblies/spec/presenters/decidim/assemblies/assembly_presenter_spec.rb +++ b/decidim-assemblies/spec/presenters/decidim/assemblies/assembly_presenter_spec.rb @@ -12,26 +12,17 @@ module Decidim describe "when no images were uploaded" do before do assembly.hero_image.purge - assembly.banner_image.purge end it "return nil for hero_image_url" do expect(subject.hero_image_url).to be_nil end - - it "return nil for banner_image_url" do - expect(subject.banner_image_url).to be_nil - end end describe "when images are attached" do it "resolves hero_image_url" do expect(subject.hero_image_url).to be_blob_url(assembly.hero_image.blob) end - - it "resolves banner_image_url" do - expect(subject.banner_image_url).to be_blob_url(assembly.banner_image.blob) - end end end end diff --git a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb index dcc537f968bbe..50e7761e4d336 100644 --- a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb +++ b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_importer_spec.rb @@ -51,12 +51,10 @@ module Decidim::Assemblies "created_by" => "citizens", "meta_scope" => Decidim::Faker::Localized.sentence(word_count: 3), "announcement" => Decidim::Faker::Localized.wrapped("

", "

") { generate_localized_title }, - "remote_hero_image_url" => hero_image_url, - "remote_banner_image_url" => banner_image_url + "remote_hero_image_url" => hero_image_url } end let(:hero_image_url) { nil } - let(:banner_image_url) { nil } it "imports the assembly correctly" do expect { subject }.to change(Decidim::Assembly, :count).by(1) @@ -139,95 +137,22 @@ module Decidim::Assemblies end end - context "when banner image URL is present and accessible" do - let(:banner_image_url) { "http://example.com/banner.jpg" } - - before do - stub_request(:get, banner_image_url) - .to_return(status: 200, body: File.read(Decidim::Dev.asset("city2.jpeg"))) - stub_request(:head, banner_image_url) - .to_return(status: 200, headers: { "Content-Type" => "image/jpeg" }) - end - - it "imports the assembly with the banner image" do - expect { subject }.to change(Decidim::Assembly, :count).by(1) - expect(subject.banner_image).to be_attached - end - - it "has no warnings" do - subject - expect(importer.warnings).to be_empty - end - end - - context "when banner image URL returns 404 error" do - let(:banner_image_url) { "http://example.com/missing-banner.jpg" } - - before do - stub_request(:get, banner_image_url) - .to_return(status: 404, body: "Not Found") - stub_request(:head, banner_image_url) - .to_return(status: 404, body: "Not Found") - end - - it "imports the assembly successfully" do - expect { subject }.to change(Decidim::Assembly, :count).by(1) - end - - it "does not attach the banner image" do - subject - expect(subject.banner_image).not_to be_attached - end - - it "collects a warning about the missing banner image" do - subject - expect(importer.warnings).to include(a_string_matching(/The banner image could not be imported \(404 Not Found\)\./i)) - end - end - - context "when both hero and banner images fail to import" do - let(:hero_image_url) { "http://example.com/missing-hero.jpg" } - let(:banner_image_url) { "http://example.com/missing-banner.jpg" } - - before do - stub_request(:get, hero_image_url).to_return(status: 404) - stub_request(:head, hero_image_url).to_return(status: 404) - stub_request(:get, banner_image_url).to_return(status: 500) - stub_request(:head, banner_image_url).to_return(status: 500) - end - - it "imports the assembly successfully" do - expect { subject }.to change(Decidim::Assembly, :count).by(1) - end - - it "collects warnings for both images" do - subject - expect(importer.warnings).to include(a_string_matching(/The hero image could not be imported/i)) - expect(importer.warnings).to include(a_string_matching(/The banner image could not be imported/i)) - expect(importer.warnings.length).to eq(2) - end - end - context "when image URL is nil" do let(:hero_image_url) { nil } - let(:banner_image_url) { nil } it "imports the assembly without images and no warnings" do expect { subject }.to change(Decidim::Assembly, :count).by(1) expect(subject.hero_image).not_to be_attached - expect(subject.banner_image).not_to be_attached expect(importer.warnings).to be_empty end end context "when image URL is empty string" do let(:hero_image_url) { "" } - let(:banner_image_url) { "" } it "imports the assembly without images and no warnings" do expect { subject }.to change(Decidim::Assembly, :count).by(1) expect(subject.hero_image).not_to be_attached - expect(subject.banner_image).not_to be_attached expect(importer.warnings).to be_empty end end diff --git a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_serializer_spec.rb b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_serializer_spec.rb index 57ec9cb121f86..2577d2432fb03 100644 --- a/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_serializer_spec.rb +++ b/decidim-assemblies/spec/serializers/decidim/assemblies/assembly_serializer_spec.rb @@ -22,7 +22,6 @@ module Decidim::Assemblies expect(serialized).to include(short_description: resource.short_description) expect(serialized).to include(description: resource.description) expect(serialized[:remote_hero_image_url]).to be_blob_url(resource.hero_image.blob) - expect(serialized[:remote_banner_image_url]).to be_blob_url(resource.banner_image.blob) expect(serialized).to include(promoted: resource.promoted) expect(serialized).to include(developer_group: resource.developer_group) expect(serialized).to include(meta_scope: resource.meta_scope) diff --git a/decidim-assemblies/spec/serializers/decidim/assemblies/open_data_assembly_serializer_spec.rb b/decidim-assemblies/spec/serializers/decidim/assemblies/open_data_assembly_serializer_spec.rb index 9d07c6c3d027c..baee2322438b5 100644 --- a/decidim-assemblies/spec/serializers/decidim/assemblies/open_data_assembly_serializer_spec.rb +++ b/decidim-assemblies/spec/serializers/decidim/assemblies/open_data_assembly_serializer_spec.rb @@ -23,7 +23,6 @@ module Decidim::Assemblies expect(serialized).to include(short_description: resource.short_description) expect(serialized).to include(description: resource.description) expect(serialized[:remote_hero_image_url]).to be_blob_url(resource.hero_image.blob) - expect(serialized[:remote_banner_image_url]).to be_blob_url(resource.banner_image.blob) expect(serialized).to include(promoted: resource.promoted) expect(serialized).to include(developer_group: resource.developer_group) expect(serialized).to include(meta_scope: resource.meta_scope) diff --git a/decidim-assemblies/spec/shared/manage_assemblies_examples.rb b/decidim-assemblies/spec/shared/manage_assemblies_examples.rb index c4a4409979ba0..c98bd31acad1e 100644 --- a/decidim-assemblies/spec/shared/manage_assemblies_examples.rb +++ b/decidim-assemblies/spec/shared/manage_assemblies_examples.rb @@ -18,7 +18,7 @@ it "updates an assembly" do fill_in_i18n(:assembly_title, "#assembly-title-tabs", **attributes[:title].except("machine_translations")) - dynamically_attach_file(:assembly_banner_image, image3_path, remove_before: true) + dynamically_attach_file(:assembly_hero_image, image3_path, remove_before: true) within ".edit_assembly" do expect(assembly_parent_id_options).not_to include(assembly.id) @@ -84,12 +84,6 @@ src = page.find("img")["src"] expect(src).to be_blob_url(hero_blob) end - - banner_blob = assembly.hero_image.blob - within %([data-active-uploads] [data-filename="#{banner_blob.filename}"]) do - src = page.find("img")["src"] - expect(src).to be_blob_url(banner_blob) - end end end diff --git a/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb b/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb index f365f2fb79242..8d59e9521f705 100644 --- a/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_imports_assembly_spec.rb @@ -29,10 +29,6 @@ "http://localhost:3000/uploads/decidim/assembly/hero_image/1/city.jpeg", "image/jpeg" ) - stub_get_request_with_format( - "http://localhost:3000/uploads/decidim/assembly/banner_image/1/city2.jpeg", - "image/jpeg" - ) stub_get_request_with_format( "http://localhost:3000/uploads/decidim/attachment/file/31/Exampledocument.pdf", "application/pdf" @@ -115,10 +111,6 @@ .to_return(status: 404, body: "Not Found") stub_request(:head, "http://example.com/missing-hero.jpg") .to_return(status: 404, body: "Not Found") - stub_request(:get, "http://localhost:3000/uploads/decidim/assembly/banner_image/1/city2.jpeg") - .to_return(status: 200, body: File.read(Decidim::Dev.asset("city2.jpeg"))) - stub_request(:head, "http://localhost:3000/uploads/decidim/assembly/banner_image/1/city2.jpeg") - .to_return(status: 200, headers: { "Content-Type" => "image/jpeg" }) within_admin_menu do click_on "Import" @@ -149,115 +141,7 @@ end end - context "when banner image URL returns 404" do - let(:json_data) { JSON.parse(File.read(Decidim::Dev.asset("assemblies.json"))) } - let(:json_file) do - Tempfile.new(["assemblies", ".json"]).tap do |file| - file.write(json_data.to_json) - file.rewind - end - end - let(:uploaded_file) do - Rack::Test::UploadedFile.new(json_file.path, "application/json") - end - - before do - json_data.first["remote_banner_image_url"] = "http://example.com/missing-banner.jpg" - - stub_request(:get, "http://example.com/missing-banner.jpg") - .to_return(status: 404, body: "Not Found") - stub_request(:head, "http://example.com/missing-banner.jpg") - .to_return(status: 404, body: "Not Found") - stub_request(:get, "http://localhost:3000/uploads/decidim/assembly/hero_image/1/city.jpeg") - .to_return(status: 200, body: File.read(Decidim::Dev.asset("city.jpeg"))) - stub_request(:head, "http://localhost:3000/uploads/decidim/assembly/hero_image/1/city.jpeg") - .to_return(status: 200, headers: { "Content-Type" => "image/jpeg" }) - - within_admin_menu do - click_on "Import" - end - - within ".import_assembly" do - fill_in_i18n( - :assembly_title, - "#assembly-title-tabs", - en: "Import assembly with 404 banner", - es: "Importación de la asamblea", - ca: "Importació de l'asamblea" - ) - fill_in :assembly_slug, with: "as-import-404-banner" - end - - dynamically_attach_file(:assembly_document, uploaded_file.path) - click_on "Import" - end - - it "imports successfully and shows a warning about the missing banner image" do - expect(page).to have_callout("Assembly successfully imported.") - expect(page).to have_callout("Import assembly with 404 banner") - - within ".flash.warning" do - expect(page).to have_content(/The banner image could not be imported \(404 Not Found\)\./i) - end - end - end - - context "when both hero and banner image URLs return 404" do - let(:json_data) { JSON.parse(File.read(Decidim::Dev.asset("assemblies.json"))) } - let(:json_file) do - Tempfile.new(["assemblies", ".json"]).tap do |file| - file.write(json_data.to_json) - file.rewind - end - end - let(:uploaded_file) do - Rack::Test::UploadedFile.new(json_file.path, "application/json") - end - - before do - json_data.first["remote_hero_image_url"] = "http://example.com/missing-hero.jpg" - json_data.first["remote_banner_image_url"] = "http://example.com/missing-banner.jpg" - - stub_request(:get, "http://example.com/missing-hero.jpg") - .to_return(status: 404, body: "Not Found") - stub_request(:head, "http://example.com/missing-hero.jpg") - .to_return(status: 404, body: "Not Found") - stub_request(:get, "http://example.com/missing-banner.jpg") - .to_return(status: 404, body: "Not Found") - stub_request(:head, "http://example.com/missing-banner.jpg") - .to_return(status: 404, body: "Not Found") - - within_admin_menu do - click_on "Import" - end - - within ".import_assembly" do - fill_in_i18n( - :assembly_title, - "#assembly-title-tabs", - en: "Import assembly with 404 images", - es: "Importación de la asamblea", - ca: "Importació de l'asamblea" - ) - fill_in :assembly_slug, with: "as-import-404-both" - end - - dynamically_attach_file(:assembly_document, uploaded_file.path) - click_on "Import" - end - - it "imports successfully and shows warnings for both missing images" do - expect(page).to have_callout("Assembly successfully imported.") - expect(page).to have_callout("Import assembly with 404 images") - - within ".flash.warning" do - expect(page).to have_content(/The hero image could not be imported \(404 Not Found\)\./i) - expect(page).to have_content(/The banner image could not be imported \(404 Not Found\)\./i) - end - end - end - - context "when both image URLs are too long and return 404" do + context "when hero image URL is too long and returns 404" do let(:json_data) { JSON.parse(File.read(Decidim::Dev.asset("assemblies.json"))) } let(:json_file) do Tempfile.new(["assemblies", ".json"]).tap do |file| @@ -269,20 +153,14 @@ Rack::Test::UploadedFile.new(json_file.path, "application/json") end let(:hero_image_url) { "http://example.com/#{"a" * 5000}.jpg" } - let(:banner_image_url) { "http://example.com/#{"b" * 5000}.jpg" } before do json_data.first["remote_hero_image_url"] = hero_image_url - json_data.first["remote_banner_image_url"] = banner_image_url stub_request(:get, hero_image_url) .to_return(status: 404, body: "Not Found") stub_request(:head, hero_image_url) .to_return(status: 404, body: "Not Found") - stub_request(:get, banner_image_url) - .to_return(status: 404, body: "Not Found") - stub_request(:head, banner_image_url) - .to_return(status: 404, body: "Not Found") within_admin_menu do click_on "Import" @@ -303,13 +181,12 @@ click_on "Import" end - it "imports successfully and shows warnings for both missing images" do + it "imports successfully and shows warnings for missing hero image" do expect(page).to have_callout("Assembly successfully imported.") expect(page).to have_callout("Import assembly with long 404 images") within ".flash.warning" do expect(page).to have_content(/The hero image could not be imported \(404 Not Found\)\./i) - expect(page).to have_content(/The banner image could not be imported \(404 Not Found\)\./i) end end end @@ -338,10 +215,6 @@ "http://localhost:3000/uploads/decidim/assembly/hero_image/1/city.jpeg", "image/jpeg" ) - stub_get_request_with_format( - "http://localhost:3000/uploads/decidim/assembly/banner_image/1/city2.jpeg", - "image/jpeg" - ) within_admin_menu do click_on "Import" 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 80bea12b830d7..e23ed6837013a 100644 --- a/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb +++ b/decidim-assemblies/spec/system/admin/admin_manages_assemblies_spec.rb @@ -86,7 +86,6 @@ 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 diff --git a/decidim-assemblies/spec/system/assemblies_social_share_spec.rb b/decidim-assemblies/spec/system/assemblies_social_share_spec.rb index a52d4aaefbcf5..1749135152554 100644 --- a/decidim-assemblies/spec/system/assemblies_social_share_spec.rb +++ b/decidim-assemblies/spec/system/assemblies_social_share_spec.rb @@ -5,12 +5,11 @@ describe "Social shares" do let(:organization) { create(:organization) } - let(:assembly) { create(:assembly, organization:, description:, short_description:, hero_image:, banner_image:) } + let(:assembly) { create(:assembly, organization:, description:, short_description:, hero_image:) } let(:content_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage) } let!(:attachment) { create(:attachment, :with_image, attached_to: assembly, file: attachment_file) } let(:description) { { en: "Description

" } } let(:short_description) { { en: "Description

" } } - let(:banner_image) { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } let(:hero_image) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") } let!(:attachment_file) { Decidim::Dev.test_file("city3.jpeg", "image/jpeg") } let(:description_image_path) { Rails.application.routes.url_helpers.rails_blob_path(description_image, only_path: true) } @@ -44,19 +43,11 @@ context "when no hero_image" do let(:hero_image) { nil } - it_behaves_like "a social share meta tag", "city.jpeg" - end - - context "when no direct images" do - let(:hero_image) { nil } - let(:banner_image) { nil } - it_behaves_like "a social share meta tag", "city3.jpeg" end context "when no attachments nor direct images" do let(:hero_image) { nil } - let(:banner_image) { nil } let(:attachment) { nil } it_behaves_like "a social share meta tag", "description_image.jpg" @@ -70,7 +61,6 @@ context "when no attachments, description image or direct images" do let(:hero_image) { nil } - let(:banner_image) { nil } let(:attachment) { nil } let(:description_image_path) { "" } let(:short_description_image_path) { "" } @@ -80,7 +70,6 @@ context "when nothing" do let(:hero_image) { nil } - let(:banner_image) { nil } let(:attachment) { nil } let(:description_image_path) { "" } let(:short_description_image_path) { "" } diff --git a/decidim-assemblies/spec/types/assembly_type_spec.rb b/decidim-assemblies/spec/types/assembly_type_spec.rb index a59648ec90844..c197eb01dd68c 100644 --- a/decidim-assemblies/spec/types/assembly_type_spec.rb +++ b/decidim-assemblies/spec/types/assembly_type_spec.rb @@ -83,14 +83,6 @@ module Assemblies end end - describe "bannerImage" do - let(:query) { "{ bannerImage }" } - - it "returns the banner image field" do - expect(response["bannerImage"]).to be_blob_url(model.banner_image.blob) - end - end - describe "promoted" do let(:query) { "{ promoted }" } diff --git a/decidim-assemblies/spec/types/integration_schema_spec.rb b/decidim-assemblies/spec/types/integration_schema_spec.rb index 506a9bf3b697c..fdedb5a24c0fb 100644 --- a/decidim-assemblies/spec/types/integration_schema_spec.rb +++ b/decidim-assemblies/spec/types/integration_schema_spec.rb @@ -77,7 +77,6 @@ translation(locale:"#{locale}") } } - bannerImage categories { id } @@ -200,7 +199,6 @@ it "returns the correct response" do data = response["assemblies"].first expect(data).to include(assembly_data) - expect(data["bannerImage"]).to be_blob_url(assembly.banner_image.blob) expect(data["heroImage"]).to be_blob_url(assembly.hero_image.blob) end @@ -231,7 +229,6 @@ translation(locale:"#{locale}") } } - bannerImage categories { id } @@ -344,7 +341,6 @@ it "returns the correct response" do data = response["assembly"] expect(data).to include(assembly_data) - expect(data["bannerImage"]).to be_blob_url(assembly.banner_image.blob) expect(data["heroImage"]).to be_blob_url(assembly.hero_image.blob) end diff --git a/decidim-core/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb b/decidim-core/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb index ddc517fdef758..f6baacff47e4f 100644 --- a/decidim-core/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb +++ b/decidim-core/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb @@ -27,12 +27,8 @@ def subtitle_text decidim_escape_translated(resource.subtitle) end - # If it is called from the landing page content block, use the background image defined there - # Else, use the banner image defined in the space (for assemblies) def image_path - return model.images_container.attached_uploader(:background_image).url if model.respond_to?(:images_container) - - attached_uploader(:banner_image).url + model.images_container.attached_uploader(:background_image).url end def has_cta? diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index 095281676a61b..f20be6b6a5bc6 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -1534,6 +1534,9 @@ en: %{organization_name} hello: Dear %{username}, subject: Inactive account deleted + participatory_space_members: + index: + members: Members passwords: update: error: There was a problem updating the password. diff --git a/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb index 2f9662df95209..72f2682a17664 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb @@ -107,7 +107,7 @@ end it "lists all the non ceased members" do - within "#assembly_members-grid" do + within ".layout-main__section" do expect(page).to have_css(".profile__user", count: 1) expect(page).to have_no_content(Decidim::ParticipatorySpace::MemberPresenter.new(ceased_member).name) diff --git a/decidim-core/spec/services/decidim/meta_image_url_resolver_spec.rb b/decidim-core/spec/services/decidim/meta_image_url_resolver_spec.rb index d428cec3d8c46..2b925313e7d6a 100644 --- a/decidim-core/spec/services/decidim/meta_image_url_resolver_spec.rb +++ b/decidim-core/spec/services/decidim/meta_image_url_resolver_spec.rb @@ -7,9 +7,8 @@ let(:organization) { create(:organization) } let(:hero_image) { nil } - let(:banner_image) { nil } let(:avatar) { nil } - let(:participatory_space) { create(:assembly, organization:, hero_image:, banner_image:) } + let(:participatory_space) { create(:assembly, organization:, hero_image:) } let(:component) { create(:proposal_component, :with_attachments_allowed, participatory_space:) } let!(:proposal) { create(:proposal, component:, body:) } let(:description_image) do @@ -38,14 +37,13 @@ shared_examples "direct images" do let(:hero_image) { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } - let(:banner_image) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") } it { is_expected.to end_with("/city.jpeg") } context "and no hero_image" do let(:hero_image) { nil } - it { is_expected.to end_with("/city2.jpeg") } + it { is_expected.to end_with("/icon.png") } end end @@ -55,7 +53,6 @@ context "when there is no image attached" do let(:hero_image) { nil } - let(:banner_image) { nil } let(:resource) { nil } before do diff --git a/decidim-participatory_processes/app/views/decidim/participatory_processes/members/index.html.erb b/decidim-participatory_processes/app/views/decidim/participatory_processes/members/index.html.erb index 3f3d8461c4f56..be6be3ee4ece3 100644 --- a/decidim-participatory_processes/app/views/decidim/participatory_processes/members/index.html.erb +++ b/decidim-participatory_processes/app/views/decidim/participatory_processes/members/index.html.erb @@ -1,4 +1,4 @@ -<% add_decidim_page_title(t(".title")) %> +<% add_decidim_page_title(t("members", scope: "decidim.participatory_space_members.index")) %> <% add_decidim_meta_tags( title: translated_attribute(current_participatory_space.title), resource: current_participatory_space) %> @@ -11,12 +11,14 @@ edit_link( ) %> -
-

- <%= t("members", scope: "decidim.assemblies.assembly_members.index") %> - <%= collection.size %> -

-
+<% content_for :aside do %> +

+ <%= t("members", scope: "decidim.participatory_space_members.index") %> +

+<% end %> + +<%= render layout: "layouts/decidim/shared/layout_two_col" do %> +
<%= render(collection) %> -
-
+ +<% end %> diff --git a/decidim-participatory_processes/config/locales/en.yml b/decidim-participatory_processes/config/locales/en.yml index 7d5060fdb9e86..bc5169360acb1 100644 --- a/decidim-participatory_processes/config/locales/en.yml +++ b/decidim-participatory_processes/config/locales/en.yml @@ -567,9 +567,6 @@ en: title: Participatory processes last_activity: new_participatory_process: 'New participatory process:' - members: - index: - title: Members pages: home: highlighted_processes: From d64d6b6ac27ad7c226268818c1e9826612522f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 10 Mar 2026 19:55:00 +0100 Subject: [PATCH 098/135] Remove automatic numbering of questions in the frontend (#16359) --- .../app/packs/stylesheets/decidim/forms/forms.scss | 2 +- .../decidim/forms/questionnaires/_questionnaire.html.erb | 6 ------ .../views/decidim/forms/questionnaires/_response.html.erb | 3 +-- .../decidim/forms/test/shared_examples/has_questionnaire.rb | 4 +--- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/decidim-forms/app/packs/stylesheets/decidim/forms/forms.scss b/decidim-forms/app/packs/stylesheets/decidim/forms/forms.scss index 7101265359f9a..f5ab26a29afb4 100644 --- a/decidim-forms/app/packs/stylesheets/decidim/forms/forms.scss +++ b/decidim-forms/app/packs/stylesheets/decidim/forms/forms.scss @@ -22,7 +22,7 @@ } &__question-label { - @apply text-black text-xl font-semibold relative before:content-[attr(data-response-idx)] before:w-6 before:h-6 md:before:absolute md:before:-left-4 md:before:-translate-x-full before:inline-flex before:justify-center before:rounded-full before:bg-background before:text-lg before:text-gray-2 before:font-semibold; + @apply text-black text-xl font-semibold; } &__question-description { diff --git a/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb b/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb index 986483dfc89ae..98d7db0fe25db 100644 --- a/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb +++ b/decidim-forms/app/views/decidim/forms/questionnaires/_questionnaire.html.erb @@ -17,7 +17,6 @@ <%= invisible_captcha %> <% response_idx = 0 %> - <% cleaned_response_idx = 1 %> <% @form.responses_by_step.each_with_index do |step_responses, step_index| %>
> @@ -39,16 +38,11 @@ response_form:, response:, response_idx:, - cleaned_response_idx:, disabled: !current_participatory_space.can_participate?(current_user) ) %> <% end %>
- <% if !(response.question.separator? || response.question.title_and_description?) %> - <% cleaned_response_idx += 1 %> - <% end %> - <% response_idx += 1 %> <% end %> diff --git a/decidim-forms/app/views/decidim/forms/questionnaires/_response.html.erb b/decidim-forms/app/views/decidim/forms/questionnaires/_response.html.erb index 5f2d3e28f2adb..2b7a276d64b90 100644 --- a/decidim-forms/app/views/decidim/forms/questionnaires/_response.html.erb +++ b/decidim-forms/app/views/decidim/forms/questionnaires/_response.html.erb @@ -24,8 +24,7 @@
<% label_options = { - class: "response-questionnaire__question-label questionnaire-question", - data: { "response-idx": cleaned_response_idx } + class: "response-questionnaire__question-label questionnaire-question" } label_options[:for] = nil if %w(matrix_single matrix_multiple single_option multiple_option).include?(response.question.question_type) %> diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb index bc3cb470bf490..33df3e9980100 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/has_questionnaire.rb @@ -192,9 +192,7 @@ def response_first_questionnaire expect(form_fields[0]).to have_i18n_content(question.body) expect(form_fields[1]).to have_i18n_content(other_question.body) - 2.times do |index| - expect(form_fields[index]).to have_css("[data-response-idx='#{index + 1}']") - end + expect(page.text.index(translated_attribute(other_question.body))).to be > page.text.index(translated_attribute(question.body)) end end From 9e1f8f6fe4217c83882a387a9e8774a0cbc5c4ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:03:24 +0200 Subject: [PATCH 099/135] Bump to dependencies: Bump graphql from 2.5.20 to 2.5.21 (#16373) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1ca4a75b8a58e..5c75bed49e13f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -453,7 +453,7 @@ GEM google-protobuf (4.33.5-x86_64-linux-gnu) bigdecimal rake (>= 13) - graphql (2.5.20) + graphql (2.5.21) base64 fiber-storage logger diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index bac1d2b30f206..08730cc6eefa9 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -447,7 +447,7 @@ GEM google-protobuf (4.33.5-x86_64-linux-gnu) bigdecimal rake (>= 13) - graphql (2.5.20) + graphql (2.5.21) base64 fiber-storage logger From 353b5d7154ec63cfb4198d8927bdf15ed7a15f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 11 Mar 2026 10:46:51 +0100 Subject: [PATCH 100/135] Add more specs for content blocks (#16323) --- .../admin/homepage_content_block_cell_spec.rb | 47 +++++++ ...blishes_unpublishes_content_blocks_spec.rb | 60 +++++++++ decidim-core/spec/system/homepage_spec.rb | 125 ++++++++++++++---- .../decidim/meetings/dates_and_map/show.erb | 14 +- .../decidim/meetings/meeting_l/image.erb | 14 +- .../stylesheets/decidim/meetings/_item.scss | 2 +- 6 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 decidim-admin/spec/cells/decidim/admin/homepage_content_block_cell_spec.rb create mode 100644 decidim-admin/spec/system/admin_publishes_unpublishes_content_blocks_spec.rb diff --git a/decidim-admin/spec/cells/decidim/admin/homepage_content_block_cell_spec.rb b/decidim-admin/spec/cells/decidim/admin/homepage_content_block_cell_spec.rb new file mode 100644 index 0000000000000..dc937e285ad5c --- /dev/null +++ b/decidim-admin/spec/cells/decidim/admin/homepage_content_block_cell_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Admin::HomepageContentBlockCell, type: :cell do + controller Decidim::Admin::OrganizationHomepageController + + subject { cell("decidim/admin/homepage_content_block", content_block).call } + + let(:organization) { create(:organization) } + let(:content_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage) } + + it "renders the content block name" do + expect(subject).to have_content("Hero image") + end + + it "renders a link to edit the content block" do + expect(subject).to have_link(href: /edit/) + end + + it "renders a link to destroy the content block" do + expect(subject).to have_css('a[data-method="delete"][href*="/content_blocks/"]') + end + + it "renders the drag handle" do + expect(subject).to have_css("[draggable=\"true\"]") + end + + context "when content block is not persisted" do + let(:content_block) { build(:content_block, organization:, manifest_name: :hero, scope_name: :homepage) } + + it "does not render edit or destroy links" do + expect(subject).to have_no_link(href: %r{/content_blocks/\d+/edit$}) + expect(subject).to have_no_css('a[data-method="delete"]') + end + end + + context "when content block has no settings" do + let(:content_block) do + create(:content_block, organization:, manifest_name: :sub_hero, scope_name: :homepage) + end + + it "does not render edit link" do + expect(subject).to have_no_link(href: /edit/) + end + end +end diff --git a/decidim-admin/spec/system/admin_publishes_unpublishes_content_blocks_spec.rb b/decidim-admin/spec/system/admin_publishes_unpublishes_content_blocks_spec.rb new file mode 100644 index 0000000000000..1fd1b638639d4 --- /dev/null +++ b/decidim-admin/spec/system/admin_publishes_unpublishes_content_blocks_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin publishes/unpublishes content blocks" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + end + + context "when publishing a content block via admin UI" do + let!(:hero_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage, weight: nil, published_at: nil) } + + it "shows published content block on the homepage" do + visit decidim_admin.edit_organization_homepage_path + page.refresh + + within ".edit_content_blocks" do + within ".js-list-available" do + expect(page).to have_css("li", text: "Hero image") + end + + first("ul.js-list-available li").drag_to(find("ul.js-list-actives")) + sleep 2 + end + + expect(hero_block.reload.published_at).not_to be_nil + + visit decidim.root_path + expect(page).to have_css("[id^=hero]") + end + end + + context "when unpublishing a content block via admin UI" do + let!(:hero_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage, weight: 1, published_at: Time.current) } + let!(:extra_block) { create(:content_block, organization:, manifest_name: :sub_hero, scope_name: :homepage, weight: 2, published_at: Time.current) } + + it "does not show unpublished content block on the homepage" do + visit decidim_admin.edit_organization_homepage_path + page.refresh + + within ".edit_content_blocks" do + within ".js-list-actives" do + expect(page).to have_css("li", text: "Hero image") + end + + first("ul.js-list-actives li").drag_to(find("ul.js-list-available")) + sleep 2 + end + + expect(hero_block.reload.published_at).to be_nil + + visit decidim.root_path + expect(page).to have_no_css("[id^=hero]") + end + end +end diff --git a/decidim-core/spec/system/homepage_spec.rb b/decidim-core/spec/system/homepage_spec.rb index c9bfcaefb6eb4..1bb784c5f5935 100644 --- a/decidim-core/spec/system/homepage_spec.rb +++ b/decidim-core/spec/system/homepage_spec.rb @@ -19,12 +19,32 @@ let(:organization) do create(:organization, official_url:) end + let!(:participatory_process) { create(:participatory_process, :promoted, organization:) } + let!(:assembly) { create(:assembly, :promoted, organization:) } + let!(:meeting_component) { create(:component, manifest_name: :meetings, organization:) } + let!(:meeting) { create(:meeting, :published, component: meeting_component) } + + let(:highlighted_content_banner_settings) do + { + "title_en" => "Hello world", + "short_description_en" => "Bye world", + "action_button_title_en" => "Go!", + "action_button_subtitle_en" => "Now", + "action_button_url" => "https://example.org" + } + end before do create(:content_block, organization:, scope_name: :homepage, manifest_name: :hero) create(:content_block, organization:, scope_name: :homepage, manifest_name: :sub_hero) create(:content_block, organization:, scope_name: :homepage, manifest_name: :how_to_participate) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :stats) create(:content_block, organization:, scope_name: :homepage, manifest_name: :footer_sub_hero) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :highlighted_content_banner, settings: highlighted_content_banner_settings) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :highlighted_processes) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :highlighted_assemblies) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :upcoming_meetings) + create(:content_block, organization:, scope_name: :homepage, manifest_name: :html, settings: { html_content: { en: "
Custom HTML Content
" } }) switch_to_host(organization.host) end @@ -93,6 +113,11 @@ let(:snippet) { "" } let(:organization) { create(:organization, official_url:, header_snippets: snippet) } + before do + allow(Decidim).to receive(:enable_html_header_snippets).and_return(false) + visit decidim.root_path + end + it "does not include the header snippets" do expect(page).to have_no_selector("meta[data-hello]", visible: :all) end @@ -305,38 +330,25 @@ ) end - context "when organization does not have the stats content block" do - let(:organization) { create(:organization) } - - it "does not show the statistics block" do - expect(page).to have_no_content("Current state of #{translated(organization.name)}") - end + before do + visit current_path end - context "when organization has the stats content block" do - let(:organization) { create(:organization) } - - before do - create(:content_block, organization:, scope_name: :homepage, manifest_name: :stats) - visit current_path + it "shows the statistics block" do + within "#statistics" do + expect(page).to have_content("Statistics") + expect(page).to have_content("Processes") + expect(page).to have_content("Participants") end + end - it "shows the statistics block" do - within "#statistics" do - expect(page).to have_content("Statistics") - expect(page).to have_content("Processes") - expect(page).to have_content("Participants") - end + it "has the correct values for the statistics" do + within ".users_count" do + expect(page).to have_content("4") end - it "has the correct values for the statistics" do - within ".users_count" do - expect(page).to have_content("4") - end - - within ".processes_count" do - expect(page).to have_content("2") - end + within ".processes_count" do + expect(page).to have_content("3") end end end @@ -395,6 +407,67 @@ end end end + + describe "content blocks" do + it "renders all content blocks on the homepage" do + expect(page).to have_css("section.hero__container") + expect(page).to have_css("#sub_hero") + expect(page).to have_css("#how_to_participate") + expect(page).to have_css("#statistics") + expect(page).to have_css("#footer_sub_hero") + expect(page).to have_css("#highlighted_content_banner") + expect(page).to have_css("#highlighted-processes") + expect(page).to have_css("#highlighted-assemblies") + expect(page).to have_css("[id^=meetings]") + expect(page).to have_css(".custom-html") + end + + it "renders content blocks in the correct order by weight" do + hero_section = page.find("section.hero__container") + sub_hero_section = page.find_by_id("sub_hero") + how_to_participate_section = page.find_by_id("how_to_participate") + stats_section = page.find_by_id("statistics") + footer_sub_hero_section = page.find_by_id("footer_sub_hero") + highlighted_content_banner = page.find_by_id("highlighted_content_banner") + + hero_position = hero_section.evaluate_script("this.getBoundingClientRect().top") + sub_hero_position = sub_hero_section.evaluate_script("this.getBoundingClientRect().top") + how_to_participate_position = how_to_participate_section.evaluate_script("this.getBoundingClientRect().top") + stats_position = stats_section.evaluate_script("this.getBoundingClientRect().top") + footer_sub_hero_position = footer_sub_hero_section.evaluate_script("this.getBoundingClientRect().top") + highlighted_content_banner_position = highlighted_content_banner.evaluate_script("this.getBoundingClientRect().top") + + expect(hero_position).to be < sub_hero_position + expect(sub_hero_position).to be < how_to_participate_position + expect(how_to_participate_position).to be < stats_position + expect(stats_position).to be < footer_sub_hero_position + expect(footer_sub_hero_position).to be < highlighted_content_banner_position + end + + it "renders each content block with its corresponding cell content" do + expect(page).to have_css("section.hero__container") + within "section.hero__container" do + expect(page).to have_content("Welcome") + end + + expect(page).to have_css("#how_to_participate") + expect(page).to have_content("How do I take part in a process?") + + expect(page).to have_css("#statistics") + expect(page).to have_content("Statistics") + + expect(page).to have_css("#footer_sub_hero") + + expect(page).to have_css("#highlighted_content_banner") + + expect(page).to have_css("#highlighted-processes") + expect(page).to have_css("#highlighted-assemblies") + expect(page).to have_css("[id^=meetings]") + + expect(page).to have_css(".custom-html") + expect(page).to have_content("Custom HTML Content") + end + end end end end diff --git a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb index f06069058dd6c..e5b8d8a5d0222 100644 --- a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb @@ -1,21 +1,21 @@
-

<%= l(start_time, format: same_month? ? "%B" : "%b") %>

+ <%= l(start_time, format: same_month? ? "%B" : "%b") %> <% unless same_month? %> -

-

-

<%= l(end_time, format: "%b") %>

+ - + <%= l(end_time, format: "%b") %> <% end %>
-

<%= l(start_time, format: "%d") %>

+ <%= l(start_time, format: "%d") %> <% unless same_day? && same_month? %> -

-

-

<%= l(end_time, format: "%d") %>

+ - + <%= l(end_time, format: "%d") %> <% end %>
-

<%= year %>

+ <%= year %>
diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb index 9b01dc40bc830..9d51b9c789dfd 100644 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb +++ b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb @@ -1,19 +1,19 @@ diff --git a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss index 57e474d6de0cc..f879442191745 100644 --- a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss +++ b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss @@ -35,7 +35,7 @@ &-month, &-day, &-year { - @apply inline-flex items-center justify-center empty:[&>p]:hidden; + @apply inline-flex items-center justify-center empty:[&>span]:hidden; } &-separator { From ecbcf50c788845b900c3b444d81260b8eb361464 Mon Sep 17 00:00:00 2001 From: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> Date: Wed, 11 Mar 2026 12:49:00 +0100 Subject: [PATCH 101/135] Fix edit proposal with attachment (#16094) Co-authored-by: Alexandru Emil Lupu --- .../app/cells/decidim/upload_modal/files.erb | 6 ++- .../app/cells/decidim/upload_modal_cell.rb | 11 +++- .../decidim/multiple_attachments_methods.rb | 23 ++++++-- .../decidim/proposals/admin/proposal_form.rb | 52 ++++++++++++++++++- .../system/admin/admin_edits_proposal_spec.rb | 27 ++++++++++ 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/decidim-core/app/cells/decidim/upload_modal/files.erb b/decidim-core/app/cells/decidim/upload_modal/files.erb index 651da3aacd0b2..594e8dd7717e2 100644 --- a/decidim-core/app/cells/decidim/upload_modal/files.erb +++ b/decidim-core/app/cells/decidim/upload_modal/files.erb @@ -42,7 +42,11 @@ <% end %> <% end %> <% if attachment_blob.present? %> - <%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %> + <% if is_persisted_attachment %> + <%= form.hidden_field attribute, value: attachment.id, id: "hidden_#{attribute}_#{attachment.id}" %> + <% else %> + <%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %> + <% end %> <% end %>
<% end %> diff --git a/decidim-core/app/cells/decidim/upload_modal_cell.rb b/decidim-core/app/cells/decidim/upload_modal_cell.rb index 055d4d1df8081..03a9af51e1578 100644 --- a/decidim-core/app/cells/decidim/upload_modal_cell.rb +++ b/decidim-core/app/cells/decidim/upload_modal_cell.rb @@ -128,7 +128,16 @@ def attachments @attachments = begin attachments = options[:attachments] || form.object.send(attribute) attachments = Array(attachments).compact_blank - attachments.map { |attachment| attachment.is_a?(String) ? ActiveStorage::Blob.find_signed(attachment) : attachment } + attachments.map do |attachment| + case attachment + when String + ActiveStorage::Blob.find_signed(attachment) + when Integer + Decidim::Attachment.find_by(id: attachment) + else + attachment + end + end.compact end end diff --git a/decidim-core/app/commands/decidim/multiple_attachments_methods.rb b/decidim-core/app/commands/decidim/multiple_attachments_methods.rb index 1dfe2e4c40949..248d604e0d7e3 100644 --- a/decidim-core/app/commands/decidim/multiple_attachments_methods.rb +++ b/decidim-core/app/commands/decidim/multiple_attachments_methods.rb @@ -41,8 +41,9 @@ def attachments_invalid? def create_attachments(first_weight: 0) weight = first_weight - # Add the weights first to the old document - @form.documents.each do |document| + # Add the weights first to the old documents + document_ids = keep_ids + Decidim::Attachment.where(id: document_ids).each do |document| document.update!(weight:) weight += 1 end @@ -59,7 +60,7 @@ def document_cleanup!(include_all_attachments: false) documents = include_all_attachments ? documents_attached_to.attachments.with_attached_file : documents_attached_to.documents documents.each do |document| - document.destroy! if @form.documents.map(&:id).exclude? document.id + document.destroy! unless keep_ids.include?(document.id) end documents_attached_to.reload @@ -98,5 +99,21 @@ def content_type_for(attachment) def blob(signed_id) ActiveStorage::Blob.find_signed(signed_id) end + + def keep_ids + documents_array = Array(@form.documents) + documents_array.map do |doc| + case doc + when Decidim::Attachment + doc.id + when Integer + doc + when String + doc.match?(/\A\d+\z/) ? doc.to_i : nil + when Hash + (doc[:id] || doc["id"]).to_i + end + end.compact + end end end diff --git a/decidim-proposals/app/forms/decidim/proposals/admin/proposal_form.rb b/decidim-proposals/app/forms/decidim/proposals/admin/proposal_form.rb index 35fbfaaaf589a..f4d392a9d53f6 100644 --- a/decidim-proposals/app/forms/decidim/proposals/admin/proposal_form.rb +++ b/decidim-proposals/app/forms/decidim/proposals/admin/proposal_form.rb @@ -27,12 +27,62 @@ def map_model(model) self.title = presenter.title(all_locales: title.is_a?(Hash)) self.body = presenter.editor_body(all_locales: body.is_a?(Hash)) - self.documents = model.attachments + self.documents = model.attachments.ids + self.add_documents = model.attachments.map { |att| { id: att.id, title: att.title } } + end + + def documents=(value) + case value + when String + super(parse_string_documents(value)) + when Integer + super([value]) + else + super + end + end + + def documents + result = super + + if should_use_add_documents?(result) + extract_ids_from_add_documents + else + result.is_a?(Array) ? result : [] + end end def notify_missing_attachment_if_errored errors.add(:add_documents, :needs_to_be_reattached) if errors.any? && add_documents.present? end + + private + + def should_use_add_documents?(result) + (result.blank? || result.is_a?(String)) && add_documents.present? + end + + def extract_ids_from_add_documents + add_documents + .select { |doc| doc.is_a?(Hash) && (doc[:id].present? || doc["id"].present?) } + .map { |doc| (doc[:id] || doc["id"]).to_i } + end + + def parse_string_documents(value) + return [] if value.blank? + + parse_document_ids(value) + end + + def parse_document_ids(value) + ids = begin + Array(JSON.parse(value)) + rescue JSON::ParserError + value.split(",").map(&:strip) + end + + ids.map(&:to_i).reject(&:zero?) + end end end end diff --git a/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb b/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb index f986b89f3f690..53d1bafe9a590 100644 --- a/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb +++ b/decidim-proposals/spec/system/admin/admin_edits_proposal_spec.rb @@ -162,6 +162,33 @@ expect(page).to have_no_content("city.jpeg") end + + it "can edit a proposal with an attachment" do + visit_component_admin + within "tr[data-id='#{proposal.id}']" do + find("button[data-controller='dropdown']").click + click_on "Edit proposal" + end + + expect(page).to have_content("Update proposal") + expect(page).to have_field("proposal_title_en") + expect(page.html).to include(document.file.blob.filename.to_s) + + fill_in_i18n :proposal_title, "#proposal-title-tabs", en: "Updated proposal title with attachments" + click_on "Update" + + expect(page).to have_content("Proposal successfully updated.") + + visit_component_admin + within "tr[data-id='#{proposal.id}']" do + find("button[data-controller='dropdown']").click + click_on "Edit proposal" + end + + expect(page).to have_field("proposal_title_en", with: "Updated proposal title with attachments") + click_on "Edit attachments" + expect(page).to have_content(document.file.blob.filename.to_s) + end end end From 742edc7a0247cd686d3e0fc243153066ba72b553 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 12 Mar 2026 07:37:20 +0100 Subject: [PATCH 102/135] Add erblint offense for admin title format (#16146) --- .erb_lint.yml | 8 ++ .erb_linters/admin_page_title_linter.rb | 3 + .../decidim/admin/admin_terms/show.html.erb | 4 +- .../decidim/admin/area_types/index.html.erb | 5 +- .../views/decidim/admin/areas/index.html.erb | 5 +- .../attachment_collections/edit.html.erb | 4 +- .../attachment_collections/index.html.erb | 4 +- .../admin/attachment_collections/new.html.erb | 4 +- .../decidim/admin/attachments/index.html.erb | 5 +- .../authorization_workflows/index.html.erb | 4 +- .../decidim/admin/block_user/new.html.erb | 1 + .../decidim/admin/components/index.html.erb | 4 +- .../admin/components/manage_trash.html.erb | 4 +- .../decidim/admin/conflicts/edit.html.erb | 4 +- .../decidim/admin/conflicts/index.html.erb | 5 +- .../decidim/admin/dashboard/show.html.erb | 6 +- .../devise/mailers/password_change.html.erb | 3 - .../reset_password_instructions.html.erb | 8 -- .../admin/help_sections/update.html.erb | 2 + .../admin/impersonatable_users/index.html.erb | 5 +- .../decidim/admin/impersonations/new.html.erb | 5 +- .../views/decidim/admin/imports/new.html.erb | 1 + .../decidim/admin/logs/_logs_list.html.erb | 2 +- .../views/decidim/admin/logs/index.html.erb | 2 +- .../impersonation_logs/index.html.erb | 5 +- .../managed_users/promotions/new.html.erb | 5 +- .../admin/newsletter_templates/show.html.erb | 5 +- .../decidim/admin/newsletters/show.html.erb | 5 +- .../admin/officializations/index.html.erb | 8 +- .../officializations/show_email.html.erb | 2 + .../edit.html.erb | 4 +- .../decidim/admin/reminders/new.html.erb | 9 +- .../decidim/admin/scope_types/index.html.erb | 5 +- .../views/decidim/admin/scopes/index.html.erb | 4 +- .../decidim/admin/share_tokens/index.html.erb | 2 + .../landing_page/_content_blocks.html.erb | 2 - .../admin/shared/landing_page/edit.html.erb | 2 + .../landing_page_content_blocks/edit.html.erb | 1 + .../admin/static_page_topics/index.html.erb | 4 +- .../decidim/admin/static_pages/edit.html.erb | 3 +- .../decidim/admin/static_pages/index.html.erb | 4 +- .../admin/statistics/_statistics.html.erb | 2 +- .../decidim/admin/statistics/index.html.erb | 2 + .../decidim/admin/taxonomies/index.html.erb | 4 +- .../admin/taxonomy_filters/index.html.erb | 4 +- .../taxonomy_filters_selector/index.html.erb | 2 + .../taxonomy_filters_selector/new.html.erb | 2 + .../taxonomy_filters_selector/show.html.erb | 2 + .../admin/taxonomy_items/edit.html.erb | 2 + .../decidim/admin/taxonomy_items/new.html.erb | 2 + .../views/decidim/admin/users/index.html.erb | 5 +- decidim-admin/config/locales/en.yml | 75 +++++++--- decidim-admin/lib/decidim/admin/menu.rb | 2 +- .../admin_dashboard_count_checks_spec.rb | 2 +- .../assemblies/admin/assemblies/edit.html.erb | 5 +- .../admin/assemblies/index.html.erb | 3 +- .../admin/assemblies/manage_trash.html.erb | 4 +- .../assemblies/admin/assemblies/new.html.erb | 4 +- .../admin/assembly_duplicates/new.html.erb | 4 +- .../admin/assembly_imports/new.html.erb | 4 +- .../admin/assembly_user_roles/edit.html.erb | 4 +- .../admin/assembly_user_roles/index.html.erb | 4 +- .../admin/assembly_user_roles/new.html.erb | 4 +- decidim-assemblies/config/locales/en.yml | 30 ++-- .../budgets/admin/projects/edit.html.erb | 5 +- .../budgets/admin/projects/new.html.erb | 5 +- decidim-budgets/config/locales/en.yml | 4 +- .../admin/conference_duplicates/new.html.erb | 4 +- .../admin/conference_invites/index.html.erb | 4 +- .../admin/conference_invites/new.html.erb | 4 +- .../conference_registrations/index.html.erb | 4 +- .../admin/conference_speakers/edit.html.erb | 5 +- .../admin/conference_speakers/index.html.erb | 4 +- .../admin/conference_speakers/new.html.erb | 5 +- .../admin/conference_user_roles/edit.html.erb | 5 +- .../conference_user_roles/index.html.erb | 5 +- .../admin/conference_user_roles/new.html.erb | 5 +- .../admin/conferences/edit.html.erb | 5 +- .../admin/conferences/index.html.erb | 3 +- .../admin/conferences/manage_trash.html.erb | 4 +- .../admin/conferences/new.html.erb | 5 +- .../conferences/admin/diplomas/edit.html.erb | 5 +- .../admin/media_links/edit.html.erb | 5 +- .../admin/media_links/index.html.erb | 5 +- .../admin/media_links/new.html.erb | 5 +- .../conferences/admin/partners/edit.html.erb | 5 +- .../conferences/admin/partners/index.html.erb | 1 + .../conferences/admin/partners/new.html.erb | 5 +- .../admin/registration_types/edit.html.erb | 5 +- .../admin/registration_types/index.html.erb | 1 + .../admin/registration_types/new.html.erb | 5 +- decidim-conferences/config/locales/en.yml | 67 +++++---- .../lib/decidim/conferences/menu.rb | 2 +- .../linters/admin_page_title_linter.rb | 46 ++++++ .../linters/admin_page_title_linter_spec.rb | 133 ++++++++++++++++++ .../elections/admin/census/edit.html.erb | 3 +- decidim-elections/config/locales/en.yml | 1 + .../forms/admin/questionnaires/edit.html.erb | 2 +- .../questionnaires/edit_questions.html.erb | 2 +- .../questionnaires/responses/index.html.erb | 4 +- .../questionnaires/responses/show.html.erb | 4 +- .../admin/initiatives/edit.html.erb | 5 +- .../admin/initiatives/index.html.erb | 5 +- .../admin/initiatives_settings/edit.html.erb | 4 +- .../admin/initiatives_types/edit.html.erb | 4 +- .../admin/initiatives_types/index.html.erb | 4 +- decidim-initiatives/config/locales/en.yml | 12 +- .../lib/decidim/initiatives/menu.rb | 2 +- .../meetings/admin/invites/index.html.erb | 5 +- .../admin/meeting_copies/new.html.erb | 5 +- .../decidim/meetings/admin/poll/edit.html.erb | 5 +- .../registration_form/edit_questions.html.erb | 2 +- .../admin/registrations/edit.html.erb | 5 +- .../registrations_attendees/index.html.erb | 5 +- decidim-meetings/config/locales/en.yml | 13 +- .../admin_manages_meetings_polls_spec.rb | 2 +- .../participatory_process_steps_controller.rb | 4 - .../new.html.erb | 5 +- .../edit.html.erb | 5 +- .../index.html.erb | 3 +- .../participatory_process_groups/new.html.erb | 5 +- .../new.html.erb | 4 +- .../participatory_process_steps/edit.html.erb | 6 +- .../index.html.erb | 5 +- .../participatory_process_steps/new.html.erb | 7 +- .../participatory_process_steps/show.html.erb | 17 --- .../edit.html.erb | 6 +- .../index.html.erb | 4 +- .../new.html.erb | 4 +- .../participatory_processes/edit.html.erb | 5 +- .../participatory_processes/index.html.erb | 3 +- .../manage_trash.html.erb | 4 +- .../participatory_processes/new.html.erb | 5 +- .../config/locales/en.yml | 52 ++++--- .../participatory_processes/admin_engine.rb | 2 +- .../decidim/participatory_processes/menu.rb | 4 +- .../proposals/admin/proposals/show.html.erb | 2 + decidim-proposals/config/locales/en.yml | 1 + .../admin/publish_responses/index.html.erb | 2 +- .../surveys/admin/responses/show.html.erb | 4 +- .../csv_census/admin/census/index.html.erb | 4 +- .../admin/census/instructions.html.erb | 7 +- .../admin/confirmations/new.html.erb | 4 +- .../admin/offline_confirmations/new.html.erb | 4 +- decidim-verifications/config/locales/en.yml | 12 +- 145 files changed, 658 insertions(+), 332 deletions(-) create mode 100644 .erb_linters/admin_page_title_linter.rb delete mode 100644 decidim-admin/app/views/decidim/admin/devise/mailers/password_change.html.erb delete mode 100644 decidim-admin/app/views/decidim/admin/devise/mailers/reset_password_instructions.html.erb create mode 100644 decidim-dev/lib/erb_lint/linters/admin_page_title_linter.rb create mode 100644 decidim-dev/spec/erb_lint/linters/admin_page_title_linter_spec.rb delete mode 100644 decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_process_steps/show.html.erb diff --git a/.erb_lint.yml b/.erb_lint.yml index 52ddcfd0aa525..00d73da81c5f8 100644 --- a/.erb_lint.yml +++ b/.erb_lint.yml @@ -24,6 +24,14 @@ linters: - text/template - application/ld+json + AdminPageTitleLinter: + enabled: true + exclude: + - decidim-admin/app/views/decidim/admin/shared/landing_page_content_blocks/edit.html.erb + - decidim-admin/app/views/decidim/admin/shared/landing_page/edit.html.erb + - decidim-admin/app/views/decidim/admin/imports/new.html.erb + - decidim-meetings/app/views/decidim/meetings/admin/poll/edit.html.erb + DeprecatedClasses: enabled: true exclude: diff --git a/.erb_linters/admin_page_title_linter.rb b/.erb_linters/admin_page_title_linter.rb new file mode 100644 index 0000000000000..7a201cfe2ff3c --- /dev/null +++ b/.erb_linters/admin_page_title_linter.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "../decidim-dev/lib/erb_lint/linters/admin_page_title_linter" diff --git a/decidim-admin/app/views/decidim/admin/admin_terms/show.html.erb b/decidim-admin/app/views/decidim/admin/admin_terms/show.html.erb index f6183f5ce914f..dd41c982b3f7b 100644 --- a/decidim-admin/app/views/decidim/admin/admin_terms/show.html.erb +++ b/decidim-admin/app/views/decidim/admin/admin_terms/show.html.erb @@ -1,8 +1,10 @@ +<% add_decidim_page_title(t(".title")) %> + <%= cell("decidim/announcement", announcement_body, callout_class: current_user.admin_terms_accepted? ? "success" : "warning" ) %>

- <%= t("title", scope: "decidim.admin.admin_terms_of_service") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/area_types/index.html.erb b/decidim-admin/app/views/decidim/admin/area_types/index.html.erb index 0d96f35a2becd..ccdbe9c1f6c2e 100644 --- a/decidim-admin/app/views/decidim/admin/area_types/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/area_types/index.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t("decidim.admin.titles.area_types")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t "decidim.admin.titles.area_types" %> + <%= t(".title") %> <% if allowed_to? :create, :area_type %> <%= link_to t("actions.add", scope: "decidim.admin"), [:new, :area_type], class: "button button__sm button__secondary new" %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/areas/index.html.erb b/decidim-admin/app/views/decidim/admin/areas/index.html.erb index 2d16f0c61a4a9..8f9882e2dbab9 100644 --- a/decidim-admin/app/views/decidim/admin/areas/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/areas/index.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t("areas", scope: "decidim.admin.titles")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t "decidim.admin.titles.areas" %> + <%= t ".title" %> <% if allowed_to? :create, :area %> <%= link_to t("actions.add", scope: "decidim.admin"), new_area_path, class: "button button__sm button__secondary new" %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/attachment_collections/edit.html.erb b/decidim-admin/app/views/decidim/admin/attachment_collections/edit.html.erb index 8a74bf61f6eba..32ec6003e1918 100644 --- a/decidim-admin/app/views/decidim/admin/attachment_collections/edit.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachment_collections/edit.html.erb @@ -1,8 +1,8 @@ -<% add_decidim_page_title(t("attachment_collections.edit.title", scope: "decidim.admin")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("attachment_collections.edit.title", scope: "decidim.admin") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/attachment_collections/index.html.erb b/decidim-admin/app/views/decidim/admin/attachment_collections/index.html.erb index 8a759b079324e..d8e70bea211e0 100644 --- a/decidim-admin/app/views/decidim/admin/attachment_collections/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachment_collections/index.html.erb @@ -1,9 +1,9 @@ -<% add_decidim_page_title(t("attachment_collections.index.attachment_collections_title", scope: "decidim.admin")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("attachment_collections.index.attachment_collections_title", scope: "decidim.admin") %> + <%= t(".title") %> <% if allowed_to? :create, :attachment_collection %> <%= link_to t("actions.attachment_collection.new", scope: "decidim.admin"), url_for(action: :new), class: "button button__sm button__secondary new" %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/attachment_collections/new.html.erb b/decidim-admin/app/views/decidim/admin/attachment_collections/new.html.erb index 67d609ef67b4c..1fb655c6e7b96 100644 --- a/decidim-admin/app/views/decidim/admin/attachment_collections/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachment_collections/new.html.erb @@ -1,7 +1,7 @@ -<% add_decidim_page_title(t("attachment_collections.new.title", scope: "decidim.admin")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("attachment_collections.new.title", scope: "decidim.admin") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/attachments/index.html.erb b/decidim-admin/app/views/decidim/admin/attachments/index.html.erb index 0852f9065557c..15273c22be758 100644 --- a/decidim-admin/app/views/decidim/admin/attachments/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachments/index.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t(".attachments_title")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t(".attachments_title") %> + <%= t(".title") %> <% if allowed_to? :create, :attachment %> <%= link_to t("actions.attachment.new", scope: "decidim.admin"), url_for(action: :new), class: "button button__sm button__secondary new" %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/authorization_workflows/index.html.erb b/decidim-admin/app/views/decidim/admin/authorization_workflows/index.html.erb index e1ec2474f0e12..934d7c2f40109 100644 --- a/decidim-admin/app/views/decidim/admin/authorization_workflows/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/authorization_workflows/index.html.erb @@ -1,8 +1,8 @@ -<% add_decidim_page_title(t("authorization_workflows", scope: "decidim.admin.titles")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("authorization_workflows", scope: "decidim.admin.titles") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/block_user/new.html.erb b/decidim-admin/app/views/decidim/admin/block_user/new.html.erb index 086ab32ea54a7..e83213f7b8852 100644 --- a/decidim-admin/app/views/decidim/admin/block_user/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/block_user/new.html.erb @@ -1,4 +1,5 @@ <% add_decidim_page_title(t(".title", name: user.name)) %> +

<%= t(".title", name: user.name) %> diff --git a/decidim-admin/app/views/decidim/admin/components/index.html.erb b/decidim-admin/app/views/decidim/admin/components/index.html.erb index 8786950970980..4b41cdf758314 100644 --- a/decidim-admin/app/views/decidim/admin/components/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/components/index.html.erb @@ -1,9 +1,9 @@ -<% add_decidim_page_title(t("components.title", scope: "decidim.admin")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("components.title", scope: "decidim.admin") %> + <%= t(".title") %> <% if allowed_to?(:create, :component) %>
diff --git a/decidim-admin/app/views/decidim/admin/components/manage_trash.html.erb b/decidim-admin/app/views/decidim/admin/components/manage_trash.html.erb index 9f57bccc47c3e..0b46cc872ac06 100644 --- a/decidim-admin/app/views/decidim/admin/components/manage_trash.html.erb +++ b/decidim-admin/app/views/decidim/admin/components/manage_trash.html.erb @@ -1,9 +1,9 @@ -<% add_decidim_page_title(t("components.manage_trash.title", scope: "decidim.admin")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("components.manage_trash.title", scope: "decidim.admin") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/conflicts/edit.html.erb b/decidim-admin/app/views/decidim/admin/conflicts/edit.html.erb index c66b11589e51f..a1c415ffdacb2 100644 --- a/decidim-admin/app/views/decidim/admin/conflicts/edit.html.erb +++ b/decidim-admin/app/views/decidim/admin/conflicts/edit.html.erb @@ -1,6 +1,8 @@ +<% add_decidim_page_title(t(".title")) %> +
-
Transfer User
+
<%= t(".title") %>
diff --git a/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb b/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb index 16117766de80e..392d6b569586b 100644 --- a/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb @@ -1,7 +1,8 @@ -<% add_decidim_page_title(t("title", scope: "decidim.admin.conflicts")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t("title", scope: "decidim.admin.conflicts") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/dashboard/show.html.erb b/decidim-admin/app/views/decidim/admin/dashboard/show.html.erb index 858a3445a11e8..b3a7ec5cc863f 100644 --- a/decidim-admin/app/views/decidim/admin/dashboard/show.html.erb +++ b/decidim-admin/app/views/decidim/admin/dashboard/show.html.erb @@ -1,8 +1,8 @@ -<% add_decidim_page_title(t("decidim.admin.titles.dashboard")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t "decidim.admin.titles.dashboard" %> <%= current_organization_name %> + <%= t(".title") %> <%= current_organization_name %>

@@ -46,6 +46,6 @@ <% end %> <% if current_user.admin_terms_accepted? %> - <%= link_to( t("title", scope: "decidim.admin.admin_terms_of_service"), admin_terms_show_path, class: "button button__text-secondary") %> + <%= link_to( t("title", scope: "decidim.admin.admin_terms.show"), admin_terms_show_path, class: "button button__text-secondary") %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/devise/mailers/password_change.html.erb b/decidim-admin/app/views/decidim/admin/devise/mailers/password_change.html.erb deleted file mode 100644 index 77839a7a15cdf..0000000000000 --- a/decidim-admin/app/views/decidim/admin/devise/mailers/password_change.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Hello <%= @resource.email %>!

- -

We are contacting you to notify you that your password has been changed.

diff --git a/decidim-admin/app/views/decidim/admin/devise/mailers/reset_password_instructions.html.erb b/decidim-admin/app/views/decidim/admin/devise/mailers/reset_password_instructions.html.erb deleted file mode 100644 index 708edf07be4be..0000000000000 --- a/decidim-admin/app/views/decidim/admin/devise/mailers/reset_password_instructions.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -

Hello <%= @resource.email %>!

- -

Someone has requested a link to change your password. You can do this through the link below.

- -

<%= link_to "Change my password", edit_password_url(@resource, reset_password_token: @token) %>

- -

If you did not request this, please ignore this email.

-

Your password will not change until you access the link above and create a new one.

diff --git a/decidim-admin/app/views/decidim/admin/help_sections/update.html.erb b/decidim-admin/app/views/decidim/admin/help_sections/update.html.erb index 6e05cc045575a..31d11247ac7d7 100644 --- a/decidim-admin/app/views/decidim/admin/help_sections/update.html.erb +++ b/decidim-admin/app/views/decidim/admin/help_sections/update.html.erb @@ -1 +1,3 @@ +<% add_decidim_page_title(t(".title")) %> + <%= render partial: "form", object: @form %> diff --git a/decidim-admin/app/views/decidim/admin/impersonatable_users/index.html.erb b/decidim-admin/app/views/decidim/admin/impersonatable_users/index.html.erb index 2fab7a6d3c532..65649997871f4 100644 --- a/decidim-admin/app/views/decidim/admin/impersonatable_users/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/impersonatable_users/index.html.erb @@ -1,4 +1,5 @@ -<% add_decidim_page_title(t("impersonatable_users", scope: "decidim.admin.titles")) %> +<% add_decidim_page_title(t(".title")) %> + <% if current_organization.available_authorizations.empty? %>
<%= cell("decidim/announcement", t(".needs_authorization_warning"), callout_class: "warning" ) %> @@ -7,7 +8,7 @@

- <%= t "decidim.admin.titles.impersonatable_users" %> + <%= t ".title" %> <% if allowed_to? :impersonate, :impersonatable_user, user: new_managed_user %> <%= link_to t(".impersonate_new_managed_user"), new_impersonatable_user_impersonation_path(:new_managed_user), class: "button button__sm button__secondary #{"disabled" if current_organization.available_authorizations.empty?}" %> <% end %> diff --git a/decidim-admin/app/views/decidim/admin/impersonations/new.html.erb b/decidim-admin/app/views/decidim/admin/impersonations/new.html.erb index cce55b1bec168..ebd237dbf6062 100644 --- a/decidim-admin/app/views/decidim/admin/impersonations/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/impersonations/new.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t("impersonate_new_managed_user", scope: "decidim.admin.impersonations.new")) %> +<% add_decidim_page_title(t(".title")) %> +

<% if creating_managed_user? %> - <%= t(".impersonate_new_managed_user") %> + <%= t(".title") %> <% else %> <% if @form.user.managed? %> <%= t(".impersonate_existing_managed_user", name: @form.user.name) %> diff --git a/decidim-admin/app/views/decidim/admin/imports/new.html.erb b/decidim-admin/app/views/decidim/admin/imports/new.html.erb index bdf151be9c30f..14a53b52b2894 100644 --- a/decidim-admin/app/views/decidim/admin/imports/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/imports/new.html.erb @@ -1,4 +1,5 @@ <% add_decidim_page_title(import_manifest.message(:title, self)) %> +

<%= import_manifest.message(:title, self) %> diff --git a/decidim-admin/app/views/decidim/admin/logs/_logs_list.html.erb b/decidim-admin/app/views/decidim/admin/logs/_logs_list.html.erb index cbac48cb47b9e..d85d0543b147f 100644 --- a/decidim-admin/app/views/decidim/admin/logs/_logs_list.html.erb +++ b/decidim-admin/app/views/decidim/admin/logs/_logs_list.html.erb @@ -1,7 +1,7 @@

- <%= t "decidim.admin.titles.admin_log" %> + <%= t "decidim.admin.logs.index.title" %>

<%= render partial: "decidim/admin/logs/filters" if defined?(display_filters) && display_filters %> diff --git a/decidim-admin/app/views/decidim/admin/logs/index.html.erb b/decidim-admin/app/views/decidim/admin/logs/index.html.erb index b08e5fe4f8509..7af432ec8134f 100644 --- a/decidim-admin/app/views/decidim/admin/logs/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/logs/index.html.erb @@ -1,4 +1,4 @@ -<% add_decidim_page_title(t("admin_log", scope: "decidim.admin.titles")) %> +<% add_decidim_page_title(t(".title")) %> <%= render partial: "decidim/admin/logs/logs_list", locals: { logs:, empty_logs: no_logs_available?, display_filters: true } %> <%= decidim_paginate logs %> diff --git a/decidim-admin/app/views/decidim/admin/managed_users/impersonation_logs/index.html.erb b/decidim-admin/app/views/decidim/admin/managed_users/impersonation_logs/index.html.erb index c1dfb56faae16..28a5487da2c1f 100644 --- a/decidim-admin/app/views/decidim/admin/managed_users/impersonation_logs/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/managed_users/impersonation_logs/index.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t("decidim.admin.titles.impersonations")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t "decidim.admin.titles.impersonations" %> + <%= t ".title" %>

diff --git a/decidim-admin/app/views/decidim/admin/managed_users/promotions/new.html.erb b/decidim-admin/app/views/decidim/admin/managed_users/promotions/new.html.erb index c890a31d0ec2a..8df79d3c485e8 100644 --- a/decidim-admin/app/views/decidim/admin/managed_users/promotions/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/managed_users/promotions/new.html.erb @@ -1,7 +1,8 @@ -<% add_decidim_page_title(t(".new_managed_user_promotion")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t(".new_managed_user_promotion") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/newsletter_templates/show.html.erb b/decidim-admin/app/views/decidim/admin/newsletter_templates/show.html.erb index 66599d17432ae..23891cac94b7e 100644 --- a/decidim-admin/app/views/decidim/admin/newsletter_templates/show.html.erb +++ b/decidim-admin/app/views/decidim/admin/newsletter_templates/show.html.erb @@ -1,7 +1,8 @@ -<% add_decidim_page_title(t(".preview", template_name: t(template_manifest.public_name_key))) %> +<% add_decidim_page_title(t(".title", template_name: t(template_manifest.public_name_key))) %> +

- <%= t ".preview", template_name: t(template_manifest.public_name_key) %> + <%= t ".title", template_name: t(template_manifest.public_name_key) %>

diff --git a/decidim-admin/app/views/decidim/admin/newsletters/show.html.erb b/decidim-admin/app/views/decidim/admin/newsletters/show.html.erb index 362cdd1ffd248..ff6c3600f26aa 100644 --- a/decidim-admin/app/views/decidim/admin/newsletters/show.html.erb +++ b/decidim-admin/app/views/decidim/admin/newsletters/show.html.erb @@ -1,7 +1,8 @@ -<% add_decidim_page_title(t(".preview")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t ".preview" %> + <%= t ".title" %>

<% if allowed_to?(:update, :newsletter, newsletter: @newsletter) %> diff --git a/decidim-admin/app/views/decidim/admin/officializations/index.html.erb b/decidim-admin/app/views/decidim/admin/officializations/index.html.erb index a190d5bed569a..c1f179dbe0460 100644 --- a/decidim-admin/app/views/decidim/admin/officializations/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/officializations/index.html.erb @@ -1,8 +1,10 @@ -<% add_decidim_page_title(t("decidim.admin.titles.participants")) %> +<% add_decidim_page_title(t(".title")) %> -
+
-

<%= t "decidim.admin.titles.participants" %>

+

+ <%= t ".title" %> +

<%= admin_filter_selector %>
diff --git a/decidim-admin/app/views/decidim/admin/officializations/show_email.html.erb b/decidim-admin/app/views/decidim/admin/officializations/show_email.html.erb index 680203b3487fb..3c306de596edb 100644 --- a/decidim-admin/app/views/decidim/admin/officializations/show_email.html.erb +++ b/decidim-admin/app/views/decidim/admin/officializations/show_email.html.erb @@ -1 +1,3 @@ +<% add_decidim_page_title(t(".title")) %> + <%= link_to user.email, "mailto:#{user.email}" %> diff --git a/decidim-admin/app/views/decidim/admin/organization_external_domain_allowlist/edit.html.erb b/decidim-admin/app/views/decidim/admin/organization_external_domain_allowlist/edit.html.erb index 2ea4feeda353e..dc82f699dd6a0 100644 --- a/decidim-admin/app/views/decidim/admin/organization_external_domain_allowlist/edit.html.erb +++ b/decidim-admin/app/views/decidim/admin/organization_external_domain_allowlist/edit.html.erb @@ -1,8 +1,8 @@ -<% add_decidim_page_title(t("edit_external_domains", scope: "decidim.admin.titles")) %> +<% add_decidim_page_title(t(".title")) %>

- <%= t("title", scope: "decidim.admin.organization_external_domain_allowlist.form") %> + <%= t(".title") %>

diff --git a/decidim-admin/app/views/decidim/admin/reminders/new.html.erb b/decidim-admin/app/views/decidim/admin/reminders/new.html.erb index c6cee2d5210a3..63ab9ef2ce1ee 100644 --- a/decidim-admin/app/views/decidim/admin/reminders/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/reminders/new.html.erb @@ -1,4 +1,11 @@ -
+<% add_decidim_page_title(t(".title")) %> + +
+
+

+ <%= t ".title" %> +

+
<%= decidim_form_for(@form, html: { class: "form form-defaults new_order_reminder" }, url: component_reminders_path(name: reminder_manifest.name), class: "form grid-container") do |form| %>
diff --git a/decidim-admin/app/views/decidim/admin/scope_types/index.html.erb b/decidim-admin/app/views/decidim/admin/scope_types/index.html.erb index 4a789ab31623c..d34ec4631dddb 100644 --- a/decidim-admin/app/views/decidim/admin/scope_types/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/scope_types/index.html.erb @@ -1,8 +1,9 @@ -<% add_decidim_page_title(t("decidim.admin.titles.scope_types")) %> +<% add_decidim_page_title(t(".title")) %> +

- <%= t "decidim.admin.titles.scope_types" %> + <%= t ".title" %> <% if allowed_to? :create, :scope_type %> <%= link_to t("actions.add", scope: "decidim.admin"), [:new, :scope_type], class: "button button__sm button__secondary new" %> diff --git a/decidim-admin/app/views/decidim/admin/scopes/index.html.erb b/decidim-admin/app/views/decidim/admin/scopes/index.html.erb index 7730666b48870..6e3f35dd52160 100644 --- a/decidim-admin/app/views/decidim/admin/scopes/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/scopes/index.html.erb @@ -1,11 +1,11 @@ -<% add_decidim_page_title(t("decidim.admin.titles.scopes")) %> +<% add_decidim_page_title(t(".title")) %>

<% if parent_scope %> <%= scope_breadcrumbs(parent_scope).join(" - ").html_safe %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_scope_path(parent_scope), class: "button button__sm button__secondary" if allowed_to? :create, :scope %><%= link_to t("actions.edit", scope: "decidim.admin"), edit_scope_path(parent_scope), class: "button button__sm button__secondary" if allowed_to? :edit, :scope, scope: parent_scope %> <% else %> - <%= t "decidim.admin.titles.scopes" %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_path, class: "button button__sm button__secondary" if allowed_to? :create, :scope %> + <%= t ".title" %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_path, class: "button button__sm button__secondary" if allowed_to? :create, :scope %> <% end %>

diff --git a/decidim-admin/app/views/decidim/admin/share_tokens/index.html.erb b/decidim-admin/app/views/decidim/admin/share_tokens/index.html.erb index e0b5e3b720961..4e498e12c866f 100644 --- a/decidim-admin/app/views/decidim/admin/share_tokens/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/share_tokens/index.html.erb @@ -1,3 +1,5 @@ +<% add_decidim_page_title(t(".title", name: resource_title)) %> +

<%= render partial: "proposals-thead" %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_accept_request_access_form.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_accept_request_access_form.html.erb deleted file mode 100644 index 3335c4eadfead..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_accept_request_access_form.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% if allowed_to?(:react_to_request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> - <%= decidim_form_for(@accept_request_form, url: request_accept_collaborative_draft_path(@collaborative_draft)) do |form| %> - <%= form.hidden_field :id, value: @collaborative_draft.id %> - <%= form.hidden_field :state, value: @collaborative_draft.state %> - <%= form.hidden_field :requester_user_id, value: requester.id %> - <%= form.submit accept_request_button_label, class: "button button__sm button__secondary w-full", data: { disable: true } %> - <% end %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_actions.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_actions.html.erb deleted file mode 100644 index cc80324fd309b..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_actions.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - -<% if allowed_to?(:edit, :collaborative_draft, collaborative_draft: @collaborative_draft) %> - -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_draft_aside.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_draft_aside.html.erb deleted file mode 100644 index 349feab7b7286..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_draft_aside.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -
- <% if @collaborative_draft.published? %> - <%= cell "decidim/proposals/collaborative_draft_link_to_proposal", @collaborative_draft %> - <% else %> -
- <%= resource_version_number(@collaborative_draft.versions_count, "h4") %> - <%= resource_version_of(@collaborative_draft.versions_count) %> - <%= link_to_other_resource_versions(collaborative_draft_version_path(@collaborative_draft, @collaborative_draft.versions.count)) %> -
- <% end %> -
- -<% if allowed_to?(:publish, :collaborative_draft, collaborative_draft: @collaborative_draft) || @collaborative_draft.requesters.include?(current_user) || allowed_to?(:request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> -
- <% if allowed_to?(:publish, :collaborative_draft, collaborative_draft: @collaborative_draft) %> -
- <%= cell "decidim/proposals/irreversible_action_modal", @collaborative_draft, action: :publish %> - -
- <%= t("publish_info", scope:"decidim.proposals.collaborative_drafts.show") %> - <%= cell "decidim/proposals/irreversible_action_modal", @collaborative_draft, action: :withdraw %> -
-
- <% end %> - - <%= render "request_access_form" %> - - <% if @collaborative_draft.requesters.include? current_user %> - - <% end %> -
-<% end %> - -
- <%= render partial: "collaborator_requests" %> -
diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_drafts.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_drafts.html.erb deleted file mode 100644 index d8c150d6c6cec..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborative_drafts.html.erb +++ /dev/null @@ -1,15 +0,0 @@ -<% if @collaborative_drafts.empty? %> - <%= cell("decidim/announcement", params[:filter].present? ? t("empty_filters", scope: "decidim.proposals.collaborative_drafts") : t("empty", scope: "decidim.proposals.collaborative_drafts")) %> -<% else %> -

<%= t("count", scope: "decidim.proposals.collaborative_drafts.index", count: @collaborative_drafts.length) %>

- - <%= order_selector available_orders, i18n_scope: "decidim.proposals.collaborative_drafts.orders" %> - -
- <% @collaborative_drafts.each do |draft| %> - <%= card_for draft %> - <% end %> -
- - <%= decidim_paginate @collaborative_drafts %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborator_requests.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborator_requests.html.erb deleted file mode 100644 index fb001f312f988..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_collaborator_requests.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% if @collaborative_draft.requesters.presence && allowed_to?(:react_to_request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> -
-

<%= t("title", scope: "decidim.proposals.collaborative_drafts.requests.collaboration_requests") %>

- - <% @collaborative_draft.requesters.each do |requester| %> -
- <%= cell "decidim/author", present(requester) %> - -
- <%= render partial: "accept_request_access_form", locals: { requester: } %> - <%= render partial: "reject_request_access_form", locals: { requester: } %> -
-
- <% end %> -
-<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb deleted file mode 100644 index ecb6049f3bbd6..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_edit_form_fields.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%= form_required_explanation %> - -<%= form.text_field :title, value: form_presenter.title, data: { controller: "character-counter" } %> -<%= text_editor_for_proposal_body(form) %> - -<% if @form.geocoding_enabled? %> - <%= form.geocoding_field :address %> -<% end %> - -<% if @form.taxonomy_filters&.any? %> - <% @form.taxonomy_filters.each do |filter| %> - <%= filter_taxonomy_items_select_field form, :taxonomies, filter %> - <% end %> -<% end %> - -<% if component_settings.attachments_allowed? %> - <%= form.attachment :documents, - multiple: false, - label: t("decidim.proposals.collaborative_drafts.new.add_file"), - button_label: t("decidim.proposals.collaborative_drafts.new.add_file"), - button_edit_label: t("decidim.proposals.collaborative_drafts.new.edit_file"), - button_class: "button button__lg button__transparent-secondary w-full", - help_text: t("attachment_legend", scope: "decidim.proposals.collaborative_drafts.edit"), - help_i18n_scope: "decidim.forms.file_help.file", - paragraph: true %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_reject_request_access_form.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_reject_request_access_form.html.erb deleted file mode 100644 index 230b80e55f585..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_reject_request_access_form.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% if allowed_to?(:react_to_request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> - <%= decidim_form_for(@reject_request_form, url: request_reject_collaborative_draft_path(@collaborative_draft)) do |form| %> - <%= form.hidden_field :id, value: @collaborative_draft.id %> - <%= form.hidden_field :state, value: @collaborative_draft.state %> - <%= form.hidden_field :requester_user_id, value: requester.id %> - <%= form.submit reject_request_button_label, class: "button button__sm button__transparent-secondary w-full", data: { disable: true } %> - <% end %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_request_access_form.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_request_access_form.html.erb deleted file mode 100644 index 05ca1ba1596e9..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/_request_access_form.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% if allowed_to?(:request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> - <%= decidim_form_for(@request_access_form, url: request_access_collaborative_draft_path(@collaborative_draft)) do |form| %> - <%= form.hidden_field :id, value: @collaborative_draft.id %> - <%= form.hidden_field :state, value: @collaborative_draft.state %> - <%= form.submit t(:request_access, scope: "decidim.proposals.collaborative_drafts.show"), class: "button button__lg button__secondary w-full", data: { disable: true } %> - <% end %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/edit.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/edit.html.erb deleted file mode 100644 index 12456fdf8c796..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/edit.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<% add_decidim_page_title(proposal_wizard_step_title(action_name)) %> - -<%= append_stylesheet_pack_tag "decidim_proposals", media: "all" %> -<%= append_javascript_pack_tag "decidim_proposals" %> - -<%= render layout: "layouts/decidim/shared/layout_center" do %> -
-

- <%= t("title", scope: "decidim.proposals.collaborative_drafts.edit") %> -

-
- - <% if translated_attribute(component_settings.new_proposal_help_text).present? %> - <%= cell("decidim/announcement", component_settings.new_proposal_help_text) %> - <% end %> - - <%= decidim_form_for(@form) do |form| %> -
- <%= render partial: "edit_form_fields", locals: { form: } %> -
- -
- <%= link_to :back, class: "button button__sm md:button__lg button__text-secondary" do %> - <%= icon "arrow-left-line" %> - <%= t("back", scope: "decidim.proposals.collaborative_drafts.edit") %> - <% end %> - - <%= form.submit t("send", scope: "decidim.proposals.collaborative_drafts.edit"), class: "button button__sm md:button__lg button__secondary", data: { disable: true } %> -
- <% end %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.html.erb deleted file mode 100644 index 8d6d8aba02770..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<% add_decidim_meta_tags( - description: translated_attribute(current_participatory_space.short_description), - title: component_name, - url: proposals_url, - resource: current_component) %> - -<%= append_stylesheet_pack_tag "decidim_proposals", media: "all" %> -<%= append_javascript_pack_tag "decidim_proposals" %> - -<% content_for :aside do %> - -

<%= component_name %>

- - <% if current_settings.creation_enabled %> - <%= action_authorized_link_to :create, new_collaborative_draft_path, permissions_holder: current_component, class: "button button__sm button__transparent-secondary", data: { "redirect_url" => new_collaborative_draft_path } do %> - <%= t("new_collaborative_draft", scope: "decidim.proposals.collaborative_drafts.new_collaborative_draft_button") %> - <%= icon "add-line" %> - <% end %> - <% end %> - - <%= render layout: "decidim/shared/filters", locals: { filter_sections: collaborative_drafts_filter_sections, search_variable: :search_text_cont, skip_to_id: "collaborative_drafts" } do %> - <%= hidden_field_tag :order, order, id: nil, class: "order_filter" %> - <% end %> - -<% end %> - -<%= render layout: "layouts/decidim/shared/layout_two_col" do %> - - <%= render partial: "decidim/shared/component_announcement" %> - -
- <%= render partial: "collaborative_drafts" %> -
- -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.js.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.js.erb deleted file mode 100644 index 0b1fb43b0a96b..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/index.js.erb +++ /dev/null @@ -1,5 +0,0 @@ -var $collaborative_drafts = $('#collaborative_drafts'); -var $orderFilterInput = $('.order_filter'); - -$collaborative_drafts.html('<%= j(render partial: "collaborative_drafts").strip.html_safe %>'); -$orderFilterInput.val('<%= order %>'); diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/new.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/new.html.erb deleted file mode 100644 index 53c610d77af9d..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/new.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<% add_decidim_page_title(t("decidim.proposals.collaborative_drafts.wizard_header.title")) %> - -<%= append_stylesheet_pack_tag "decidim_proposals", media: "all" %> -<%= append_javascript_pack_tag "decidim_proposals" %> - -<%= render layout: "layouts/decidim/shared/layout_center" do %> -
-

- <%= t("title", scope: "decidim.proposals.collaborative_drafts.wizard_header") %> -

-
- - <% if translated_attribute(component_settings.new_proposal_help_text).present? %> - <%= cell("decidim/announcement", component_settings.new_proposal_help_text) %> - <% end %> - - <%= decidim_form_for(@form) do |form| %> -
- <%= render partial: "edit_form_fields", locals: { form: } %> -
- -
- <%= link_to collaborative_drafts_path, class: "button button__sm md:button__lg button__text-secondary" do %> - <%= icon "arrow-left-line" %> - <%= t("back_from_collaborative_draft", scope: "decidim.proposals.collaborative_drafts.wizard_aside").html_safe %> - <% end %> - <%= form.submit t("send", scope: "decidim.proposals.collaborative_drafts.new"), class: "button button__sm md:button__lg button__secondary", data: { disable: true } %> -
- <% end %> -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/show.html.erb b/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/show.html.erb deleted file mode 100644 index 5a20c5bc9d4a2..0000000000000 --- a/decidim-proposals/app/views/decidim/proposals/collaborative_drafts/show.html.erb +++ /dev/null @@ -1,74 +0,0 @@ -<% add_decidim_page_title(t("name", scope: "decidim.proposals.collaborative_drafts")) %> -<% add_decidim_meta_tags( - description: present(@collaborative_draft).body, - title: present(@collaborative_draft).title, - url: collaborative_draft_url(@collaborative_draft.id), - resource: @collaborative_draft) %> - -<%= append_stylesheet_pack_tag "decidim_proposals", media: "all" %> -<%= append_javascript_pack_tag "decidim_proposals" %> - -<% content_for :aside do %> - <%= render partial: "collaborative_draft_aside" %> -<% end %> - -<%= render layout: "layouts/decidim/shared/layout_item", locals: { back_path: collaborative_drafts_path } do %> - -
- <%= cell("decidim/announcement", t("info-message", scope: "decidim.proposals.collaborative_drafts.show").html_safe) if current_user.nil? || allowed_to?(:request_access, :collaborative_draft, collaborative_draft: @collaborative_draft) %> - -

<%= present(@collaborative_draft).title(html_escape: true) %>

- - <% if component_settings.geocoding_enabled? %> -
- <%= render partial: "decidim/shared/static_map", locals: { icon_name: "proposals", geolocalizable: @collaborative_draft } %> -
- <% end %> - - <% if @collaborative_draft.state %> -
- - <%= humanize_collaborative_draft_state(@collaborative_draft.state) %> - -
- <% end %> - -
"> -
-
- <%= cell "decidim/coauthorships", @collaborative_draft, context_actions: [] %> -
- <%= render "decidim/shared/resource_actions", resource: @collaborative_draft do %> - <%= render "decidim/proposals/collaborative_drafts/collaborative_actions" %> - <% end %> -
-
-
- -
-
- <%= decidim_sanitize_editor present(@collaborative_draft).body(links: true) %> -
-
- - <%= attachments_for @collaborative_draft %> - - <%= cell "decidim/tags", @collaborative_draft %> - -
- <%= cell "decidim/comments_button", nil %> -
- <%= cell "decidim/share_widget", @collaborative_draft %> -
-
- - <% content_for :item_footer do %> - <%= comments_for @collaborative_draft %> - - - <% end %> - -<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/_proposal_aside.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/_proposal_aside.html.erb index 93bfc2b63b011..04a5ffd3b106c 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/_proposal_aside.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/_proposal_aside.html.erb @@ -14,6 +14,5 @@ <% end %>
- <%= cell "decidim/proposals/proposal_link_to_collaborative_draft", @proposal %> <%= cell "decidim/proposals/proposal_link_to_rejected_emendation", @proposal %>
diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb index aa0cce1621f7f..ff8eccc70113c 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/index.html.erb @@ -20,13 +20,6 @@ <%= icon "add-line" %> <% end %> <% end %> - - <% if component_settings.collaborative_drafts_enabled? %> - <%= link_to collaborative_drafts_path, class: "button button__sm button__transparent-secondary" do %> - <%= t("collaborative_drafts_list", scope: "decidim.proposals.proposals.index") %> - <%= icon "edit-2-line" %> - <% end %> - <% end %> <%= render layout: "decidim/shared/filters", locals: { filter_sections: , search_variable: :search_text_cont, skip_to_id: "proposals" } do %> diff --git a/decidim-proposals/app/views/decidim/proposals/versions/show.html.erb b/decidim-proposals/app/views/decidim/proposals/versions/show.html.erb index 26ebbbb3b8a20..16d38f3a2c9d9 100644 --- a/decidim-proposals/app/views/decidim/proposals/versions/show.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/versions/show.html.erb @@ -1,10 +1,5 @@ <% add_decidim_page_title(t("changes_at_title", scope: "decidim.version.show", title: versioned_resource.title)) %> <% add_decidim_page_title(t("decidim.proposals.versions.index.title")) %> -<% -if versioned_resource.is_a?(Decidim::Proposals::CollaborativeDraft) - add_decidim_page_title(Decidim::Proposals::CollaborativeDraft.model_name.human(count: 2)) -end -%> <% content_for :aside do %> <%= cell( diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index 93a925b3fabb1..5dc98243f4b0f 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -499,7 +499,6 @@ en: deleted_proposals_info: Deleted proposals can be restored from the trash. preview: Preview view_deleted_proposals: View deleted proposals - deprecation_warning_collaborative_drafts_html: "⚠️ Deprecation Notice. The Collaborative Drafts feature will be removed in Decidim v0.32. Organizations using it can switch to the new proposal co-authorship feature." evaluation_assignments: create: invalid: There was a problem assigning proposals to a evaluator. @@ -744,103 +743,6 @@ en: all: All amendments: Amendments proposals: Proposals - collaborative_drafts: - collaborative_draft: - publish: - error: There was a problem publishing the collaborative draft. - irreversible_action_modal: - body: After publishing the draft as a proposal, it will not be editable anymore. The proposal will not accept new authors or contributions. - cancel: Cancel - ok: Publish as a Proposal - title: The following action is irreversible - success: Collaborative draft published successfully as a proposal. - withdraw: - error: There was a problem closing the collaborative draft. - irreversible_action_modal: - body: After closing the draft, it will not be editable anymore. The draft will not accept new authors or contributions. - cancel: Cancel - ok: Withdraw the collaborative draft - title: The following action is irreversible - success: Collaborative draft withdrawn successfully. - create: - error: There was a problem creating this collaborative draft. - success: Collaborative draft successfully created. - edit: - attachment_legend: Add a document or an image - back: Back - send: Send - title: Edit collaborative draft - empty: There are no collaborative drafts yet - empty_filters: There is no collaborative draft with this criteria - filters: - all: All - amendment: Amendments - open: Open - published: Published - related_to: Related to - search: Search - state: Status - withdrawn: Withdrawn - filters_small_view: - close_modal: Close modal - filter: Filter - filter_by: Filter by - unfold: Unfold - index: - count: - one: "%{count} collaborative draft" - other: "%{count} collaborative drafts" - name: Collaborative drafts - new: - add_file: Add file - edit_file: Edit file - send: Continue - new_collaborative_draft_button: - new_collaborative_draft: New collaborative draft - orders: - label: 'Order drafts by:' - most_contributed: Most contributed - random: Random - recent: Recent - requests: - accepted_request: - error: Could not be accepted as a collaborator, please try again later. - success: "@%{user} has been accepted as a collaborator successfully." - access_requested: - error: Your request could not be completed, please try again later. - success: Your request to collaborate has been successfully sent. - collaboration_requests: - accept_request: Accept - reject_request: Reject - title: Collaboration requests - rejected_request: - error: Could not be rejected as a collaborator, please try again later. - success: "@%{user} has been successfully rejected as a collaborator." - show: - edit: Edit - final_proposal: Final proposal - final_proposal_help_text: This draft is finished. Check out the final proposal - hidden_authors_count: - one: and %{count} more person - other: and %{count} more people - info-message: This is a collaborative draft for a proposal. This means that you can help their authors to shape the proposal using the comment section below or improve it directly by requesting access to edit it. Once the authors grant you access, you will be able to make changes to this draft. - publish: Publish - publish_info: Publish this version of the draft or - published_proposal: Published proposal - request_access: Request access - requested_access: Access requested - withdraw: withdraw the draft - states: - open: Open - published: Published - withdrawn: Withdrawn - update: - error: There was a problem saving the collaborative draft. - success: Collaborative draft successfully updated. - wizard_aside: - back_from_collaborative_draft: Back to collaborative drafts - wizard_header: - title: Create your collaborative draft content_blocks: highlighted_proposals: name: Proposals @@ -942,7 +844,6 @@ en: voted: Voted index: click_here: See all proposals - collaborative_drafts_list: Access collaborative drafts count: one: "%{count} proposal" other: "%{count} proposals" @@ -989,8 +890,6 @@ en: hidden_likes_count: one: and %{count} more person other: and %{count} more people - link_to_collaborative_draft_help_text: This proposal is the result of a collaborative draft. Review the history - link_to_collaborative_draft_text: See the collaborative draft link_to_promoted_emendation_help_text: This proposal is a promoted emendation link_to_promoted_emendation_text: See the rejected emendation. link_to_proposal_from_emendation_help_text: This is a rejected emendation diff --git a/decidim-proposals/db/data/20260224210316_remove_collaborative_drafts_references.rb b/decidim-proposals/db/data/20260224210316_remove_collaborative_drafts_references.rb new file mode 100644 index 0000000000000..f3c4a620eeca1 --- /dev/null +++ b/decidim-proposals/db/data/20260224210316_remove_collaborative_drafts_references.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +class RemoveCollaborativeDraftsReferences < ActiveRecord::Migration[7.2] + class CollaborativeDraft < ApplicationRecord + self.table_name = "decidim_proposals_collaborative_drafts" + end + + class CollaborativeDraftCollaboratorRequest < ApplicationRecord + self.table_name = "decidim_proposals_collaborative_draft_collaborator_requests" + end + + class Notification < ApplicationRecord + self.table_name = "decidim_notifications" + end + + class Follow < ApplicationRecord + self.table_name = "decidim_follows" + end + + class Moderation < ApplicationRecord + self.table_name = "decidim_moderations" + end + + class Coauthorship < ApplicationRecord + self.table_name = "decidim_coauthorships" + end + + class ActionLog < ApplicationRecord + self.table_name = "decidim_action_logs" + end + + class Comment < ApplicationRecord + self.table_name = "decidim_comments_comments" + end + + class ResourceLink < ApplicationRecord + self.table_name = "decidim_resource_links" + end + + class Categorization < ApplicationRecord + self.table_name = "decidim_categorizations" + end + + class Attachment < ApplicationRecord + self.table_name = "decidim_attachments" + end + + class Version < ApplicationRecord + self.table_name = "versions" + end + + COLLABORATIVE_DRAFT_TYPE = "Decidim::Proposals::CollaborativeDraft" + COLLABORATIVE_DRAFT_COLLABORATOR_REQUEST_TYPE = "Decidim::Proposals::CollaborativeDraftCollaboratorRequest" + + def up + delete_notifications + delete_follows + delete_reports + delete_coauthorships + delete_action_logs + delete_comments + delete_resource_links + delete_categorizations + delete_attachments + delete_paper_trail_versions_for_collaborative_drafts + delete_paper_trail_versions_for_collaborator_requests + end + + def down + raise ActiveRecord::IrreversibleMigration + end + + private + + def delete_notifications + Notification.where(decidim_resource_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_follows + Follow.where(decidim_followable_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_reports + Moderation.where(decidim_reportable_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_coauthorships + Coauthorship.where(coauthorable_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_action_logs + ActionLog.where(resource_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_comments + Comment.where(decidim_commentable_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_resource_links + ResourceLink.where(from_type: COLLABORATIVE_DRAFT_TYPE).delete_all + ResourceLink.where(to_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_categorizations + Categorization.where(categorizable_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_attachments + Attachment.where(attached_to_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_paper_trail_versions_for_collaborative_drafts + Version.where(item_type: COLLABORATIVE_DRAFT_TYPE).delete_all + end + + def delete_paper_trail_versions_for_collaborator_requests + Version.where(item_type: COLLABORATIVE_DRAFT_COLLABORATOR_REQUEST_TYPE).delete_all + end +end diff --git a/decidim-proposals/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb b/decidim-proposals/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb index 5f9159ac28fa7..ad68bb8c55711 100644 --- a/decidim-proposals/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb +++ b/decidim-proposals/db/migrate/20200827154156_add_commentable_counter_cache_to_proposals.rb @@ -1,12 +1,20 @@ # frozen_string_literal: true class AddCommentableCounterCacheToProposals < ActiveRecord::Migration[5.2] + class Proposal < ApplicationRecord + self.table_name = "decidim_proposals_proposals" + end + + class CollaborativeDraft < ApplicationRecord + self.table_name = "decidim_proposals_collaborative_drafts" + end + def change add_column :decidim_proposals_proposals, :comments_count, :integer, null: false, default: 0, index: true add_column :decidim_proposals_collaborative_drafts, :comments_count, :integer, null: false, default: 0, index: true - Decidim::Proposals::Proposal.reset_column_information - Decidim::Proposals::Proposal.unscoped.find_each(&:update_comments_count) - Decidim::Proposals::CollaborativeDraft.unscoped.reset_column_information - Decidim::Proposals::CollaborativeDraft.unscoped.find_each(&:update_comments_count) + Proposal.reset_column_information + Proposal.unscoped.find_each(&:update_comments_count) + CollaborativeDraft.unscoped.reset_column_information + CollaborativeDraft.unscoped.find_each(&:update_comments_count) end end diff --git a/decidim-proposals/db/migrate/20210310120812_add_followable_counter_cache_to_collaborative_drafts.rb b/decidim-proposals/db/migrate/20210310120812_add_followable_counter_cache_to_collaborative_drafts.rb index e025e2b806382..3ac3bb2817967 100644 --- a/decidim-proposals/db/migrate/20210310120812_add_followable_counter_cache_to_collaborative_drafts.rb +++ b/decidim-proposals/db/migrate/20210310120812_add_followable_counter_cache_to_collaborative_drafts.rb @@ -1,13 +1,17 @@ # frozen_string_literal: true class AddFollowableCounterCacheToCollaborativeDrafts < ActiveRecord::Migration[5.2] + class CollaborativeDraft < ApplicationRecord + self.table_name = "decidim_proposals_collaborative_drafts" + end + def change add_column :decidim_proposals_collaborative_drafts, :follows_count, :integer, null: false, default: 0, index: true reversible do |dir| dir.up do - Decidim::Proposals::CollaborativeDraft.reset_column_information - Decidim::Proposals::CollaborativeDraft.find_each do |record| + CollaborativeDraft.reset_column_information + CollaborativeDraft.find_each do |record| record.class.reset_counters(record.id, :follows) end end diff --git a/decidim-proposals/db/migrate/20250515132352_drop_collaborative_drafts_tables.rb b/decidim-proposals/db/migrate/20250515132352_drop_collaborative_drafts_tables.rb new file mode 100644 index 0000000000000..afb94a2a087cf --- /dev/null +++ b/decidim-proposals/db/migrate/20250515132352_drop_collaborative_drafts_tables.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class DropCollaborativeDraftsTables < ActiveRecord::Migration[7.2] + def up + drop_table :decidim_proposals_collaborative_drafts, if_exists: true + drop_table :decidim_proposals_collaborative_draft_collaborator_requests, if_exists: true + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/decidim-proposals/lib/decidim/proposals.rb b/decidim-proposals/lib/decidim/proposals.rb index b0a8d41e8506d..92fcd7af32f3c 100644 --- a/decidim-proposals/lib/decidim/proposals.rb +++ b/decidim-proposals/lib/decidim/proposals.rb @@ -17,7 +17,6 @@ module Proposals autoload :ProposalSerializer, "decidim/proposals/proposal_serializer" autoload :DownloadYourDataProposalSerializer, "decidim/proposals/download_your_data_proposal_serializer" autoload :CommentableProposal, "decidim/proposals/commentable_proposal" - autoload :CommentableCollaborativeDraft, "decidim/proposals/commentable_collaborative_draft" autoload :MarkdownToProposals, "decidim/proposals/markdown_to_proposals" autoload :ParticipatoryTextSection, "decidim/proposals/participatory_text_section" autoload :DocToMarkdown, "decidim/proposals/doc_to_markdown" diff --git a/decidim-proposals/lib/decidim/proposals/commentable_collaborative_draft.rb b/decidim-proposals/lib/decidim/proposals/commentable_collaborative_draft.rb deleted file mode 100644 index c9b846f04761e..0000000000000 --- a/decidim-proposals/lib/decidim/proposals/commentable_collaborative_draft.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Proposals - # The data store for a Proposal in the Decidim::Proposals component. - module CommentableCollaborativeDraft - extend ActiveSupport::Concern - include Decidim::Comments::Commentable - - included do - # Public: Overrides the `commentable?` Commentable concern method. - def commentable? - component.settings.comments_enabled? - end - - # Public: Overrides the `accepts_new_comments?` Commentable concern method. - def accepts_new_comments? - commentable? && !component.current_settings.comments_blocked - end - - # Public: Overrides the `comments_have_alignment?` Commentable concern method. - def comments_have_alignment? - true - end - - # Public: Overrides the `comments_have_votes?` Commentable concern method. - def comments_have_votes? - true - end - - # Public: Override Commentable concern method `users_to_notify_on_comment_created` - def users_to_notify_on_comment_created - followers - end - end - end - end -end diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index 1331709bbd082..2e93c914a6f6c 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -68,7 +68,6 @@ settings.attribute :geocoding_enabled, type: :boolean, default: false settings.attribute :attachments_allowed, type: :boolean, default: false settings.attribute :resources_permissions_enabled, type: :boolean, default: true - settings.attribute :collaborative_drafts_enabled, type: :boolean, default: false settings.attribute :participatory_texts_enabled, type: :boolean, default: false, readonly: ->(context) { Decidim::Proposals::Proposal.where(component: context[:component]).any? } @@ -114,12 +113,6 @@ resource.searchable = true end - component.register_resource(:collaborative_draft) do |resource| - resource.model_class_name = "Decidim::Proposals::CollaborativeDraft" - resource.card = "decidim/proposals/collaborative_draft" - resource.reported_content_cell = "decidim/proposals/collaborative_drafts/reported_content" - end - component.register_stat :proposals_count, primary: true, admin: false, diff --git a/decidim-proposals/lib/decidim/proposals/engine.rb b/decidim-proposals/lib/decidim/proposals/engine.rb index bab41848b2548..0dff6b36ea4ab 100644 --- a/decidim-proposals/lib/decidim/proposals/engine.rb +++ b/decidim-proposals/lib/decidim/proposals/engine.rb @@ -28,16 +28,6 @@ class Engine < ::Rails::Engine end end end - resources :collaborative_drafts, except: [:destroy] do - member do - post :request_access, controller: "collaborative_draft_collaborator_requests" - post :request_accept, controller: "collaborative_draft_collaborator_requests" - post :request_reject, controller: "collaborative_draft_collaborator_requests" - post :withdraw - post :publish - end - resources :versions, only: [:show] - end scope "/proposals" do root to: "proposals#index" end @@ -45,16 +35,12 @@ class Engine < ::Rails::Engine end initializer "decidim_proposals.register_icons" do - Decidim.icons.register(name: "Decidim::Proposals::CollaborativeDraft", icon: "draft-line", category: "activity", - description: "Collaborative draft", engine: :proposals) Decidim.icons.register(name: "Decidim::Proposals::Proposal", icon: "chat-new-line", category: "activity", description: "Proposal", engine: :proposals) Decidim.icons.register(name: "participatory_texts_item", icon: "bookmark-line", description: "Index item", category: "participatory_texts", engine: :proposals) Decidim.icons.register(name: "scan-line", icon: "scan-line", category: "system", description: "", engine: :proposals) - Decidim.icons.register(name: "edit-2-line", icon: "edit-2-line", - category: "action", description: "Edit icon for Collaborative Drafts", engine: :proposals) Decidim.icons.register(name: "bookmark-line", icon: "bookmark-line", category: "system", description: "", engine: :proposals) Decidim.icons.register(name: "arrow-right-s-fill", icon: "arrow-right-s-fill", category: "system", description: "", engine: :proposals) diff --git a/decidim-proposals/lib/decidim/proposals/seeds.rb b/decidim-proposals/lib/decidim/proposals/seeds.rb index bb176f950587b..4d9330b9bcc38 100644 --- a/decidim-proposals/lib/decidim/proposals/seeds.rb +++ b/decidim-proposals/lib/decidim/proposals/seeds.rb @@ -36,12 +36,8 @@ def call end Decidim::Comments::Seed.comments_for(proposal) - - create_collaborative_draft!(component:) end - update_traceability!(component:) - create_report!(reportable: Decidim::Proposals::Proposal.take, current_user: Decidim::User.take) hide_report!(reportable: Decidim::Proposals::Proposal.take) end @@ -74,7 +70,6 @@ def create_component! can_accumulate_votes_beyond_threshold: [true, false].sample, attachments_allowed: [true, false].sample, amendments_enabled: participatory_space.id.odd?, - collaborative_drafts_enabled: true, geocoding_enabled: [true, false].sample }, step_settings: @@ -231,54 +226,6 @@ def create_proposal_notes!(proposal:) body: ::Faker::Lorem.paragraphs(number: 2).join("\n") ) end - - def create_collaborative_draft!(component:) - n = rand(5) - state = if n > 3 - "published" - elsif n > 2 - "withdrawn" - else - "open" - end - author = Decidim::User.where(organization:).all.sample - - draft = Decidim.traceability.perform_action!("create", Decidim::Proposals::CollaborativeDraft, author) do - draft = Decidim::Proposals::CollaborativeDraft.new( - component:, - title: ::Faker::Lorem.sentence(word_count: 2), - body: ::Faker::Lorem.paragraphs(number: 2).join("\n"), - state:, - published_at: Time.current - ) - draft.coauthorships.build(author: participatory_space.organization) - draft.save! - draft - end - - case n - when 2 - authors = Decidim::User.where(organization:).all.sample(5) - authors.each do |local_author| - Decidim::Coauthorship.create(coauthorable: draft, author: local_author) - end - when 3 - author2 = Decidim::User.where(organization:).all.sample - Decidim::Coauthorship.create(coauthorable: draft, author: author2) - end - - Decidim::Comments::Seed.comments_for(draft) - end - - def update_traceability!(component:) - Decidim.traceability.update!( - Decidim::Proposals::CollaborativeDraft.all.sample, - Decidim::User.where(organization:).all.sample, - component:, - title: ::Faker::Lorem.sentence(word_count: 2), - body: ::Faker::Lorem.paragraphs(number: 2).join("\n") - ) - end end end end diff --git a/decidim-proposals/lib/decidim/proposals/test/factories.rb b/decidim-proposals/lib/decidim/proposals/test/factories.rb index 99ecf54ff8aee..e9938f126ba6d 100644 --- a/decidim-proposals/lib/decidim/proposals/test/factories.rb +++ b/decidim-proposals/lib/decidim/proposals/test/factories.rb @@ -171,23 +171,6 @@ def generate_state_title(token, skip_injection: false) end end - trait :with_collaborative_drafts_enabled do - settings do - { - collaborative_drafts_enabled: true - } - end - end - - trait :with_attachments_allowed_and_collaborative_drafts_enabled do - settings do - { - attachments_allowed: true, - collaborative_drafts_enabled: true - } - end - end - trait :with_minimum_votes_per_user do transient do minimum_votes_per_user { 3 } @@ -481,49 +464,6 @@ def generate_state_title(token, skip_injection: false) author { build(:user, organization: proposal.organization, skip_injection:) } end - factory :collaborative_draft, class: "Decidim::Proposals::CollaborativeDraft" do - transient do - skip_injection { false } - users { nil } - end - - title { generate_localized_title(:collaborative_draft_title, skip_injection:)["en"] } - body { generate_localized_description(:collaborative_draft_body, skip_injection:)["en"] } - component { create(:proposal_component, skip_injection:) } - address { "#{Faker::Address.street_name}, #{Faker::Address.city}" } - state { "open" } - - after(:build) do |collaborative_draft, evaluator| - if collaborative_draft.component - users = evaluator.users || [create(:user, organization: collaborative_draft.component.participatory_space.organization, skip_injection: evaluator.skip_injection)] - users.each do |user| - collaborative_draft.coauthorships.build(author: user) - end - end - end - - trait :participant_author do - after :build do |draft, evaluator| - draft.coauthorships.target.clear - user = build(:user, organization: draft.component.participatory_space.organization, skip_injection: evaluator.skip_injection) - draft.coauthorships.build(author: user) - end - end - - trait :published do - state { "published" } - published_at { Time.current } - end - - trait :open do - state { "open" } - end - - trait :withdrawn do - state { "withdrawn" } - end - end - factory :participatory_text, class: "Decidim::Proposals::ParticipatoryText" do transient do skip_injection { false } diff --git a/decidim-proposals/spec/cells/decidim/proposals/collaborative_draft_cell_spec.rb b/decidim-proposals/spec/cells/decidim/proposals/collaborative_draft_cell_spec.rb deleted file mode 100644 index e8eaf79eccb69..0000000000000 --- a/decidim-proposals/spec/cells/decidim/proposals/collaborative_draft_cell_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Proposals::CollaborativeDraftCell, type: :cell do - controller Decidim::Proposals::CollaborativeDraftsController - - subject { my_cell.call(:show) } - - let(:my_cell) { cell("decidim/proposals/collaborative_draft", collaborative_draft, context:) } - - let(:component) { create(:proposal_component, :with_collaborative_drafts_enabled) } - let(:author) { create(:user, :confirmed, organization: component.organization) } - let!(:collaborative_draft) { create(:collaborative_draft, component:) } - let(:authors) { create_list(:user, 5, organization: component.organization) } - let(:collaborative_draft_va) { create(:collaborative_draft, component:, users: authors) } - let(:context) { nil } - - before do - allow(controller).to receive(:current_user).and_return(author) - end - - context "when rendering a collaborative_draft" do - it "renders the card" do - expect(subject).to have_css('[id^="proposals__collaborative_draft"]') - end - - it "renders the collaborative_draft title and link" do - expect(subject).to have_content(collaborative_draft.title) - href = Decidim::ResourceLocatorPresenter.new(collaborative_draft).path - expect(subject).to have_link(href:) - end - - it "renders the card author" do - expect(subject).to have_content(collaborative_draft.authors.first.name) - expect(subject).to have_css("[data-author]", count: 1) - end - - describe "with coauthors" do - let(:collaborative_draft) { create(:collaborative_draft, component:, users: authors) } - - it "renders the first three authors" do - expect(subject).to have_css("[data-author]", count: 3) - end - - it "indicates number of remaining authors" do - expect(subject.find("[data-remaining-authors]")).to have_content("+2") - end - end - - context "with open state" do - let(:collaborative_draft) { create(:collaborative_draft, :open, component:) } - - it "renders the card with the .success class" do - expect(subject).to have_css(".success") - end - - it "renders the open state" do - expect(subject).to have_content("Open") - end - end - - context "with withdrawn state" do - let(:collaborative_draft) { create(:collaborative_draft, :withdrawn, component:) } - - it "renders the card with the .alert class" do - expect(subject).to have_css(".alert") - end - - it "renders the open state" do - expect(subject).to have_content("Withdrawn") - end - end - - context "with published state" do - let(:collaborative_draft) { create(:collaborative_draft, :published, component:) } - - it "renders the card with the .success class" do - expect(subject).to have_css(".success") - end - - it "renders the open state" do - within ".label" do - expect(subject).to have_content("Published") - end - end - end - end -end diff --git a/decidim-proposals/spec/cells/decidim/proposals/collaborative_drafts/reported_content_cell_spec.rb b/decidim-proposals/spec/cells/decidim/proposals/collaborative_drafts/reported_content_cell_spec.rb deleted file mode 100644 index d31628386aa05..0000000000000 --- a/decidim-proposals/spec/cells/decidim/proposals/collaborative_drafts/reported_content_cell_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim::Proposals::CollaborativeDrafts - describe ReportedContentCell, type: :cell do - controller Decidim::Proposals::CollaborativeDraftsController - - let!(:collaborative_draft) { create(:collaborative_draft, body: { "en" => "a nice body" }) } - - context "when rendering" do - it "renders the collaborative draft's body" do - html = cell("decidim/reported_content", collaborative_draft).call - expect(html).to have_content("a nice body") - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/accept_access_to_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/accept_access_to_collaborative_draft_spec.rb deleted file mode 100644 index 88322b37455a5..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/accept_access_to_collaborative_draft_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe AcceptAccessToCollaborativeDraft do - let(:component) { create(:proposal_component) } - let(:state) { :open } - let(:collaborative_draft) { create(:collaborative_draft, state, component:, users: [author1, author2]) } - let(:id) { collaborative_draft.id } - let(:requester_user) { create(:user, :confirmed, organization: component.organization) } - let(:requester_user_id) { requester_user.id } - let(:author1) { create(:user, :confirmed, organization: component.organization) } - let(:author2) { create(:user, :confirmed, organization: component.organization) } - let(:current_user) { author1 } - let(:current_organization) { component.organization } - let(:form) { AcceptAccessToCollaborativeDraftForm.from_params(form_params).with_context(current_user:, current_organization:) } - let(:form_params) do - { - state:, - id:, - requester_user_id: - } - end - - describe "Author (current_user) accepts access to requester to collaborate" do - let(:command) { described_class.new(form, current_user) } - - before do - collaborative_draft.collaborator_requests.create!(user: requester_user) - end - - context "when the collaborative draft is open" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it "removes the requester from requestors of the collaborative draft" do - expect do - command.call - end.to change(collaborative_draft.requesters, :count).by(-1) - end - - it "adds the requester as a co-author of the collaborative draft" do - command.call - updated_draft = CollaborativeDraft.find(collaborative_draft.id) - expect(updated_draft.authors).to include(requester_user) - end - - it "notifies the requester and authors of the collaborative draft that access to requester has been accepted" do - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event: "decidim.events.proposals.collaborative_draft_access_accepted", - event_class: Decidim::Proposals::CollaborativeDraftAccessAcceptedEvent, - resource: collaborative_draft, - affected_users: collaborative_draft.notifiable_identities - [requester_user], - extra: { - requester_id: requester_user_id - } - ) - - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event: "decidim.events.proposals.collaborative_draft_access_requester_accepted", - event_class: Decidim::Proposals::CollaborativeDraftAccessRequesterAcceptedEvent, - resource: collaborative_draft, - affected_users: [requester_user] - ) - - command.call - end - end - - context "when the collaborative draft is withdrawn" do - let(:state) { :withdrawn } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not accept the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - - it "does not add the requester as a co-author of the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.authors, :count) - end - end - - context "when the collaborative draft is published" do - let(:state) { :published } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not accept the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - - it "does not add the requester as a co-author of the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.authors, :count) - end - end - - context "when the requester is missing" do - let(:requester_user_id) { nil } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not accept the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the current_user is missing" do - let(:current_user) { nil } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not accept the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the requester is not as a requestor" do - let(:not_requester_user) { create(:user, :confirmed, organization: component.organization) } - let(:requester_user_id) { not_requester_user.id } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not accept the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/create_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/create_collaborative_draft_spec.rb deleted file mode 100644 index 2c674bcafd0cc..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/create_collaborative_draft_spec.rb +++ /dev/null @@ -1,161 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe CreateCollaborativeDraft do - let(:form_klass) { CollaborativeDraftForm } - let(:component) { create(:proposal_component, :with_collaborative_drafts_enabled) } - let(:organization) { component.organization } - let(:user) { create(:user, :confirmed, organization:) } - let(:form) do - form_klass.from_params( - form_params - ).with_context( - current_user: user, - current_organization: organization, - current_participatory_space: component.participatory_space, - current_component: component - ) - end - - let(:author) { create(:user, organization:) } - - let(:has_address) { false } - let(:address) { nil } - let(:latitude) { 40.1234 } - let(:longitude) { 2.1234 } - let(:attachment_params) { nil } - - describe "call" do - let(:form_params) do - { - title: "This is the collaborative draft title", - body: "This is the collaborative draft body", - address:, - has_address:, - latitude:, - longitude:, - add_documents: attachment_params - } - end - - let(:command) do - described_class.new(form, author) - end - - describe "when the form is not valid" do - before do - allow(form).to receive(:invalid?).and_return(true) - end - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not create a collaborative draft" do - expect do - command.call - end.not_to change(Decidim::Proposals::CollaborativeDraft, :count) - end - end - - describe "when the form is valid" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it_behaves_like "fires an ActiveSupport::Notification event", "decidim.proposals.create_collaborative_draft:before" - it_behaves_like "fires an ActiveSupport::Notification event", "decidim.proposals.create_collaborative_draft:after" - - it "creates a new collaborative draft" do - expect do - command.call - end.to change(Decidim::Proposals::CollaborativeDraft, :count).by(1) - end - - context "with an author" do - it "sets the author" do - command.call - collaborative_draft = Decidim::Proposals::CollaborativeDraft.last - - expect(collaborative_draft.coauthorships.count).to eq(1) - expect(collaborative_draft.authors.count).to eq(1) - expect(collaborative_draft.authors.first).to eq(author) - end - end - - it "traces the action", versioning: true do - expect(Decidim.traceability) - .to receive(:perform_action!) - .with( - :create, - Decidim::Proposals::CollaborativeDraft, - user, - visibility: "public-only" - ).and_call_original - - expect { command.call }.to change(Decidim::ActionLog, :count) - action_log = Decidim::ActionLog.last - expect(action_log.version).to be_present - end - - context "when the has address checkbox is checked" do - let(:has_address) { true } - - context "when the address is present" do - let(:address) { "Some address" } - - before do - Geocoder::Lookup::Test.add_stub( - address, - [{ "latitude" => latitude, "longitude" => longitude }] - ) - end - - it "sets the latitude and longitude" do - command.call - collaborative_draft = Decidim::Proposals::CollaborativeDraft.last - - expect(collaborative_draft.latitude).to eq(latitude) - expect(collaborative_draft.longitude).to eq(longitude) - end - end - end - - context "when attachments are allowed" do - let(:component) { create(:proposal_component, :with_attachments_allowed) } - let(:attachment_params) do - [ - { - title: "My attachment", - file: upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") - } - ] - end - - it "creates an attachment for the proposal" do - expect { command.call }.to change(Decidim::Attachment, :count).by(1) - last_collaborative_draft = Decidim::Proposals::CollaborativeDraft.last - last_attachment = Decidim::Attachment.last - expect(last_attachment.attached_to).to eq(last_collaborative_draft) - end - - context "when attachment is left blank" do - let(:attachment_params) do - { - title: "" - } - end - - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - end - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/publish_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/publish_collaborative_draft_spec.rb deleted file mode 100644 index 281fca3a200f6..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/publish_collaborative_draft_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe PublishCollaborativeDraft do - let(:component) { create(:proposal_component) } - let(:taxonomies) { [create(:taxonomy, :with_parent, organization: component.organization)] } - let(:state) { :open } - let!(:collaborative_draft) { create(:collaborative_draft, component:, state:, taxonomies:) } - let!(:attachment) { Decidim::Attachment.create(attachment_params) } - let(:attachment_params) do - { - title: "My attachment", - file: Decidim::Dev.test_file("city.jpeg", "image/jpeg"), - attached_to: collaborative_draft - } - end - let(:current_user) { collaborative_draft.creator_author } - let(:command) { described_class.new(collaborative_draft, current_user) } - - describe "call" do - context "when the user is not a coauthor" do - let(:current_user) { create(:user, organization: component.organization) } - - it "broadcasts invalid" do - expect(collaborative_draft.authored_by?(current_user)).to be(false) - expect { command.call }.to broadcast(:invalid) - end - end - - context "when the resource is withdrawn" do - let(:state) { :withdrawn } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - end - - context "when the resource is published" do - let(:state) { :published } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - end - - context "when everything is ok" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it "creates a new proposal" do - expect { command.call } - .to change(Decidim::Proposals::Proposal, :count) - .by(1) - end - - it "transfers the attributes correctly" do - command.call - proposal = Decidim::Proposals::Proposal.last - - expect(proposal.taxonomies).to eq(collaborative_draft.taxonomies) - expect(proposal.address).to eq(collaborative_draft.address) - expect(proposal.attachments).to eq(collaborative_draft.attachments) - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/reject_access_to_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/reject_access_to_collaborative_draft_spec.rb deleted file mode 100644 index 2ae82cb14861f..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/reject_access_to_collaborative_draft_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe RejectAccessToCollaborativeDraft do - let(:component) { create(:proposal_component) } - let(:state) { :open } - let(:collaborative_draft) { create(:collaborative_draft, state, component:, users: [author1, author2]) } - let(:id) { collaborative_draft.id } - let(:requester_user) { create(:user, :confirmed, organization: component.organization) } - let(:requester_user_id) { requester_user.id } - let(:author1) { create(:user, :confirmed, organization: component.organization) } - let(:author2) { create(:user, :confirmed, organization: component.organization) } - let(:current_user) { author1 } - let(:current_organization) { component.organization } - let(:form) { RejectAccessToCollaborativeDraftForm.from_params(form_params).with_context(current_user:, current_organization:) } - let(:form_params) do - { - state:, - id:, - requester_user_id: - } - end - - describe "Author (current_user) rejects access to requester to collaborate" do - let(:command) { described_class.new(form, current_user) } - - before do - collaborative_draft.collaborator_requests.create!(user: requester_user) - end - - context "when the collaborative draft is open" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it "removes the requester from requestors of the collaborative draft" do - expect do - command.call - end.to change(collaborative_draft.requesters, :count).by(-1) - end - - it "notifies the requester and authors of the collaborative draft that access to requester has been rejected" do - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event: "decidim.events.proposals.collaborative_draft_access_rejected", - event_class: Decidim::Proposals::CollaborativeDraftAccessRejectedEvent, - resource: collaborative_draft, - affected_users: collaborative_draft.authors, - extra: { - requester_id: requester_user_id - } - ) - - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event: "decidim.events.proposals.collaborative_draft_access_requester_rejected", - event_class: Decidim::Proposals::CollaborativeDraftAccessRequesterRejectedEvent, - resource: collaborative_draft, - affected_users: [requester_user] - ) - - command.call - end - end - - context "when the collaborative draft is withdrawn" do - let(:state) { :withdrawn } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not reject the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the collaborative draft is published" do - let(:state) { :published } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not reject the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the requester is missing" do - let(:requester_user_id) { nil } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not reject the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the current_user is missing" do - let(:current_user) { nil } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not reject the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the requester is not as a requestor" do - let(:not_requester) { create(:user, :confirmed, organization: component.organization) } - let(:requester_user_id) { not_requester.id } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not reject the request for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/request_access_to_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/request_access_to_collaborative_draft_spec.rb deleted file mode 100644 index 9e442af2fa588..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/request_access_to_collaborative_draft_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe RequestAccessToCollaborativeDraft do - let(:component) { create(:proposal_component) } - let(:state) { :open } - - let(:collaborative_draft) { create(:collaborative_draft, state, component:, users: [author1, author2]) } - let(:id) { collaborative_draft.id } - let(:form) { RequestAccessToCollaborativeDraftForm.from_params(form_params).with_context(current_user:) } - let(:form_params) do - { - state:, - id: - } - end - let(:current_user) { create(:user, :confirmed, organization: component.organization) } - let(:author1) { create(:user, :confirmed, organization: component.organization) } - let(:author2) { create(:user, :confirmed, organization: component.organization) } - - describe "User requests to collaborate" do - let(:command) { described_class.new(form, current_user) } - - context "when the collaborative draft is open" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it "creates a new request for the collaborative draft" do - expect do - command.call - end.to change(collaborative_draft.requesters, :count).by(1) - end - - it "notifies all authors of the collaborative_draft that access has been requested" do - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event: "decidim.events.proposals.collaborative_draft_access_requested", - event_class: Decidim::Proposals::CollaborativeDraftAccessRequestedEvent, - resource: collaborative_draft, - affected_users: collaborative_draft.authors, - extra: { - requester_id: current_user.id - } - ) - - command.call - end - end - - context "when the collaborative draft is withdrawn" do - let(:state) { :withdrawn } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not create a new requestor for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - - context "when the collaborative draft is published" do - let(:state) { :published } - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not create a new requestor for the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft.requesters, :count) - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/update_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/update_collaborative_draft_spec.rb deleted file mode 100644 index cd89f4976d601..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/update_collaborative_draft_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe UpdateCollaborativeDraft do - let(:form_klass) { CollaborativeDraftForm } - - let(:component) { create(:proposal_component) } - let(:organization) { component.organization } - let(:form) do - form_klass.from_params( - form_params - ).with_context( - current_organization: organization, - current_participatory_space: component.participatory_space, - current_component: component - ) - end - - let!(:collaborative_draft) { create(:collaborative_draft, component:, users: [author]) } - let(:author) { create(:user, organization:) } - - let(:has_address) { false } - let(:address) { nil } - let(:latitude) { 40.1234 } - let(:longitude) { 2.1234 } - - describe "call" do - let(:form_params) do - { - title: "This is the collaborative draft title", - body: "This is the collaborative draft body", - address:, - has_address: - } - end - - let(:command) do - described_class.new(form, author, collaborative_draft) - end - - describe "when the form is not valid" do - before do - allow(form).to receive(:invalid?).and_return(true) - end - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not update the proposal" do - expect do - command.call - end.not_to change(collaborative_draft, :title) - end - end - - describe "when the collaborative draft is not editable by the user" do - before do - allow(collaborative_draft).to receive(:editable_by?).and_return(false) - end - - it "broadcasts invalid" do - expect { command.call }.to broadcast(:invalid) - end - - it "does not update the collaborative draft" do - expect do - command.call - end.not_to change(collaborative_draft, :title) - end - end - - describe "when the form is valid" do - it "broadcasts ok" do - expect { command.call }.to broadcast(:ok) - end - - it_behaves_like "fires an ActiveSupport::Notification event", "decidim.proposals.update_collaborative_draft:before" - it_behaves_like "fires an ActiveSupport::Notification event", "decidim.proposals.update_collaborative_draft:after" - - it "updates the collaborative draft" do - expect do - command.call - end.to change(collaborative_draft, :title) - end - - it "creates a new version for the collaborative draft", versioning: true do - expect do - command.call - end.to change { collaborative_draft.versions.count }.by(1) - expect(collaborative_draft.versions.last.whodunnit).to eq author.to_gid.to_s - end - - context "with an author" do - it "sets the author" do - command.call - collaborative_draft = Decidim::Proposals::CollaborativeDraft.last - - expect(collaborative_draft.coauthorships.count).to eq(1) - expect(collaborative_draft.authors.count).to eq(1) - expect(collaborative_draft.authors.first).to eq(author) - end - end - - context "when geocoding is enabled" do - let(:component) { create(:proposal_component, :with_geocoding_enabled) } - - context "when the has address checkbox is checked" do - let(:has_address) { true } - - context "when the address is present" do - let(:address) { "Some address" } - - before do - stub_geocoding(address, [latitude, longitude]) - end - - it "sets the latitude and longitude" do - command.call - collaborative_draft = Decidim::Proposals::CollaborativeDraft.last - - expect(collaborative_draft.latitude).to eq(latitude) - expect(collaborative_draft.longitude).to eq(longitude) - end - end - end - end - end - end - end - end -end diff --git a/decidim-proposals/spec/commands/decidim/proposals/withdraw_collaborative_draft_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/withdraw_collaborative_draft_spec.rb deleted file mode 100644 index 9da9c9d56c3f6..0000000000000 --- a/decidim-proposals/spec/commands/decidim/proposals/withdraw_collaborative_draft_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe WithdrawCollaborativeDraft do - describe "call" do - let(:component) { create(:proposal_component) } - let(:organization) { component.organization } - let!(:current_user) { create(:user, organization:) } - let(:follower) { create(:user, organization:) } - let(:other_author) { create(:user, organization:) } - let(:state) { :open } - let(:collaborative_draft) { create(:collaborative_draft, component:, state:, users: [current_user, other_author]) } - let!(:follow) { create(:follow, followable: current_user, user: follower) } - let(:event) { "decidim.events.proposals.collaborative_draft_withdrawn" } - let(:event_class) { Decidim::Proposals::CollaborativeDraftWithdrawnEvent } - - it "broadcasts ok" do - expect { described_class.call(collaborative_draft, current_user) }.to broadcast(:ok) - end - - it "broadcasts invalid when the user is not a coauthor" do - expect { described_class.call(collaborative_draft, follower) }.to broadcast(:invalid) - end - - context "when the resource is withdrawn" do - let(:state) { :withdrawn } - - it "broadcasts invalid" do - expect { described_class.call(collaborative_draft, follower) }.to broadcast(:invalid) - end - end - - context "when the resource is published" do - let(:state) { :published } - - it "broadcasts invalid" do - expect { described_class.call(collaborative_draft, follower) }.to broadcast(:invalid) - end - end - - describe "events" do - subject do - described_class.new(collaborative_draft, current_user) - end - - it "notifies the collaborative draft is withdrawn to coauthors" do - affected_users = collaborative_draft.authors - [current_user] - expect(Decidim::EventsManager) - .to receive(:publish) - .with( - event:, - event_class:, - resource: collaborative_draft, - affected_users: affected_users.uniq, - extra: { - author_id: current_user.id - } - ).ordered - subject.call - end - end - end - end - end -end diff --git a/decidim-proposals/spec/controllers/decidim/proposals/collaborative_draft_collaborator_requests_controller_spec.rb b/decidim-proposals/spec/controllers/decidim/proposals/collaborative_draft_collaborator_requests_controller_spec.rb deleted file mode 100644 index f436b375d492a..0000000000000 --- a/decidim-proposals/spec/controllers/decidim/proposals/collaborative_draft_collaborator_requests_controller_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe CollaborativeDraftCollaboratorRequestsController do - let(:component) { create(:proposal_component, :with_creation_enabled, :with_collaborative_drafts_enabled) } - let(:params) { { component_id: component.id } } - let(:user) { create(:user, :confirmed, organization: component.organization) } - let(:author) { create(:user, :confirmed, organization: component.organization) } - let!(:collaborative_draft) { create(:collaborative_draft, component:, users: [author]) } - let(:user2) { create(:user, :confirmed, organization: component.organization) } - - before do - request.env["decidim.current_organization"] = component.organization - request.env["decidim.current_participatory_space"] = component.participatory_space - request.env["decidim.current_component"] = component - end - - describe "POST request_access" do - before do - sign_in user, scope: :user - end - - it "creates a new access request for the given collaborative_draft" do - expect { post :request_access, params: { id: collaborative_draft.id, state: collaborative_draft.state } }.to change { - collaborative_draft.reload - collaborative_draft.requesters.count - }.by(1) - - expect(response).to have_http_status(:found) - end - end - - describe "POST request_accept" do - before do - sign_in author, scope: :user - end - - it "accepts a request from another user to the given collaborative_draft" do - expect(collaborative_draft.requesters.count).to eq 0 - expect(collaborative_draft.coauthorships.count).to eq 1 - - expect(response).to have_http_status(:ok) - end - end - - describe "POST request_reject" do - before do - sign_in user2, scope: :user - post :request_access, params: { id: collaborative_draft.id, state: collaborative_draft.state } - sign_in author, scope: :user - end - - it "accepts a request from another user to the given collaborative_draft" do - expect(collaborative_draft.requesters.count).to eq 1 - - expect(response).to have_http_status(:found) - end - end - end - end -end diff --git a/decidim-proposals/spec/controllers/decidim/proposals/collaborative_drafts_controller_spec.rb b/decidim-proposals/spec/controllers/decidim/proposals/collaborative_drafts_controller_spec.rb deleted file mode 100644 index 7809a143257d8..0000000000000 --- a/decidim-proposals/spec/controllers/decidim/proposals/collaborative_drafts_controller_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe CollaborativeDraftsController do - let(:component) { create(:proposal_component, :with_creation_enabled, :with_collaborative_drafts_enabled) } - let(:params) { { component_id: component.id } } - let(:user) { create(:user, :confirmed, organization: component.organization) } - let(:author) { create(:user, :confirmed, organization: component.organization) } - let!(:collaborative_draft) { create(:collaborative_draft, component:, users: [author]) } - let(:user2) { create(:user, :confirmed, organization: component.organization) } - - before do - request.env["decidim.current_organization"] = component.organization - request.env["decidim.current_participatory_space"] = component.participatory_space - request.env["decidim.current_component"] = component - end - - describe "GET index" do - context "when invoked without parameters" do - it "returns a list of open collaborative drafts by updated_at" do - get :index - - expect(response).to have_http_status(:ok) - expect(assigns[:collaborative_drafts]).not_to be_empty - expect(subject).to render_template("decidim/proposals/collaborative_drafts/index") - end - end - end - - describe "GET show" do - it "returns details of a collaborative_draft" do - get :show, params: { id: collaborative_draft.id } - - expect(response).to have_http_status(:ok) - expect(assigns(:collaborative_draft)).to be_a(Decidim::Proposals::CollaborativeDraft) - expect(subject).to render_template("decidim/proposals/collaborative_drafts/show") - end - end - - context "when creating a collaborative draft (wizard)" do - before do - sign_in user, scope: :user - end - - let(:component) { create(:proposal_component, :with_creation_enabled, :with_collaborative_drafts_enabled) } - - describe "GET new" do - it "renders the empty form" do - get(:new, params:) - expect(response).to have_http_status(:ok) - expect(subject).to render_template(:new) - end - end - end - - describe "POST create" do - let(:params) do - { - component_id: component.id, - collaborative_draft: { - title: collaborative_draft.title, - body: collaborative_draft.body - } - } - end - - context "when creation is not enabled" do - let(:component) { create(:proposal_component, :with_collaborative_drafts_enabled) } - - it "redirects" do - post(:create, params:) - expect(response).to have_http_status(:found) - end - end - - context "when creation is enabled" do - it "creates a collaborative draft" do - post(:create, params:) - expect(response).to have_http_status(:found) - end - end - end - - describe "GET edit" do - before do - sign_in author, scope: :user - end - - it "renders the edit form" do - get :edit, params: { id: collaborative_draft.id } - expect(response).to have_http_status(:ok) - expect(assigns(:collaborative_draft)).to be_a(Decidim::Proposals::CollaborativeDraft) - expect(subject).to render_template(:edit) - end - end - - describe "POST update" do - let(:params) do - { - component_id: component.id, - id: collaborative_draft.id, - collaborative_draft: { - title: Decidim::Faker::Localized.sentence, - body: Decidim::Faker::Localized.sentence(word_count: 2) - } - } - end - - it "updates the collaborative draft" do - put(:update, params:) - expect(assigns(:collaborative_draft)).to be_a(Decidim::Proposals::CollaborativeDraft) - expect(response).to have_http_status(:found) - end - end - - context "with collaborative drafts disabled" do - let(:component) { create(:proposal_component) } - - describe "GET index" do - it "renders not found page" do - expect { get :index }.to raise_error(ActionController::RoutingError) - end - end - end - end - end -end diff --git a/decidim-proposals/spec/db/data/remove_collaborative_drafts_references_spec.rb b/decidim-proposals/spec/db/data/remove_collaborative_drafts_references_spec.rb new file mode 100644 index 0000000000000..7b2ca305bb531 --- /dev/null +++ b/decidim-proposals/spec/db/data/remove_collaborative_drafts_references_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "./db/data/20260224210316_remove_collaborative_drafts_references" + +describe RemoveCollaborativeDraftsReferences do + let(:migrator) do + described_class.new.tap do |m| + m.verbose = false + end + end + + let(:organization) { create(:organization) } + let(:user) { create(:user, organization:) } + let(:another_user) { create(:user, organization:) } + + let(:collaborative_draft_type) { "Decidim::Proposals::CollaborativeDraft" } + let(:collaborative_draft_collaborator_request_type) { "Decidim::Proposals::CollaborativeDraftCollaboratorRequest" } + let(:draft_id) { 999_999 } + let(:collaborator_request_id) { 888_888 } + + class Version < ApplicationRecord + self.table_name = "versions" + end + + describe "#up" do + context "with notifications" do + let!(:notification) do + notification = create(:notification, user:) + notification.update_column(:decidim_resource_type, collaborative_draft_type) # rubocop:disable Rails/SkipsModelValidations + notification.update_column(:decidim_resource_id, draft_id) # rubocop:disable Rails/SkipsModelValidations + notification + end + + let!(:other_notification) do + create(:notification, user:) + end + + it "deletes notifications referencing collaborative drafts" do + expect(Decidim::Notification.where(decidim_resource_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::Notification.where(decidim_resource_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other notifications intact" do + migrator.migrate(:up) + expect(Decidim::Notification.find_by(id: other_notification.id)).to be_present + end + end + + context "with follows" do + let!(:follow) do + follow = create(:follow, user:) + follow.update_column(:decidim_followable_type, collaborative_draft_type) # rubocop:disable Rails/SkipsModelValidations + follow.update_column(:decidim_followable_id, draft_id) # rubocop:disable Rails/SkipsModelValidations + follow + end + + let!(:other_follow) do + create(:follow, user: another_user) + end + + it "deletes follows referencing collaborative drafts" do + expect(Decidim::Follow.where(decidim_followable_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::Follow.where(decidim_followable_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other follows" do + migrator.migrate(:up) + expect(Decidim::Follow.find_by(id: other_follow.id)).to be_present + end + end + + context "with coauthorships" do + let!(:coauthorship) do + coauthorship = create(:coauthorship) + coauthorship.update_column(:coauthorable_type, collaborative_draft_type) # rubocop:disable Rails/SkipsModelValidations + coauthorship.update_column(:coauthorable_id, draft_id) # rubocop:disable Rails/SkipsModelValidations + coauthorship + end + + let!(:other_coauthorship) do + create(:coauthorship) + end + + it "deletes coauthorships referencing collaborative drafts" do + expect(Decidim::Coauthorship.where(coauthorable_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::Coauthorship.where(coauthorable_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other coauthorships" do + migrator.migrate(:up) + expect(Decidim::Coauthorship.find_by(id: other_coauthorship.id)).to be_present + end + end + + context "with action logs" do + let!(:action_log) do + table = "decidim_action_logs" + columns = { + decidim_organization_id: organization.id, + user_id: user.id, + user_type: "Decidim::User", + resource_type: collaborative_draft_type, + resource_id: draft_id, + action: "create", + visibility: "public-only", + created_at: Time.current, + updated_at: Time.current + } + ActiveRecord::Base.connection.execute( + "INSERT INTO #{table} (#{columns.keys.join(", ")}) VALUES (#{columns.values.map { |v| ActiveRecord::Base.connection.quote(v) }.join(", ")})" + ) + Decidim::ActionLog.last + end + + let!(:other_action_log) do + create(:action_log, user: another_user, organization:) + end + + it "deletes action logs referencing collaborative drafts" do + expect(Decidim::ActionLog.where(resource_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::ActionLog.where(resource_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other action logs" do + migrator.migrate(:up) + expect(Decidim::ActionLog.find_by(id: other_action_log.id)).to be_present + end + end + + context "with moderations (reports)" do + let!(:moderation) do + moderation = create(:moderation) + moderation.update_column(:decidim_reportable_type, collaborative_draft_type) # rubocop:disable Rails/SkipsModelValidations + moderation.update_column(:decidim_reportable_id, draft_id) # rubocop:disable Rails/SkipsModelValidations + moderation + end + + let!(:other_moderation) do + create(:moderation) + end + + it "deletes moderations referencing collaborative drafts" do + expect(Decidim::Moderation.where(decidim_reportable_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::Moderation.where(decidim_reportable_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other moderations" do + migrator.migrate(:up) + expect(Decidim::Moderation.find_by(id: other_moderation.id)).to be_present + end + end + + context "with comments" do + let!(:comment) do + comment = create(:comment) + comment.update_column(:decidim_commentable_type, collaborative_draft_type) # rubocop:disable Rails/SkipsModelValidations + comment.update_column(:decidim_commentable_id, draft_id) # rubocop:disable Rails/SkipsModelValidations + comment + end + + let!(:other_comment) do + create(:comment) + end + + it "deletes comments referencing collaborative drafts" do + expect(Decidim::Comments::Comment.where(decidim_commentable_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Decidim::Comments::Comment.where(decidim_commentable_type: collaborative_draft_type).count).to eq(0) + end + + it "keeps other comments" do + migrator.migrate(:up) + expect(Decidim::Comments::Comment.find_by(id: other_comment.id)).to be_present + end + end + + context "with paper trail versions" do + let!(:version_for_draft) do + Version.create!( + item_type: collaborative_draft_type, + item_id: draft_id, + event: "update", + whodunnit: user.id.to_s, + object: "{}", + object_changes: "{}", + created_at: Time.current + ) + end + + let!(:version_for_collaborator_request) do + Version.create!( + item_type: collaborative_draft_collaborator_request_type, + item_id: collaborator_request_id, + event: "update", + whodunnit: user.id.to_s, + object: "{}", + object_changes: "{}", + created_at: Time.current + ) + end + + let!(:other_version) do + Version.create!( + item_type: "Decidim::Proposal", + item_id: 123, + event: "update", + whodunnit: user.id.to_s, + object: "{}", + object_changes: "{}", + created_at: Time.current + ) + end + + it "deletes paper trail versions for collaborative drafts" do + expect(Version.where(item_type: collaborative_draft_type).count).to eq(1) + migrator.migrate(:up) + expect(Version.where(item_type: collaborative_draft_type).count).to eq(0) + end + + it "deletes paper trail versions for collaborator requests" do + expect(Version.where(item_type: collaborative_draft_collaborator_request_type).count).to eq(1) + migrator.migrate(:up) + expect(Version.where(item_type: collaborative_draft_collaborator_request_type).count).to eq(0) + end + + it "keeps other versions intact" do + migrator.migrate(:up) + expect(Version.find_by(id: other_version.id)).to be_present + end + end + + context "when tables are empty" do + it "does not raise an error" do + expect { migrator.migrate(:up) }.not_to raise_error + end + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_accepted_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_accepted_event_spec.rb deleted file mode 100644 index a830af6ebc858..0000000000000 --- a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_accepted_event_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Proposals::CollaborativeDraftAccessAcceptedEvent do - include_context "when a simple event" - - let(:event_name) { "decidim.events.proposals.collaborative_draft_access_accepted" } - let(:resource) { create(:collaborative_draft, title: "It is my collaborative draft") } - let(:resource_path) { Decidim::ResourceLocatorPresenter.new(resource).path } - let(:resource_title) { resource.title } - let(:author) { resource.authors.first } - let(:author_id) { author.id } - let(:author_presenter) { Decidim::UserPresenter.new(author) } - let(:author_path) { author_presenter.profile_path } - let(:author_name) { author_presenter.name } - let(:author_nickname) { author_presenter.nickname } - let(:requester) { create(:user, :confirmed, organization: resource.organization) } - let(:requester_presenter) { Decidim::UserPresenter.new(requester) } - let(:requester_id) { requester.id } - let(:requester_name) { requester.name } - let(:requester_nickname) { requester_presenter.nickname } - let(:requester_path) { requester_presenter.profile_path } - let(:extra) { { requester_id: } } - - context "when the notification is for coauthor users" do - let(:email_subject) { "#{requester_name} has been accepted to access as a contributor of the #{resource_title}." } - let(:email_intro) { %(#{requester_name} has been accepted to access as a contributor of the
#{decidim_html_escape(resource_title)} collaborative draft.) } - let(:email_outro) { %(You have received this notification because you are a collaborator of #{decidim_html_escape(resource_title)}.) } - let(:notification_title) { %(#{requester_name} #{requester_nickname} has been accepted to access as a contributor of the #{decidim_html_escape(resource_title)} collaborative draft.) } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end - - context "when the notification is for the requester" do - let(:event_name) { "decidim.events.proposals.collaborative_draft_access_requester_accepted" } - let(:email_subject) { "You have been accepted as a contributor of #{resource_title}." } - let(:email_intro) { %(You have been accepted to access as a contributor of the #{decidim_html_escape(resource_title)} collaborative draft.) } - let(:email_outro) { %(You have received this notification because you requested to become a collaborator of #{decidim_html_escape(resource_title)}.) } - let(:notification_title) { %(You have been accepted to access as a contributor of the #{decidim_html_escape(resource_title)} collaborative draft.) } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end -end diff --git a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_rejected_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_rejected_event_spec.rb deleted file mode 100644 index 125bb7a2f379e..0000000000000 --- a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_rejected_event_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Proposals::CollaborativeDraftAccessRejectedEvent do - include_context "when a simple event" - - let(:event_name) { "decidim.events.proposals.collaborative_draft_access_rejected" } - let(:resource) { create(:collaborative_draft, title: "It is my collaborative draft") } - let(:resource_path) { Decidim::ResourceLocatorPresenter.new(resource).path } - let(:resource_title) { decidim_html_escape(resource.title) } - let(:author) { resource.authors.first } - let(:author_id) { author.id } - let(:author_presenter) { Decidim::UserPresenter.new(author) } - let(:author_path) { author_presenter.profile_path } - let(:author_name) { author_presenter.name } - let(:author_nickname) { author_presenter.nickname } - let(:requester) { create(:user, :confirmed, organization: resource.organization) } - let(:requester_presenter) { Decidim::UserPresenter.new(requester) } - let(:requester_id) { requester.id } - let(:requester_name) { requester.name } - let(:requester_nickname) { requester_presenter.nickname } - let(:requester_path) { requester_presenter.profile_path } - let(:extra) { { requester_id: } } - - context "when the notification is for coauthor users" do - let(:email_subject) { "#{requester_name} has been rejected to access as a contributor of the #{translated(resource.title)} collaborative draft." } - let(:email_intro) { %(#{requester_name} has been rejected to access as a contributor of the #{resource_title} collaborative draft.) } - let(:email_outro) { %(You have received this notification because you are a collaborator of #{resource_title}.) } - let(:notification_title) { %(#{requester_name} #{requester_nickname} has been rejected to access as a contributor of the #{resource_title} collaborative draft.) } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end - - context "when the notification is for the requester" do - let(:event_name) { "decidim.events.proposals.collaborative_draft_access_requester_rejected" } - let(:email_subject) { "You have been rejected as a contributor of #{translated(resource.title)}." } - let(:email_intro) { %(You have been rejected to access as a contributor of the #{resource_title} collaborative draft.) } - let(:email_outro) { %(You have received this notification because you requested to become a collaborator of #{resource_title}.) } - let(:notification_title) { %(You have been rejected to access as a contributor of the #{resource_title} collaborative draft.) } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end -end diff --git a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_requested_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_requested_event_spec.rb deleted file mode 100644 index faa8e33bc3511..0000000000000 --- a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_access_requested_event_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Proposals::CollaborativeDraftAccessRequestedEvent do - include_context "when a simple event" - - let(:event_name) { "decidim.events.proposals.collaborative_draft_access_requested" } - let(:resource) { create(:collaborative_draft, title: "It is my collaborative draft") } - let(:resource_path) { Decidim::ResourceLocatorPresenter.new(resource).path } - let(:resource_title) { resource.title } - let(:author) { resource.authors.first } - let(:author_id) { author.id } - let(:author_presenter) { Decidim::UserPresenter.new(author) } - let(:author_path) { author_presenter.profile_path } - let(:author_name) { author_presenter.name } - let(:author_nickname) { author_presenter.nickname } - let(:requester) { create(:user, :confirmed, organization: resource.organization) } - let(:requester_name) { requester.name } - let(:requester_id) { requester.id } - let(:requester_presenter) { Decidim::UserPresenter.new(requester) } - let(:requester_path) { requester_presenter.profile_path } - let(:requester_nickname) { requester_presenter.nickname } - let(:extra) { { requester_id: } } - - context "when the notification is for coauthor users" do - let(:notification_title) { %(#{requester_name} #{requester_nickname} requested access to contribute to the #{decidim_html_escape(resource_title)} collaborative draft. Please accept or reject the request.) } - let(:email_outro) { %(You have received this notification because you are a collaborator of #{decidim_html_escape(resource_title)}.) } - let(:email_intro) { %(#{requester_name} requested access as a contributor. You can accept or reject the request from the #{decidim_html_escape(resource_title)} collaborative draft page.) } - let(:email_subject) { "#{requester_name} requested access to contribute to #{resource_title}." } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end -end diff --git a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_withdrawn_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_withdrawn_event_spec.rb deleted file mode 100644 index fbfb06b153ff5..0000000000000 --- a/decidim-proposals/spec/events/decidim/proposals/collaborative_draft_withdrawn_event_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Decidim::Proposals::CollaborativeDraftWithdrawnEvent do - include_context "when a simple event" - - let(:event_name) { "decidim.events.proposals.collaborative_draft_withdrawn" } - let(:resource) { create(:collaborative_draft, title: "It is my collaborative draft") } - let(:resource_path) { Decidim::ResourceLocatorPresenter.new(resource).path } - let(:resource_title) { decidim_html_escape(resource.title) } - let(:author) { resource.authors.first } - let(:author_id) { author.id } - let(:author_presenter) { Decidim::UserPresenter.new(author) } - let(:author_path) { author_presenter.profile_path } - let(:author_url) { author_presenter.profile_url } - let(:author_name) { author_presenter.name } - let(:author_nickname) { author_presenter.nickname } - let(:extra) { { author_id: } } - - context "when the notification is for coauthor users" do - let(:notification_title) { %(#{author_name} #{author_nickname}withdrawn the #{resource_title} collaborative draft.) } - let(:email_outro) { %(You have received this notification because you are a collaborator of #{resource_title}.) } - let(:email_intro) { %(#{author_name} #{author_nickname} withdrawn the #{resource_title} collaborative draft.) } - let(:email_subject) { "#{author_name} #{author_nickname} withdrawn the #{decidim_sanitize(resource_title)} collaborative draft." } - - it_behaves_like "a simple event" - it_behaves_like "a simple event email" - it_behaves_like "a simple event notification" - end -end diff --git a/decidim-proposals/spec/forms/decidim/proposals/admin/accept_access_to_collaborative_draft_form_spec.rb b/decidim-proposals/spec/forms/decidim/proposals/admin/accept_access_to_collaborative_draft_form_spec.rb deleted file mode 100644 index 035fb37b2fe76..0000000000000 --- a/decidim-proposals/spec/forms/decidim/proposals/admin/accept_access_to_collaborative_draft_form_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - module Admin - describe AcceptAccessToCollaborativeDraftForm do - subject { form } - - let(:organization) { create(:organization) } - let(:collaborative_draft) { create(:collaborative_draft, :open) } - let(:state) { collaborative_draft.state } - let(:id) { collaborative_draft.id } - let(:current_user) { create(:user, organization:) } - let(:requester_user) { create(:user, organization:) } - let(:requester_user_id) { requester_user.id } - let(:params) do - { - state:, - requester_user_id:, - id: - } - end - - let(:form) do - described_class.from_params(params) - end - - before do - collaborative_draft.collaborator_requests.create!(user: requester_user) - end - - context "when everything is OK" do - it { is_expected.to be_valid } - end - - context "when the state is not valid" do - let(:state) { "foo" } - - it { is_expected.to be_invalid } - end - - context "when there is no state" do - let(:state) { nil } - - it { is_expected.to be_invalid } - end - - context "when there is no collaborative_draft id" do - let(:id) { nil } - - it { is_expected.to be_invalid } - end - - context "when there is no requester user id" do - let(:requester_user_id) { nil } - - it { is_expected.to be_invalid } - end - - context "when the requester user is not a requester" do - let(:not_requester_user) { create(:user, organization:) } - let(:requester_user_id) { not_requester_user.id } - - it { is_expected.to be_invalid } - end - end - end - end -end diff --git a/decidim-proposals/spec/forms/decidim/proposals/admin/reject_access_to_collaborative_draft_form_spec.rb b/decidim-proposals/spec/forms/decidim/proposals/admin/reject_access_to_collaborative_draft_form_spec.rb deleted file mode 100644 index 1b3b9f2351fb9..0000000000000 --- a/decidim-proposals/spec/forms/decidim/proposals/admin/reject_access_to_collaborative_draft_form_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - module Admin - describe RejectAccessToCollaborativeDraftForm do - subject { form } - - let(:organization) { create(:organization) } - let(:collaborative_draft) { create(:collaborative_draft, :open) } - let(:state) { collaborative_draft.state } - let(:id) { collaborative_draft.id } - let(:current_user) { create(:user, organization:) } - let(:requester_user) { create(:user, organization:) } - let(:requester_user_id) { requester_user.id } - let(:params) do - { - state:, - requester_user_id:, - id: - } - end - - let(:form) do - described_class.from_params(params) - end - - before do - collaborative_draft.collaborator_requests.create!(user: requester_user) - end - - context "when everything is OK" do - it { is_expected.to be_valid } - end - - context "when the state is not valid" do - let(:state) { "foo" } - - it { is_expected.to be_invalid } - end - - context "when there is no state" do - let(:state) { nil } - - it { is_expected.to be_invalid } - end - - context "when there is no collaborative_draft id" do - let(:id) { nil } - - it { is_expected.to be_invalid } - end - - context "when there is no requester user id" do - let(:requester_user_id) { nil } - - it { is_expected.to be_invalid } - end - - context "when the requester user is not a requester" do - let(:not_requester_user) { create(:user, organization:) } - let(:requester_user_id) { not_requester_user.id } - - it { is_expected.to be_invalid } - end - end - end - end -end diff --git a/decidim-proposals/spec/forms/decidim/proposals/admin/request_access_to_collaborative_draft_form_spec.rb b/decidim-proposals/spec/forms/decidim/proposals/admin/request_access_to_collaborative_draft_form_spec.rb deleted file mode 100644 index b3add690d54cc..0000000000000 --- a/decidim-proposals/spec/forms/decidim/proposals/admin/request_access_to_collaborative_draft_form_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - module Admin - describe RequestAccessToCollaborativeDraftForm do - subject { form } - - let(:organization) { create(:organization) } - let(:collaborative_draft) { create(:collaborative_draft, :open) } - let(:state) { collaborative_draft.state } - let(:id) { collaborative_draft.id } - let(:current_user) { create(:user, organization:) } - let(:params) do - { - state:, - id: - } - end - - let(:form) do - described_class.from_params(params) - end - - context "when everything is OK" do - it { is_expected.to be_valid } - end - - context "when the state is not valid" do - let(:state) { "foo" } - - it { is_expected.to be_invalid } - end - - context "when there is no state" do - let(:state) { nil } - - it { is_expected.to be_invalid } - end - - context "when there is no collaborative_draft id" do - let(:id) { nil } - - it { is_expected.to be_invalid } - end - end - end - end -end diff --git a/decidim-proposals/spec/jobs/decidim/proposals/hide_all_created_by_author_job_spec.rb b/decidim-proposals/spec/jobs/decidim/proposals/hide_all_created_by_author_job_spec.rb index 097dd1bbfa0be..e9b92aeb3de88 100644 --- a/decidim-proposals/spec/jobs/decidim/proposals/hide_all_created_by_author_job_spec.rb +++ b/decidim-proposals/spec/jobs/decidim/proposals/hide_all_created_by_author_job_spec.rb @@ -12,12 +12,4 @@ let(:not_hideable) { create(:proposal, component:) } end end - - context "when collaborative_draft" do - it_behaves_like "has hideable resource" do - let(:component) { create(:proposal_component, :with_collaborative_drafts_enabled, organization:) } - let(:hideable) { create(:collaborative_draft, component:, users: [author]) } - let(:not_hideable) { create(:collaborative_draft, component:) } - end - end end diff --git a/decidim-proposals/spec/models/decidim/proposals/collaborative_draft_spec.rb b/decidim-proposals/spec/models/decidim/proposals/collaborative_draft_spec.rb deleted file mode 100644 index e035e9402b304..0000000000000 --- a/decidim-proposals/spec/models/decidim/proposals/collaborative_draft_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe CollaborativeDraft do - subject { collaborative_draft } - - let(:organization) { component.participatory_space.organization } - let(:component) { create(:proposal_component) } - let(:collaborative_draft) { create(:collaborative_draft, component:) } - let(:coauthorable) { collaborative_draft } - - include_examples "coauthorable" - include_examples "has taxonomies" - include_examples "resourceable" - - it { is_expected.to be_valid } - it { is_expected.to be_versioned } - - describe "#users_to_notify_on_comment_created" do - let!(:follows) { create_list(:follow, 3, followable: subject) } - let(:followers) { follows.map(&:user) } - let(:participatory_space) { subject.component.participatory_space } - let(:organization) { participatory_space.organization } - let!(:participatory_process_admin) do - create(:process_admin, participatory_process: participatory_space) - end - - it "returns the followers" do - expect(subject.users_to_notify_on_comment_created).to match_array(followers.push(collaborative_draft.creator_author)) - end - end - - describe "#editable_by?" do - let(:author) { create(:user, organization:) } - - context "when user is author" do - let(:collaborative_draft) do - cd = create(:collaborative_draft, component:, updated_at: Time.current) - Decidim::Coauthorship.create(author:, coauthorable: cd) - cd - end - - it { is_expected.to be_editable_by(author) } - end - - context "when user is not the author" do - let(:collaborative_draft) { create(:collaborative_draft, component:, updated_at: Time.current) } - - it { is_expected.not_to be_editable_by(author) } - end - end - end - end -end diff --git a/decidim-proposals/spec/presenters/decidim/proposals/collaborative_draft_presenter_spec.rb b/decidim-proposals/spec/presenters/decidim/proposals/collaborative_draft_presenter_spec.rb deleted file mode 100644 index 006f06f1ca0ec..0000000000000 --- a/decidim-proposals/spec/presenters/decidim/proposals/collaborative_draft_presenter_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -module Decidim - module Proposals - describe CollaborativeDraftPresenter, type: :helper do - subject { described_class.new(collaborative_draft) } - - let(:collaborative_draft) { build(:collaborative_draft, body: content) } - - describe "when content contains urls" do - let(:content) { <<~EOCONTENT } - Content with URLs of anchor type and text urls like https://decidim.org. - And a malicious click me - EOCONTENT - let(:result) { <<~EORESULT } - Content with URLs of anchor type and text urls like https://decidim.org. - And a malicious click me - EORESULT - - it "converts all URLs to links and strips attributes in anchors" do - expect(subject.body(links: true, strip_tags: true)).to eq(result) - end - end - end - end -end diff --git a/decidim-proposals/spec/requests/collaborative_draft_search_spec.rb b/decidim-proposals/spec/requests/collaborative_draft_search_spec.rb deleted file mode 100644 index 7989b4669ab2b..0000000000000 --- a/decidim-proposals/spec/requests/collaborative_draft_search_spec.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe "Collaborative draft search" do - include Decidim::ComponentPathHelper - - subject { response.body } - - let(:component) do - create( - :proposal_component, - :with_creation_enabled, - settings: { collaborative_drafts_enabled: true } - ) - end - let(:user) { create(:user, :confirmed, organization:) } - let(:participatory_space) { component.participatory_space } - let(:organization) { participatory_space.organization } - let(:filter_params) { {} } - - let!(:collaborative_draft1) { create(:collaborative_draft, title: { en: "A doggo" }, component:) } - let!(:collaborative_draft2) { create(:collaborative_draft, body: { en: "There is a doggo in the office" }, component:) } - let!(:collaborative_draft3) { create(:collaborative_draft, :open, component:) } - let!(:collaborative_draft4) { create(:collaborative_draft, :published, component:) } - let!(:collaborative_draft5) { create(:collaborative_draft, :withdrawn, component:) } - let!(:collaborative_draft6) { create(:collaborative_draft, component:) } - let!(:collaborative_draft7) { create(:collaborative_draft, component:) } - - let(:meetings_component) { create(:component, manifest_name: "meetings", participatory_space:) } - let(:meeting) { create(:meeting, :published, component: meetings_component) } - - let(:dummy_component) { create(:component, manifest_name: "dummy", participatory_space:) } - let(:dummy_resource) { create(:dummy_resource, component: dummy_component) } - - let(:request_path) { Decidim::EngineRouter.main_proxy(component).collaborative_drafts_path } - - before do - meeting.link_resources([collaborative_draft6], "drafts_from_meeting") - collaborative_draft6.link_resources([meeting], "drafts_from_meeting") - dummy_resource.link_resources([collaborative_draft7], "included_collaborative_drafts") - collaborative_draft7.link_resources([dummy_resource], "included_collaborative_drafts") - - get( - request_path, - params: { filter: filter_params }, - headers: { "HOST" => component.organization.host } - ) - end - - it_behaves_like "a resource search", :collaborative_draft - it_behaves_like "a resource search with taxonomies", :collaborative_draft - - it "displays all collaborative drafts except published and withdrawn without any filters" do - expect(subject).to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft7.title)) - end - - context "when searching by text" do - let(:filter_params) { { search_text_cont: "doggo" } } - - it "displays only the collaborative drafts containing the search_text" do - expect(subject).to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft7.title)) - end - end - - context "when searching by state" do - let(:filter_params) { { with_any_state: states } } - - context "and the status is open" do - let(:states) { %w(open) } - - it "displays only open collaborative drafts" do - expect(subject).to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft7.title)) - end - end - - context "and the status is withdrawn" do - let(:states) { %w(withdrawn) } - - it "displays only withdrawn collaborative drafts" do - expect(subject).not_to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft7.title)) - end - end - - context "and the status is published" do - let(:states) { %w(published) } - - it "displays only withdrawn proposals" do - expect(subject).not_to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft7.title)) - end - end - end - - context "when searching by related to" do - let(:filter_params) { { related_to: } } - - context "and related to is set to meetings" do - let(:related_to) { "Decidim::Meetings::Meeting".underscore } - - it "displays only proposals related to meetings" do - expect(subject).not_to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft7.title)) - end - end - - context "and related to is set to resources" do - let(:related_to) { "Decidim::Dev::DummyResource".underscore } - - it "displays only proposals related to resources" do - expect(subject).not_to have_escaped_html(translated(collaborative_draft1.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft2.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft3.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft4.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft5.title)) - expect(subject).not_to have_escaped_html(translated(collaborative_draft6.title)) - expect(subject).to have_escaped_html(translated(collaborative_draft7.title)) - end - end - end -end diff --git a/decidim-proposals/spec/system/admin/admin_moderates_user_spec.rb b/decidim-proposals/spec/system/admin/admin_moderates_user_spec.rb index ea5719d83b895..b771a98d1ca85 100644 --- a/decidim-proposals/spec/system/admin/admin_moderates_user_spec.rb +++ b/decidim-proposals/spec/system/admin/admin_moderates_user_spec.rb @@ -9,10 +9,4 @@ let(:component) { create(:proposal_component, organization:) } let(:content) { create(:proposal, :participant_author, :published, component:) } end - it_behaves_like "hideable resource during block" do - let(:reportable) { content.reload.creator.identity } - - let(:component) { create(:proposal_component, organization:) } - let(:content) { create(:collaborative_draft, :participant_author, :published, component:) } - end end diff --git a/decidim-proposals/spec/system/collaborative_draft_social_share_spec.rb b/decidim-proposals/spec/system/collaborative_draft_social_share_spec.rb deleted file mode 100644 index ef700cad3283b..0000000000000 --- a/decidim-proposals/spec/system/collaborative_draft_social_share_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "decidim/core/test/shared_examples/social_share_examples" - -describe "Social shares" do - let(:organization) { create(:organization) } - let(:collaborative_draft) { create(:collaborative_draft, component:, body:) } - let!(:attachment) { create(:attachment, :with_image, attached_to: collaborative_draft, file: attachment_file) } - let(:resource) { collaborative_draft } - let(:participatory_process) { create(:participatory_process, hero_image:, organization:) } - let(:hero_image) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") } - let(:component) { create(:proposal_component, participatory_space: participatory_process, settings: { collaborative_drafts_enabled: true }) } - let(:content_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage) } - let(:body) { { en: "Description

" } } - let!(:attachment_file) { Decidim::Dev.test_file("city3.jpeg", "image/jpeg") } - let(:description_image_path) { Rails.application.routes.url_helpers.rails_blob_path(description_image, only_path: true) } - let(:description_image) do - ActiveStorage::Blob.create_and_upload!( - io: File.open(Decidim::Dev.asset("city.jpeg")), - filename: "description_image.jpg", - content_type: "image/jpeg" - ) - end - let(:block_attachment_file) { Decidim::Dev.test_file("icon.png", "image/png") } - - before do - if content_block - content_block.images_container.background_image = block_attachment_file - content_block.save! - end - switch_to_host(organization.host) - end - - it_behaves_like "a social share meta tag", "city3.jpeg" - it_behaves_like "a social share widget" - it_behaves_like "a social share via QR code" do - let(:card_image) { "city3.jpeg" } - end - - context "when no attachment images" do - let!(:attachment) { nil } - - it_behaves_like "a social share meta tag", "description_image.jpg" - end - - context "when no attachments nor description images" do - let(:attachment) { nil } - let(:description_image_path) { "" } - - it_behaves_like "a social share meta tag", "city2.jpeg" - end - - context "when listing all collaborative drafts" do - let(:resource) { main_component_path(component) } - - it_behaves_like "a social share meta tag", "city2.jpeg" - end -end diff --git a/decidim-proposals/spec/system/collaborative_drafts_fields_spec.rb b/decidim-proposals/spec/system/collaborative_drafts_fields_spec.rb deleted file mode 100644 index 5eb496f3e0537..0000000000000 --- a/decidim-proposals/spec/system/collaborative_drafts_fields_spec.rb +++ /dev/null @@ -1,276 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Collaborative drafts" do - include_context "with a component" - let(:manifest_name) { "proposals" } - - let(:root_taxonomy) { create(:taxonomy, organization:) } - let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) } - let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:, participatory_space_manifests: [participatory_process.manifest.name]) } - let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) } - let!(:user) { create(:user, :confirmed, organization:) } - let(:taxonomy_filter_ids) { [taxonomy_filter.id] } - - let(:address) { "Some address" } - let(:latitude) { 40.1234 } - let(:longitude) { 2.1234 } - - let(:collaborative_draft_title) { "More sidewalks and less roads" } - let(:collaborative_draft_body) { "Cities need more people, not more cars" } - - before do - stub_geocoding(address, [latitude, longitude]) - end - - matcher :have_author do |name| - match { |node| node.has_selector?("[data-author]", text: name) } - match_when_negated { |node| node.has_no_selector?("[data-author]", text: name) } - end - - context "when creating a new collaborative_draft" do - context "when the user is logged in" do - before do - login_as user, scope: :user - end - - context "with creation enabled" do - let!(:component) do - create(:proposal_component, - :with_creation_enabled, - manifest:, - participatory_space: participatory_process, - settings: { - collaborative_drafts_enabled: true, - taxonomy_filters: taxonomy_filter_ids - }) - end - - it "creates a new collaborative draft", :slow do - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "More sidewalks and less roads" - fill_in :collaborative_draft_body, with: "Cities need more people, not more cars" - select decidim_sanitize_translated(taxonomy.name), from: "taxonomies-#{taxonomy_filter.id}" - - find("*[type=submit]").click - end - - expect(page).to have_callout("Collaborative draft successfully created.") - expect(page).to have_content("More sidewalks and less roads") - expect(page).to have_content("Cities need more people, not more cars") - expect(page).to have_content(decidim_sanitize_translated(taxonomy.name)) - expect(page).to have_author(user.name) - end - - context "when no taxonomy filter is selected" do - let(:taxonomy_filter_ids) { [] } - - it "creates a proposal without taxonomies" do - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "More sidewalks and less roads" - fill_in :collaborative_draft_body, with: "Cities need more people, not more cars" - expect(page).to have_no_content(decidim_sanitize_translated(root_taxonomy.name)) - - find("*[type=submit]").click - end - - click_on "Publish" - - expect(page).to have_callout("Collaborative draft successfully created.") - expect(page).to have_content("More sidewalks and less roads") - expect(page).to have_content("Cities need more people, not more cars") - expect(page).to have_no_content(decidim_sanitize_translated(taxonomy.name)) - expect(page).to have_author(user.name) - end - end - - context "when there are errors on the form", :slow do - before do - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "More sidewalks and less roads" - fill_in :collaborative_draft_body, with: "Cities" - - find("*[type=submit]").click - end - end - - it "shows the form with the error message" do - expect(page).to have_content("There was a problem creating this collaborative draft.") - expect(page).to have_field(:collaborative_draft_title, with: "More sidewalks and less roads") - expect(page).to have_field(:collaborative_draft_body, with: "Cities") - end - - it "allows returning to the index" do - click_on "Back to collaborative drafts" - - expect(page).to have_content("There are no collaborative drafts yet") - end - end - - context "when geocoding is enabled" do - let!(:component) do - create(:proposal_component, - :with_creation_enabled, - manifest:, - participatory_space: participatory_process) - end - - before do - component.update!(settings: { - geocoding_enabled: true, - collaborative_drafts_enabled: true - }) - end - - it "creates a new collaborative draft", :slow do - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "More sidewalks and less roads" - fill_in :collaborative_draft_body, with: "Cities need more people, not more cars" - fill_in_geocoding :collaborative_draft_address, with: address - - find("*[type=submit]").click - end - - expect(page).to have_callout("Collaborative draft successfully created.") - expect(page).to have_content("More sidewalks and less roads") - expect(page).to have_content("Cities need more people, not more cars") - expect(page).to have_content(address) - expect(page).to have_author(user.name) - end - - it_behaves_like( - "a record with front-end geocoding address field", - Decidim::Proposals::CollaborativeDraft, - within_selector: ".new_collaborative_draft", - address_field: :collaborative_draft_address - ) do - let(:geocoded_success_message) { "Collaborative draft successfully created." } - let(:geocoded_address_value) { address } - let(:geocoded_address_coordinates) { [latitude, longitude] } - - before do - # Prepare the view for submission (other than the address field) - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "More sidewalks and less roads" - fill_in :collaborative_draft_body, with: "Cities need more people, not more cars" - end - end - end - end - - context "when the user is not authorized" do - context "and there is only an authorization required" do - before do - permissions = { - create: { - authorization_handlers: { - "dummy_authorization_handler" => { "options" => {} } - } - } - } - - component.update!(permissions:) - end - - it "redirects to the authorization form" do - visit_component - click_on "Access collaborative drafts" - click_on "New collaborative draft" - expect(page).to have_content("We need to verify your identity") - expect(page).to have_content("Verify with Example authorization") - end - end - - context "and there are more than one authorization required" do - before do - permissions = { - create: { - authorization_handlers: { - "dummy_authorization_handler" => { "options" => {} }, - "another_dummy_authorization_handler" => { "options" => {} } - } - } - } - - component.update!(permissions:) - end - - it "redirects to pending onboarding authorizations page" do - visit_component - click_on "Access collaborative drafts" - click_on "New collaborative draft" - expect(page).to have_content("You are almost ready to create a proposal") - expect(page).to have_css("a[data-verification]", count: 2) - end - end - end - - context "when attachments are allowed" do - let!(:component) do - create(:proposal_component, - :with_creation_enabled, - :with_attachments_allowed_and_collaborative_drafts_enabled, - manifest:, - participatory_space: participatory_process) - end - - it "creates a new collaborative draft with attachments" do - visit new_collaborative_draft_path - - within ".new_collaborative_draft" do - fill_in :collaborative_draft_title, with: "Collaborative draft with attachments" - fill_in :collaborative_draft_body, with: "This is my collaborative draft and I want to upload attachments." - end - - dynamically_attach_file(:collaborative_draft_documents, Decidim::Dev.asset("city.jpeg")) - - within ".new_collaborative_draft" do - find("*[type=submit]").click - end - - expect(page).to have_callout("Collaborative draft successfully created.") - - within "#panel-images" do - expect(page).to have_css("img[src*=\"city.jpeg\"]", count: 1) - end - end - end - end - - context "when creation is not enabled" do - let!(:component) do - create(:proposal_component, - :with_collaborative_drafts_enabled, - manifest:, - participatory_space: participatory_process) - end - - it "does not show the creation button" do - visit_component - click_on "Access collaborative drafts" - expect(page).to have_no_link("New collaborative draft") - end - end - end - end -end - -def new_collaborative_draft_path - visit_component - "#{current_proposal_path}/collaborative_drafts/new" -end - -def current_proposal_path - current_path.sub("/proposals", "") -end diff --git a/decidim-proposals/spec/system/collaborative_drafts_spec.rb b/decidim-proposals/spec/system/collaborative_drafts_spec.rb deleted file mode 100644 index c215d8468f879..0000000000000 --- a/decidim-proposals/spec/system/collaborative_drafts_spec.rb +++ /dev/null @@ -1,367 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Explore Collaborative Drafts", versioning: true do - include Decidim::Proposals::ApplicationHelper - include ActionView::Helpers::TextHelper - - include_context "with a component" - - let(:manifest_name) { "proposals" } - let!(:author) { create(:user, :confirmed, organization:) } - let!(:user) { create(:user, :confirmed, organization:) } - let(:participatory_process) { create(:participatory_process, :with_steps, organization:) } - let!(:component) do - create(:proposal_component, - :with_creation_enabled, - manifest:, - participatory_space: participatory_process, - organization:, - settings: { - collaborative_drafts_enabled: true, - taxonomy_filters: [taxonomy_filter.id] - }) - end - let(:root_taxonomy) { create(:taxonomy, organization:) } - let!(:taxonomy) { create(:taxonomy, skip_injection: true, parent: root_taxonomy, organization:) } - let!(:taxonomy2) { create(:taxonomy, skip_injection: true, parent: root_taxonomy, organization:) } - let!(:taxonomy3) { create(:taxonomy, skip_injection: true, parent: root_taxonomy, organization:) } - let(:taxonomies) { [taxonomy] } - let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) } - let!(:taxonomy_filter_item2) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy2) } - let!(:taxonomy_filter_item3) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy3) } - let!(:collaborative_draft) { create(:collaborative_draft, :open, component:, taxonomies:, users: [author]) } - let!(:collaborative_draft_no_tags) { create(:collaborative_draft, :open, component:) } - - let!(:open_collaborative_draft) { create(:collaborative_draft, :open, component:, taxonomies:) } - let!(:withdrawn_collaborative_draft) { create(:collaborative_draft, :withdrawn, component:, taxonomies: [taxonomy2]) } - let!(:published_collaborative_draft) { create(:collaborative_draft, :published, component:, taxonomies: [taxonomy3]) } - - let(:request_access_form) { Decidim::Proposals::RequestAccessToCollaborativeDraftForm.from_params(state: collaborative_draft.state, id: collaborative_draft.id) } - let!(:other_user) { create(:user, :confirmed, organization:) } - let(:request_access_from_other_user) { Decidim::Proposals::RequestAccessToCollaborativeDraft.new(request_access_form, other_user) } - - let(:selector) { '[id^="proposals__collaborative_draft"]' } - - context "with collaborative drafts enabled" do - before do - visit main_component_path(component) - click_on "Access collaborative drafts" - end - - describe "Renders collaborative drafts index" do - it "shows Open Drafts by default" do - first ".card__list" do - expect(page).to have_css(".label.success", text: "Open") - end - within "#dropdown-menu-filters" do - expect(find(:css, "input[name='filter[with_any_state][]'][value='open']")).to be_checked - end - end - - it "renders links to each collaborative draft details" do - collaborative_drafts_count = Decidim::Proposals::CollaborativeDraft.open.where(component:).count - expect(page).to have_css(selector, count: collaborative_drafts_count) - end - - it "shows state filters" do - within "[data-filters]" do - expect(page).to have_field("All") - expect(page).to have_field("Open") - expect(page).to have_field("Withdrawn") - expect(page).to have_field("Published") - end - end - - it "shows taxonomy filters" do - within "[data-filters]" do - expect(page).to have_field("All") - [taxonomy, taxonomy2, taxonomy3].each do |tax| - expect(page).to have_field(decidim_sanitize_translated(tax.name)) - end - end - end - end - - describe "renders collaborative draft details" do - before do - click_on "proposals__collaborative_draft_#{collaborative_draft.id}" - end - - let(:html_body) { strip_tags(collaborative_draft.body).gsub(/\n/, " ").strip } - let(:stripped_body) { %(alert("collaborative_draft_body"); #{html_body}) } - - it "shows the title" do - expect(page).to have_content(collaborative_draft.title) - end - - it "shows the body" do - expect(page).to have_content(stripped_body) - end - - it "shows the state" do - expect(page).to have_css(".label", text: "Open") - end - - context "when geocoding is enabled" do - let!(:component) do - create(:proposal_component, - :with_creation_enabled, - manifest:, - participatory_space: participatory_process, - organization:, - settings: { - collaborative_drafts_enabled: true, - geocoding_enabled: true - }) - end - let!(:collaborative_draft) { create(:collaborative_draft, :open, component:, address:, taxonomies:, users: [author]) } - let(:address) { "Some address" } - let(:latitude) { 40.1234 } - let(:longitude) { 2.1234 } - - before do - stub_geocoding(address, [latitude, longitude]) - end - - it "shows the title" do - expect(page).to have_content(collaborative_draft.title) - end - - it "shows the body" do - expect(page).to have_content(stripped_body) - end - - it "shows the address" do - expect(page).to have_content(collaborative_draft.address) - end - end - - context "without taxonomies" do - before do - visit_component - click_on "Access collaborative drafts" - click_on "proposals__collaborative_draft_#{collaborative_draft_no_tags.id}" - end - - it "does not show any tag" do - expect(page).to have_no_selector("ul.tags") - end - end - - context "with a taxonomy" do - it "shows tags for taxonomy" do - expect(page).to have_css("ul.tag-container") - within "ul.tag-container" do - expect(page).to have_content(decidim_sanitize_translated(taxonomy.name)) - end - end - end - - context "when a collaborative draft has comments" do - let(:author) { create(:user, :confirmed, organization: component.organization) } - let!(:comments) { create_list(:comment, 3, commentable: collaborative_draft) } - - before do - visit current_path - end - - it "shows the comments" do - comments.each do |comment| - expect(page).to have_content(comment.body.values.first) - end - end - end - - context "when publishing as a proposal" do - before do - within "main" do - expect(page).to have_content(collaborative_draft.title) - end - login_as author, scope: :user - sleep(1) - visit current_path - within ".main-bar__links-desktop" do - expect(page).to have_css("#trigger-dropdown-account") - end - end - - it "shows the publish button" do - expect(page).to have_button(text: "Publish") - end - - context "when the publish button is clicked" do - before do - click_on "Publish" - end - - it "shows the a modal" do - within "[id$='publish-irreversible-action-modal'][aria-modal]" do - expect(page).to have_css("h3", text: "The following action is irreversible") - expect(page).to have_button(text: "Publish as a Proposal") - end - click_on "Publish as a Proposal" - expect(page).to have_content("Collaborative draft published successfully as a proposal.") - end - end - end - - context "when visits a guest user" do - it "shows an announcement to collaborate" do - within "[data-announcement]" do - expect(page).to have_css("strong", text: "collaborative draft") - end - end - end - - context "when visits an non author user" do - before do - within "main" do - expect(page).to have_content(collaborative_draft.title) - end - login_as user, scope: :user - sleep(1) - visit current_path - within ".main-bar__links-desktop" do - expect(page).to have_css("#trigger-dropdown-account") - end - end - - it "shows an announcement to collaborate" do - within "[data-announcement]" do - expect(page).to have_css("strong", text: "collaborative draft") - end - end - - it "renders a button to request access" do - expect(page).to have_button(text: "Request access") - end - - context "when the user requests access" do - before do - click_on "Request access" - expect(page).to have_button("Access requested", disabled: true) - end - - it "renders an flash informing about the request" do - expect(page).to have_css("[data-alert-box].success") - within "[data-alert-box].success" do - expect(page).to have_content("Your request to collaborate has been successfully sent") - end - end - - it "removes the announcement to collaborate" do - expect(page).to have_no_css("[data-alert-box].secondary") - end - - it "shows that access has been requested" do - expect(page).to have_css("button[disabled]", text: "Access requested") - end - - context "when the author receives the request" do - before do - within ".main-bar__links-desktop" do - expect(page).to have_css("#trigger-dropdown-account") - end - relogin_as author, scope: :user - visit current_path - within ".main-bar__links-desktop" do - expect(page).to have_css("#trigger-dropdown-account") - end - end - - it "lists the user in Collaboration Requests" do - expect(page).to have_content("Collaboration requests") - expect(page).to have_css("#request_#{user.id}") - end - - it "shows the button to accept the request" do - expect(page).to have_button(text: "Accept") - end - - it "shows the button to reject the request" do - expect(page).to have_button("Reject") - end - - context "when the request is accepted and the contributor visits the draft" do - before do - click_on "Accept" - expect(page).to have_content("@#{user.nickname} has been accepted as a collaborator successfully") - relogin_as user, scope: :user - visit current_path - expect(page).to have_css("span.main-bar__avatar") - end - - it "shows the user as a coauthor" do - expect(page).to have_css("#content div.author__coauthors .author__name", text: user.name) - end - - it "removes the announcement to collaborate" do - expect(page).to have_no_css("#new_accept_access_to_collaborative_draft_") - expect(page).to have_no_css("#new_reject_access_to_collaborative_draft_") - end - - it "does not show the buttons to publish or withdraw" do - expect(page).to have_no_button("Publish") - expect(page).to have_no_button("withdraw the draft") - end - - it "shows a button to edit" do - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - expect(page).to have_css("#collaborative_draft_edit", text: "Edit") - end - - it "does not show the Collaboration Requests from other users" do - request_access_from_other_user.call - visit current_path - - expect(page).to have_no_content("Collaboration requests") - end - end - end - end - end - - context "when the author visits the collaborative draft" do - before do - within "main" do - expect(page).to have_content(collaborative_draft.title) - end - login_as author, scope: :user - sleep(1) - visit current_path - within ".main-bar__links-desktop" do - expect(page).to have_css("#trigger-dropdown-account") - end - end - - it "removes the announcement to collaborate" do - expect(page).to have_no_css("callout") - end - - it "shows the buttons to publish or withdraw" do - expect(page).to have_button("Publish") - expect(page).to have_button("withdraw the draft") - end - - it "shows a button to edit" do - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - expect(page).to have_css("#collaborative_draft_edit", text: "Edit") - end - end - end - end - - context "with collaborative drafts disabled" do - let(:component) { create(:proposal_component, manifest:, participatory_space: participatory_process) } - - before do - visit main_component_path(component) - end - - it "does not show the Collaborative drafts access button" do - expect(page).to have_no_content("Access collaborative drafts") - end - end -end diff --git a/decidim-proposals/spec/system/collaborative_drafts_versions_spec.rb b/decidim-proposals/spec/system/collaborative_drafts_versions_spec.rb deleted file mode 100644 index 0d71c04a29b44..0000000000000 --- a/decidim-proposals/spec/system/collaborative_drafts_versions_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Explore versions", versioning: true do - include_context "with a component" - let(:component) { create(:proposal_component, :with_creation_enabled, :with_collaborative_drafts_enabled, organization:) } - - let(:manifest_name) { "proposals" } - let!(:author) { create(:user, :confirmed, organization:) } - - let(:collaborative_draft_path) do - Decidim::ResourceLocatorPresenter.new(collaborative_draft).path - end - let(:original_title) { "The original title" } - let(:edited_title) { "The edited title" } - let(:original_body) { "Original body, consequuntur cupiditate non reprehenderit est vero fugiat" } - let(:edited_body) { "Edited body, Rerum assumenda blanditiis voluptatum autem, praesentium necessitatibus est" } - let!(:collaborative_draft) { create(:collaborative_draft, component:, title: original_title, body: original_body) } - - before do - Decidim.traceability.update!( - collaborative_draft, - author, - title: edited_title, - body: edited_body - ) - visit collaborative_draft_path - end - - context "when visiting versions index" do - before do - click_on "see other versions", match: :first - end - - it "lists all versions" do - expect(page).to have_link("Version 1 of 2") - expect(page).to have_link("Version 2 of 2") - end - end - - context "when showing version" do - before do - click_on "see other versions", match: :first - - click_on("Version 2 of 2") - end - - it_behaves_like "accessible page" - - it "shows the version author and creation date" do - within ".version__author" do - expect(page).to have_content(author.name) - expect(page).to have_content(Time.zone.today.strftime("%d/%m/%Y")) - end - end - - it "shows the changed attributes" do - expect(page).to have_content("Changes at") - - within "#diff-for-title" do - expect(page).to have_content("Title") - - within ".diff > ul > .del" do - expect(page).to have_content(original_title) - end - - within ".diff > ul > .ins" do - expect(page).to have_content(edited_title) - end - end - - within "#diff-for-body" do - expect(page).to have_content("Body") - - within ".diff > ul > .del" do - expect(page).to have_content(original_body) - end - - within ".diff > ul > .ins" do - expect(page).to have_content(edited_body) - end - end - end - end - - context "when visiting the collaborative draft details" do - before do - Decidim.traceability.update!( - collaborative_draft, - author, - title: "Edited title another time" - ) - visit collaborative_draft_path - end - - it "shows number of versions" do - expect(page).to have_content("(of #{collaborative_draft.versions.count})") - end - end -end diff --git a/decidim-proposals/spec/system/edit_collaborative_draft_spec.rb b/decidim-proposals/spec/system/edit_collaborative_draft_spec.rb deleted file mode 100644 index b36105d6f66c3..0000000000000 --- a/decidim-proposals/spec/system/edit_collaborative_draft_spec.rb +++ /dev/null @@ -1,168 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Edit collaborative_drafts" do - include_context "with a component" - let!(:component) { create(:proposal_component, :with_collaborative_drafts_enabled, organization:) } - let(:manifest_name) { "proposals" } - - let!(:user) { create(:user, :confirmed, organization: participatory_process.organization) } - let!(:another_user) { create(:user, :confirmed, organization: participatory_process.organization) } - let!(:collaborative_draft) { create(:collaborative_draft, users: [user], component:) } - - before do - switch_to_host user.organization.host - end - - describe "editing my own collaborative draft" do - let(:new_title) { "This is my collaborative_draft new title" } - let(:new_body) { "This is my collaborative_draft new body" } - - before do - login_as user, scope: :user - end - - it "can be updated" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - click_on "Edit" - - expect(page).to have_content "Edit collaborative draft" - - within "form.edit_collaborative_draft" do - fill_in :collaborative_draft_title, with: new_title - fill_in :collaborative_draft_body, with: new_body - click_on "Send" - end - - expect(page).to have_content(new_title) - expect(page).to have_content(new_body) - end - - context "when attachment is enabled" do - context "and after collaborative draft creation" do - let!(:component) do - create(:proposal_component, - :with_attachments_allowed_and_collaborative_drafts_enabled, - manifest:, - participatory_space: participatory_process) - end - - it "can be updated" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - click_on "Edit" - - dynamically_attach_file(:collaborative_draft_documents, Decidim::Dev.asset("city.jpeg")) - - within "form.edit_collaborative_draft" do - find("*[type=submit]").click - end - - expect(page).to have_callout("Collaborative draft successfully updated.") - end - end - end - - context "when rich text editor is enabled" do - before do - organization.update(rich_text_editor_in_public_views: true) - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - click_on "Edit" - end - - it_behaves_like "having a rich text editor", "edit_collaborative_draft", "basic" - end - - context "when updating with wrong data" do - it "returns an error message" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - click_on "Edit" - - within "form.edit_collaborative_draft" do - fill_in :collaborative_draft_body, with: "A" - click_on "Send" - end - - # The character counters are doubled because there is a separate screen reader character counter. - expect(page).to have_content("At least 15 characters", count: 4) - - within "form.edit_collaborative_draft" do - fill_in :collaborative_draft_body, with: "WE DO NOT WANT TO SHOUT IN THE PROPOSAL BODY TEXT!" - click_on "Send" - end - - expect(page).to have_content("is using too many capital letters (over 25% of the text)") - end - - it "keeps the submitted values" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - find("#dropdown-trigger-resource-#{collaborative_draft.id}").click - click_on "Edit" - - within "form.edit_collaborative_draft" do - fill_in :collaborative_draft_title, with: "A draft with a title" - fill_in :collaborative_draft_body, with: "ỲÓÜ WÄNTt TÙ ÚPDÀTÉ À PRÖPÔSÁL or a COLLABORATIVE DRAFT" - end - click_on "Send" - - expect(page).to have_css("input[value='A draft with a title']") - expect(page).to have_content("ỲÓÜ WÄNTt TÙ ÚPDÀTÉ À PRÖPÔSÁL") - end - end - end - - describe "editing someone else's proposal" do - before do - login_as another_user, scope: :user - end - - it "renders an error" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - expect(page).to have_no_content("Edit collaborative draft") - visit "#{current_path}/edit" - - expect(page).to have_content("not authorized") - end - end - - describe "editing my proposal outside the time limit" do - let!(:collaborative_draft) { create(:collaborative_draft, users: [user], component:, created_at: 1.hour.ago) } - - before do - login_as another_user, scope: :user - end - - it "renders an error" do - visit_component - - click_on "Access collaborative drafts" - click_on collaborative_draft.title - expect(page).to have_no_content("Edit collaborative draft") - visit "#{current_path}/edit" - - expect(page).to have_content("not authorized") - end - end -end diff --git a/decidim-proposals/spec/system/proposals_social_share_spec.rb b/decidim-proposals/spec/system/proposals_social_share_spec.rb index a0ed831c3ec68..99ead00f5b3a8 100644 --- a/decidim-proposals/spec/system/proposals_social_share_spec.rb +++ b/decidim-proposals/spec/system/proposals_social_share_spec.rb @@ -7,7 +7,7 @@ let(:organization) { create(:organization) } let(:participatory_process) { create(:participatory_process, hero_image:, organization:) } let(:hero_image) { Decidim::Dev.test_file("city2.jpeg", "image/jpeg") } - let(:component) { create(:proposal_component, participatory_space: participatory_process, settings: { collaborative_drafts_enabled: true }) } + let(:component) { create(:proposal_component, participatory_space: participatory_process) } let(:proposal) { create(:proposal, component:, body:) } let(:content_block) { create(:content_block, organization:, manifest_name: :hero, scope_name: :homepage) } let!(:attachment) { create(:attachment, :with_image, attached_to: proposal, file: attachment_file) } @@ -58,7 +58,7 @@ end context "when the resource's component is not published" do - let(:component) { create(:proposal_component, :unpublished, participatory_space: participatory_process, settings: { collaborative_drafts_enabled: true }) } + let(:component) { create(:proposal_component, :unpublished, participatory_space: participatory_process) } let(:proposal) { create(:proposal, :published, component:, body:) } it_behaves_like "a 404 page" do diff --git a/docs/modules/develop/pages/classes/models.adoc b/docs/modules/develop/pages/classes/models.adoc index 1fbe9b37c552a..a64c13477f20f 100644 --- a/docs/modules/develop/pages/classes/models.adoc +++ b/docs/modules/develop/pages/classes/models.adoc @@ -87,7 +87,6 @@ Most commonly used concerns are: - `Decidim::Forms::HasQuestionnaire` - `Decidim::Initiatives::HasArea` - `Decidim::Initiatives::InitiativeSlug` -- `Decidim::Proposals::CommentableCollaborativeDraft` - `Decidim::Proposals::CommentableProposal` - `Decidim::Proposals::ParticipatoryTextSection` - `Decidim::Proposals::Evaluable` diff --git a/docs/modules/services/pages/aitools.adoc b/docs/modules/services/pages/aitools.adoc index 72dc6a98af3ee..7e89e2b5bb89f 100644 --- a/docs/modules/services/pages/aitools.adoc +++ b/docs/modules/services/pages/aitools.adoc @@ -71,7 +71,6 @@ Decidim::Ai::SpamDetection.resource_models = { "Decidim::Debates::Debate" => "Decidim::Ai::SpamDetection::Resource::Debate", "Decidim::Meetings::Meeting" => "Decidim::Ai::SpamDetection::Resource::Meeting", "Decidim::Proposals::Proposal" => "Decidim::Ai::SpamDetection::Resource::Proposal", - "Decidim::Proposals::CollaborativeDraft" => "Decidim::Ai::SpamDetection::Resource::CollaborativeDraft", "Decidim::User" => "Decidim::Ai::SpamDetection::Resource::UserBaseEntity" } From e0816617f1025c1decfb1416137623228f8b2886 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:47:59 +0200 Subject: [PATCH 117/135] Bump to dependencies: Bump loofah from 2.25.0 to 2.25.1 (#16420) * Bump to dependencies: Bump loofah from 2.25.0 to 2.25.1 Bumps [loofah](https://github.com/flavorjones/loofah) from 2.25.0 to 2.25.1. - [Release notes](https://github.com/flavorjones/loofah/releases) - [Changelog](https://github.com/flavorjones/loofah/blob/main/CHANGELOG.md) - [Commits](https://github.com/flavorjones/loofah/compare/v2.25.0...v2.25.1) --- updated-dependencies: - dependency-name: loofah dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5b768aa4fcd75..d08eb1c827d71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -547,7 +547,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.25.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index cf106b173868f..ad8aac6616510 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -541,7 +541,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.25.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) From ebf56d798b493244ac47b30fbd60204700e22313 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Wed, 18 Mar 2026 10:32:44 +0200 Subject: [PATCH 118/135] Remove ActiveSupport::Configurable as is deprecated (#16366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove ActiveSupport::Configurable as is deprecated * Fix mailer * Fix Expectations * Address Coderabbit's recommendations * Fix small typo * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena * Add RELEASE_NOTES * Fix mardownlint errors --------- Co-authored-by: Andrés Pereira de Lucena --- .github/actions/spelling/expect.txt | 1 + RELEASE_NOTES.md | 83 ++- decidim-admin/lib/decidim/admin.rb | 2 - decidim-ai/lib/decidim/ai.rb | 2 - .../lib/decidim/ai/language/language.rb | 13 +- .../ai/spam_detection/spam_detection.rb | 107 ++-- decidim-api/lib/decidim/api.rb | 46 +- decidim-budgets/lib/decidim/budgets.rb | 2 - .../app/mailers/decidim/application_mailer.rb | 2 +- ...pload_settings_to_decidim_organizations.rb | 28 +- decidim-core/lib/decidim/core.rb | 547 +++++++----------- decidim-dev/lib/decidim/dev.rb | 2 - .../generators/test/generator_examples.rb | 49 +- .../lib/decidim/initiatives.rb | 60 +- decidim-meetings/lib/decidim/meetings.rb | 20 +- decidim-proposals/lib/decidim/proposals.rb | 16 +- decidim-system/lib/decidim/system.rb | 12 +- .../lib/decidim/verifications.rb | 10 +- 18 files changed, 452 insertions(+), 550 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 1890c2169ce4e..cc6ad0b8f7327 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -515,6 +515,7 @@ markercluster marydoe Massot matrixmultiple +mattr matutes maxtimeout mcdoggo diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1b677ae56ecc3..bf20db8ab62e6 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -298,7 +298,88 @@ After the rack upgrade, the filters are defined as follows: You can read more about this change on PR [#16103](https://github.com/decidim/decidim/pull/16103). -### 5.3. [[TITLE OF THE CHANGE]] +### 5.3. Decidim Configuration changes + +Once you have upgraded to this version, you may need to check your configuration. Previously, we were using `ActiveSupport::Configurable` to handle Decidim configuration. Now, this has been deprecated with Rails, and it will be removed in the next Rails version. + +We went ahead and changed the way we handle Decidim configuration, trying to keep the same API as before. + +Previously, you may had an initializer with some content like: + +```ruby +Decidim.configure do |config| + config.force_ssl = true + # some other configuration +end +``` + +Now we try to keep the same, but if there is some kind of custom configuration that you may have, you will need to change it to: + +```ruby +Decidim.force_ssl = true +``` + +#### Decidim module developer instructions + +If you are a module developer, you may want to change your plugin structure to remove `ActiveSupport::Configurable` calls. + +If you were using something like: + +```ruby +module Decidim + module Ai + module SpamDetection + include ActiveSupport::Configurable + + config_accessor :reporting_user_email do + "my default value" + end + # some other configuration + end + end +end +``` + +You can refactor to the following: + +```ruby +module Decidim + module Ai + module SpamDetection + + mattr_accessor :reporting_user_email, default: "my default value" + + # some other configuration + end + end +end +``` + +To keep the same API, you may want to add the following to your module definition + +```ruby +module Decidim + module Ai + module SpamDetection + class << self + def config = self + + def configure + yield self + end + end + + mattr_accessor :reporting_user_email, default: "my default value" + + # some other configuration + end + end +end +``` + +You can read more about this change on PR [#16366](https://github.com/decidim/decidim/pull/16366). + +### 5.4. [[TITLE OF THE CHANGE]] In order to [[REASONING (e.g. improve the maintenance of the code base)]] we have changed... diff --git a/decidim-admin/lib/decidim/admin.rb b/decidim-admin/lib/decidim/admin.rb index a7778ccba621b..652672dbd120a 100644 --- a/decidim-admin/lib/decidim/admin.rb +++ b/decidim-admin/lib/decidim/admin.rb @@ -15,8 +15,6 @@ module Admin autoload :Import, "decidim/admin/import" autoload :CustomImport, "decidim/admin/custom_import" - include ActiveSupport::Configurable - # Public: Stores an instance of ViewHooks def self.view_hooks @view_hooks ||= ViewHooks.new diff --git a/decidim-ai/lib/decidim/ai.rb b/decidim-ai/lib/decidim/ai.rb index 87aaf1f9d8653..18ef28036508a 100644 --- a/decidim-ai/lib/decidim/ai.rb +++ b/decidim-ai/lib/decidim/ai.rb @@ -7,7 +7,5 @@ module Ai autoload :StrategyRegistry, "decidim/ai/strategy_registry" autoload :SpamDetection, "decidim/ai/spam_detection/spam_detection" autoload :Language, "decidim/ai/language/language" - - include ActiveSupport::Configurable end end diff --git a/decidim-ai/lib/decidim/ai/language/language.rb b/decidim-ai/lib/decidim/ai/language/language.rb index 6e74db26f4348..d72d165ab4236 100644 --- a/decidim-ai/lib/decidim/ai/language/language.rb +++ b/decidim-ai/lib/decidim/ai/language/language.rb @@ -4,7 +4,14 @@ module Decidim module Ai module Language autoload :Formatter, "decidim/ai/language/formatter" - include ActiveSupport::Configurable + + class << self + def config = self + + def configure + yield self + end + end # Text cleanup service # @@ -15,9 +22,7 @@ module Language # # your code # end # end - config_accessor :formatter do - Decidim::Env.new("DECIDIM_AI_LANGUAGE_FORMATTER", "Decidim::Ai::Language::Formatter").value - end + mattr_accessor :formatter, default: Decidim::Env.new("DECIDIM_AI_LANGUAGE_FORMATTER", "Decidim::Ai::Language::Formatter").value end end end diff --git a/decidim-ai/lib/decidim/ai/spam_detection/spam_detection.rb b/decidim-ai/lib/decidim/ai/spam_detection/spam_detection.rb index ea7f0db937cfb..80d691ce1076d 100644 --- a/decidim-ai/lib/decidim/ai/spam_detection/spam_detection.rb +++ b/decidim-ai/lib/decidim/ai/spam_detection/spam_detection.rb @@ -3,8 +3,6 @@ module Decidim module Ai module SpamDetection - include ActiveSupport::Configurable - autoload :Service, "decidim/ai/spam_detection/service" module Resource @@ -27,31 +25,31 @@ module Strategy autoload :Bayes, "decidim/ai/spam_detection/strategy/bayes" end + class << self + def config = self + + def configure + yield self + end + end + # When the engine is consistently marking spam content without errors, # you can skip human intervention by enabling this functionality - config_accessor :hide_reported_resources_automatically do - Decidim::Env.new("DECIDIM_SPAM_HIDE_REPORTED_RESOURCES_AUTOMATICALLY", false).present? - end + mattr_accessor :hide_reported_resources_automatically, default: Decidim::Env.new("DECIDIM_SPAM_HIDE_REPORTED_RESOURCES_AUTOMATICALLY", false).present? # This is the email address used by the spam engine to # properly identify the user that will report users and content - config_accessor :reporting_user_email do - Decidim::Env.new("DECIDIM_SPAM_REPORTING_USER", "decidim-reporting-user@example.org").value - end + mattr_accessor :reporting_user_email, default: Decidim::Env.new("DECIDIM_SPAM_REPORTING_USER", "decidim-reporting-user@example.org").value # You can configure the spam threshold for the spam detection service. # The threshold is a float value between 0 and 1. # The default value is 0.75 # Any value below the threshold will be considered spam. - config_accessor :resource_score_threshold do - Decidim::Env.new("DECIDIM_SPAM_DETECTION_RESOURCE_SCORE_THRESHOLD", 0.75).to_f - end + mattr_accessor :resource_score_threshold, default: Decidim::Env.new("DECIDIM_SPAM_DETECTION_RESOURCE_SCORE_THRESHOLD", 0.75).to_f # You can configure the spam delay for the spam detection service. # The default value is 30 seconds - config_accessor :spam_detection_delay do - Decidim::Env.new("DECIDIM_SPAM_DETECTION_DELAY_IN_SECONDS", 30).to_i.seconds - end + mattr_accessor :spam_detection_delay, default: Decidim::Env.new("DECIDIM_SPAM_DETECTION_DELAY_IN_SECONDS", 30).to_i.seconds # Registered analyzers. # You can register your own analyzer by adding a new entry to this array. @@ -70,48 +68,41 @@ module Strategy # } # } # } - config_accessor :resource_analyzers do - [ - { - name: :bayes, - strategy: Decidim::Ai::SpamDetection::Strategy::Bayes, - options: { - adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE", "redis"), - params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL", "redis://localhost:6379/2") } - } + mattr_accessor :resource_analyzers, default: [ + { + name: :bayes, + strategy: Decidim::Ai::SpamDetection::Strategy::Bayes, + options: { + adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE", "redis"), + params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_RESOURCE_URL", "redis://localhost:6379/2") } } - ] - end + } + ] - # This config_accessor allows the implementers to change the class being used by the classifier, + # This setting allows the implementers to change the class being used by the classifier, # in order to change the finder method. or even define own resource visibility criteria. # This is the place where new resources can be registered following the pattern # Resource => Handler - config_accessor :resource_models do - @models ||= begin - models = {} - models["Decidim::Comments::Comment"] = "Decidim::Ai::SpamDetection::Resource::Comment" if Decidim.module_installed?("comments") - models["Decidim::Debates::Debate"] = "Decidim::Ai::SpamDetection::Resource::Debate" if Decidim.module_installed?("debates") - models["Decidim::Initiative"] = "Decidim::Ai::SpamDetection::Resource::Initiative" if Decidim.module_installed?("initiatives") - models["Decidim::Meetings::Meeting"] = "Decidim::Ai::SpamDetection::Resource::Meeting" if Decidim.module_installed?("meetings") - models["Decidim::Proposals::Proposal"] = "Decidim::Ai::SpamDetection::Resource::Proposal" if Decidim.module_installed?("proposals") - models - end + mattr_accessor :resource_models + self.resource_models ||= begin + models = {} + models["Decidim::Comments::Comment"] = "Decidim::Ai::SpamDetection::Resource::Comment" if Decidim.module_installed?("comments") + models["Decidim::Debates::Debate"] = "Decidim::Ai::SpamDetection::Resource::Debate" if Decidim.module_installed?("debates") + models["Decidim::Initiative"] = "Decidim::Ai::SpamDetection::Resource::Initiative" if Decidim.module_installed?("initiatives") + models["Decidim::Meetings::Meeting"] = "Decidim::Ai::SpamDetection::Resource::Meeting" if Decidim.module_installed?("meetings") + models["Decidim::Proposals::Proposal"] = "Decidim::Ai::SpamDetection::Resource::Proposal" if Decidim.module_installed?("proposals") + models end # Spam detection service class. # If you want to use a different spam detection service, you can use a class service having the following contract - config_accessor :resource_detection_service do - Decidim::Env.new("DECIDIM_SPAM_DETECTION_RESOURCE_SERVICE", "Decidim::Ai::SpamDetection::Service").value - end + mattr_accessor :resource_detection_service, default: Decidim::Env.new("DECIDIM_SPAM_DETECTION_RESOURCE_SERVICE", "Decidim::Ai::SpamDetection::Service").value # You can configure the spam threshold for the spam detection service. # The threshold is a float value between 0 and 1. # The default value is 0.75 # Any value below the threshold will be considered spam. - config_accessor :user_score_threshold do - Decidim::Env.new("DECIDIM_SPAM_DETECTION_USER_SCORE_THRESHOLD", 0.75).to_f - end + mattr_accessor :user_score_threshold, default: Decidim::Env.new("DECIDIM_SPAM_DETECTION_USER_SCORE_THRESHOLD", 0.75).to_f # Registered analyzers. # You can register your own analyzer by adding a new entry to this array. @@ -130,32 +121,24 @@ module Strategy # } # } # } - config_accessor :user_analyzers do - [ - { - name: :bayes, - strategy: Decidim::Ai::SpamDetection::Strategy::Bayes, - options: { - adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER", "redis"), - params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL", "redis://localhost:6379/3") } - } + mattr_accessor :user_analyzers, default: [ + { + name: :bayes, + strategy: Decidim::Ai::SpamDetection::Strategy::Bayes, + options: { + adapter: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER", "redis"), + params: { url: ENV.fetch("DECIDIM_SPAM_DETECTION_BACKEND_USER_REDIS_URL", "redis://localhost:6379/3") } } - ] - end + } + ] - # This config_accessor allows the implementers to change the class being used by the classifier, + # This setting allows the implementers to change the class being used by the classifier, # in order to change the finder method or what a hidden user really is. - config_accessor :user_models do - { - "Decidim::User" => "Decidim::Ai::SpamDetection::Resource::UserBaseEntity" - } - end + mattr_accessor :user_models, default: { "Decidim::User" => "Decidim::Ai::SpamDetection::Resource::UserBaseEntity" } # Spam detection service class. # If you want to use a different spam detection service, you can use a class service having the following contract - config_accessor :user_detection_service do - Decidim::Env.new("DECIDIM_SPAM_DETECTION_USER_SERVICE", "Decidim::Ai::SpamDetection::Service").value - end + mattr_accessor :user_detection_service, default: Decidim::Env.new("DECIDIM_SPAM_DETECTION_USER_SERVICE", "Decidim::Ai::SpamDetection::Service").value # this is the generic resource classifier class. If you need to change your own class, please change the # configuration of `Decidim::Ai::SpamDetection.detection_service` variable. diff --git a/decidim-api/lib/decidim/api.rb b/decidim-api/lib/decidim/api.rb index 11f9beec80ec2..632f18eb5e53e 100644 --- a/decidim-api/lib/decidim/api.rb +++ b/decidim-api/lib/decidim/api.rb @@ -11,54 +11,44 @@ module Decidim # This module holds all business logic related to exposing a Public API for # decidim. module Api - include ActiveSupport::Configurable + class << self + def config = self - # defines the schema max_per_page to configure GraphQL pagination - config_accessor :schema_max_per_page do - Decidim::Env.new("API_SCHEMA_MAX_PER_PAGE", 50).to_i + def configure + yield self + end end + # defines the schema max_per_page to configure GraphQL pagination + mattr_accessor :schema_max_per_page, default: Decidim::Env.new("API_SCHEMA_MAX_PER_PAGE", 50).to_i + # defines the schema max_complexity to configure GraphQL query complexity - config_accessor :schema_max_complexity do - Decidim::Env.new("API_SCHEMA_MAX_COMPLEXITY", 5000).to_i - end + mattr_accessor :schema_max_complexity, default: Decidim::Env.new("API_SCHEMA_MAX_COMPLEXITY", 5000).to_i # defines how many aliases are permitted in a query - config_accessor :max_aliases do - Decidim::Env.new("API_SCHEMA_MAX_ALIASES", 5).to_i - end + mattr_accessor :max_aliases, default: Decidim::Env.new("API_SCHEMA_MAX_ALIASES", 5).to_i # defines the schema max_depth to configure GraphQL query max_depth - config_accessor :schema_max_depth do - Decidim::Env.new("API_SCHEMA_MAX_DEPTH", 15).to_i - end + mattr_accessor :schema_max_depth, default: Decidim::Env.new("API_SCHEMA_MAX_DEPTH", 15).to_i - config_accessor :disclose_system_version do - Decidim::Env.new("DECIDIM_API_DISCLOSE_SYSTEM_VERSION").present? - end + mattr_accessor :disclose_system_version, default: Decidim::Env.new("DECIDIM_API_DISCLOSE_SYSTEM_VERSION").present? # makes the API authentication necessary in order to access it # access it. - config_accessor :force_api_authentication do - Decidim::Env.new("DECIDIM_API_FORCE_API_AUTHENTICATION", nil).present? - end + mattr_accessor :force_api_authentication, default: Decidim::Env.new("DECIDIM_API_FORCE_API_AUTHENTICATION", nil).present? # allows anonymous introspection queries # If you are not sure, leave it set to false. In this way only administrator users will be able to access the introspection query. # Otherwise, anyone can access it, causing security issues. - config_accessor :enable_anonymous_introspection do - Decidim::Env.new("DECIDIM_API_ENABLE_ANONYMOUS_INTROSPECTION", nil).present? - end + mattr_accessor :enable_anonymous_introspection, default: Decidim::Env.new("DECIDIM_API_ENABLE_ANONYMOUS_INTROSPECTION", nil).present? # The expiration time of the JWT tokens, after which issued token will # expire. Recommended to match the value of # `DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN`. - config_accessor :jwt_expires_in do - Decidim::Env.new( - "DECIDIM_API_JWT_EXPIRES_IN", - Decidim::Env.new("DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN", "120").value - ).to_i - end + mattr_accessor :jwt_expires_in, default: Decidim::Env.new( + "DECIDIM_API_JWT_EXPIRES_IN", + Decidim::Env.new("DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN", "120").value + ).to_i # This declares all the types an interface or union can resolve to. This needs # to be done in order to be able to have them found. This is a shortcoming of diff --git a/decidim-budgets/lib/decidim/budgets.rb b/decidim-budgets/lib/decidim/budgets.rb index 4dc2640d35cfc..0300db4cd0eac 100644 --- a/decidim-budgets/lib/decidim/budgets.rb +++ b/decidim-budgets/lib/decidim/budgets.rb @@ -12,7 +12,5 @@ module Decidim module Budgets autoload :ProjectSerializer, "decidim/budgets/project_serializer" autoload :OrderPDF, "decidim/budgets/budget_order_pdf" - - include ActiveSupport::Configurable end end diff --git a/decidim-core/app/mailers/decidim/application_mailer.rb b/decidim-core/app/mailers/decidim/application_mailer.rb index 53a2c128ae5e8..91ebe07735599 100644 --- a/decidim-core/app/mailers/decidim/application_mailer.rb +++ b/decidim-core/app/mailers/decidim/application_mailer.rb @@ -29,7 +29,7 @@ def current_locale def set_smtp return if organization.nil? || organization.smtp_settings.blank? || organization.smtp_settings.except("from", "from_label", "from_email").all?(&:blank?) - mail.reply_to = mail.reply_to || Decidim.config.mailer_reply + mail.reply_to = mail.reply_to || Decidim.config.mailer_sender mail.delivery_method.settings.merge!( address: organization.smtp_settings["address"], port: organization.smtp_settings["port"], diff --git a/decidim-core/db/migrate/20200730142511_add_file_upload_settings_to_decidim_organizations.rb b/decidim-core/db/migrate/20200730142511_add_file_upload_settings_to_decidim_organizations.rb index e5e2cf800090f..056088402ae2b 100644 --- a/decidim-core/db/migrate/20200730142511_add_file_upload_settings_to_decidim_organizations.rb +++ b/decidim-core/db/migrate/20200730142511_add_file_upload_settings_to_decidim_organizations.rb @@ -6,23 +6,21 @@ def change reversible do |dir| dir.up do - Decidim.configure do |config| - # Even when these configurations have been deleted, they are available - # in the config object if they are defined by the initializer. - attachment_size = config.fetch(:maximum_attachment_size, 10.megabytes) - avatar_size = config.fetch(:maximum_avatar_size, 5.megabytes) + # Even when these configurations have been deleted, they are available + # in the config object if they are defined by the initializer. + attachment_size = Decidim.maximum_attachment_size || 10.megabytes + avatar_size = Decidim.maximum_avatar_size || 5.megabytes - # Update all organizations with the default file upload settings. - Decidim::Organization.all.each do |organization| - organization.update( - file_upload_settings: default_settings.merge( - "maximum_file_size" => { - "default" => attachment_size / 1.megabyte, - "avatar" => avatar_size / 1.megabyte - } - ) + # Update all organizations with the default file upload settings. + Decidim::Organization.all.each do |organization| + organization.update( + file_upload_settings: default_settings.merge( + "maximum_file_size" => { + "default" => attachment_size / 1.megabyte, + "avatar" => avatar_size / 1.megabyte + } ) - end + ) end end end diff --git a/decidim-core/lib/decidim/core.rb b/decidim-core/lib/decidim/core.rb index ff24b86d4595e..74fe786b60943 100644 --- a/decidim-core/lib/decidim/core.rb +++ b/decidim-core/lib/decidim/core.rb @@ -149,7 +149,13 @@ module Commands autoload :RestoreResource, "decidim/commands/restore_resource" end - include ActiveSupport::Configurable + class << self + def config = self + + def configure + yield self + end + end # Loads seeds from all engines. def self.seed! @@ -243,80 +249,54 @@ def self.reset_all_column_information end # Exposes a configuration option: The application name String. - config_accessor :application_name do - config.application_name = Decidim::Env.new("DECIDIM_APPLICATION_NAME", "My Application Name").to_s - end + mattr_accessor :application_name, default: Decidim::Env.new("DECIDIM_APPLICATION_NAME", "My Application Name").to_s # Exposes a configuration option: The email String to use as sender in all # the mails. - config_accessor :mailer_sender do - Decidim::Env.new("DECIDIM_MAILER_SENDER", "change-me@example.org").to_s - end + mattr_accessor :mailer_sender, default: Decidim::Env.new("DECIDIM_MAILER_SENDER", "change-me@example.org").to_s # Whether SSL should be forced or not. - config_accessor :force_ssl do - if Decidim::Env.new("DECIDIM_FORCE_SSL", "auto").default_or_present_if_exists.to_s == "auto" - Rails.env.starts_with?("production") || Rails.env.starts_with?("staging") - else - Decidim::Env.new("DECIDIM_FORCE_SSL").present? - end - end + mattr_accessor :force_ssl + self.force_ssl ||= if Decidim::Env.new("DECIDIM_FORCE_SSL", "auto").default_or_present_if_exists.to_s == "auto" + Rails.env.starts_with?("production") || Rails.env.starts_with?("staging") + else + Decidim::Env.new("DECIDIM_FORCE_SSL").present? + end # CDN host configuration - config_accessor :storage_cdn_host do - Decidim::Env.new("STORAGE_CDN_HOST", nil).to_s - end + mattr_accessor :storage_cdn_host, default: Decidim::Env.new("STORAGE_CDN_HOST", nil).to_s # Which storage provider is going to be used for the application, provides support for the most popular options. - config_accessor :storage_provider do - Decidim::Env.new("STORAGE_PROVIDER", "local").to_s - end + mattr_accessor :storage_provider, default: Decidim::Env.new("STORAGE_PROVIDER", "local").to_s # VAPID public key that will be used to sign the Push API requests. - config_accessor :vapid_public_key do - Decidim::Env.new("VAPID_PUBLIC_KEY", nil).to_s - end + mattr_accessor :vapid_public_key, default: Decidim::Env.new("VAPID_PUBLIC_KEY", nil).to_s # VAPID private key that will be used to sign the Push API requests. - config_accessor :vapid_private_key do - Decidim::Env.new("VAPID_PRIVATE_KEY", nil).to_s - end + mattr_accessor :vapid_private_key, default: Decidim::Env.new("VAPID_PRIVATE_KEY", nil).to_s # Having this on true will change the way the svg assets are being served. - config_accessor :cors_enabled do - Decidim::Env.new("DECIDIM_CORS_ENABLED", "false").present? - end + mattr_accessor :cors_enabled, default: Decidim::Env.new("DECIDIM_CORS_ENABLED", "false").present? # Exposes a configuration option: The application available locales. - config_accessor :available_locales do - Decidim::Env.new("DECIDIM_AVAILABLE_LOCALES", %w(en bg ar ca cs da de el eo es es-MX es-PY et eu fa fi-pl fi fr fr-CA ga gl hr - hu id is it ja ko lb lt lv mt nl no pl pt pt-BR ro ru sk sl sr sv tr uk vi zh-CN zh-TW).join(",")).to_array - end + mattr_accessor :available_locales, default: Decidim::Env.new("DECIDIM_AVAILABLE_LOCALES", %w(en bg ar ca cs da de el eo es es-MX es-PY et eu fa fi-pl + fi fr fr-CA ga gl hr hu id is it ja ko lb lt lv mt nl no pl pt pt-BR ro ru sk sl + sr sv tr uk vi zh-CN zh-TW).join(",")).to_array # Exposes a configuration option: The application default locale. - config_accessor :default_locale do - (Decidim::Env.new("DECIDIM_DEFAULT_LOCALE", "en").presence || :en).to_s - end + mattr_accessor :default_locale, default: (Decidim::Env.new("DECIDIM_DEFAULT_LOCALE", "en").presence || :en).to_s # Users that have not logged in for this period of time will be deleted - config_accessor :delete_inactive_users_after_days do - Decidim::Env.new("DELETE_INACTIVE_USERS_AFTER_DAYS", 365).to_i - end + mattr_accessor :delete_inactive_users_after_days, default: Decidim::Env.new("DELETE_INACTIVE_USERS_AFTER_DAYS", 365).to_i # The minimum allowed inactivity period for deleting participants. - config_accessor :minimum_inactivity_period do - Decidim::Env.new("DECIDIM_MINIMUM_INACTIVITY_PERIOD_IN_DAYS", 30).to_i - end + mattr_accessor :minimum_inactivity_period, default: Decidim::Env.new("DECIDIM_MINIMUM_INACTIVITY_PERIOD_IN_DAYS", 30).to_i # Users will be warned for the first time this amount of days before the final removal - config_accessor :delete_inactive_users_first_warning_days_before do - Decidim::Env.new("DECIDIM_DELETE_INACTIVE_USERS_FIRST_WARNING_DAYS_BEFORE", 30).to_i - end + mattr_accessor :delete_inactive_users_first_warning_days_before, default: Decidim::Env.new("DECIDIM_DELETE_INACTIVE_USERS_FIRST_WARNING_DAYS_BEFORE", 30).to_i # Users will be warned for the last time this amount of days before the final removal - config_accessor :delete_inactive_users_last_warning_days_before do - Decidim::Env.new("DECIDIM_DELETE_INACTIVE_USERS_LAST_WARNING_DAYS_BEFORE", 7).to_i - end + mattr_accessor :delete_inactive_users_last_warning_days_before, default: Decidim::Env.new("DECIDIM_DELETE_INACTIVE_USERS_LAST_WARNING_DAYS_BEFORE", 7).to_i # Returns the inactivity threshold (in days) to trigger the first warning email. def self.first_warning_inactive_users_after_days @@ -331,9 +311,7 @@ def self.last_warning_inactive_users_after_days # Disable the redirection to the external host when performing redirect back # For more details https://github.com/rails/rails/issues/39643 # Additional context: This has been revealed as an issue during a security audit on Future of Europe installation - config_accessor :allow_open_redirects do - Decidim::Env.new("DECIDIM_ALLOW_OPEN_REDIRECTS").present? - end + mattr_accessor :allow_open_redirects, default: Decidim::Env.new("DECIDIM_ALLOW_OPEN_REDIRECTS").present? # Exposes a configuration option: an array of symbols representing processors # that will be automatically executed when a content is parsed or rendered. @@ -348,64 +326,60 @@ def self.last_warning_inactive_users_after_days # # Decidim::ContentParsers::UserParser < BaseParser # Decidim::ContentRenderers::UserRenderer < BaseRenderer - config_accessor :content_processors do - Decidim::Env.new("DECIDIM_CONTENT_PROCESSORS", "").to_array - end + mattr_accessor :content_processors, default: Decidim::Env.new("DECIDIM_CONTENT_PROCESSORS", "").to_array # Exposes a configuration option: an object to configure geocoder - config_accessor :geocoder + mattr_accessor :geocoder # Exposes a configuration option: an object to configure the mapping # functionality. See Decidim::Map for more information. - config_accessor :maps do - if Decidim::Env.new("MAPS_STATIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).present? - - @maps ||= begin - static_provider = Decidim::Env.new("MAPS_STATIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).to_s - maps = { - provider: static_provider, - api_key: Decidim::Env.new("MAPS_STATIC_API_KEY", ENV.fetch("MAPS_API_KEY", nil)).to_s, - static: false, - dynamic: false - } - - maps[:geocoding] = { host: ENV["MAPS_GEOCODING_HOST"], use_https: true } if ENV["MAPS_GEOCODING_HOST"] - - static_url = ENV.fetch("MAPS_STATIC_URL", nil) - static_url = "https://image.maps.hereapi.com/mia/v3/base/mc/overlay" if static_provider == "here" - maps[:static] = { url: static_url } if static_url - - dynamic_provider = Decidim::Env.new("MAPS_DYNAMIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).to_s - - if dynamic_provider - dynamic_url = ENV.fetch("MAPS_DYNAMIC_URL", nil) - maps[:dynamic] = { - provider: dynamic_provider, - api_key: Decidim::Env.new("MAPS_DYNAMIC_API_KEY", ENV.fetch("MAPS_API_KEY", nil)).to_s - } - maps[:dynamic][:tile_layer] = {} - maps[:dynamic][:tile_layer][:url] = dynamic_url if dynamic_url - maps[:dynamic][:tile_layer][:attribution] = ENV["MAPS_ATTRIBUTION"] if ENV["MAPS_ATTRIBUTION"] - end - if dynamic_provider && ENV["MAPS_EXTRA_VARS"].present? - vars = URI.decode_www_form(ENV.fetch("MAPS_EXTRA_VARS", nil)) - vars.each do |key, value| - # perform a naive type conversion - maps[:dynamic][:tile_layer][key] = case value - when /^true$|^false$/i - value.downcase == "true" - when /\A[-+]?\d+\z/ - value.to_i - else - value - end - end - end - - maps - end - end - end + mattr_accessor :maps + self.maps ||= if Decidim::Env.new("MAPS_STATIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).present? + @maps ||= begin + static_provider = Decidim::Env.new("MAPS_STATIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).to_s + maps = { + provider: static_provider, + api_key: Decidim::Env.new("MAPS_STATIC_API_KEY", ENV.fetch("MAPS_API_KEY", nil)).to_s, + static: false, + dynamic: false + } + + maps[:geocoding] = { host: ENV["MAPS_GEOCODING_HOST"], use_https: true } if ENV["MAPS_GEOCODING_HOST"] + + static_url = ENV.fetch("MAPS_STATIC_URL", nil) + static_url = "https://image.maps.hereapi.com/mia/v3/base/mc/overlay" if static_provider == "here" + maps[:static] = { url: static_url } if static_url + + dynamic_provider = Decidim::Env.new("MAPS_DYNAMIC_PROVIDER", ENV.fetch("MAPS_PROVIDER", nil)).to_s + + if dynamic_provider.present? + dynamic_url = ENV.fetch("MAPS_DYNAMIC_URL", nil) + maps[:dynamic] = { + provider: dynamic_provider, + api_key: Decidim::Env.new("MAPS_DYNAMIC_API_KEY", ENV.fetch("MAPS_API_KEY", nil)).to_s + } + maps[:dynamic][:tile_layer] = {} + maps[:dynamic][:tile_layer][:url] = dynamic_url if dynamic_url + maps[:dynamic][:tile_layer][:attribution] = ENV["MAPS_ATTRIBUTION"] if ENV["MAPS_ATTRIBUTION"] + end + if dynamic_provider.present? && Decidim::Env.new("MAPS_EXTRA_VARS").present? + vars = URI.decode_www_form(Decidim::Env.new("MAPS_EXTRA_VARS").to_s) + vars.each do |key, value| + # perform a naive type conversion + maps[:dynamic][:tile_layer][key] = case value + when /^true$|^false$/i + value.downcase == "true" + when /\A[-+]?\d+\z/ + value.to_i + else + value + end + end + end + + maps + end + end # Exposes a configuration option: a custom method to generate references. # If overwritten, it should handle both component resources and participatory spaces. @@ -419,191 +393,138 @@ def self.last_warning_inactive_users_after_days # (MEET for meetings or PROJ for projects). # 2017-02: Year-Month of the resource creation date # 6589: ID of the resource - config_accessor :reference_generator do - lambda do |resource, component| - ref = "" - - if resource.is_a?(Decidim::HasComponent) && component.present? - # It is a component resource - ref = component.participatory_space.organization.reference_prefix - elsif resource.is_a?(Decidim::Participable) - # It is a participatory space - ref = resource.organization.reference_prefix - end - - class_identifier = resource.class.name.demodulize[0..3].upcase - year_month = (resource.created_at || Time.current).strftime("%Y-%m") - - [ref, class_identifier, year_month, resource.id].join("-") + mattr_accessor :reference_generator + self.reference_generator ||= lambda do |resource, component| + ref = "" + + if resource.is_a?(Decidim::HasComponent) && component.present? + # It is a component resource + ref = component.participatory_space.organization.reference_prefix + elsif resource.is_a?(Decidim::Participable) + # It is a participatory space + ref = resource.organization.reference_prefix end + class_identifier = resource.class.name.demodulize[0..3].upcase + year_month = (resource.created_at || Time.current).strftime("%Y-%m") + + [ref, class_identifier, year_month, resource.id].join("-") end # Exposes a configuration option: the IPs that are allowed to access the system - config_accessor :system_accesslist_ips do - Decidim::Env.new("DECIDIM_SYSTEM_ACCESSLIST_IPS").to_array - end + mattr_accessor :system_accesslist_ips, default: Decidim::Env.new("DECIDIM_SYSTEM_ACCESSLIST_IPS").to_array # Exposes a configuration option: the currency unit - config_accessor :currency_unit do - if Decidim::Env.new("DECIDIM_CURRENCY_UNIT", "€").present? - Decidim::Env.new("DECIDIM_CURRENCY_UNIT", "€").to_s - else - "€" - end - end + mattr_accessor :currency_unit, default: if Decidim::Env.new("DECIDIM_CURRENCY_UNIT", "€").present? + Decidim::Env.new("DECIDIM_CURRENCY_UNIT", "€").to_s + else + "€" + end # Exposes a configuration option: The image uploader quality. - config_accessor :image_uploader_quality do - Decidim::Env.new("DECIDIM_IMAGE_UPLOADER_QUALITY", "80").to_i - end + mattr_accessor :image_uploader_quality, default: Decidim::Env.new("DECIDIM_IMAGE_UPLOADER_QUALITY", "80").to_i # The number of reports which a resource can receive before hiding it - config_accessor :max_reports_before_hiding do - Decidim::Env.new("DECIDIM_MAX_REPORTS_BEFORE_HIDING", "3").to_i - end + mattr_accessor :max_reports_before_hiding, default: Decidim::Env.new("DECIDIM_MAX_REPORTS_BEFORE_HIDING", "3").to_i # Allow organization's administrators to inject custom HTML into the frontend - config_accessor :enable_html_header_snippets do - Decidim::Env.new("DECIDIM_ENABLE_HTML_HEADER_SNIPPETS").present? - end + mattr_accessor :enable_html_header_snippets, default: Decidim::Env.new("DECIDIM_ENABLE_HTML_HEADER_SNIPPETS").present? # Allow organization's administrators to track newsletter links - config_accessor :track_newsletter_links do - if Decidim::Env.new("DECIDIM_TRACK_NEWSLETTER_LINKS", "auto").default_or_present_if_exists.to_s == "auto" - true - else - Decidim.force_ssl - end - end + mattr_accessor :track_newsletter_links + self.track_newsletter_links ||= if Decidim::Env.new("DECIDIM_TRACK_NEWSLETTER_LINKS", "auto").default_or_present_if_exists.to_s == "auto" + true + else + Decidim.force_ssl + end # Time that download your data files are available in server - config_accessor :download_your_data_expiry_time do - Decidim::Env.new("DECIDIM_DOWNLOAD_YOUR_DATA_EXPIRY_TIME", "7").to_i.days - end + mattr_accessor :download_your_data_expiry_time, default: Decidim::Env.new("DECIDIM_DOWNLOAD_YOUR_DATA_EXPIRY_TIME", "7").to_i.days # Max requests in a time period to prevent DoS attacks. Only applied on production. - config_accessor :throttling_max_requests do - Decidim::Env.new("DECIDIM_THROTTLING_MAX_REQUESTS", "100").to_i - end + mattr_accessor :throttling_max_requests, default: Decidim::Env.new("DECIDIM_THROTTLING_MAX_REQUESTS", "100").to_i # Time window in which the throttling is applied. - config_accessor :throttling_period do - Decidim::Env.new("DECIDIM_THROTTLING_PERIOD", "1").to_i.minutes - end + mattr_accessor :throttling_period, default: Decidim::Env.new("DECIDIM_THROTTLING_PERIOD", "1").to_i.minutes # Time window were users can access the website even if their email is not confirmed. - config_accessor :unconfirmed_access_for do - Decidim::Env.new("DECIDIM_UNCONFIRMED_ACCESS_FOR", "0").to_i.days - end + mattr_accessor :unconfirmed_access_for, default: Decidim::Env.new("DECIDIM_UNCONFIRMED_ACCESS_FOR", "0").to_i.days # Allow machine translations - config_accessor :enable_machine_translations do - Decidim::Env.new("DECIDIM_ENABLE_MACHINE_TRANSLATION", false).present? - end + mattr_accessor :enable_machine_translations, default: Decidim::Env.new("DECIDIM_ENABLE_MACHINE_TRANSLATION", false).present? # How long can a user remained logged in before the session expires. Notice that # this is also maximum time that user can idle before getting automatically signed out. - config_accessor :expire_session_after do - Decidim::Env.new("DECIDIM_EXPIRE_SESSION_AFTER", "30").to_i.minutes - end + mattr_accessor :expire_session_after, default: Decidim::Env.new("DECIDIM_EXPIRE_SESSION_AFTER", "30").to_i.minutes # Defines how long the OAuth access tokens and API access tokens are valid. # Defaults to the default value as defined in Doorkeeper. - config_accessor :oauth_access_token_expires_in do - Decidim::Env.new("DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN", "120").to_i.minutes - end + mattr_accessor :oauth_access_token_expires_in, default: Decidim::Env.new("DECIDIM_OAUTH_ACCESS_TOKEN_EXPIRES_IN", "120").to_i.minutes # If set to true, users have option to "remember me". Notice that expire_session_after will not take # effect when the user wants to be remembered. - config_accessor :enable_remember_me do - if Decidim::Env.new("DECIDIM_ENABLE_REMEMBER_ME", "auto").default_or_present_if_exists.to_s == "auto" - true - else - Decidim::Env.new("DECIDIM_ENABLE_REMEMBER_ME", "auto").default_or_present_if_exists - end - end + mattr_accessor :enable_remember_me + self.enable_remember_me ||= if Decidim::Env.new("DECIDIM_ENABLE_REMEMBER_ME", "auto").default_or_present_if_exists.to_s == "auto" + true + else + Decidim::Env.new("DECIDIM_ENABLE_REMEMBER_ME", "auto").default_or_present_if_exists + end # Defines how often session_timeouter.js checks time between current moment and last request - config_accessor :session_timeout_interval do - Decidim::Env.new("DECIDIM_SESSION_TIMEOUT_INTERVAL", "10").to_i.seconds - end + mattr_accessor :session_timeout_interval, default: Decidim::Env.new("DECIDIM_SESSION_TIMEOUT_INTERVAL", "10").to_i.seconds # Exposes a configuration option: an object to configure Etherpad - config_accessor :etherpad do - if Decidim::Env.new("ETHERPAD_SERVER").present? && Decidim::Env.new("ETHERPAD_API_KEY").present? - { - server: Decidim::Env.new("ETHERPAD_SERVER").to_s, - api_key: Decidim::Env.new("ETHERPAD_API_KEY").to_s, - api_version: Decidim::Env.new("ETHERPAD_API_VERSION", "1.2.1").to_s - } - end - end + mattr_accessor :etherpad + self.etherpad ||= if Decidim::Env.new("ETHERPAD_SERVER").present? && Decidim::Env.new("ETHERPAD_API_KEY").present? + { + server: Decidim::Env.new("ETHERPAD_SERVER").to_s, + api_key: Decidim::Env.new("ETHERPAD_API_KEY").to_s, + api_version: Decidim::Env.new("ETHERPAD_API_VERSION", "1.2.1").to_s + } + end # A base path for the uploads. If set, make sure it ends in a slash. # Uploads will be set to `/uploads/`. This can be useful if you # want to use the same uploads place for both staging and production # environments, but in different folders. - config_accessor :base_uploads_path do - (Decidim::Env.new("DECIDIM_BASE_UPLOADS_PATH").presence&.to_s) - end + mattr_accessor :base_uploads_path, default: (Decidim::Env.new("DECIDIM_BASE_UPLOADS_PATH").presence&.to_s) # The name of the class to deliver SMS codes to users. # # Check the example in `decidim-verifications`. - config_accessor :sms_gateway_service do - Decidim::Env.new("DECIDIM_SMS_GATEWAY_SERVICE", nil).value - end + mattr_accessor :sms_gateway_service, default: Decidim::Env.new("DECIDIM_SMS_GATEWAY_SERVICE", nil).value # The name of the class used to generate a timestamp from a document. # # Check the example in `decidim-initiatives` - config_accessor :timestamp_service do - Decidim::Env.new("DECIDIM_TIMESTAMP_SERVICE", nil).value - end + mattr_accessor :timestamp_service, default: Decidim::Env.new("DECIDIM_TIMESTAMP_SERVICE", nil).value # The name of the class used to process a pdf and add a signature to the # document. # # Check the example in `decidim-initiatives` - config_accessor :pdf_signature_service do - Decidim::Env.new("DECIDIM_PDF_SIGNATURE_SERVICE", nil).value - end + mattr_accessor :pdf_signature_service, default: Decidim::Env.new("DECIDIM_PDF_SIGNATURE_SERVICE", nil).value # The name of the class to translate user content. # - config_accessor :machine_translation_service do - Decidim::Env.new("DECIDIM_MACHINE_TRANSLATION_SERVICE", nil).value - end + mattr_accessor :machine_translation_service, default: Decidim::Env.new("DECIDIM_MACHINE_TRANSLATION_SERVICE", nil).value - config_accessor :maximum_attachment_size do - Decidim::Env.new("DECIDIM_MAXIMUM_ATTACHMENT_SIZE", "10").to_i - end + mattr_accessor :maximum_attachment_size, default: Decidim::Env.new("DECIDIM_MAXIMUM_ATTACHMENT_SIZE", "10").to_i - config_accessor :maximum_avatar_size do - Decidim::Env.new("DECIDIM_MAXIMUM_AVATAR_SIZE", "5").to_i - end + mattr_accessor :maximum_avatar_size, default: Decidim::Env.new("DECIDIM_MAXIMUM_AVATAR_SIZE", "5").to_i # Social Networking services used for social sharing - config_accessor :social_share_services do - Decidim::Env.new("DECIDIM_SOCIAL_SHARE_SERVICES", "X, Facebook, WhatsApp, Telegram").to_array - end + mattr_accessor :social_share_services, default: Decidim::Env.new("DECIDIM_SOCIAL_SHARE_SERVICES", "X, Facebook, WhatsApp, Telegram").to_array # The Decidim::Exporters::CSV's default column separator - config_accessor :default_csv_col_sep do - Decidim::Env.new("DECIDIM_DEFAULT_CSV_COL_SEP", ";").to_s - end + mattr_accessor :default_csv_col_sep, default: Decidim::Env.new("DECIDIM_DEFAULT_CSV_COL_SEP", ";").to_s # Exposes a configuration option: HTTP_X_FORWARDED_HOST header follow-up. # If a caching system is in place, it can also allow cache and log poisoning attacks, # allowing attackers to control the contents of caches and logs that could be used for other attacks. - config_accessor :follow_http_x_forwarded_host do - Decidim::Env.new("DECIDIM_FOLLOW_HTTP_X_FORWARDED_HOST").present? - end + mattr_accessor :follow_http_x_forwarded_host, default: Decidim::Env.new("DECIDIM_FOLLOW_HTTP_X_FORWARDED_HOST").present? # The list of roles a user can have, not considering the space-specific roles. - config_accessor :user_roles do - Decidim::Env.new("DECIDIM_USER_ROLES", "admin,user_manager").to_array - end + mattr_accessor :user_roles, default: Decidim::Env.new("DECIDIM_USER_ROLES", "admin,user_manager").to_array # The list of visibility options for amendments. An Array of Strings that # serve both as locale keys and values to construct the input collection in @@ -612,21 +533,15 @@ def self.last_warning_inactive_users_after_days # This collection is used in Decidim::Admin::SettingsHelper to generate a # radio buttons collection input field form for a Decidim::Component # step setting :amendments_visibility. - config_accessor :amendments_visibility_options do - Decidim::Env.new("DECIDIM_AMENDMENTS_VISIBILITY_OPTIONS", "all,participants").to_array - end + mattr_accessor :amendments_visibility_options, default: Decidim::Env.new("DECIDIM_AMENDMENTS_VISIBILITY_OPTIONS", "all,participants").to_array # Exposes a configuration option: The maximum length for conversation # messages. - config_accessor :maximum_conversation_message_length do - Decidim::Env.new("DECIDIM_MAXIMUM_CONVERSATION_MESSAGE_LENGTH", 1000).to_i - end + mattr_accessor :maximum_conversation_message_length, default: Decidim::Env.new("DECIDIM_MAXIMUM_CONVERSATION_MESSAGE_LENGTH", 1000).to_i # Defines the name of the cookie used to check if the user has given consent # to store local data in their browser. - config_accessor :consent_cookie_name do - Decidim::Env.new("DECIDIM_CONSENT_COOKIE_NAME", "decidim-consent").to_s - end + mattr_accessor :consent_cookie_name, default: Decidim::Env.new("DECIDIM_CONSENT_COOKIE_NAME", "decidim-consent").to_s # Defines data consent categories. Note that when adding an item you need to # add following i18n entries also (change 'foo' with the name of the data @@ -634,134 +549,104 @@ def self.last_warning_inactive_users_after_days # # layouts.decidim.data_consent.details.items.foo.service # layouts.decidim.data_consent.details.items.foo.description - config_accessor :consent_categories do - [ - { - slug: "essential", - mandatory: true, - items: [ - { - type: "cookie", - name: "_session_id" - }, - { - type: "cookie", - name: Decidim.consent_cookie_name - }, - { - type: "local_storage", - name: "pwaInstallPromptSeen" - } - ] - }, - { - slug: "preferences", - mandatory: false - }, - { - slug: "analytics", - mandatory: false - }, - { - slug: "marketing", - mandatory: false - } - ] - end + mattr_accessor :consent_categories, default: [ + { + slug: "essential", + mandatory: true, + items: [ + { + type: "cookie", + name: "_session_id" + }, + { + type: "cookie", + name: Decidim.consent_cookie_name + }, + { + type: "local_storage", + name: "pwaInstallPromptSeen" + } + ] + }, + { + slug: "preferences", + mandatory: false + }, + { + slug: "analytics", + mandatory: false + }, + { + slug: "marketing", + mandatory: false + } + ] # Denied passwords. Array may contain strings and regex entries. - config_accessor :denied_passwords do - Decidim::Env.new("DECIDIM_DENIED_PASSWORDS").to_array(separator: ", ") - end + mattr_accessor :denied_passwords, default: Decidim::Env.new("DECIDIM_DENIED_PASSWORDS").to_array(separator: ", ") # Ignores strings similar to email / domain on password validation if too short - config_accessor :password_similarity_length do - Decidim::Env.new("DECIDIM_PASSWORD_SIMILARITY_LENGTH", 4).to_i - end + mattr_accessor :password_similarity_length, default: Decidim::Env.new("DECIDIM_PASSWORD_SIMILARITY_LENGTH", 4).to_i # Defines if admins are required to have stronger passwords than other users - config_accessor :admin_password_strong do - Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_STRONG", true).present? - end + mattr_accessor :admin_password_strong, default: Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_STRONG", true).present? - config_accessor :admin_password_expiration_days do - Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_EXPIRATION_DAYS", 90).to_i - end + mattr_accessor :admin_password_expiration_days, default: Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_EXPIRATION_DAYS", 90).to_i - config_accessor :admin_password_min_length do - Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_MIN_LENGTH", 15).to_i - end + mattr_accessor :admin_password_min_length, default: Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_MIN_LENGTH", 15).to_i - config_accessor :admin_password_repetition_times do - Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_REPETITION_TIMES", 5).to_i - end + mattr_accessor :admin_password_repetition_times, default: Decidim::Env.new("DECIDIM_ADMIN_PASSWORD_REPETITION_TIMES", 5).to_i # This is an internal key that allow us to properly configure the caching key separator. This is useful for redis cache store # as it creates some namespaces within the cached data. # use `config.cache_key_separator = ":"` in your initializer to have namespaced data - config_accessor :cache_key_separator do - Decidim::Env.new("DECIDIM_CACHE_KEY_SEPARATOR", "/").to_s - end + mattr_accessor :cache_key_separator, default: Decidim::Env.new("DECIDIM_CACHE_KEY_SEPARATOR", "/").to_s # This is the maximum time that the cache will be stored. If nil, the cache will be stored indefinitely. # Currently, cache is applied in the Cells where the method `cache_hash` is defined. - config_accessor :cache_expiry_time do - Decidim::Env.new("DECIDIM_CACHE_EXPIRATION_TIME", "1440").to_i.minutes - end + mattr_accessor :cache_expiry_time, default: Decidim::Env.new("DECIDIM_CACHE_EXPIRATION_TIME", "1440").to_i.minutes # Same as before, but specifically for cell displaying stats - config_accessor :stats_cache_expiry_time do - Decidim::Env.new("DECIDIM_STATS_CACHE_EXPIRATION_TIME", 10).to_i.minutes - end + mattr_accessor :stats_cache_expiry_time, default: Decidim::Env.new("DECIDIM_STATS_CACHE_EXPIRATION_TIME", 10).to_i.minutes # Enable/Disable the service worker - config_accessor :service_worker_enabled do - Decidim::Env.new("DECIDIM_SERVICE_WORKER_ENABLED", Rails.env.exclude?("development")).present? - end + mattr_accessor :service_worker_enabled, default: Decidim::Env.new("DECIDIM_SERVICE_WORKER_ENABLED", Rails.env.exclude?("development")).present? # List of static pages' slugs that can include content blocks - config_accessor :page_blocks do - Decidim::Env.new("DECIDIM_PAGE_BLOCKS", "terms-of-service").to_array - end + mattr_accessor :page_blocks, default: Decidim::Env.new("DECIDIM_PAGE_BLOCKS", "terms-of-service").to_array # The default max last activity users to be shown - config_accessor :default_max_last_activity_users do - Decidim::Env.new("DECIDIM_DEFAULT_MAX_LAST_ACTIVITY_USERS", 6).to_i - end + mattr_accessor :default_max_last_activity_users, default: Decidim::Env.new("DECIDIM_DEFAULT_MAX_LAST_ACTIVITY_USERS", 6).to_i # List of additional content security policies to be appended to the default ones # This is useful for adding custom CSPs for external services like Here Maps, YouTube, etc. # Read more: https://docs.decidim.org/en/develop/configure/initializer#_content_security_policy - config_accessor :content_security_policies_extra do - {} - end - - config_accessor :omniauth_providers do - { - developer: { - enabled: Rails.env.local?, - icon: "phone-line" - }, - facebook: { - enabled: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_ID").present?, - app_id: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_ID", nil), - app_secret: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_SECRET", nil), - icon_path: "media/images/facebook.svg" - }, - twitter: { - enabled: Decidim::Env.new("OMNIAUTH_TWITTER_API_KEY").present?, - api_key: Decidim::Env.new("OMNIAUTH_TWITTER_API_KEY", nil), - api_secret: Decidim::Env.new("OMNIAUTH_TWITTER_API_SECRET", nil), - icon_path: "media/images/twitter-x.svg" - }, - google_oauth2: { - enabled: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_ID").present?, - icon_path: "media/images/google.svg", - client_id: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_ID", nil), - client_secret: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_SECRET", nil) - } + mattr_accessor :content_security_policies_extra, default: {} + + mattr_accessor :omniauth_providers, default: { + developer: { + enabled: Rails.env.local?, + icon: "phone-line" + }, + facebook: { + enabled: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_ID").present?, + app_id: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_ID", nil).value, + app_secret: Decidim::Env.new("OMNIAUTH_FACEBOOK_APP_SECRET", nil).value, + icon_path: "media/images/facebook.svg" + }, + twitter: { + enabled: Decidim::Env.new("OMNIAUTH_TWITTER_API_KEY").present?, + api_key: Decidim::Env.new("OMNIAUTH_TWITTER_API_KEY", nil).value, + api_secret: Decidim::Env.new("OMNIAUTH_TWITTER_API_SECRET", nil).value, + icon_path: "media/images/twitter-x.svg" + }, + google_oauth2: { + enabled: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_ID").present?, + icon_path: "media/images/google.svg", + client_id: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_ID", nil).value, + client_secret: Decidim::Env.new("OMNIAUTH_GOOGLE_CLIENT_SECRET", nil).value } - end + } CoreDataManifest = Data.define(:name, :collection, :serializer, :include_in_open_data) @@ -1041,15 +926,11 @@ def self.organization_settings(model) # Decidim::MachineTranslationResourceJob due to a ActiveJob::DeserializationError. # In some Decidim Installations, ActiveJob can be configured to discard jobs failing with # ActiveJob::DeserializationError - config_accessor :machine_translation_delay do - 0.seconds - end + mattr_accessor :machine_translation_delay, default: 0.seconds # The etiquette validator is applied to the create and edit forms of Proposals, Meetings, # and Debates for both regular and admin users. - config_accessor :enable_etiquette_validator do - true - end + mattr_accessor :enable_etiquette_validator, default: true def self.machine_translation_service_klass return unless Decidim.enable_machine_translations diff --git a/decidim-dev/lib/decidim/dev.rb b/decidim-dev/lib/decidim/dev.rb index 1124c364c66bb..7a58180893492 100644 --- a/decidim-dev/lib/decidim/dev.rb +++ b/decidim-dev/lib/decidim/dev.rb @@ -22,8 +22,6 @@ module Decidim # create external libraries that create test apps and test themselves against # them. module Dev - include ActiveSupport::Configurable - autoload :DummyTranslator, "decidim/dev/dummy_translator" # Public: Finds an asset. diff --git a/decidim-generators/lib/decidim/generators/test/generator_examples.rb b/decidim-generators/lib/decidim/generators/test/generator_examples.rb index f295ac3faa266..80031621d27ec 100644 --- a/decidim-generators/lib/decidim/generators/test/generator_examples.rb +++ b/decidim-generators/lib/decidim/generators/test/generator_examples.rb @@ -675,80 +675,67 @@ it "env vars generate secrets application" do expect(result[1]).to be_success, result[0] - # Test onto the initializer when ENV vars are empty strings or undefined - json_off = initializer_config_for(test_app, env_off) initializer_off.each do |key, value| - current = json_off[key] + # Test onto the initializer when ENV vars are empty strings or undefined + current = rails_value("Decidim.#{key}", test_app, env_off) expect(current).to eq(value), "Initializer (#{key}) = (#{current}) expected to match Env:OFF (#{value})" - end - # Test onto the initializer when ENV vars are set to the string "false" - json_false = initializer_config_for(test_app, env_false) - initializer_off.each do |key, value| - current = json_false[key] + # Test onto the initializer when ENV vars are set to the string "false" + current = rails_value("Decidim.#{key}", test_app, env_false) expect(current).to eq(value), "Initializer (#{key}) = (#{current}) expected to match Env:FALSE (#{value})" end # Test onto the initializer when ENV vars are set - json_on = initializer_config_for(test_app, env_on) initializer_on.each do |key, value| - current = json_on[key] + current = rails_value("Decidim.#{key}", test_app, env_on) expect(current).to eq(value), "Initializer (#{key}) = (#{current}) expected to match Env:ON (#{value})" end # Test onto the initializer when ENV vars are set to OpenStreetMap configuration - json_on = initializer_config_for(test_app, env_maps_osm) initializer_maps_osm.each do |key, value| - current = json_on[key] + current = rails_value("Decidim.#{key}", test_app, env_maps_osm) expect(current).to eq(value), "Initializer (#{key}) = (#{current}) expected to match Env:Maps OSM (#{value})" end # Test onto the initializer when ENV vars are set to OpenStreetMap-HERE mix configuration - json_on = initializer_config_for(test_app, env_maps_mix) initializer_maps_mix.each do |key, value| - current = json_on[key] + current = rails_value("Decidim.#{key}", test_app, env_maps_mix) expect(current).to eq(value), "Initializer (#{key}) = (#{current}) expected to match Env:Maps MIX (#{value})" end # Test onto the initializer with ENV vars OFF for the API module - json_off = initializer_config_for(test_app, env_off, "Decidim::Api") api_initializer_off.each do |key, value| - current = json_off[key] + current = rails_value("Decidim::Api.#{key}", test_app, env_off) expect(current).to eq(value), "API Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars ON for the API module - json_on = initializer_config_for(test_app, env_on, "Decidim::Api") api_initializer_on.each do |key, value| - current = json_on[key] + current = rails_value("Decidim::Api.#{key}", test_app, env_on) expect(current).to eq(value), "API Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars OFF for the Proposals module - json_off = initializer_config_for(test_app, env_off, "Decidim::Proposals") proposals_initializer_off.each do |key, value| - current = json_off[key] + current = rails_value("Decidim::Proposals.#{key}", test_app, env_off) expect(current).to eq(value), "Proposals Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars ON for the Proposals module - json_on = initializer_config_for(test_app, env_on, "Decidim::Proposals") proposals_initializer_on.each do |key, value| - current = json_on[key] + current = rails_value("Decidim::Proposals.#{key}", test_app, env_on) expect(current).to eq(value), "Proposals Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars OFF for the Meetings module - json_off = initializer_config_for(test_app, env_off, "Decidim::Meetings") meetings_initializer_off.each do |key, value| - current = json_off[key] + current = rails_value("Decidim::Meetings.#{key}", test_app, env_off) expect(current).to eq(value), "Meetings Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars ON for the Meetings module - json_on = initializer_config_for(test_app, env_on, "Decidim::Meetings") meetings_initializer_on.each do |key, value| - current = json_on[key] + current = rails_value("Decidim::Meetings.#{key}", test_app, env_on) expect(current).to eq(value), "Meetings Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end @@ -803,16 +790,14 @@ expect(result[1]).to be_success, result[0] # Test onto the initializer with ENV vars OFF for the Initiatives module - json_off = initializer_config_for(test_app, env_off, "Decidim::Initiatives") initiatives_initializer_off.each do |key, value| - current = json_off[key] + current = rails_value("Decidim::Initiatives.#{key}", test_app, env_off) expect(current).to eq(value), "Initiatives Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end # Test onto the initializer with ENV vars ON for the Initiatives module - json_on = initializer_config_for(test_app, env_on, "Decidim::Initiatives") initiatives_initializer_on.each do |key, value| - current = json_on[key] + current = rails_value("Decidim::Initiatives.#{key}", test_app, env_on) expect(current).to eq(value), "Initiatives Initializer (#{key}) = (#{current}) expected to match Env (#{value})" end end @@ -891,10 +876,6 @@ end end -def initializer_config_for(path, env, mod = "Decidim") - JSON.parse cmd_capture(path, "bin/rails runner 'puts #{mod}.config.to_json'", env:) -end - def rails_value(value, path, env) JSON.parse cmd_capture(path, "bin/rails runner 'puts #{value}.to_json'", env:) end diff --git a/decidim-initiatives/lib/decidim/initiatives.rb b/decidim-initiatives/lib/decidim/initiatives.rb index 76782ec752893..097c03c2fca1c 100644 --- a/decidim-initiatives/lib/decidim/initiatives.rb +++ b/decidim-initiatives/lib/decidim/initiatives.rb @@ -17,87 +17,67 @@ module Initiatives autoload :ApplicationFormPDF, "decidim/initiatives/application_form_pdf" autoload :ValidatableAuthorizations, "decidim/initiatives/validatable_authorizations" - include ActiveSupport::Configurable + class << self + def config = self + + def configure + yield self + end + end # Public setting that defines whether creation is allowed to any validated # user or not. Defaults to true. - config_accessor :creation_enabled do - Decidim::Env.new("INITIATIVES_CREATION_ENABLED", "auto").present? - end + mattr_accessor :creation_enabled, default: Decidim::Env.new("INITIATIVES_CREATION_ENABLED", "auto").present? # Minimum number of committee members required to pass the initiative to # technical validation phase. Only applies to initiatives created by # individuals. - config_accessor :minimum_committee_members do - Decidim::Env.new("INITIATIVES_MINIMUM_COMMITTEE_MEMBERS", 2).to_i - end + mattr_accessor :minimum_committee_members, default: Decidim::Env.new("INITIATIVES_MINIMUM_COMMITTEE_MEMBERS", 2).to_i # Number of days available to collect supports after an initiative has been # published. - config_accessor :default_signature_time_period_length do - Decidim::Env.new("INITIATIVES_DEFAULT_SIGNATURE_TIME_PERIOD_LENGTH", 120).to_i - end + mattr_accessor :default_signature_time_period_length, default: Decidim::Env.new("INITIATIVES_DEFAULT_SIGNATURE_TIME_PERIOD_LENGTH", 120).to_i # Components enabled for a new initiative - config_accessor :default_components do - Decidim::Env.new("INITIATIVES_DEFAULT_COMPONENTS", "pages, meetings, blogs").to_array - end + mattr_accessor :default_components, default: Decidim::Env.new("INITIATIVES_DEFAULT_COMPONENTS", "pages, meetings, blogs").to_array # Notifies when the given percentage of supports is reached for an # initiative. - config_accessor :first_notification_percentage do - Decidim::Env.new("INITIATIVES_FIRST_NOTIFICATION_PERCENTAGE", 33).to_i - end + mattr_accessor :first_notification_percentage, default: Decidim::Env.new("INITIATIVES_FIRST_NOTIFICATION_PERCENTAGE", 33).to_i # Notifies when the given percentage of supports is reached for an # initiative. - config_accessor :second_notification_percentage do - Decidim::Env.new("INITIATIVES_SECOND_NOTIFICATION_PERCENTAGE", 66).to_i - end + mattr_accessor :second_notification_percentage, default: Decidim::Env.new("INITIATIVES_SECOND_NOTIFICATION_PERCENTAGE", 66).to_i # Sets the expiration time for the statistic data. - config_accessor :stats_cache_expiration_time do - Decidim::Env.new("INITIATIVES_STATS_CACHE_EXPIRATION_TIME", 5).to_i.minutes - end + mattr_accessor :stats_cache_expiration_time, default: Decidim::Env.new("INITIATIVES_STATS_CACHE_EXPIRATION_TIME", 5).to_i.minutes # Maximum amount of time in validating state. # After this time the initiative will be moved to # discarded state. - config_accessor :max_time_in_validating_state do - Decidim::Env.new("INITIATIVES_MAX_TIME_IN_VALIDATING_STATE", 60).to_i.days - end + mattr_accessor :max_time_in_validating_state, default: Decidim::Env.new("INITIATIVES_MAX_TIME_IN_VALIDATING_STATE", 60).to_i.days # Print functionality enabled. Allows the user to get # a printed version of the initiative from the administration # panel. - config_accessor :print_enabled do - Decidim::Env.new("INITIATIVES_PRINT_ENABLED", "auto").to_s == "true" - end + mattr_accessor :print_enabled, default: Decidim::Env.new("INITIATIVES_PRINT_ENABLED", "auto").to_s == "true" # Set a service to generate a timestamp on each vote. The # attribute is the name of a class whose instances are # initialized with a string containing the data to be # timestamped and respond to a timestamp method - config_accessor :timestamp_service do - Decidim::Env.new("DECIDIM_TIMESTAMP_SERVICE", nil).value - end + mattr_accessor :timestamp_service, default: Decidim::Env.new("DECIDIM_TIMESTAMP_SERVICE", nil).value # Set a service to add a signature to pdf of signatures. # The attribute is the name of a class whose instances are # initialized with the document to be signed and respond to a # signed_pdf method with the signature added - config_accessor :pdf_signature_service do - Decidim::Env.new("DECIDIM_PDF_SIGNATURE_SERVICE", nil).value - end + mattr_accessor :pdf_signature_service, default: Decidim::Env.new("DECIDIM_PDF_SIGNATURE_SERVICE", nil).value # This flag allows creating authorizations to unauthorized users. - config_accessor :do_not_require_authorization do - Decidim::Env.new("INITIATIVES_DO_NOT_REQUIRE_AUTHORIZATION").present? - end + mattr_accessor :do_not_require_authorization, default: Decidim::Env.new("INITIATIVES_DO_NOT_REQUIRE_AUTHORIZATION").present? # Encryption secret to use with signatures metadata - config_accessor :signature_handler_encryption_secret do - Decidim::Env.new("INITIATIVES_SIGNATURE_HANDLER_ENCRYPTION_SECRET", "personal user metadata").to_s - end + mattr_accessor :signature_handler_encryption_secret, default: Decidim::Env.new("INITIATIVES_SIGNATURE_HANDLER_ENCRYPTION_SECRET", "personal user metadata").to_s end end diff --git a/decidim-meetings/lib/decidim/meetings.rb b/decidim-meetings/lib/decidim/meetings.rb index 70cb31297b36e..8431be49a1e12 100644 --- a/decidim-meetings/lib/decidim/meetings.rb +++ b/decidim-meetings/lib/decidim/meetings.rb @@ -17,20 +17,20 @@ module Meetings autoload :UserResponsesSerializer, "decidim/meetings/user_responses_serializer" autoload :SchemaOrgEventMeetingSerializer, "decidim/meetings/schema_org_event_meeting_serializer" - include ActiveSupport::Configurable + class << self + def config = self - # Public Setting that defines the interval when the upcoming meeting will be sent - config_accessor :upcoming_meeting_notification do - Decidim::Env.new("MEETINGS_UPCOMING_MEETING_NOTIFICATION", 2).to_i.days + def configure + yield self + end end - config_accessor :embeddable_services do - Decidim::Env.new("MEETINGS_EMBEDDABLE_SERVICES", "www.youtube.com www.twitch.tv meet.jit.si").to_array(separator: " ") - end + # Public Setting that defines the interval when the upcoming meeting will be sent + mattr_accessor :upcoming_meeting_notification, default: Decidim::Env.new("MEETINGS_UPCOMING_MEETING_NOTIFICATION", 2).to_i.days - config_accessor :waiting_list_enabled do - Decidim::Env.new("MEETINGS_WAITING_LIST_ENABLED", true).present? - end + mattr_accessor :embeddable_services, default: Decidim::Env.new("MEETINGS_EMBEDDABLE_SERVICES", "www.youtube.com www.twitch.tv meet.jit.si").to_array(separator: " ") + + mattr_accessor :waiting_list_enabled, default: Decidim::Env.new("MEETINGS_WAITING_LIST_ENABLED", true).present? end module ContentParsers diff --git a/decidim-proposals/lib/decidim/proposals.rb b/decidim-proposals/lib/decidim/proposals.rb index 92fcd7af32f3c..1bc4b4b9e9dd9 100644 --- a/decidim-proposals/lib/decidim/proposals.rb +++ b/decidim-proposals/lib/decidim/proposals.rb @@ -23,19 +23,21 @@ module Proposals autoload :OdtToMarkdown, "decidim/proposals/odt_to_markdown" autoload :Evaluable, "decidim/proposals/evaluable" - include ActiveSupport::Configurable + class << self + def config = self + + def configure + yield self + end + end # Public Setting that defines how many proposals will be shown in the # participatory_space_highlighted_elements view hook - config_accessor :participatory_space_highlighted_proposals_limit do - Decidim::Env.new("PROPOSALS_PARTICIPATORY_SPACE_HIGHLIGHTED_PROPOSALS_LIMIT", 4).to_i - end + mattr_accessor :participatory_space_highlighted_proposals_limit, default: Decidim::Env.new("PROPOSALS_PARTICIPATORY_SPACE_HIGHLIGHTED_PROPOSALS_LIMIT", 4).to_i # Public Setting that defines how many proposals will be shown in the # process_group_highlighted_elements view hook - config_accessor :process_group_highlighted_proposals_limit do - Decidim::Env.new("PROPOSALS_PROCESS_GROUP_HIGHLIGHTED_PROPOSALS_LIMIT", 3).to_i - end + mattr_accessor :process_group_highlighted_proposals_limit, default: Decidim::Env.new("PROPOSALS_PROCESS_GROUP_HIGHLIGHTED_PROPOSALS_LIMIT", 3).to_i def self.proposal_states_colors { diff --git a/decidim-system/lib/decidim/system.rb b/decidim-system/lib/decidim/system.rb index 3267f8718c740..73acc7848b8a3 100644 --- a/decidim-system/lib/decidim/system.rb +++ b/decidim-system/lib/decidim/system.rb @@ -9,11 +9,15 @@ module Decidim # eye view of the whole system. # module System - include ActiveSupport::Configurable + class << self + def config = self - # The length of API secrets generated for API users. - config_accessor :api_users_secret_length do - ENV.fetch("DECIDIM_SYSTEM_API_USERS_SECRET_LENGTH", 32) + def configure + yield self + end end + + # The length of API secrets generated for API users. + mattr_accessor :api_users_secret_length, default: ENV.fetch("DECIDIM_SYSTEM_API_USERS_SECRET_LENGTH", 32).to_i end end diff --git a/decidim-verifications/lib/decidim/verifications.rb b/decidim-verifications/lib/decidim/verifications.rb index 8e09001ce910f..c358cb4e46241 100644 --- a/decidim-verifications/lib/decidim/verifications.rb +++ b/decidim-verifications/lib/decidim/verifications.rb @@ -27,10 +27,14 @@ def self.authorization_handlers end module Verifications - include ActiveSupport::Configurable + class << self + def config = self - config_accessor :document_types do - Decidim::Env.new("VERIFICATIONS_DOCUMENT_TYPES", "identification_number,passport").to_array + def configure + yield self + end end + + mattr_accessor :document_types, default: Decidim::Env.new("VERIFICATIONS_DOCUMENT_TYPES", "identification_number,passport").to_array end end From a3753c7374e002a10c08f02fcc8b26e4dcd93bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 18 Mar 2026 10:14:33 +0100 Subject: [PATCH 119/135] Fix meetings end date when it is multiyear (#16393) Co-authored-by: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> --- .../app/packs/stylesheets/decidim/_cards.scss | 2 +- .../assets/tailwind/tailwind.config.js.erb | 2 +- .../decidim/meetings/dates_and_map/show.erb | 4 + .../decidim/meetings/dates_and_map_cell.rb | 20 +- .../decidim/meetings/meeting_l/image.erb | 4 + .../cells/decidim/meetings/meeting_l_cell.rb | 6 + .../meetings/dates_and_map_cell_spec.rb | 189 ++++++++++++++++++ .../decidim/meetings/meeting_l_cell_spec.rb | 73 +++++++ 8 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 decidim-meetings/spec/cells/decidim/meetings/dates_and_map_cell_spec.rb diff --git a/decidim-core/app/packs/stylesheets/decidim/_cards.scss b/decidim-core/app/packs/stylesheets/decidim/_cards.scss index 0f717c986c397..589af50bbdfe5 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_cards.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_cards.scss @@ -128,7 +128,7 @@ } &-day { - @apply text-black text-2xl font-bold; + @apply text-black text-xl font-semibold; } &-year { diff --git a/decidim-core/lib/decidim/assets/tailwind/tailwind.config.js.erb b/decidim-core/lib/decidim/assets/tailwind/tailwind.config.js.erb index 05c20574eaa06..ab4ebdf43b876 100644 --- a/decidim-core/lib/decidim/assets/tailwind/tailwind.config.js.erb +++ b/decidim-core/lib/decidim/assets/tailwind/tailwind.config.js.erb @@ -68,7 +68,7 @@ module.exports = { sans: ["Source Sans Pro", "ui-sans-serif", "system-ui", "sans-serif"] }, fontSize: { - xs: ["13px", "16px"], + xs: ["12px", "16px"], sm: ["14px", "18px"], md: ["16px", "20px"], lg: ["18px", "23px"], diff --git a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb index e5b8d8a5d0222..aecf60d9d6450 100644 --- a/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/dates_and_map/show.erb @@ -16,6 +16,10 @@
<%= year %> + <% unless same_year? %> + - + <%= end_year %> + <% end %>
diff --git a/decidim-meetings/app/cells/decidim/meetings/dates_and_map_cell.rb b/decidim-meetings/app/cells/decidim/meetings/dates_and_map_cell.rb index ea48a3038d187..0f14269da4ede 100644 --- a/decidim-meetings/app/cells/decidim/meetings/dates_and_map_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/dates_and_map_cell.rb @@ -21,14 +21,30 @@ def year l model.start_time, format: "%Y" end + def end_year + return nil if model.end_time.blank? + + l model.end_time, format: "%Y" + end + private def same_month? - start_time.month == end_time.month + return true if end_time.blank? + + start_time.year == end_time.year && start_time.month == end_time.month end def same_day? - start_time.day == end_time.day + return true if end_time.blank? + + start_time.to_date == end_time.to_date + end + + def same_year? + return true if end_time.blank? + + start_time.year == end_time.year end def display_map? diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb index 9d51b9c789dfd..977d2cc7454ef 100644 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb +++ b/decidim-meetings/app/cells/decidim/meetings/meeting_l/image.erb @@ -15,5 +15,9 @@ <%= l(meeting.start_time, format: "%Y") %> + <% unless same_year? %> + - + <%= l(meeting.end_time, format: "%Y") %> + <% end %> diff --git a/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb b/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb index c6133ac07db81..e9d59ae479c6e 100644 --- a/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb +++ b/decidim-meetings/app/cells/decidim/meetings/meeting_l_cell.rb @@ -48,6 +48,12 @@ def same_day? meeting.start_time.to_date == meeting.end_time.to_date end + def same_year? + return true if meeting.end_time.blank? + + meeting.start_time.year == meeting.end_time.year + end + def metadata_cell "decidim/meetings/meeting_card_metadata" end diff --git a/decidim-meetings/spec/cells/decidim/meetings/dates_and_map_cell_spec.rb b/decidim-meetings/spec/cells/decidim/meetings/dates_and_map_cell_spec.rb new file mode 100644 index 0000000000000..d004677644f2d --- /dev/null +++ b/decidim-meetings/spec/cells/decidim/meetings/dates_and_map_cell_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Meetings + describe DatesAndMapCell, type: :cell do + controller Decidim::Meetings::MeetingsController + + subject { my_cell.call } + + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 4, 5, 0), end_time: Time.new(2020, 10, 15, 12, 0, 0, 0)) } + let(:my_cell) { cell("decidim/meetings/dates_and_map", meeting) } + + context "when rendering" do + it "renders the calendar container" do + expect(subject).to have_css(".meeting__calendar-container") + end + + it "shows the start time's month" do + expect(subject).to have_css(".meeting__calendar-month", text: "October") + end + + it "shows the start time's day" do + expect(subject).to have_css(".meeting__calendar-day", text: "15") + end + + it "shows the start time's year" do + expect(subject).to have_css(".meeting__calendar-year", text: "2020") + end + + it "does not show separator" do + expect(subject).to have_no_css(".meeting__calendar-separator") + end + end + + context "when meeting spans multiple days in the same month" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: Time.new(2020, 10, 17, 12, 0, 0, 0)) } + + it "shows the start day" do + expect(subject).to have_css(".meeting__calendar-day", text: "15") + end + + it "shows the end day" do + expect(subject).to have_css(".meeting__calendar-day", text: "17") + end + + it "shows the separator" do + expect(subject).to have_css(".meeting__calendar-separator") + end + end + + context "when meeting spans multiple months" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: Time.new(2020, 11, 17, 12, 0, 0, 0)) } + + it "shows the start month" do + expect(subject).to have_css(".meeting__calendar-month", text: "Oct") + end + + it "shows the end month" do + expect(subject).to have_css(".meeting__calendar-month", text: "Nov") + end + + it "shows the start day" do + expect(subject).to have_css(".meeting__calendar-day", text: "15") + end + + it "shows the end day" do + expect(subject).to have_css(".meeting__calendar-day", text: "17") + end + + it "shows month separator" do + expect(subject).to have_css(".meeting__calendar-separator") + end + end + + context "when meeting spans multiple years" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 12, 15, 10, 0, 0, 0), end_time: Time.new(2021, 1, 17, 12, 0, 0, 0)) } + + it "shows the start year" do + expect(subject).to have_css(".meeting__calendar-year", text: "2020") + end + + it "shows the end year" do + expect(subject).to have_css(".meeting__calendar-year", text: "2021") + end + + it "shows the separator" do + expect(subject).to have_css(".meeting__calendar-separator") + end + end + + context "when meeting spans same month and day across different years" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2024, 1, 15, 10, 0, 0, 0), end_time: Time.new(2025, 1, 15, 12, 0, 0, 0)) } + + it "shows the start year" do + expect(subject).to have_css(".meeting__calendar-year", text: "2024") + end + + it "shows the end year" do + expect(subject).to have_css(".meeting__calendar-year", text: "2025") + end + + it "shows year separator" do + expect(subject).to have_css(".meeting__calendar-separator") + end + end + + describe "#end_year" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/dates_and_map", meeting) } + + it "returns nil when end_time is blank" do + expect(my_cell.end_year).to be_nil + end + + it "returns formatted year when end_time is present" do + meeting.update!(end_time: Time.new(2021, 5, 20, 12, 0, 0, 0)) + expect(my_cell.end_year).to eq("2021") + end + end + + describe "#same_month?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/dates_and_map", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_month?)).to be true + end + + it "returns true when same month" do + meeting.update!(end_time: Time.new(2020, 10, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_month?)).to be true + end + + it "returns false when different months" do + meeting.update!(end_time: Time.new(2020, 11, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_month?)).to be false + end + + it "returns false when same month but different years" do + meeting.update!(end_time: Time.new(2021, 10, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_month?)).to be false + end + end + + describe "#same_day?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/dates_and_map", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_day?)).to be true + end + + it "returns true when same day" do + meeting.update!(end_time: Time.new(2020, 10, 15, 18, 0, 0, 0)) + expect(my_cell.send(:same_day?)).to be true + end + + it "returns false when different days" do + meeting.update!(end_time: Time.new(2020, 10, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_day?)).to be false + end + + it "returns false when same day but different years" do + meeting.update!(end_time: Time.new(2021, 10, 15, 12, 0, 0, 0)) + expect(my_cell.send(:same_day?)).to be false + end + end + + describe "#same_year?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/dates_and_map", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_year?)).to be true + end + + it "returns true when same year" do + meeting.update!(end_time: Time.new(2020, 12, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_year?)).to be true + end + + it "returns false when different years" do + meeting.update!(end_time: Time.new(2021, 1, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_year?)).to be false + end + end + end +end diff --git a/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb b/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb index fb3531db9de88..9a7fb5eff059f 100644 --- a/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb +++ b/decidim-meetings/spec/cells/decidim/meetings/meeting_l_cell_spec.rb @@ -94,6 +94,22 @@ module Decidim::Meetings end end + context "when meeting spans multiple years" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 12, 15, 10, 0, 0, 0), end_time: Time.new(2021, 1, 17, 12, 0, 0, 0)) } + + it "shows the start year" do + expect(subject).to have_css(".card__calendar-year", text: "2020") + end + + it "shows the end year" do + expect(subject).to have_css(".card__calendar-year", text: "2021") + end + + it "shows the separator" do + expect(subject).to have_css(".card__calendar-separator") + end + end + context "when title contains special html entities" do let!(:original_title) { meeting.title["en"] } @@ -123,5 +139,62 @@ module Decidim::Meetings expect(subject).to have_content(decidim_escape_translated(meeting.component.participatory_space.title)) end end + + describe "#same_month?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/meeting_l", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_month?)).to be true + end + + it "returns true when same month" do + meeting.update!(end_time: Time.new(2020, 10, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_month?)).to be true + end + + it "returns false when different months" do + meeting.update!(end_time: Time.new(2020, 11, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_month?)).to be false + end + end + + describe "#same_day?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/meeting_l", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_day?)).to be true + end + + it "returns true when same day" do + meeting.update!(end_time: Time.new(2020, 10, 15, 18, 0, 0, 0)) + expect(my_cell.send(:same_day?)).to be true + end + + it "returns false when different days" do + meeting.update!(end_time: Time.new(2020, 10, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_day?)).to be false + end + end + + describe "#same_year?" do + let!(:meeting) { create(:meeting, :published, start_time: Time.new(2020, 10, 15, 10, 0, 0, 0), end_time: nil) } + let(:my_cell) { cell("decidim/meetings/meeting_l", meeting) } + + it "returns true when end_time is blank" do + expect(my_cell.send(:same_year?)).to be true + end + + it "returns true when same year" do + meeting.update!(end_time: Time.new(2020, 12, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_year?)).to be true + end + + it "returns false when different years" do + meeting.update!(end_time: Time.new(2021, 1, 20, 12, 0, 0, 0)) + expect(my_cell.send(:same_year?)).to be false + end + end end end From 1c73aec16e3d16757283a558f9dd431a4db50fe1 Mon Sep 17 00:00:00 2001 From: Lucas Carrias Date: Wed, 18 Mar 2026 06:31:28 -0300 Subject: [PATCH 120/135] Fix budgets list still displayed empty after voting on all budgets (#16406) * fix: exclude progress budgets from non_voted_budgets query * improve test readbility and coverage --- .../decidim/budgets/budgets_list_cell.rb | 3 +- .../decidim/budgets/budgets_list_cell_spec.rb | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/decidim-budgets/app/cells/decidim/budgets/budgets_list_cell.rb b/decidim-budgets/app/cells/decidim/budgets/budgets_list_cell.rb index e5f196d91df65..5167807fcb88a 100644 --- a/decidim-budgets/app/cells/decidim/budgets/budgets_list_cell.rb +++ b/decidim-budgets/app/cells/decidim/budgets/budgets_list_cell.rb @@ -35,7 +35,8 @@ def progress_budgets end def non_voted_budgets - budgets.where.not(id: voted.map(&:id)) + budgets_to_exclude = progress_budgets + voted + budgets.where.not(id: budgets_to_exclude.map(&:id)) end def highlighted? diff --git a/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb b/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb index b16e03f5b2727..57b008abedf97 100644 --- a/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb +++ b/decidim-budgets/spec/cells/decidim/budgets/budgets_list_cell_spec.rb @@ -69,5 +69,43 @@ module Decidim::Budgets end end end + + describe "#show" do + subject { my_cell.call(:show) } + + context "when some budgets have not been voted" do + before do + allow(my_cell).to receive(:non_voted_budgets).and_return(budgets) + end + + it "renders budgets list" do + expect(subject).to have_css("#budgets") + expect(subject).to have_content("2 budgets") + end + end + + context "when all budgets have been voted" do + before do + allow(my_cell).to receive(:non_voted_budgets).and_return([]) + end + + it "does not render budgets list" do + expect(subject).to have_no_css("#budgets") + expect(subject).to have_no_content("0 budgets") + end + end + + context "when all budgets are either voted or in progress" do + before do + allow(my_cell).to receive(:voted).and_return([budgets.first]) + allow(my_cell).to receive(:progress_budgets).and_return([budgets.last]) + end + + it "does not render budgets list" do + expect(subject).to have_no_css("#budgets") + expect(subject).to have_no_content("0 budgets") + end + end + end end end From 2109351924f653ba557c2b9cac73dcf3bfc8fd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 18 Mar 2026 11:54:12 +0100 Subject: [PATCH 121/135] Split proposals import specs (#16404) Co-authored-by: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> --- .../admin/import_proposal_answers_spec.rb | 43 ----- ...mport_proposals_answers_from_file_spec.rb} | 40 ++++- ...t_proposals_from_another_component_spec.rb | 115 ++++++++++++++ .../admin/import_proposals_from_file_spec.rb | 43 +++++ .../system/admin/import_proposals_spec.rb | 149 ------------------ 5 files changed, 197 insertions(+), 193 deletions(-) delete mode 100644 decidim-proposals/spec/system/admin/import_proposal_answers_spec.rb rename decidim-proposals/spec/{shared/admin_manages_proposal_answer_imports_examples.rb => system/admin/import_proposals_answers_from_file_spec.rb} (83%) create mode 100644 decidim-proposals/spec/system/admin/import_proposals_from_another_component_spec.rb create mode 100644 decidim-proposals/spec/system/admin/import_proposals_from_file_spec.rb delete mode 100644 decidim-proposals/spec/system/admin/import_proposals_spec.rb diff --git a/decidim-proposals/spec/system/admin/import_proposal_answers_spec.rb b/decidim-proposals/spec/system/admin/import_proposal_answers_spec.rb deleted file mode 100644 index ebabe9468e8c2..0000000000000 --- a/decidim-proposals/spec/system/admin/import_proposal_answers_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Import proposal answers" do - let(:organization) { create(:organization, available_locales: [:en, :ca, :es]) } - let(:component) { create(:proposal_component, organization:) } - let(:proposals) { create_list(:proposal, amount, component:) } - - let(:manifest_name) { "proposals" } - let(:participatory_space) { component.participatory_space } - let(:user) { create(:user, organization:) } - - let(:answers) do - proposals.map do |proposal| - { - id: proposal.id, - state: %w(accepted rejected evaluating).sample, - "answer/en": Faker::Lorem.sentence, - "answer/ca": Faker::Lorem.sentence, - "answer/es": Faker::Lorem.sentence - } - end - end - - let(:missing_answers) do - proposals.map do |proposal| - { - id: proposal.id, - state: %w(accepted rejected evaluating).sample, - "answer/fi": Faker::Lorem.sentence, - hello: "world" - } - end - end - - let(:amount) { rand(1..5) } - let(:json_file) { Rails.root.join("tmp/import_proposal_answers.json") } - - include_context "when managing a component as an admin" - - it_behaves_like "admin manages proposal answer imports" -end diff --git a/decidim-proposals/spec/shared/admin_manages_proposal_answer_imports_examples.rb b/decidim-proposals/spec/system/admin/import_proposals_answers_from_file_spec.rb similarity index 83% rename from decidim-proposals/spec/shared/admin_manages_proposal_answer_imports_examples.rb rename to decidim-proposals/spec/system/admin/import_proposals_answers_from_file_spec.rb index c26ed05330be2..cb3318f276c8a 100644 --- a/decidim-proposals/spec/shared/admin_manages_proposal_answer_imports_examples.rb +++ b/decidim-proposals/spec/system/admin/import_proposals_answers_from_file_spec.rb @@ -1,6 +1,44 @@ # frozen_string_literal: true -shared_examples "admin manages proposal answer imports" do +require "spec_helper" + +describe "Import proposals answers from a file" do + let(:organization) { create(:organization, available_locales: [:en, :ca, :es]) } + let(:component) { create(:proposal_component, organization:) } + let(:proposals) { create_list(:proposal, amount, component:) } + + let(:manifest_name) { "proposals" } + let(:participatory_space) { component.participatory_space } + let(:user) { create(:user, organization:) } + + let(:answers) do + proposals.map do |proposal| + { + id: proposal.id, + state: %w(accepted rejected evaluating).sample, + "answer/en": Faker::Lorem.sentence, + "answer/ca": Faker::Lorem.sentence, + "answer/es": Faker::Lorem.sentence + } + end + end + + let(:missing_answers) do + proposals.map do |proposal| + { + id: proposal.id, + state: %w(accepted rejected evaluating).sample, + "answer/fi": Faker::Lorem.sentence, + hello: "world" + } + end + end + + let(:amount) { rand(1..5) } + let(:json_file) { Rails.root.join("tmp/import_proposal_answers.json") } + + include_context "when managing a component as an admin" + before do click_on "Import" click_on "Import answers from a file" diff --git a/decidim-proposals/spec/system/admin/import_proposals_from_another_component_spec.rb b/decidim-proposals/spec/system/admin/import_proposals_from_another_component_spec.rb new file mode 100644 index 0000000000000..3fec822f4f7fe --- /dev/null +++ b/decidim-proposals/spec/system/admin/import_proposals_from_another_component_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Import proposals from another component" do + let(:component) { create(:proposal_component) } + let(:participatory_space) { component.participatory_space } + let!(:origin_component) { create(:proposal_component, participatory_space:) } + let(:organization) { component.organization } + + let(:manifest_name) { "proposals" } + let(:user) { create(:user, organization:) } + + include_context "when managing a component as an admin" do + let!(:component) { create(:proposal_component, participatory_space:) } + end + + before do + click_on "Import" + click_on "Import proposals from another component" + end + + it "does not show state checkboxes before a component is selected" do + expect(page).to have_no_css("#states-container input[type='checkbox']") + end + + it "dynamically loads state checkboxes after selecting an origin component" do + within ".import_proposals" do + select origin_component.name["en"], from: "Origin component" + end + + within "#states-container" do + expect(page).to have_unchecked_field("Accepted") + expect(page).to have_unchecked_field("Rejected") + expect(page).to have_unchecked_field("Evaluating") + expect(page).to have_unchecked_field("Not answered") + end + end + + it "hides state checkboxes when the component selection is cleared" do + within ".import_proposals" do + select origin_component.name["en"], from: "Origin component" + end + + expect(page).to have_css("#states-container input[type='checkbox']") + + within ".import_proposals" do + select "Please select a component", from: "Origin component" + end + + expect(page).to have_no_css("#states-container input[type='checkbox']") + end + + context "when importing proposals filtered by state" do + let!(:accepted_proposals) { create_list(:proposal, 2, :accepted, component: origin_component) } + let!(:rejected_proposals) { create_list(:proposal, 1, :rejected, component: origin_component) } + + it "only imports proposals matching the selected state" do + within ".import_proposals" do + select origin_component.name["en"], from: "Origin component" + check "Accepted" + end + + click_on "Import proposals" + + expect(page).to have_content("The import process has started. We will let you know once it has finished.") + perform_enqueued_jobs + visit current_path + + expect(Decidim::Proposals::Proposal.where(component:).count).to eq(2) + end + end + + context "with a custom state on the origin component" do + let(:custom_title) { { "en" => "Under review" } } + let!(:custom_state) do + create(:proposal_state, component: origin_component, token: "under_review", title: custom_title) + end + + it "shows the custom state alongside the default ones" do + within ".import_proposals" do + select origin_component.name["en"], from: "Origin component" + end + + within "#states-container" do + expect(page).to have_unchecked_field("Under review") + expect(page).to have_unchecked_field("Accepted") + expect(page).to have_unchecked_field("Not answered") + end + end + + context "when importing proposals with the custom state" do + let!(:custom_state_on_target) do + create(:proposal_state, component:, token: "under_review", title: custom_title) + end + let!(:custom_proposals) { create_list(:proposal, 2, component: origin_component, state: "under_review") } + let!(:accepted_proposals) { create_list(:proposal, 1, :accepted, component: origin_component) } + + it "only imports proposals in the custom state" do + within ".import_proposals" do + select origin_component.name["en"], from: "Origin component" + check "Under review" + end + + click_on "Import proposals" + + expect(page).to have_content("The import process has started. We will let you know once it has finished.") + perform_enqueued_jobs + visit current_path + + expect(Decidim::Proposals::Proposal.where(component:).count).to eq(2) + end + end + end +end diff --git a/decidim-proposals/spec/system/admin/import_proposals_from_file_spec.rb b/decidim-proposals/spec/system/admin/import_proposals_from_file_spec.rb new file mode 100644 index 0000000000000..ad16ab64cc3a3 --- /dev/null +++ b/decidim-proposals/spec/system/admin/import_proposals_from_file_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Import proposals from a file" do + let(:manifest_name) { "proposals" } + let(:user) { create(:user, organization:) } + let(:organization) { component.organization } + let(:participatory_space) { component.participatory_space } + + include_context "when managing a component as an admin" do + let!(:component) { create(:proposal_component, participatory_space:) } + end + + before do + click_on "Import" + click_on "Import proposals from a file" + end + + it "has start import button" do + expect(page).to have_content("Import") + end + + it "returns error without a file" do + click_on "Import" + expect(page).to have_content("There is an error in this field") + end + + it "does not change proposal amount if one imported row fails" do + dynamically_attach_file(:import_file, Decidim::Dev.asset("import_proposals_broken.csv")) + + click_on "Import" + expect(page).to have_content("Found an error in the import file on line 4") + expect(Decidim::Proposals::Proposal.count).to eq(0) + end + + it "creates proposals after successfully import" do + dynamically_attach_file(:import_file, Decidim::Dev.asset("import_proposals.csv")) + click_on "Import" + expect(page).to have_content("3 proposals successfully imported") + expect(Decidim::Proposals::Proposal.count).to eq(3) + end +end diff --git a/decidim-proposals/spec/system/admin/import_proposals_spec.rb b/decidim-proposals/spec/system/admin/import_proposals_spec.rb deleted file mode 100644 index b1d602d926434..0000000000000 --- a/decidim-proposals/spec/system/admin/import_proposals_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe "Import proposals" do - let(:component) { create(:proposal_component) } - let(:organization) { component.organization } - - let(:manifest_name) { "proposals" } - let(:participatory_space) { component.participatory_space } - let(:user) { create(:user, organization:) } - - include_context "when managing a component as an admin" do - let!(:component) { create(:proposal_component, participatory_space:) } - end - - describe "import from a file" do - before do - click_on "Import" - click_on "Import proposals from a file" - end - - it "has start import button" do - expect(page).to have_content("Import") - end - - it "returns error without a file" do - click_on "Import" - expect(page).to have_content("There is an error in this field") - end - - it "does not change proposal amount if one imported row fails" do - dynamically_attach_file(:import_file, Decidim::Dev.asset("import_proposals_broken.csv")) - - click_on "Import" - expect(page).to have_content("Found an error in the import file on line 4") - expect(Decidim::Proposals::Proposal.count).to eq(0) - end - - it "creates proposals after successfully import" do - dynamically_attach_file(:import_file, Decidim::Dev.asset("import_proposals.csv")) - click_on "Import" - expect(page).to have_content("3 proposals successfully imported") - expect(Decidim::Proposals::Proposal.count).to eq(3) - end - end - - describe "import from another component" do - let!(:origin_component) { create(:proposal_component, participatory_space:) } - - before do - click_on "Import" - click_on "Import proposals from another component" - end - - it "does not show state checkboxes before a component is selected" do - expect(page).to have_no_css("#states-container input[type='checkbox']") - end - - it "dynamically loads state checkboxes after selecting an origin component" do - within ".import_proposals" do - select origin_component.name["en"], from: "Origin component" - end - - within "#states-container" do - expect(page).to have_unchecked_field("Accepted") - expect(page).to have_unchecked_field("Rejected") - expect(page).to have_unchecked_field("Evaluating") - expect(page).to have_unchecked_field("Not answered") - end - end - - it "hides state checkboxes when the component selection is cleared" do - within ".import_proposals" do - select origin_component.name["en"], from: "Origin component" - end - - expect(page).to have_css("#states-container input[type='checkbox']") - - within ".import_proposals" do - select "Please select a component", from: "Origin component" - end - - expect(page).to have_no_css("#states-container input[type='checkbox']") - end - - context "when importing proposals filtered by state" do - let!(:accepted_proposals) { create_list(:proposal, 2, :accepted, component: origin_component) } - let!(:rejected_proposals) { create_list(:proposal, 1, :rejected, component: origin_component) } - - it "only imports proposals matching the selected state" do - within ".import_proposals" do - select origin_component.name["en"], from: "Origin component" - check "Accepted" - end - - click_on "Import proposals" - - expect(page).to have_content("The import process has started. We will let you know once it has finished.") - perform_enqueued_jobs - visit current_path - - expect(Decidim::Proposals::Proposal.where(component:).count).to eq(2) - end - end - - context "with a custom state on the origin component" do - let(:custom_title) { { "en" => "Under review" } } - let!(:custom_state) do - create(:proposal_state, component: origin_component, token: "under_review", title: custom_title) - end - - it "shows the custom state alongside the default ones" do - within ".import_proposals" do - select origin_component.name["en"], from: "Origin component" - end - - within "#states-container" do - expect(page).to have_unchecked_field("Under review") - expect(page).to have_unchecked_field("Accepted") - expect(page).to have_unchecked_field("Not answered") - end - end - - context "when importing proposals with the custom state" do - let!(:custom_state_on_target) do - create(:proposal_state, component:, token: "under_review", title: custom_title) - end - let!(:custom_proposals) { create_list(:proposal, 2, component: origin_component, state: "under_review") } - let!(:accepted_proposals) { create_list(:proposal, 1, :accepted, component: origin_component) } - - it "only imports proposals in the custom state" do - within ".import_proposals" do - select origin_component.name["en"], from: "Origin component" - check "Under review" - end - - click_on "Import proposals" - - expect(page).to have_content("The import process has started. We will let you know once it has finished.") - perform_enqueued_jobs - visit current_path - - expect(Decidim::Proposals::Proposal.where(component:).count).to eq(2) - end - end - end - end -end From f9b51bbee1aff66e7b02ce8b0ec4c2a3c80dd89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 18 Mar 2026 15:19:21 +0100 Subject: [PATCH 122/135] Move the "Check fingerprint" button to the versions page (#16395) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Tom Greenwood <101816158+greenwoodt@users.noreply.github.com> --- .../app/cells/decidim/versions_list/show.erb | 25 +++++++++++++++++ .../shared_examples/fingerprint_examples.rb | 2 ++ .../decidim/proposals/proposals/show.html.erb | 27 ++----------------- .../spec/system/fingerprint_proposal_spec.rb | 2 +- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/decidim-core/app/cells/decidim/versions_list/show.erb b/decidim-core/app/cells/decidim/versions_list/show.erb index 80ad196ce942e..c937ab87ebd44 100644 --- a/decidim-core/app/cells/decidim/versions_list/show.erb +++ b/decidim-core/app/cells/decidim/versions_list/show.erb @@ -1,3 +1,28 @@ <% reverse_ordered_versions.each_with_index do |version, index| %> <%= cell("decidim/versions_list_item", version, index:, versioned_resource:, version_path:, i18n_scope:, total:) %> <% end %> + +<% if versioned_resource.class.include?(Decidim::Fingerprintable) %> + <% fingerprint_id = dom_id(versioned_resource, :fingerprint_dialog) %> +
+ <%= content_tag :button, t("decidim.fingerprint.check"), class: "button button__lg button__text-secondary font-normal text-sm underline", data: { dialog_open: fingerprint_id } %> +
+ <%= decidim_modal id: fingerprint_id, class: "fingerprint-modal" do %> +
+ <%= icon "fingerprint-line" %> +

<%= t "decidim.fingerprint.title" %>

+
+

<%= t "decidim.fingerprint.explanation" %>

+
+ <%= t "decidim.fingerprint.value" %>: + <%= decidim_html_escape versioned_resource.fingerprint.value %> +
+
+ <%= t "decidim.fingerprint.source" %>: + <%= decidim_html_escape versioned_resource.fingerprint.source %> +
+

<%= t("decidim.fingerprint.replicate_help", online_calculator_link: link_to(t("decidim.fingerprint.online_calculator_name"), "https://www.md5calc.com/sha256", target: "_blank", rel: "noopener")).html_safe %>

+
+
+ <% end %> +<% end %> diff --git a/decidim-core/lib/decidim/core/test/shared_examples/fingerprint_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/fingerprint_examples.rb index e5fea59acbcdc..e9a70ef6c7472 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/fingerprint_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/fingerprint_examples.rb @@ -5,6 +5,7 @@ it "shows a fingerprint" do visit(resource_locator(fingerprintable).path) + click_on("see other versions") click_on("Check fingerprint") within ".fingerprint-modal" do @@ -19,6 +20,7 @@ it "shows the fingerprint source with correct spacing" do visit(resource_locator(fingerprintable).path) + click_on("see other versions") click_on("Check fingerprint") within ".fingerprint-modal" do diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb index 5755671bc00bd..c5115880abcaa 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/show.html.erb @@ -114,32 +114,9 @@ extra_admin_link( <%= comments_for @proposal %> - <%= decidim_modal id: fingerprint_id, class: "fingerprint-modal" do %> -
- <%= icon "fingerprint-line" %> -

<%= t "decidim.fingerprint.title" %>

-
-

<%= t "decidim.fingerprint.explanation" %>

-
- <%= t "decidim.fingerprint.value" %>: - <%= decidim_html_escape @proposal.fingerprint.value %> -
-
- <%= t "decidim.fingerprint.source" %>: - <%= @proposal.fingerprint.source %> -
-

<%= t("decidim.fingerprint.replicate_help", online_calculator_link: link_to(t("decidim.fingerprint.online_calculator_name"), "http://www.md5calc.com/sha256", target: "_blank", rel: "noopener")).html_safe %>

-
-
- <% end %> <% end %> <% end %> diff --git a/decidim-proposals/spec/system/fingerprint_proposal_spec.rb b/decidim-proposals/spec/system/fingerprint_proposal_spec.rb index e37238cb801aa..900a4a74118f2 100644 --- a/decidim-proposals/spec/system/fingerprint_proposal_spec.rb +++ b/decidim-proposals/spec/system/fingerprint_proposal_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Fingerprint proposal" do +describe "Fingerprint proposal", versioning: true do let(:manifest_name) { "proposals" } let!(:fingerprintable) do From 6a5556441d95907e08ccea8ff0b01b183411e3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Bol=C3=ADvar=20=28FBO=29?= Date: Wed, 18 Mar 2026 16:03:11 +0100 Subject: [PATCH 123/135] Prevent taxonomy importer from removing custom component settings (#15992) --- .../lib/decidim/maintenance/taxonomy_importer.rb | 2 +- .../spec/lib/maintenance/taxonomy_importer_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/decidim-core/lib/decidim/maintenance/taxonomy_importer.rb b/decidim-core/lib/decidim/maintenance/taxonomy_importer.rb index da2404ac81d8a..304d4b48ac1ad 100644 --- a/decidim-core/lib/decidim/maintenance/taxonomy_importer.rb +++ b/decidim-core/lib/decidim/maintenance/taxonomy_importer.rb @@ -85,7 +85,7 @@ def import_filter(root, data) component = GlobalID::Locator.locate(component_id) if component begin - component.update!(settings: { taxonomy_filters: [filter.id.to_s] }) + component.update!(settings: component.settings.to_h.merge(taxonomy_filters: [filter.id.to_s])) result[:components_assigned][filter.internal_name[organization.default_locale]] ||= [] result[:components_assigned][filter.internal_name[organization.default_locale]] << component_id rescue ActiveRecord::RecordInvalid diff --git a/decidim-core/spec/lib/maintenance/taxonomy_importer_spec.rb b/decidim-core/spec/lib/maintenance/taxonomy_importer_spec.rb index 5fb2f7ab52221..4734638aa6257 100644 --- a/decidim-core/spec/lib/maintenance/taxonomy_importer_spec.rb +++ b/decidim-core/spec/lib/maintenance/taxonomy_importer_spec.rb @@ -167,6 +167,19 @@ module Decidim::Maintenance end end + context "when component has existing settings" do + before do + component.update!(settings: { comments_enabled: false }) + end + + it "preserves existing settings when assigning taxonomy filters" do + expect(component.reload.settings.comments_enabled).to be(false) + subject.import! + expect(component.reload.settings[:taxonomy_filters]).to eq([filter.id.to_s]) + expect(component.settings.comments_enabled).to be(false) + end + end + context "when a filter already exists" do let!(:root_taxonomy) { create(:taxonomy, organization:, name: { organization.default_locale => "New root taxonomy" }) } let!(:filter) { create(:taxonomy_filter, root_taxonomy:, participatory_space_manifests: ["participatory_processes"]) } From d5b48234090fd2faa06a2da6aeb0dfc0e8a4a8a2 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 19 Mar 2026 09:04:27 +0200 Subject: [PATCH 124/135] Fix members being invited twice (#16401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Private members are invited twice * Add migration * Fix member spec * Fix import issues * Add coderabbit recommendations * Refactor to use ActiveRecord models * Add migration * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Andrés Pereira de Lucena --- .../participatory_space/create_member.rb | 11 +- .../participatory_space/create_member_spec.rb | 13 ++ .../spec/system/member_import_via_csv_spec.rb | 2 +- decidim-admin/spec/system/member_spec.rb | 2 +- .../decidim/participatory_space/member.rb | 2 + ...0314081619_add_index_on_decidim_members.rb | 25 ++++ .../lib/decidim/core/test/factories.rb | 5 +- .../add_index_on_decidim_members_spec.rb | 125 ++++++++++++++++++ 8 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 decidim-core/db/migrate/20260314081619_add_index_on_decidim_members.rb create mode 100644 decidim-core/spec/db/migrate/add_index_on_decidim_members_spec.rb diff --git a/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb b/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb index 32ad418fa1f7f..8e62d3c06e215 100644 --- a/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb +++ b/decidim-admin/app/commands/decidim/admin/participatory_space/create_member.rb @@ -52,12 +52,11 @@ def create_member title: user.name } ) do - Decidim::ParticipatorySpace::Member.find_or_create_by!( - user:, - participatory_space: @member_to, - role: form.role, - published: form.published - ) + member = Decidim::ParticipatorySpace::Member.where(user:, participatory_space: @member_to).first_or_initialize + member.role = form.role + member.published = form.published + member.save! + member end end diff --git a/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb b/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb index 1ca15f31bb0c5..903dc8d24465c 100644 --- a/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/participatory_space/create_member_spec.rb @@ -45,6 +45,19 @@ module Decidim::Admin::ParticipatorySpace expect(members.count).to eq 1 end + it "creates the member only one time" do + expect(Decidim::ParticipatorySpace::Member.where(user:).count).to eq 0 + + subject.call + + expect(Decidim::ParticipatorySpace::Member.where(user:).count).to eq 1 + + form = double(invalid?: false, email:, current_user:, name:, role:, published: false) + described_class.new(form, participatory_space, via_csv:).call + + expect(Decidim::ParticipatorySpace::Member.where(user:).count).to eq 1 + end + it "creates a new user with no application admin privileges" do subject.call expect(Decidim::User.last).not_to be_admin diff --git a/decidim-admin/spec/system/member_import_via_csv_spec.rb b/decidim-admin/spec/system/member_import_via_csv_spec.rb index 8be1fb11dfe3f..8e38f2f22e3ed 100644 --- a/decidim-admin/spec/system/member_import_via_csv_spec.rb +++ b/decidim-admin/spec/system/member_import_via_csv_spec.rb @@ -30,7 +30,7 @@ context "when there are existing users" do before do - create_list(:assembly_member, 3, participatory_space: assembly, user: create(:user, organization: assembly.organization)) + create_list(:assembly_member, 3, participatory_space: assembly, organization: assembly.organization) visit current_path end diff --git a/decidim-admin/spec/system/member_spec.rb b/decidim-admin/spec/system/member_spec.rb index 2afe0b3eec5e7..06f6053943b7c 100644 --- a/decidim-admin/spec/system/member_spec.rb +++ b/decidim-admin/spec/system/member_spec.rb @@ -8,7 +8,7 @@ let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:assembly) { create(:assembly, organization:, has_members: true) } - let!(:members) { create_list(:assembly_member, 26, participatory_space: assembly, user: create(:user, organization: assembly.organization)) } + let!(:members) { create_list(:assembly_member, 26, participatory_space: assembly, organization: assembly.organization) } before do switch_to_host(organization.host) diff --git a/decidim-core/app/models/decidim/participatory_space/member.rb b/decidim-core/app/models/decidim/participatory_space/member.rb index 1be8ee4ed73f2..afbad0d55d99f 100644 --- a/decidim-core/app/models/decidim/participatory_space/member.rb +++ b/decidim-core/app/models/decidim/participatory_space/member.rb @@ -14,6 +14,8 @@ class Member < ApplicationRecord delegate :email, :name, to: :user + validates :user, uniqueness: { scope: [:participatory_space_id, :participatory_space_type] } + scope :by_participatory_space, ->(participatory_space) { where(participatory_space_id: participatory_space.id, participatory_space_type: participatory_space.class.to_s) } scope :published, -> { where(published: true) } diff --git a/decidim-core/db/migrate/20260314081619_add_index_on_decidim_members.rb b/decidim-core/db/migrate/20260314081619_add_index_on_decidim_members.rb new file mode 100644 index 0000000000000..d54fc8ff456c1 --- /dev/null +++ b/decidim-core/db/migrate/20260314081619_add_index_on_decidim_members.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class AddIndexOnDecidimMembers < ActiveRecord::Migration[8.1] + class Member < ApplicationRecord + self.table_name = :decidim_members + end + + def up + Member.where(decidim_user_id: nil).delete_all + + Member.find_each do |member| + member.delete if Member.where( + decidim_user_id: member.decidim_user_id, + participatory_space_type: member.participatory_space_type, + participatory_space_id: member.participatory_space_id + ).count > 1 + end + + add_index(:decidim_members, [:decidim_user_id, :participatory_space_type, :participatory_space_id], name: "unique_space_members", unique: true) + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index c2f24ce3dcaaf..3e9addd7e3863 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -310,9 +310,10 @@ def generate_title(field = nil, skip_injection:) factory :assembly_member, class: "Decidim::ParticipatorySpace::Member" do transient do skip_injection { false } + organization { create(:organization, skip_injection:) } end - user - participatory_space { create(:assembly, organization: user.organization, skip_injection:) } + user { create(:user, :confirmed, organization:, skip_injection:) } + participatory_space { create(:assembly, organization:, skip_injection:) } end factory :identity, class: "Decidim::Identity" do diff --git a/decidim-core/spec/db/migrate/add_index_on_decidim_members_spec.rb b/decidim-core/spec/db/migrate/add_index_on_decidim_members_spec.rb new file mode 100644 index 0000000000000..26d732fea8c00 --- /dev/null +++ b/decidim-core/spec/db/migrate/add_index_on_decidim_members_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require "spec_helper" +require_relative "../../../db/migrate/20260314081619_add_index_on_decidim_members" + +describe "AddIndexOnDecidimMembers", type: :migration do + let(:organization) { create(:organization) } + let(:user1) { create(:user, :confirmed, organization:) } + let(:user2) { create(:user, :confirmed, organization:) } + let(:participatory_space1) { create(:participatory_process, organization:) } + let(:participatory_space2) { create(:participatory_process, organization:) } + + let(:member_class) { Decidim::ParticipatorySpace::Member } + + before do + if ActiveRecord::Base.connection.index_exists?(:decidim_members, name: "unique_space_members") + ActiveRecord::Base.connection.remove_index(:decidim_members, name: "unique_space_members") + end + end + + # Restore the index after tests + after do + unless ActiveRecord::Base.connection.index_exists?(:decidim_members, name: "unique_space_members") + ActiveRecord::Base.connection.add_index(:decidim_members, + [:decidim_user_id, :participatory_space_type, :participatory_space_id], + name: "unique_space_members", + unique: true) + + end + end + + def create_member(user_id:, space_id:, space_type:, role: "role") + user = user_id.nil? ? nil : Decidim::User.find(user_id) + space = space_type.constantize.find(space_id) + member = build(:member, user:, participatory_space: space, role: { en: role }, published: true) + member.save!(validate: false) + end + + describe "#migrate :up" do + let(:migration) { AddIndexOnDecidimMembers.new } + + context "when there are rows with nil decidim_user_id" do + before do + create_member(user_id: nil, space_id: participatory_space1.id, space_type: participatory_space1.class.name) + end + + it "removes rows with nil decidim_user_id" do + members = member_class.where(decidim_user_id: nil) + expect(members.count).to eq(1) + + migration.migrate(:up) + + expect(members.count).to eq(0) + end + end + + context "when there are duplicate users in the same space" do + before do + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name, role: "role1") + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name, role: "role2") + end + + it "removes duplicates keeping the one with highest id" do + members = member_class.where(decidim_user_id: user1.id, participatory_space_id: participatory_space1.id) + expect(members.count).to eq(2) + + migration.migrate(:up) + + expect(members.count).to eq(1) + end + end + + context "when there are duplicates across different spaces" do + before do + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name, role: "role1") + create_member(user_id: user1.id, space_id: participatory_space2.id, space_type: participatory_space2.class.name, role: "role2") + end + + it "keeps members from different spaces" do + members = member_class.where(decidim_user_id: user1.id) + expect(members.count).to eq(2) + + migration.migrate(:up) + + expect(members.count).to eq(2) + end + end + + context "when adding the unique index" do + before do + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name) + create_member(user_id: user2.id, space_id: participatory_space2.id, space_type: participatory_space2.class.name) + end + + it "adds a unique index on the composite columns" do + migration.migrate(:up) + + expect do + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name, role: "another_role") + end.to raise_error(ActiveRecord::RecordNotUnique) + end + end + + context "with valid data" do + before do + create_member(user_id: user1.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name) + create_member(user_id: user2.id, space_id: participatory_space1.id, space_type: participatory_space1.class.name) + create_member(user_id: user1.id, space_id: participatory_space2.id, space_type: participatory_space2.class.name) + end + + it "creates the unique index successfully" do + migration.migrate(:up) + expect(member_class.count).to eq(3) + end + end + end + + describe "#migrate :down" do + let(:migration) { AddIndexOnDecidimMembers.new } + + it "raises IrreversibleMigration" do + expect { migration.migrate(:down) }.to raise_error(ActiveRecord::IrreversibleMigration) + end + end +end From dc59434f476e4f2c05f7133df8cf1d447bc0796e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 19 Mar 2026 08:56:55 +0100 Subject: [PATCH 125/135] Fix cursor for hover and icon for draggable (#16358) --- .../decidim/admin/content_block/show.erb | 2 +- .../stylesheets/decidim/admin/_forms.scss | 1 - .../decidim/admin/_table-list.scss | 2 +- .../decidim/admin/_taxonomies.scss | 2 +- .../admin/questions/_question.html.erb | 2 +- .../admin/questionnaires/_question.html.erb | 6 +-- .../questionnaires/_questions_form.html.erb | 12 ++++- .../admin/questionnaires/_separator.html.erb | 6 +-- .../_title_and_description.html.erb | 6 +-- .../shared_examples/manage_questionnaires.rb | 2 + .../draggable_behavior.rb | 47 +++++++++++++++++++ .../index.html.erb | 2 +- 12 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/draggable_behavior.rb diff --git a/decidim-admin/app/cells/decidim/admin/content_block/show.erb b/decidim-admin/app/cells/decidim/admin/content_block/show.erb index 33489bbad5023..180f89352c6b0 100644 --- a/decidim-admin/app/cells/decidim/admin/content_block/show.erb +++ b/decidim-admin/app/cells/decidim/admin/content_block/show.erb @@ -12,7 +12,7 @@ <%= icon "delete-bin-line", role: "img", "aria-hidden": true %> <% end %> <% end %> - <%= icon "menu-line", role: "img", "aria-hidden": true %> + <%= icon("draggable", class: "dragger hover:cursor-grab") %> diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_forms.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_forms.scss index 177d7990713a9..4662dd50e3d61 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_forms.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_forms.scss @@ -54,7 +54,6 @@ transition: all 0.2s ease; svg[role="img"] { - cursor: default !important; transition: color 0.2s ease; } 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 1f4a3820de00f..6f3e3e703236d 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_table-list.scss @@ -141,7 +141,7 @@ } .dragging-handle { - @apply cursor-ns-resize align-top p-3; + @apply cursor-grab align-top p-3; .dragger { @apply text-2xl; diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss index 170141d6365eb..d91cb4a52b95e 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss @@ -14,7 +14,7 @@ } td { - @apply align-top cursor-ns-resize; + @apply align-top cursor-grab; &.js-drag-handle .dragger { @apply mt-1; diff --git a/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb b/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb index c3e1c339d8996..f8ac93c41c981 100644 --- a/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb +++ b/decidim-elections/app/views/decidim/elections/admin/questions/_question.html.erb @@ -6,7 +6,7 @@

<% if editable %> - <%= icon("drag-move-2-fill") %> + <%= icon("draggable", class: "dragger hover:cursor-grab") %> <% else %> <%= icon("lock-line") %> <% end %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb index 448407fae80dd..d6fe2cac0727a 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_question.html.erb @@ -2,11 +2,11 @@ <% is_expanded = question.errors.any? %>
-
+ <%= content_tag :div, class: ["card-divider", ("hover:cursor-grab" if editable)] do %>

<% if editable %> - <%== icon("drag-move-2-fill") %> + <%= icon("draggable", class: "dragger") %> <% end %> <%= dynamic_title(translated_attribute(question.body), class: "question-title-statement", max_length: 50, omission: "...", placeholder: t(".question")) %> @@ -30,7 +30,7 @@ <% end %>

-
+ <% end %>
" aria-hidden="<%= is_expanded ? "false" : "true" %>">
diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb index 4175abb88025b..a3abecdefc6d0 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_questions_form.html.erb @@ -40,7 +40,15 @@ <%= cell("decidim/announcement", t(".already_responded_warning"), callout_class: "warning" ) %> <% end %> -
+ <% list_attributes = { + class: "questionnaire-questions-list flex flex-col py-6 gap-6 last:pb-0", + id: "questionnaire-questions-list" + } + list_attributes[:"data-draggable-table"] = true if questionnaire.questions_editable? + list_attributes[:"data-sort-url"] = "#" if questionnaire.questions_editable? + list_attributes[:"data-draggable-handle"] = ".card-divider" if questionnaire.questions_editable? %> + + <%= content_tag :div, list_attributes do %> <% @form.questions.each_with_index do |question, index| %> <%= fields_for "questions[questions][]", question do |question_form| %> <% if question.separator? %> @@ -67,7 +75,7 @@ <% end %> <% end %> <% end %> -
+ <% end %>
<%= append_javascript_pack_tag "decidim_forms_admin" %> diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb index 3434d2004ea1f..a837a409d0e7d 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_separator.html.erb @@ -1,11 +1,11 @@ <% question = form.object %>
-
+ <%= content_tag :div, class: ["card-divider", ("hover:cursor-grab" if editable)] do %>

<% if editable %> - <%== icon("drag-move-2-fill") %> + <%= icon("draggable", class: "dragger") %> <% end %> <%= dynamic_title(t(".separator"), class: "question-title-statement", max_length: 50, omission: "...", placeholder: t(".separator")) %> @@ -32,5 +32,5 @@ <%= form.hidden_field :position, value: question.position || 0, disabled: !editable %> <%= form.hidden_field :deleted, disabled: !editable %> -

+ <% end %>
diff --git a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb index 6f5c0418be9fc..2bc153f12dfac 100644 --- a/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb +++ b/decidim-forms/app/views/decidim/forms/admin/questionnaires/_title_and_description.html.erb @@ -3,11 +3,11 @@
-
+ <%= content_tag :div, class: ["card-divider", ("hover:cursor-grab" if editable)] do %>

<% if editable %> - <%== icon("drag-move-2-fill") %> + <%= icon("draggable", class: "dragger") %> <% end %> <%= dynamic_title(translated_attribute(question.body), class: "question-title-statement", max_length: 50, omission: "...", placeholder: t(".title_and_description")) %> @@ -71,6 +71,6 @@ <%= form.hidden_field :position, value: question.position || 0, disabled: !editable %> <%= form.hidden_field :deleted, disabled: !editable %> -

+ <% end %>
diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb index 3692f08c9f7c3..b298a45457a1b 100644 --- a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires.rb @@ -6,6 +6,7 @@ require "decidim/forms/test/shared_examples/manage_questionnaires/update_questions" require "decidim/forms/test/shared_examples/manage_questionnaires/add_display_conditions" require "decidim/forms/test/shared_examples/manage_questionnaires/update_display_conditions" +require "decidim/forms/test/shared_examples/manage_questionnaires/draggable_behavior" shared_examples_for "manage questionnaires" do let(:body) do @@ -33,6 +34,7 @@ it_behaves_like "update questions" it_behaves_like "add display conditions" it_behaves_like "update display conditions" + it_behaves_like "manage questionnaire draggable behavior" end context "when the questionnaire is already responded" do diff --git a/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/draggable_behavior.rb b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/draggable_behavior.rb new file mode 100644 index 0000000000000..668dcf0951d9b --- /dev/null +++ b/decidim-forms/lib/decidim/forms/test/shared_examples/manage_questionnaires/draggable_behavior.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "spec_helper" + +shared_examples_for "manage questionnaire draggable behavior" do + let!(:question) { create(:questionnaire_question, body:, questionnaire:) } + + context "when questionnaire has no responses (editable)" do + before do + visit current_path + end + + it "shows draggable data attributes for questions list" do + expect(page).to have_css("[data-draggable-table]") + expect(page).to have_css("[data-draggable-handle]") + end + + describe "when hovering over card divider" do + it "shows resize cursor for editable questions" do + within first(".questionnaire-question") do + expect(page).to have_css(".card-divider.hover\\:cursor-grab") + end + end + end + end + + context "when questionnaire has responses (not editable)" do + let!(:response) { create(:response, question:, questionnaire:) } + + before do + visit current_path + end + + it "does not show draggable data attributes for questions list" do + expect(page).to have_no_css("[data-draggable-table]") + expect(page).to have_no_css("[data-draggable-handle]") + end + + describe "when hovering over card divider" do + it "does not show resize cursor for non-editable questions" do + within first(".questionnaire-question") do + expect(page).to have_no_css(".card-divider.hover\\:cursor-grab") + end + end + end + end +end diff --git a/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_process_steps/index.html.erb b/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_process_steps/index.html.erb index 44ffe2896f37d..64d9e6462e7b9 100644 --- a/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_process_steps/index.html.erb +++ b/decidim-participatory_processes/app/views/decidim/participatory_processes/admin/participatory_process_steps/index.html.erb @@ -25,7 +25,7 @@

">
- <%= icon "drag-move-2-line" %> + <%= icon("draggable", class: "dragger hover:cursor-grab") %> <% if step.active? %> <% end %> From 26f490b47c6529044ccf74c7f1a8a3d6d56f1cfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:59:45 +0100 Subject: [PATCH 126/135] Bump to dependencies: Bump webmock from 3.26.1 to 3.26.2 (#16426) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d08eb1c827d71..442e40d5dbdae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -928,7 +928,7 @@ GEM web-push (3.1.0) jwt (~> 3.0) openssl (>= 3.0) - webmock (3.26.1) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index ad8aac6616510..b12adb49ee307 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -916,7 +916,7 @@ GEM web-push (3.1.0) jwt (~> 3.0) openssl (>= 3.0) - webmock (3.26.1) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) From 48ddac44f92e102f507b41c06abaaee92f3779c4 Mon Sep 17 00:00:00 2001 From: Lucas Carrias Date: Thu, 19 Mar 2026 05:54:10 -0300 Subject: [PATCH 127/135] Improve date picker usability by selecting the date on click (#16425) * feat: select date on click * chore: remove redudant button from test DOM * lint: remove empty line * chore: remove css * test: assert that the input value is not empty --- .../stylesheets/decidim/admin/_datepicker.scss | 4 ---- .../assembly_admin/assembly_admin.test.js | 4 ---- .../src/decidim/datepicker/generate_datepicker.js | 14 +------------- .../packs/stylesheets/decidim/_datepicker.scss | 8 -------- .../spec/system/date_picker/date_picker_spec.rb | 15 +++------------ 5 files changed, 4 insertions(+), 41 deletions(-) diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_datepicker.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_datepicker.scss index b76d31889ed95..975500a8ba8a0 100644 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/_datepicker.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_datepicker.scss @@ -22,10 +22,6 @@ @apply top-2; } - &__pick-calendar { - @apply z-[3]; - } - &__close-calendar { @apply z-[3]; } diff --git a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js index 833fd91e73e64..dded25d18a7ea 100644 --- a/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js +++ b/decidim-assemblies/app/packs/src/decidim/assemblies/controllers/assembly_admin/assembly_admin.test.js @@ -363,7 +363,6 @@ describe("AssemblyAdminController", () => {
-
@@ -388,7 +387,6 @@ describe("AssemblyAdminController", () => { @@ -411,7 +409,6 @@ describe("AssemblyAdminController", () => { @@ -433,7 +430,6 @@ describe("AssemblyAdminController", () => { diff --git a/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js b/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js index 543b7d94befb2..a00c9cad2e1fa 100644 --- a/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js +++ b/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js @@ -46,13 +46,6 @@ export default function generateDatePicker(input, row, formats) { closeCalendar.setAttribute("class", "datepicker__close-calendar button button__transparent-secondary button__xs"); closeCalendar.setAttribute("type", "button"); - const pickCalendar = document.createElement("button"); - pickCalendar.innerText = i18n.select; - pickCalendar.setAttribute("class", "datepicker__pick-calendar button button__secondary button__xs"); - pickCalendar.setAttribute("disabled", true); - pickCalendar.setAttribute("type", "button"); - - datePickerContainer.appendChild(pickCalendar); datePickerContainer.appendChild(closeCalendar); dateColumn.appendChild(datePickerContainer); @@ -103,15 +96,10 @@ export default function generateDatePicker(input, row, formats) { let pickedDate = null; datePicker.addEventListener("selectDate", (event) => { - pickCalendar.removeAttribute("disabled"); pickedDate = event.detail; - }); - - pickCalendar.addEventListener("click", (event) => { - event.preventDefault(); + prevDate = pickedDate; date.value = displayDate(datePicker.value, formats); - prevDate = pickedDate; if (input.type === "date") { input.value = `${pickedDate}`; } else if (input.type === "datetime-local") { diff --git a/decidim-core/app/packs/stylesheets/decidim/_datepicker.scss b/decidim-core/app/packs/stylesheets/decidim/_datepicker.scss index 5a4118508c8f2..42d574831e55b 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_datepicker.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_datepicker.scss @@ -102,14 +102,6 @@ @apply absolute z-30 bottom-2 right-1; } - &__pick-calendar { - @apply absolute z-30 bottom-2 right-16 mr-1; - - &:disabled { - @apply bg-gray; - } - } - &__close-clock { @apply absolute z-30 bottom-2 right-1; } diff --git a/decidim-core/spec/system/date_picker/date_picker_spec.rb b/decidim-core/spec/system/date_picker/date_picker_spec.rb index 34f9a5d8090c9..48d8bad9e649a 100644 --- a/decidim-core/spec/system/date_picker/date_picker_spec.rb +++ b/decidim-core/spec/system/date_picker/date_picker_spec.rb @@ -100,7 +100,6 @@ def protect_against_forgery? formatted_month = format("%02d", month) find("td > span", text: "20", match: :first).click - find(".datepicker__pick-calendar").click find(".datepicker__clock-button").click find(".datepicker__hour-up").click @@ -145,17 +144,13 @@ def protect_against_forgery? expect(page).to have_css("#example_input_date_datepicker") end - it "has disabled select button" do - find(".datepicker__calendar-button").click - expect(page).to have_button("Select", disabled: true) - end - context "when choosing a date" do - it "enables the select button" do + it "hides the datepicker calendar" do find(".datepicker__calendar-button").click yesterday = Date.yesterday.strftime("%-d") find("td > span", text: yesterday, match: :first).click - expect(page).to have_button("Select", disabled: false) + expect(find_by_id("example_input_date").value).not_to eq("") + expect(page).to have_css("#example_input_date_datepicker", visible: :hidden) end end end @@ -175,11 +170,9 @@ def protect_against_forgery? find('span > input[name="year"]').set("1994") find(".wc-datepicker__next-month-button").click find("td > span", text: "20", match: :first).click - find(".datepicker__pick-calendar").click find(".datepicker__calendar-button").click element = find("td.wc-datepicker__date--selected") expect(element).to have_content("20") - expect(page).to have_button("Select", disabled: false) end end end @@ -508,7 +501,6 @@ def protect_against_forgery? formatted_month = format("%02d", month) find("td > span", text: "20", match: :first).click - find(".datepicker__pick-calendar").click find(".datepicker__clock-button").click find(".datepicker__hour-up").click @@ -764,7 +756,6 @@ def protect_against_forgery? formatted_month = format("%02d", month) find("td > span", text: "20", match: :first).click - find(".datepicker__pick-calendar").click find(".datepicker__clock-button").click find(".datepicker__hour-up").click From c5e5d8ab35b897811784b173b612d59639859140 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Thu, 19 Mar 2026 12:21:58 +0200 Subject: [PATCH 128/135] Fix component settings disabled when it is readonly (#16398) * Fix Component Settings select fields are not marked as disabled * Fix typo Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../app/helpers/decidim/admin/settings_helper.rb | 3 ++- .../spec/helpers/settings_helper_spec.rb | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/decidim-admin/app/helpers/decidim/admin/settings_helper.rb b/decidim-admin/app/helpers/decidim/admin/settings_helper.rb index 3e5d1f57f2d38..076d95d7811c6 100644 --- a/decidim-admin/app/helpers/decidim/admin/settings_helper.rb +++ b/decidim-admin/app/helpers/decidim/admin/settings_helper.rb @@ -108,7 +108,8 @@ def render_select_form_field(form, attribute, name, i18n_scope, options) html = form.select( name, choices, - { include_blank: attribute.include_blank, label: options[:label] } + { include_blank: attribute.include_blank, label: options[:label] }, + { disabled: options[:readonly] || false } ) html << content_tag(:p, options[:help_text], class: "help-text") if options[:help_text] html diff --git a/decidim-admin/spec/helpers/settings_helper_spec.rb b/decidim-admin/spec/helpers/settings_helper_spec.rb index 3b96d3708d281..0b64b1d7fa75a 100644 --- a/decidim-admin/spec/helpers/settings_helper_spec.rb +++ b/decidim-admin/spec/helpers/settings_helper_spec.rb @@ -55,10 +55,23 @@ def render_input expect(form).to receive(:select).with( :test, full_choices, - options + options, { disabled: false } ) render_input end + + context "when the field should be disabled" do + let(:options) { { include_blank: false, readonly: true, label: "A test" } } + + it "is supported" do + expect(form).to receive(:select).with( + :test, + full_choices, + options.except(:readonly), { disabled: true } + ) + render_input + end + end end describe "booleans" do From 3b29c3cdc5af04b38c1e611291db0b732d537a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Verg=C3=A9s?= Date: Thu, 19 Mar 2026 14:37:40 +0100 Subject: [PATCH 129/135] Prevent infinite loops on FindAndUpdateDescendantsJob (#16295) * track already updated models * rubocop * move to a depth detector approach * add a spec covering the original scenario for the bug * add log message if recursion is reached --- .../find_and_update_descendants_job.rb | 10 +++++-- .../jobs/decidim/update_search_indexes_job.rb | 4 +-- decidim-core/lib/decidim/searchable.rb | 8 ++--- .../find_and_update_descendants_job_spec.rb | 8 +++++ decidim-core/spec/lib/searchable_spec.rb | 4 +-- decidim-meetings/spec/models/meeting_spec.rb | 30 +++++++++++++++++++ 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/decidim-core/app/jobs/decidim/find_and_update_descendants_job.rb b/decidim-core/app/jobs/decidim/find_and_update_descendants_job.rb index 21d3cbc635ab8..92489eaccff45 100644 --- a/decidim-core/app/jobs/decidim/find_and_update_descendants_job.rb +++ b/decidim-core/app/jobs/decidim/find_and_update_descendants_job.rb @@ -4,8 +4,14 @@ module Decidim # Update search indexes for each descendants of a given element class FindAndUpdateDescendantsJob < ApplicationJob queue_as :default + MAX_DEPTH = 5 + + def perform(element, current_depth = 0) + if current_depth >= MAX_DEPTH + Rails.logger.warn "Max depth of #{MAX_DEPTH} reached for element #{element.class.name} with id #{element.id}. Stopping recursion." + return + end - def perform(element) descendants_collector = components_for(element) descendants_collector << element.comments.to_a if element.respond_to?(:comments) @@ -14,7 +20,7 @@ def perform(element) descendants_collector.each do |descendants| next if descendants.blank? - Decidim::UpdateSearchIndexesJob.perform_later(descendants) + Decidim::UpdateSearchIndexesJob.perform_later(descendants, current_depth + 1) end end diff --git a/decidim-core/app/jobs/decidim/update_search_indexes_job.rb b/decidim-core/app/jobs/decidim/update_search_indexes_job.rb index 5c8904ccc3706..575096e8f3bb6 100644 --- a/decidim-core/app/jobs/decidim/update_search_indexes_job.rb +++ b/decidim-core/app/jobs/decidim/update_search_indexes_job.rb @@ -4,8 +4,8 @@ module Decidim class UpdateSearchIndexesJob < ApplicationJob queue_as :default - def perform(elements) - elements.each { |element| element.try(:try_update_index_for_search_resource) } + def perform(elements, current_depth = 0) + elements.each { |element| element.try(:try_update_index_for_search_resource, current_depth) } end end end diff --git a/decidim-core/lib/decidim/searchable.rb b/decidim-core/lib/decidim/searchable.rb index 4f93222f8c5a8..9ad46b191f57d 100644 --- a/decidim-core/lib/decidim/searchable.rb +++ b/decidim-core/lib/decidim/searchable.rb @@ -101,7 +101,7 @@ def add_to_index_as_search_resource # Public: after_update callback to update index information of the model. # - def try_update_index_for_search_resource + def try_update_index_for_search_resource(current_depth = 0) return unless self.class.searchable_resource?(self) org = self.class.search_resource_fields_mapper.retrieve_organization(self) @@ -124,13 +124,13 @@ def try_update_index_for_search_resource searchables_in_org.destroy_all end - find_and_update_descendants + find_and_update_descendants(current_depth) end private - def find_and_update_descendants - Decidim::FindAndUpdateDescendantsJob.perform_later(self) + def find_and_update_descendants(current_depth = 0) + Decidim::FindAndUpdateDescendantsJob.perform_later(self, current_depth) end def contents_to_searchable_resource_attributes(fields, locale) diff --git a/decidim-core/spec/jobs/decidim/find_and_update_descendants_job_spec.rb b/decidim-core/spec/jobs/decidim/find_and_update_descendants_job_spec.rb index 21181a90fb265..65a3d2c03af17 100644 --- a/decidim-core/spec/jobs/decidim/find_and_update_descendants_job_spec.rb +++ b/decidim-core/spec/jobs/decidim/find_and_update_descendants_job_spec.rb @@ -39,6 +39,14 @@ end.to have_enqueued_job(Decidim::UpdateSearchIndexesJob).exactly(:twice) end + context "when recursion reaches max depth" do + it "does not update search indexes" do + expect do + Decidim::FindAndUpdateDescendantsJob.perform_now(participatory_process, described_class::MAX_DEPTH) + end.not_to have_enqueued_job(Decidim::UpdateSearchIndexesJob) + end + end + context "when participatory process has no descendants" do let(:proposal_component) { nil } let(:post_component) { nil } diff --git a/decidim-core/spec/lib/searchable_spec.rb b/decidim-core/spec/lib/searchable_spec.rb index fa824b2f6b134..988dd21d5744b 100644 --- a/decidim-core/spec/lib/searchable_spec.rb +++ b/decidim-core/spec/lib/searchable_spec.rb @@ -70,7 +70,7 @@ module Decidim context "when searchable does not have component" do it "enqueues the job when participatory process is updated" do - expect(Decidim::FindAndUpdateDescendantsJob).to receive(:perform_later).with(participatory_process) + expect(Decidim::FindAndUpdateDescendantsJob).to receive(:perform_later).with(participatory_process, 0) participatory_process.update!(published_at: nil) end @@ -81,7 +81,7 @@ module Decidim let!(:resource) { create(:proposal, :official, component: proposal_component) } it "enqueues the job when participatory process is updated" do - expect(Decidim::FindAndUpdateDescendantsJob).to receive(:perform_later).with(participatory_process) + expect(Decidim::FindAndUpdateDescendantsJob).to receive(:perform_later).with(participatory_process, 0) participatory_process.update!(published_at: nil) end diff --git a/decidim-meetings/spec/models/meeting_spec.rb b/decidim-meetings/spec/models/meeting_spec.rb index 3e28d519d0e32..891a144995733 100644 --- a/decidim-meetings/spec/models/meeting_spec.rb +++ b/decidim-meetings/spec/models/meeting_spec.rb @@ -478,5 +478,35 @@ module Decidim::Meetings end end end + + describe "search index updates with linked meetings" do + let(:organization) { create(:organization, available_locales: [:en]) } + let(:space_a) { create(:participatory_process, organization:) } + let(:space_b) { create(:participatory_process, organization:) } + let(:component_a) { create(:meeting_component, participatory_space: space_a) } + let(:component_b) { create(:meeting_component, participatory_space: space_b) } + let!(:meeting_a) { create(:meeting, :published, component: component_a, title: { en: "Meeting A" }) } + let!(:meeting_b) { create(:meeting, :published, component: component_b, title: { en: "Meeting B" }) } + + before do + create(:meeting_link, meeting: meeting_a, component: component_b) + create(:meeting_link, meeting: meeting_b, component: component_a) + end + + it "does not enqueue descendants indexing indefinitely" do + clear_enqueued_jobs + clear_performed_jobs + + perform_enqueued_jobs(only: [Decidim::FindAndUpdateDescendantsJob, Decidim::UpdateSearchIndexesJob]) do + meeting_a.update!(title: { en: "Updated meeting A" }) + end + + find_jobs = performed_jobs.count { |job| job[:job] == Decidim::FindAndUpdateDescendantsJob } + update_jobs = performed_jobs.count { |job| job[:job] == Decidim::UpdateSearchIndexesJob } + + expect(find_jobs).to be <= Decidim::FindAndUpdateDescendantsJob::MAX_DEPTH + 1 + expect(update_jobs).to be <= Decidim::FindAndUpdateDescendantsJob::MAX_DEPTH + end + end end end From f3c14fe4a1ef9647ea457a1724ec2db089f29624 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:40:00 +0100 Subject: [PATCH 130/135] Bump to dependencies: Bump json from 2.19.1 to 2.19.2 (#16433) * Bump to dependencies: Bump json from 2.19.1 to 2.19.2 Bumps [json](https://github.com/ruby/json) from 2.19.1 to 2.19.2. - [Release notes](https://github.com/ruby/json/releases) - [Changelog](https://github.com/ruby/json/blob/master/CHANGES.md) - [Commits](https://github.com/ruby/json/compare/v2.19.1...v2.19.2) --- updated-dependencies: - dependency-name: json dependency-version: 2.19.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 442e40d5dbdae..6dcfe7dd64769 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -511,7 +511,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.1) + json (2.19.2) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index b12adb49ee307..444c4c4290478 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -505,7 +505,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.1) + json (2.19.2) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) From d4e2aa7cad26504f5c52911f2140914147eea555 Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Thu, 19 Mar 2026 17:11:42 +0200 Subject: [PATCH 131/135] Fix flaky spec on `admin_moderates_user_spec.rb` (#16432) --- .../lib/decidim/admin/test/manage_hide_content_examples.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/decidim-admin/lib/decidim/admin/test/manage_hide_content_examples.rb b/decidim-admin/lib/decidim/admin/test/manage_hide_content_examples.rb index 67e0ec58d1624..adbb0a5cc694b 100644 --- a/decidim-admin/lib/decidim/admin/test/manage_hide_content_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/manage_hide_content_examples.rb @@ -53,6 +53,7 @@ fill_in :block_user_justification, with: "This user is a spammer" * 2 # to have at least 15 chars click_on I18n.t("decidim.admin.block_user.new.action") + expect(page).to have_content(I18n.t("decidim.admin.officializations.block.success")) expect(content.reload).to be_hidden From 2134e521bfa9b9f2c20b92856ecb4ab593822032 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:01:42 +0100 Subject: [PATCH 132/135] Bump to dependencies: Bump bcrypt from 3.1.21 to 3.1.22 (#16439) * Bump to dependencies: Bump bcrypt from 3.1.21 to 3.1.22 Bumps [bcrypt](https://github.com/bcrypt-ruby/bcrypt-ruby) from 3.1.21 to 3.1.22. - [Release notes](https://github.com/bcrypt-ruby/bcrypt-ruby/releases) - [Changelog](https://github.com/bcrypt-ruby/bcrypt-ruby/blob/master/CHANGELOG) - [Commits](https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.21...v3.1.22) --- updated-dependencies: - dependency-name: bcrypt dependency-version: 3.1.22 dependency-type: indirect ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 2 +- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6dcfe7dd64769..36bbfd35db2b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -290,7 +290,7 @@ GEM ast (2.4.3) base64 (0.3.0) batch-loader (2.0.6) - bcrypt (3.1.21) + bcrypt (3.1.22) better_html (2.2.0) actionview (>= 7.0) activesupport (>= 7.0) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 444c4c4290478..ebf717a53510e 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -290,7 +290,7 @@ GEM ast (2.4.3) base64 (0.3.0) batch-loader (2.0.6) - bcrypt (3.1.21) + bcrypt (3.1.22) better_html (2.2.0) actionview (>= 7.0) activesupport (>= 7.0) From 255c5e3c755ccf6ae678ba9bf63929de2f6cfad8 Mon Sep 17 00:00:00 2001 From: MariaDascaluPublicis <166501615+MariaDascaluPublicis@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:09:05 +0200 Subject: [PATCH 133/135] Fix datepicker position when is placed at the bottom of the page (#15300) * fix datepicker position when is placed at the bottom of the page * Added guard condition * Dynamically adjust the datepicker position * Apply coderabbit recommendation * Apply latest coderabbot recommendations * Compute position for timepicker as well * Renamed file and added test file * Fixed pipeline * Small fix * Fixed tests * Merged test files Signed-off-by: andra-panaite --------- Signed-off-by: andra-panaite Co-authored-by: andra-panaite Co-authored-by: Alexandru Emil Lupu --- .../datepicker/datepicker_functions.js | 26 ++ .../decidim/datepicker/generate_datepicker.js | 3 +- .../decidim/datepicker/generate_timepicker.js | 5 +- ...r_functions_adjust_picker_position.test.js | 234 ++++++++++++++++++ 4 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 decidim-core/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js diff --git a/decidim-core/app/packs/src/decidim/datepicker/datepicker_functions.js b/decidim-core/app/packs/src/decidim/datepicker/datepicker_functions.js index ca351caf064a9..4c52502f66c1e 100644 --- a/decidim-core/app/packs/src/decidim/datepicker/datepicker_functions.js +++ b/decidim-core/app/packs/src/decidim/datepicker/datepicker_functions.js @@ -1,5 +1,31 @@ // Utility helper functions for the date and time picker functionality +export const adjustPickerPosition = (input, datePickerContainer, selector) => { + const parent = input.closest(selector); + + if (getComputedStyle(parent).position === "static") { + parent.style.position = "relative"; + } + + const rect = input.getBoundingClientRect(); + const calendarHeight = datePickerContainer.offsetHeight; + const spaceAbove = rect.top; + const spaceBelow = window.innerHeight - rect.bottom; + const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove; + + if (openBelow) { + // Open below + datePickerContainer.style.top = `${input.offsetHeight}px`; + datePickerContainer.style.bottom = ""; + } else { + // Open above + datePickerContainer.style.top = ""; + datePickerContainer.style.bottom = `${input.offsetHeight}px`; + } + + datePickerContainer.style.right = "0px"; +}; + export const setHour = (value, format) => { const hour = value.split(":")[0]; if (format === 12) { diff --git a/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js b/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js index a00c9cad2e1fa..c87fbcae06802 100644 --- a/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js +++ b/decidim-core/app/packs/src/decidim/datepicker/generate_datepicker.js @@ -1,6 +1,6 @@ /* eslint-disable require-jsdoc */ import icon from "src/decidim/refactor/moved/icon" -import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions" +import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions" import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners" import { getDictionary } from "src/decidim/refactor/moved/i18n" @@ -124,6 +124,7 @@ export default function generateDatePicker(input, row, formats) { }; pickedDate = null; datePickerContainer.style.display = "block"; + adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column"); document.addEventListener("click", datePickerDisplay); diff --git a/decidim-core/app/packs/src/decidim/datepicker/generate_timepicker.js b/decidim-core/app/packs/src/decidim/datepicker/generate_timepicker.js index da308b657a0c0..ca10820a84b6a 100644 --- a/decidim-core/app/packs/src/decidim/datepicker/generate_timepicker.js +++ b/decidim-core/app/packs/src/decidim/datepicker/generate_timepicker.js @@ -2,7 +2,7 @@ /* eslint max-lines: ["error", 310] */ import icon from "src/decidim/refactor/moved/icon" -import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue } from "src/decidim/datepicker/datepicker_functions" +import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions" import { timeKeyDownListener, timeBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"; import { getDictionary } from "src/decidim/refactor/moved/i18n"; @@ -29,7 +29,6 @@ export default function generateTimePicker(input, row, formats) { clock.setAttribute("disabled", input.attributes.disabled); }; - timeColumn.appendChild(time); timeColumn.appendChild(clock); @@ -279,6 +278,8 @@ export default function generateTimePicker(input, row, formats) { event.preventDefault(); timePicker.style.display = "block"; document.addEventListener("click", timePickerDisplay); + adjustPickerPosition(time, timePicker, ".datepicker__time-column") + hours.value = hourDisplay(hour); minutes.value = minuteDisplay(minute); }); diff --git a/decidim-core/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js b/decidim-core/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js new file mode 100644 index 0000000000000..290c5c15c9b43 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js @@ -0,0 +1,234 @@ +/* global jest */ + +import { adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"; + +describe("adjustDatePickerPosition", () => { + let input = null; + let parent = null; + let datePickerContainer = null; + + let originalInnerHeight = window.innerHeight; + + beforeEach(() => { + // Setup DOM structure + parent = document.createElement("div"); + parent.className = "datepicker__date-column"; + document.body.appendChild(parent); + + input = document.createElement("input"); + Reflect.defineProperty(input, "offsetHeight", { + configurable: true, + value: 40 + }); + parent.appendChild(input); + + datePickerContainer = document.createElement("div"); + datePickerContainer.className = "datepicker__container"; + parent.appendChild(datePickerContainer); + + // Mock offsetHeight for calendar + Reflect.defineProperty(datePickerContainer, "offsetHeight", { + configurable: true, + value: 300 + }); + + // store original viewport height + originalInnerHeight = window.innerHeight; + }); + + afterEach(() => { + document.body.removeChild(parent); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: originalInnerHeight + }); + + jest.restoreAllMocks(); + }); + + it("sets parent position to relative when static", () => { + parent.style.position = "static"; + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(parent.style.position).toBe("relative"); + }); + + it("does not change parent position when already positioned", () => { + parent.style.position = "absolute"; + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(parent.style.position).toBe("absolute"); + }); + + it("opens below when sufficient space below", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 100, + bottom: 140 + }); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: 800 + }); + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(datePickerContainer.style.top).toBe("40px"); + expect(datePickerContainer.style.bottom).toBe(""); + }); + + it("opens above when insufficient space below", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 400, + bottom: 440 + }); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: 500 + }); + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(datePickerContainer.style.top).toBe(""); + expect(datePickerContainer.style.bottom).toBe("40px"); + }); + + it("prefers opening below when space is equal above and below", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 250, + bottom: 290 + }); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: 540 + }); + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(datePickerContainer.style.top).toBe("40px"); + expect(datePickerContainer.style.bottom).toBe(""); + }); + + it("always sets right position to 0px", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 100, + bottom: 140 + }); + + adjustPickerPosition(input, datePickerContainer, ".datepicker__date-column"); + + expect(datePickerContainer.style.right).toBe("0px"); + }); +}); + + +describe("adjustTimePickerPosition", () => { + let input = null; + let parent = null; + let timePicker = null; + + let originalInnerHeight = window.innerHeight; + + beforeEach(() => { + parent = document.createElement("div"); + parent.className = "datepicker__time-column"; + document.body.appendChild(parent); + + input = document.createElement("input"); + Reflect.defineProperty(input, "offsetHeight", { + configurable: true, + value: 30 + }); + parent.appendChild(input); + + timePicker = document.createElement("div"); + timePicker.className = "timepicker__container"; + parent.appendChild(timePicker); + + Reflect.defineProperty(timePicker, "offsetHeight", { + configurable: true, + value: 200 + }); + + // store original value before any test mutates it + originalInnerHeight = window.innerHeight; + }); + + afterEach(() => { + // restore DOM + document.body.removeChild(parent); + + // restore window.innerHeight (fix for CodeRabbit warning) + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: originalInnerHeight + }); + + jest.restoreAllMocks(); + }); + + it("sets parent position to relative when static", () => { + parent.style.position = "static"; + + adjustPickerPosition(input, timePicker, ".datepicker__time-column"); + + expect(parent.style.position).toBe("relative"); + }); + + it("opens below when there is enough space", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 100, + bottom: 130 + }); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: 700 + }); + + adjustPickerPosition(input, timePicker, ".datepicker__time-column"); + + expect(timePicker.style.top).toBe("30px"); + expect(timePicker.style.bottom).toBe(""); + }); + + it("opens above when there is not enough space below", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 400, + bottom: 430 + }); + + Reflect.defineProperty(window, "innerHeight", { + writable: true, + configurable: true, + value: 500 + }); + + adjustPickerPosition(input, timePicker, ".datepicker__time-column"); + + expect(timePicker.style.top).toBe(""); + expect(timePicker.style.bottom).toBe("30px"); + }); + + it("always aligns to the right", () => { + jest.spyOn(input, "getBoundingClientRect").mockReturnValue({ + top: 100, + bottom: 130 + }); + + adjustPickerPosition(input, timePicker, ".datepicker__time-column"); + + expect(timePicker.style.right).toBe("0px"); + }); +}); From 4413e768376ff7b582196cbc7b7e177948ee5063 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 06:39:56 +0100 Subject: [PATCH 134/135] Bump to dependencies: Bump nokogiri from 1.19.1 to 1.19.2 (#16447) * Bump to dependencies: Bump nokogiri from 1.19.1 to 1.19.2 Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.19.1...v1.19.2) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.19.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * chore: sync decidim-generators/Gemfile.lock --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- Gemfile.lock | 4 ++-- decidim-generators/Gemfile.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 36bbfd35db2b9..24d321f7ef3f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -585,9 +585,9 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.1-arm64-darwin) + nokogiri (1.19.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index ebf717a53510e..46468cf84c75b 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -579,7 +579,7 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) From 113296879f461e9caee8521aec15ffd33ff5ba5d Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Fri, 20 Mar 2026 12:34:26 +0200 Subject: [PATCH 135/135] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrés Pereira de Lucena --- .../app/views/decidim/devise/shared/_tos_fields.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb index 44781dad2ef08..c82345725ad9a 100644 --- a/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb +++ b/decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb @@ -7,7 +7,7 @@ <%= cell content_block.manifest.cell, content_block %> <% end %> - <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service", locale: current_locale))), label_options: { class: "form__wrapper-checkbox-label" }, "aria-describedby": "terms_of_service_summary", "required": "required" %> + <%= form.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), decidim.page_path("terms-of-service", locale: current_locale))), label_options: { class: "form__wrapper-checkbox-label" }, "aria-describedby": "terms_of_service_summary" %>