diff --git a/decidim-accountability/app/commands/decidim/accountability/admin/update_result_dates.rb b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_dates.rb
new file mode 100644
index 0000000000000..5e7445341a7f1
--- /dev/null
+++ b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_dates.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Accountability
+ module Admin
+ # A command with all the business logic when an admin batch updates results dates.
+ class UpdateResultDates < Decidim::Command
+ # Public: Initializes the command.
+ #
+ # start_date - the start date to update
+ # end_date - the end date to update
+ # result_ids - the results ids to update
+ # current_user - the user performing the action
+ def initialize(start_date, end_date, result_ids, current_user)
+ @start_date = start_date
+ @end_date = end_date
+ @result_ids = result_ids
+ @current_user = current_user
+ end
+
+ # Executes the command. Broadcasts these events:
+ #
+ # - :ok when everything is valid
+ # - :invalid if the form was not valid and we could not proceed.
+ #
+ # Returns nothing.
+ def call
+ return broadcast(:invalid) if (start_date.blank? && end_date.blank?) || result_ids.blank?
+
+ update_results_dates
+
+ broadcast(:ok)
+ end
+
+ private
+
+ attr_reader :start_date, :end_date, :result_ids, :current_user
+
+ def update_results_dates
+ Decidim::Accountability::Result.where(id: result_ids).find_each do |result|
+ next if result.start_date == start_date && result.end_date == end_date
+
+ result.update!(start_date:, end_date:)
+
+ # Trace the action to keep track of changes
+ Decidim.traceability.perform_action!(
+ "update",
+ result,
+ current_user
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/app/commands/decidim/accountability/admin/update_result_status.rb b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_status.rb
new file mode 100644
index 0000000000000..10ae5bd9d4921
--- /dev/null
+++ b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_status.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Accountability
+ module Admin
+ # A command with all the business logic when an admin batch updates results status.
+ class UpdateResultStatus < Decidim::Command
+ # Public: Initializes the command.
+ #
+ # status_id - the status id to update
+ # result_ids - the results ids to update
+ # current_user - the user performing the action
+ def initialize(status_id, result_ids, current_user)
+ @status_id = status_id
+ @result_ids = result_ids
+ @current_user = current_user
+ end
+
+ # Executes the command. Broadcasts these events:
+ #
+ # - :ok when everything is valid
+ # - :invalid if the form was not valid and we could not proceed.
+ #
+ # Returns nothing.
+ def call
+ return broadcast(:invalid) if status_id.blank? || result_ids.blank?
+
+ update_results_status
+
+ broadcast(:ok)
+ end
+
+ private
+
+ attr_reader :status_id, :result_ids, :current_user
+
+ def update_results_status
+ Decidim::Accountability::Result.where(id: result_ids).find_each do |result|
+ next if result.decidim_accountability_status_id == status_id
+
+ status = Decidim::Accountability::Status.find_by(id: status_id)
+
+ next if status.blank?
+
+ result.update!(
+ decidim_accountability_status_id: status_id,
+ progress: status.progress
+ )
+
+ # Trace the action to keep track of changes
+ Decidim.traceability.perform_action!(
+ "update",
+ result,
+ current_user
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/app/commands/decidim/accountability/admin/update_result_taxonomies.rb b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_taxonomies.rb
new file mode 100644
index 0000000000000..4d109d3940b51
--- /dev/null
+++ b/decidim-accountability/app/commands/decidim/accountability/admin/update_result_taxonomies.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Accountability
+ module Admin
+ # A command with all the business logic when an admin batch updates results taxonomies.
+ class UpdateResultTaxonomies < UpdateResourcesTaxonomies
+ # Public: Initializes the command.
+ #
+ # taxonomy_ids - the taxonomy ids to update
+ # result_ids - the results ids to update.
+ def initialize(taxonomy_ids, result_ids, organization)
+ super(taxonomy_ids, Decidim::Accountability::Result.where(id: result_ids), organization)
+ end
+ end
+ end
+ end
+end
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
new file mode 100644
index 0000000000000..a89efd03afd5c
--- /dev/null
+++ b/decidim-accountability/app/controllers/decidim/accountability/admin/results_bulk_actions_controller.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Accountability
+ module Admin
+ class ResultsBulkActionsController < Admin::ApplicationController
+ include Decidim::ApplicationHelper
+ include Decidim::SanitizeHelper
+ include Decidim::Admin::ComponentTaxonomiesHelper
+
+ def update_taxonomies
+ enforce_permission_to :create, :bulk_update
+
+ Admin::UpdateResultTaxonomies.call(result_params[:taxonomies], result_ids, current_organization) do
+ on(:invalid_taxonomies) do
+ flash[:alert] = I18n.t(
+ "results.update_taxonomies.select_a_taxonomy",
+ scope: "decidim.accountability.admin"
+ )
+ end
+
+ on(:invalid_resources) do
+ flash[:alert] = I18n.t(
+ "results.update_taxonomies.select_a_result",
+ scope: "decidim.accountability.admin"
+ )
+ end
+
+ on(:update_resources_taxonomies) do |response|
+ if response[:successful].any?
+ flash[:notice] = t(
+ "results.update_taxonomies.success",
+ taxonomies: response[:taxonomies].map { |taxonomy| decidim_escape_translated(taxonomy.name) }.to_sentence,
+ results: response[:successful].map { |resource| decidim_escape_translated(resource.title) }.to_sentence,
+ scope: "decidim.accountability.admin"
+ )
+ end
+ if response[:errored].any?
+ flash[:alert] = t(
+ "results.update_taxonomies.invalid",
+ taxonomies: response[:taxonomies].map { |taxonomy| decidim_escape_translated(taxonomy.name) }.to_sentence,
+ results: response[:errored].map { |resource| decidim_escape_translated(resource.title) }.to_sentence,
+ scope: "decidim.accountability.admin"
+ )
+ end
+ end
+ end
+
+ redirect_to results_path
+ end
+
+ def update_status
+ enforce_permission_to :create, :bulk_update
+
+ UpdateResultStatus.call(result_params[:decidim_accountability_status_id], result_ids, current_user) do
+ on(:ok) do
+ flash[:notice] = I18n.t("results.update_status.success", scope: "decidim.accountability.admin")
+ redirect_to results_path
+ end
+
+ on(:invalid) do
+ flash[:alert] = I18n.t("results.update_status.invalid", scope: "decidim.accountability.admin")
+ redirect_to results_path
+ end
+ end
+ end
+
+ def update_dates
+ enforce_permission_to :create, :bulk_update
+
+ UpdateResultDates.call(result_params[:start_date], result_params[:end_date], result_ids, current_user) do
+ on(:ok) do
+ flash[:notice] = I18n.t("results.update_dates.success", scope: "decidim.accountability.admin")
+ redirect_to results_path
+ end
+
+ on(:invalid) do
+ flash[:alert] = I18n.t("results.update_dates.invalid", scope: "decidim.accountability.admin")
+ redirect_to results_path
+ end
+ end
+ end
+
+ private
+
+ def result_ids
+ result_params[:result_ids].map { |ids| ids.split(",") }.flatten.map(&:to_i)
+ end
+
+ def result_params
+ @result_params ||= params.require(:result_bulk_actions).permit(
+ :decidim_accountability_status_id,
+ :start_date,
+ :end_date,
+ result_ids: [],
+ taxonomies: []
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/app/controllers/decidim/accountability/admin/results_controller.rb b/decidim-accountability/app/controllers/decidim/accountability/admin/results_controller.rb
index 12fb525feb87a..f985a4a8f354c 100644
--- a/decidim-accountability/app/controllers/decidim/accountability/admin/results_controller.rb
+++ b/decidim-accountability/app/controllers/decidim/accountability/admin/results_controller.rb
@@ -7,14 +7,15 @@ module Admin
class ResultsController < Admin::ApplicationController
include Decidim::ApplicationHelper
include Decidim::SanitizeHelper
+ include Decidim::Admin::ComponentTaxonomiesHelper
include Decidim::Accountability::Admin::Filterable
include Decidim::Admin::HasTrashableResources
- helper_method :results, :parent_result, :parent_results, :statuses, :present
+ helper_method :results, :parent_result, :parent_results, :statuses, :present, :bulk_actions_form
def collection
parent_id = params[:parent_id].presence
- @collection ||= Result.where(component: current_component, parent_id:).page(params[:page]).per(15)
+ @collection ||= Result.where(component: current_component, parent_id:).page(params[:page]).per(15).order(created_at: :asc)
end
def new
@@ -103,6 +104,10 @@ def parent_results
def statuses
@statuses ||= Status.where(component: current_component)
end
+
+ def bulk_actions_form
+ @bulk_actions_form ||= ResultBulkActionsForm.new(result_ids: [])
+ end
end
end
end
diff --git a/decidim-accountability/app/forms/decidim/accountability/admin/result_bulk_actions_form.rb b/decidim-accountability/app/forms/decidim/accountability/admin/result_bulk_actions_form.rb
new file mode 100644
index 0000000000000..eaf83c6890370
--- /dev/null
+++ b/decidim-accountability/app/forms/decidim/accountability/admin/result_bulk_actions_form.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Accountability
+ module Admin
+ # This class holds a Form to create/update results from Decidim's admin panel.
+ class ResultBulkActionsForm < Decidim::Form
+ include Decidim::TranslationsHelper
+ include Decidim::HasTaxonomyFormAttributes
+
+ attribute :result_ids, Array[Integer]
+ attribute :start_date, Decidim::Attributes::LocalizedDate
+ attribute :end_date, Decidim::Attributes::LocalizedDate
+ attribute :decidim_accountability_status_id, Integer
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin.js b/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_form.js
similarity index 52%
rename from decidim-accountability/app/packs/entrypoints/decidim_accountability_admin.js
rename to decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_form.js
index d687a49e8d50b..76905daf9c664 100644
--- a/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin.js
+++ b/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_form.js
@@ -1,2 +1 @@
-import "src/decidim/accountability/admin/index"
import "src/decidim/accountability/admin/result_form"
diff --git a/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_index.js b/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_index.js
new file mode 100644
index 0000000000000..4c09bee39847d
--- /dev/null
+++ b/decidim-accountability/app/packs/entrypoints/decidim_accountability_admin_index.js
@@ -0,0 +1 @@
+import "src/decidim/accountability/admin/index"
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index.js
index a45c0890e5e71..26c965931c973 100644
--- a/decidim-accountability/app/packs/src/decidim/accountability/admin/index.js
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index.js
@@ -1,9 +1,19 @@
+import Counter from "src/decidim/accountability/admin/index/counter";
+import ActionButton from "src/decidim/accountability/admin/index/action_button";
+import ActionForm from "src/decidim/accountability/admin/index/action_form";
+import ActionSelector from "src/decidim/accountability/admin/index/action_selector";
+import SelectAll from "src/decidim/accountability/admin/index/select_all";
+
$(() => {
- $("#result_decidim_accountability_status_id").change(function () {
- /* eslint-disable no-invalid-this */
- const progress = $(this).find(":selected").data("progress")
- if (progress || progress === 0) {
- $("#result_progress").val(progress);
- }
- });
+ const counter = new Counter();
+ const actionButton = new ActionButton(counter);
+ const actionForm = new ActionForm(counter);
+ const actionSelector = new ActionSelector();
+ const selectAll = new SelectAll(counter);
+
+ counter.init();
+ actionButton.init();
+ actionForm.init();
+ actionSelector.init();
+ selectAll.init();
})
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_button.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_button.js
new file mode 100644
index 0000000000000..98e67a36d9ec5
--- /dev/null
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_button.js
@@ -0,0 +1,42 @@
+class ActionButton {
+ constructor(counter) {
+ this.counter = counter;
+ this.actionButton = document.querySelector("[data-action-button]");
+ this.actionForms = document.querySelectorAll("[data-action-form]");
+ }
+
+ init() {
+ this.counter.checkboxes.forEach((checkbox) => {
+ checkbox.addEventListener("change", () => this.onCheckboxChange());
+ });
+
+ this.toggleActionButton();
+ }
+
+ onCheckboxChange() {
+ this.toggleActionButton();
+ this.toggleActionForms();
+ }
+
+ toggleActionButton() {
+ const selectedIds = this.counter.getSelectedItems();
+
+ if (selectedIds.length > 0) {
+ this.actionButton.classList.remove("hide");
+ } else {
+ this.actionButton.classList.add("hide");
+ }
+ }
+
+ toggleActionForms() {
+ const selectedIds = this.counter.getSelectedItems();
+
+ if (selectedIds.length === 0) {
+ this.actionForms.forEach((form) => {
+ form.classList.add("hide");
+ });
+ }
+ }
+}
+
+export default ActionButton;
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_form.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_form.js
new file mode 100644
index 0000000000000..46076801c3801
--- /dev/null
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_form.js
@@ -0,0 +1,46 @@
+class ActionForm {
+ constructor(counter) {
+ this.counter = counter;
+ this.checkboxes = document.querySelectorAll("[data-result-checkbox]");
+ this.idFields = document.querySelectorAll("[data-result-ids-field]");
+ this.cancelButtons = document.querySelectorAll("[data-cancel-button]");
+ }
+
+ init() {
+ this.checkboxes.forEach((checkbox) => {
+ checkbox.addEventListener("change", () => this.onCheckboxChange());
+ });
+
+ this.cancelButtons.forEach((button) => {
+ button.addEventListener("click", () => this.onCancelButtonClick());
+ });
+
+ this.updateResultIdsHiddenField();
+ }
+
+ onCheckboxChange() {
+ this.updateResultIdsHiddenField();
+ }
+
+ onCancelButtonClick() {
+ this.hideAllForms();
+ }
+
+ hideAllForms() {
+ const forms = document.querySelectorAll("[data-action-form]");
+ forms.forEach((form) => {
+ form.classList.add("hide");
+ });
+ }
+
+ updateResultIdsHiddenField() {
+ const selectedIds = this.counter.getSelectedItems().
+ map((checkbox) => checkbox.dataset.resultId);
+
+ this.idFields.forEach((field) => {
+ field.value = selectedIds.join(",");
+ });
+ }
+}
+
+export default ActionForm;
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_selector.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_selector.js
new file mode 100644
index 0000000000000..31f353ca1f41a
--- /dev/null
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/action_selector.js
@@ -0,0 +1,37 @@
+class ActionSelector {
+ init() {
+ this.dropdownElement = document.querySelector("#js-bulk-actions-dropdown");
+ const buttons = this.dropdownElement.querySelectorAll("button");
+
+ buttons.forEach((button) => {
+ button.addEventListener("click", this.onActionClick.bind(this));
+ });
+ }
+
+ onActionClick(event) {
+ const action = event.target.dataset.action;
+
+ this.closeDropdown();
+ this.hideAllForms();
+ this.showForm(action);
+ }
+
+ closeDropdown() {
+ this.dropdownElement.classList.remove("is-open");
+ }
+
+ showForm(action) {
+ const form = document.querySelector(`[data-action-form="${action}"]`);
+
+ form.classList.remove("hide");
+ }
+
+ hideAllForms() {
+ const forms = document.querySelectorAll("[data-action-form]");
+ forms.forEach((form) => {
+ form.classList.add("hide");
+ });
+ }
+}
+
+export default ActionSelector;
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index/counter.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/counter.js
new file mode 100644
index 0000000000000..4dd08a41ae420
--- /dev/null
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/counter.js
@@ -0,0 +1,40 @@
+/**
+ * Counter class handles the selection of results and updates the counter
+ */
+class Counter {
+ constructor() {
+ this.checkboxes = document.querySelectorAll("[data-result-checkbox]");
+ this.counterElement = document.querySelector("[data-selected-count]");
+ }
+
+ init() {
+ if (!this.checkboxes.length) {
+ return;
+ }
+
+ this.checkboxes.forEach((checkbox) => {
+ checkbox.addEventListener("change", () => this.updateCounter());
+ });
+
+ this.updateCounter();
+ }
+
+ updateCounter() {
+ if (this.counterElement) {
+ const count = this.getSelectedItems().length;
+ this.counterElement.textContent = count;
+
+ if (count === 0) {
+ this.counterElement.classList.add("hide");
+ } else {
+ this.counterElement.classList.remove("hide");
+ }
+ }
+ }
+
+ getSelectedItems() {
+ return Array.from(this.checkboxes).filter((checkbox) => checkbox.checked);
+ }
+}
+
+export default Counter;
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/index/select_all.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/select_all.js
new file mode 100644
index 0000000000000..50288ef5c0385
--- /dev/null
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/index/select_all.js
@@ -0,0 +1,29 @@
+class SelectAll {
+ constructor(counter) {
+ this.counter = counter;
+ this.checkboxes = this.counter.checkboxes;
+ this.selectAllButton = document.querySelector("[data-select-all]");
+ }
+
+ init() {
+ if (this.selectAllButton) {
+ this.selectAllButton.addEventListener("click", (event) => this.onSelectAllClick(event));
+ }
+ }
+
+ onSelectAllClick(event) {
+ event.preventDefault();
+
+ const someUnchecked = Array.from(this.checkboxes).some((checkbox) => !checkbox.checked);
+
+ // If some checkboxes are unchecked, check all of them
+ // Otherwise, uncheck all checkboxes
+ this.checkboxes.forEach((checkbox) => {
+ checkbox.checked = someUnchecked;
+ // Trigger change event to update other components
+ checkbox.dispatchEvent(new Event("change"));
+ });
+ }
+}
+
+export default SelectAll;
diff --git a/decidim-accountability/app/packs/src/decidim/accountability/admin/result_form.js b/decidim-accountability/app/packs/src/decidim/accountability/admin/result_form.js
index 82933efcde0b2..9028d33b4b313 100644
--- a/decidim-accountability/app/packs/src/decidim/accountability/admin/result_form.js
+++ b/decidim-accountability/app/packs/src/decidim/accountability/admin/result_form.js
@@ -10,4 +10,12 @@ $(() => {
attachGeocoding($resultAddress);
}
}
+
+ $("#result_decidim_accountability_status_id").change(function () {
+ /* eslint-disable no-invalid-this */
+ const progress = $(this).find(":selected").data("progress")
+ if (progress || progress === 0) {
+ $("#result_progress").val(progress);
+ }
+ });
});
diff --git a/decidim-accountability/app/permissions/decidim/accountability/admin/permissions.rb b/decidim-accountability/app/permissions/decidim/accountability/admin/permissions.rb
index faf9292049492..81957033c4818 100644
--- a/decidim-accountability/app/permissions/decidim/accountability/admin/permissions.rb
+++ b/decidim-accountability/app/permissions/decidim/accountability/admin/permissions.rb
@@ -11,6 +11,7 @@ def permissions
permission_action.allow! if can_perform_actions_on?(:status, status)
permission_action.allow! if can_perform_actions_on?(:timeline_entry, timeline_entry)
permission_action.allow! if can_perform_actions_on?(:import_projects, nil)
+ permission_action.allow! if can_perform_actions_on?(:bulk_update, nil)
permission_action
end
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/_result-tr.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/_result-tr.html.erb
index d1a5b610dde31..098225b446cb8 100644
--- a/decidim-accountability/app/views/decidim/accountability/admin/results/_result-tr.html.erb
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/_result-tr.html.erb
@@ -1,4 +1,7 @@
+
+ <%= check_box_tag "result_ids_s[]", result.id, false, data: { result_id: result.id, result_checkbox: true } %>
+ |
<%= result.id %>
|
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/_results-thead.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/_results-thead.html.erb
index 5645c7f17303e..783cd2bf6e5b9 100644
--- a/decidim-accountability/app/views/decidim/accountability/admin/results/_results-thead.html.erb
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/_results-thead.html.erb
@@ -1,5 +1,8 @@
+
+ <%= check_box_tag "results_bulk", "all", false, data: { "select-all" => true } %>
+ |
<%= sort_link(query, :id, t("models.result.fields.id", scope: "decidim.accountability"), default_order: :desc ) %>
|
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dates_form.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dates_form.html.erb
new file mode 100644
index 0000000000000..b81dcba2dcec1
--- /dev/null
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dates_form.html.erb
@@ -0,0 +1,19 @@
+
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dropdown.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dropdown.html.erb
new file mode 100644
index 0000000000000..07328df48fa7d
--- /dev/null
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_dropdown.html.erb
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_status_form.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_status_form.html.erb
new file mode 100644
index 0000000000000..ee103b7999a68
--- /dev/null
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_status_form.html.erb
@@ -0,0 +1,16 @@
+
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_submit_buttons.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_submit_buttons.html.erb
new file mode 100644
index 0000000000000..ccd4503e963cf
--- /dev/null
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_submit_buttons.html.erb
@@ -0,0 +1,4 @@
+
+ <%= submit_tag(submit_text, class: "button button__sm button__secondary small button--simple float-left") %>
+
+
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_taxonomies_form.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_taxonomies_form.html.erb
new file mode 100644
index 0000000000000..a58fabbfa4530
--- /dev/null
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/bulk_actions/_taxonomies_form.html.erb
@@ -0,0 +1,18 @@
+
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/edit.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/edit.html.erb
index 3ad1e8f5181be..235e5e5e3ccca 100644
--- a/decidim-accountability/app/views/decidim/accountability/admin/results/edit.html.erb
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/edit.html.erb
@@ -18,4 +18,4 @@
-<%= append_javascript_pack_tag "decidim_accountability_admin" %>
+<%= append_javascript_pack_tag "decidim_accountability_admin_form" %>
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/index.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/index.html.erb
index 53da25f57a2b0..4b562c816598d 100644
--- a/decidim-accountability/app/views/decidim/accountability/admin/results/index.html.erb
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/index.html.erb
@@ -1,33 +1,43 @@
<% add_decidim_page_title(t(".title")) %>
-
<%= decidim_paginate results %>
+
+<%= append_javascript_pack_tag "decidim_accountability_admin_index" %>
diff --git a/decidim-accountability/app/views/decidim/accountability/admin/results/new.html.erb b/decidim-accountability/app/views/decidim/accountability/admin/results/new.html.erb
index 353b6a78c018c..10e27342d432d 100644
--- a/decidim-accountability/app/views/decidim/accountability/admin/results/new.html.erb
+++ b/decidim-accountability/app/views/decidim/accountability/admin/results/new.html.erb
@@ -17,4 +17,4 @@
<% end %>
-<%= append_javascript_pack_tag "decidim_accountability_admin" %>
+<%= append_javascript_pack_tag "decidim_accountability_admin_form" %>
diff --git a/decidim-accountability/config/assets.rb b/decidim-accountability/config/assets.rb
index a6ca6ea3bd1c4..b7449da9fbcad 100644
--- a/decidim-accountability/config/assets.rb
+++ b/decidim-accountability/config/assets.rb
@@ -5,6 +5,7 @@
Decidim::Webpacker.register_path("#{base_path}/app/packs")
Decidim::Webpacker.register_entrypoints(
decidim_accountability: "#{base_path}/app/packs/entrypoints/decidim_accountability.js",
- decidim_accountability_admin: "#{base_path}/app/packs/entrypoints/decidim_accountability_admin.js",
- decidim_accountability_admin_imports: "#{base_path}/app/packs/entrypoints/decidim_accountability_admin_imports.js"
+ decidim_accountability_admin_form: "#{base_path}/app/packs/entrypoints/decidim_accountability_admin_form.js",
+ decidim_accountability_admin_imports: "#{base_path}/app/packs/entrypoints/decidim_accountability_admin_imports.js",
+ decidim_accountability_admin_index: "#{base_path}/app/packs/entrypoints/decidim_accountability_admin_index.js"
)
diff --git a/decidim-accountability/config/locales/en.yml b/decidim-accountability/config/locales/en.yml
index 83714092d7c12..dcb28c841494f 100644
--- a/decidim-accountability/config/locales/en.yml
+++ b/decidim-accountability/config/locales/en.yml
@@ -114,6 +114,23 @@ en:
other: "%{count} projects queued to be imported. You will be notified by email, once completed."
title: Import projects from another component
results:
+ bulk_actions:
+ dates_form:
+ change_dates: Change date
+ end_date: End date
+ start_date: Start date
+ dropdown:
+ actions: Actions
+ change_dates: Change dates
+ change_status: Change status
+ change_taxonomies: Change taxonomies
+ status_form:
+ change_status: Change status
+ status: Status
+ submit_buttons:
+ cancel: Cancel
+ taxonomies_form:
+ change_taxonomies: Change taxonomies
create:
invalid: There was a problem creating this result.
success: Result successfully created.
@@ -121,6 +138,7 @@ en:
title: Edit result
update: Update result
index:
+ selected: Selected
title: Results
manage_trash:
title: Deleted results
@@ -130,6 +148,17 @@ en:
update:
invalid: There was a problem updating this result.
success: Result successfully updated.
+ update_dates:
+ invalid: There was a problem updating the results dates
+ success: Results dates successfully updated
+ update_status:
+ invalid: There was a problem updating the results status
+ success: Results status successfully updated
+ update_taxonomies:
+ invalid: Could not update taxonomies %{taxonomies} for results %{results}
+ select_a_result: Select a result
+ select_a_taxonomy: Select a taxonomy
+ success: Successfully updated taxonomies %{taxonomies} for results %{results}
shared:
subnav:
statuses: Statuses
diff --git a/decidim-accountability/lib/decidim/accountability/admin_engine.rb b/decidim-accountability/lib/decidim/accountability/admin_engine.rb
index 4ebf72a8c7cfd..1f1b6848ca979 100644
--- a/decidim-accountability/lib/decidim/accountability/admin_engine.rb
+++ b/decidim-accountability/lib/decidim/accountability/admin_engine.rb
@@ -19,6 +19,12 @@ class AdminEngine < ::Rails::Engine
patch :restore
end
+ collection do
+ post :update_taxonomies, controller: "results_bulk_actions"
+ post :update_status, controller: "results_bulk_actions"
+ post :update_dates, controller: "results_bulk_actions"
+ end
+
get :proposals_picker, on: :collection
get :manage_trash, on: :collection
diff --git a/decidim-accountability/spec/commands/admin/update_result_dates_spec.rb b/decidim-accountability/spec/commands/admin/update_result_dates_spec.rb
new file mode 100644
index 0000000000000..ce4be7db6818b
--- /dev/null
+++ b/decidim-accountability/spec/commands/admin/update_result_dates_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim::Accountability::Admin
+ describe UpdateResultDates do
+ subject { described_class.new(start_date, end_date, result_ids, user) }
+
+ let(:organization) { create(:organization) }
+ let(:participatory_process) { create(:participatory_process, organization:) }
+ let(:current_component) { create(:accountability_component, participatory_space: participatory_process) }
+ let(:start_date) { Date.current }
+ let(:end_date) { Date.current + 1.month }
+ let(:results) { create_list(:result, 3, component: current_component) }
+ let(:result_ids) { results.map(&:id) }
+ let(:user) { create(:user, :admin, :confirmed, organization:) }
+
+ context "when everything is ok" do
+ it "updates the result dates" do
+ subject.call
+
+ results.each do |result|
+ expect(result.reload.start_date).to eq(start_date)
+ expect(result.reload.end_date).to eq(end_date)
+ end
+ end
+
+ it "traces the action", versioning: true do
+ expect(Decidim.traceability)
+ .to receive(:perform_action!)
+ .with("update", kind_of(Decidim::Accountability::Result), user)
+ .exactly(results.count).times
+
+ subject.call
+ end
+
+ it "broadcasts ok" do
+ expect { subject.call }.to broadcast(:ok)
+ end
+
+ context "when a result already has the same dates" do
+ let!(:result_with_dates) do
+ create(:result, component: current_component, start_date:, end_date:)
+ end
+ let(:result_ids) { results.map(&:id) + [result_with_dates.id] }
+
+ it "does not trace the action for that result" do
+ expect(Decidim.traceability)
+ .to receive(:perform_action!)
+ .with("update", kind_of(Decidim::Accountability::Result), user)
+ .exactly(results.count).times
+
+ subject.call
+ end
+ end
+
+ context "when updating only start_date" do
+ let(:end_date) { nil }
+
+ it "updates only the start date" do
+ subject.call
+
+ results.each do |result|
+ expect(result.reload.start_date).to eq(start_date)
+ expect(result.reload.end_date).to be_nil
+ end
+ end
+ end
+
+ context "when updating only end_date" do
+ let(:start_date) { nil }
+
+ it "updates only the end date" do
+ subject.call
+
+ results.each do |result|
+ expect(result.reload.start_date).to be_nil
+ expect(result.reload.end_date).to eq(end_date)
+ end
+ end
+ end
+ end
+
+ context "when both dates are nil" do
+ let(:start_date) { nil }
+ let(:end_date) { nil }
+
+ it "broadcasts invalid" do
+ expect { subject.call }.to broadcast(:invalid)
+ end
+ end
+
+ context "when result_ids is empty" do
+ let(:result_ids) { [] }
+
+ it "broadcasts invalid" do
+ expect { subject.call }.to broadcast(:invalid)
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/spec/commands/admin/update_result_status_spec.rb b/decidim-accountability/spec/commands/admin/update_result_status_spec.rb
new file mode 100644
index 0000000000000..2628e7effd408
--- /dev/null
+++ b/decidim-accountability/spec/commands/admin/update_result_status_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim::Accountability::Admin
+ describe UpdateResultStatus do
+ subject { described_class.new(status_id, result_ids, user) }
+
+ let(:organization) { create(:organization) }
+ let(:participatory_process) { create(:participatory_process, organization:) }
+ let(:current_component) { create(:accountability_component, participatory_space: participatory_process) }
+ let(:status) { create(:status, component: current_component) }
+ let(:status_id) { status.id }
+ let(:results) { create_list(:result, 3, component: current_component) }
+ let(:result_ids) { results.map(&:id) }
+ let(:user) { create(:user, :admin, :confirmed, organization:) }
+
+ context "when everything is ok" do
+ it "updates the result status" do
+ subject.call
+
+ results.each do |result|
+ expect(result.reload.status).to eq(status)
+ end
+ end
+
+ it "traces the action", versioning: true do
+ expect(Decidim.traceability)
+ .to receive(:perform_action!)
+ .with("update", kind_of(Decidim::Accountability::Result), user)
+ .exactly(results.count).times
+
+ subject.call
+ end
+
+ it "broadcasts ok" do
+ expect { subject.call }.to broadcast(:ok)
+ end
+
+ context "when a result already has the status" do
+ let!(:result_with_status) { create(:result, component: current_component, status:) }
+ let(:result_ids) { results.map(&:id) + [result_with_status.id] }
+
+ it "does not trace the action for that result" do
+ expect(Decidim.traceability)
+ .to receive(:perform_action!)
+ .with("update", kind_of(Decidim::Accountability::Result), user)
+ .exactly(results.count).times
+
+ subject.call
+ end
+ end
+ end
+
+ context "when status_id is nil" do
+ let(:status_id) { nil }
+
+ it "broadcasts invalid" do
+ expect { subject.call }.to broadcast(:invalid)
+ end
+ end
+
+ context "when result_ids is empty" do
+ let(:result_ids) { [] }
+
+ it "broadcasts invalid" do
+ expect { subject.call }.to broadcast(:invalid)
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/spec/commands/admin/update_result_taxonomies_spec.rb b/decidim-accountability/spec/commands/admin/update_result_taxonomies_spec.rb
new file mode 100644
index 0000000000000..197b38c58c3da
--- /dev/null
+++ b/decidim-accountability/spec/commands/admin/update_result_taxonomies_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim
+ module Accountability
+ describe Admin::UpdateResultTaxonomies do
+ describe "call" do
+ let!(:resource) { create(:result) }
+ let(:organization) { resource.organization }
+ let!(:taxonomy_one) { create(:taxonomy, :with_parent, organization:) }
+ let!(:taxonomy) { create(:taxonomy, :with_parent, organization:) }
+ let(:taxonomy_ids) { [taxonomy.id] }
+ let(:result_ids) { [resource.id] }
+ let(:command) { described_class.new(taxonomy_ids, result_ids, organization) }
+
+ subject { command.call }
+
+ context "with no taxonomy" do
+ let(:taxonomy_ids) { [] }
+
+ it { is_expected.to broadcast(:invalid_taxonomies) }
+ end
+
+ context "with no resources" do
+ let(:result_ids) { [] }
+
+ it { is_expected.to broadcast(:invalid_resources) }
+ end
+
+ context "when the taxonomy is the same as the resource's taxonomy" do
+ before do
+ resource.update!(taxonomies: [taxonomy])
+ end
+
+ it "does not update the resource" do
+ expect(resource).not_to receive(:update!)
+ expect(subject).to broadcast(:update_resources_taxonomies)
+ end
+ end
+
+ context "when the taxonomy is different from the resource's taxonomy" do
+ before do
+ resource.update!(taxonomies: [taxonomy_one])
+ end
+
+ it "updates the resource" do
+ expect(subject).to broadcast(:update_resources_taxonomies)
+ expect(resource.reload.taxonomies.first).to eq(taxonomy)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/spec/controllers/decidim/accountability/admin/results_bulk_actions_controller_spec.rb b/decidim-accountability/spec/controllers/decidim/accountability/admin/results_bulk_actions_controller_spec.rb
new file mode 100644
index 0000000000000..b1759d603fd83
--- /dev/null
+++ b/decidim-accountability/spec/controllers/decidim/accountability/admin/results_bulk_actions_controller_spec.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim::Accountability
+ describe Admin::ResultsBulkActionsController do
+ routes { Decidim::Accountability::AdminEngine.routes }
+
+ let(:organization) { create(:organization) }
+ let(:participatory_space) { create(:participatory_process, organization:) }
+ let(:current_component) { create(:accountability_component, participatory_space:) }
+ let(:results) { create_list(:result, 3, component: current_component) }
+ let(:result_ids) { results.map(&:id) }
+ let(:current_user) { create(:user, :confirmed, :admin, organization:) }
+
+ before do
+ request.env["decidim.current_organization"] = organization
+ request.env["decidim.current_component"] = current_component
+ sign_in current_user
+ end
+
+ describe "POST update_status" do
+ let(:status) { create(:status, component: current_component) }
+ let(:params) do
+ {
+ result_bulk_actions: {
+ decidim_accountability_status_id: status.id,
+ result_ids:
+ }
+ }
+ end
+
+ it "updates the status of the results" do
+ post(:update_status, params:)
+
+ expect(response).to have_http_status(:found)
+ results.each do |result|
+ expect(result.reload.status).to eq(status)
+ end
+ end
+
+ context "when parameters are invalid" do
+ let(:params) do
+ {
+ result_bulk_actions: {
+ decidim_accountability_status_id: nil,
+ result_ids:
+ }
+ }
+ end
+
+ it "redirects with an error message" do
+ post(:update_status, params:)
+
+ expect(response).to have_http_status(:found)
+ expect(flash[:alert]).not_to be_empty
+ end
+ end
+ end
+
+ describe "POST update_dates" do
+ let(:start_date) { Date.current }
+ let(:end_date) { Date.current + 1.month }
+ let(:params) do
+ {
+ result_bulk_actions: {
+ start_date:,
+ end_date:,
+ result_ids:
+ }
+ }
+ end
+
+ it "updates the dates of the results" do
+ post(:update_dates, params:)
+
+ expect(response).to have_http_status(:found)
+ results.each do |result|
+ expect(result.reload.start_date).to eq(start_date)
+ expect(result.reload.end_date).to eq(end_date)
+ end
+ end
+
+ context "when parameters are invalid" do
+ let(:params) do
+ {
+ result_bulk_actions: {
+ start_date: nil,
+ end_date: nil,
+ result_ids:
+ }
+ }
+ end
+
+ it "redirects with an error message" do
+ post(:update_dates, params:)
+
+ expect(response).to have_http_status(:found)
+ expect(flash[:alert]).not_to be_empty
+ end
+ end
+ end
+
+ describe "POST update_taxonomies" do
+ let(:taxonomy) { create(:taxonomy, :with_parent, organization:) }
+ let(:params) do
+ {
+ result_bulk_actions: {
+ taxonomies: [taxonomy.id],
+ result_ids:
+ }
+ }
+ end
+
+ it "updates the taxonomies of the results" do
+ post(:update_taxonomies, params:)
+
+ expect(response).to have_http_status(:found)
+ results.each do |result|
+ expect(result.reload.taxonomies.first).to eq(taxonomy)
+ end
+ end
+
+ context "when parameters are invalid" do
+ let(:params) do
+ {
+ result_bulk_actions: {
+ taxonomies: [],
+ result_ids:
+ }
+ }
+ end
+
+ it "redirects with an error message" do
+ post(:update_taxonomies, params:)
+
+ expect(response).to have_http_status(:found)
+ expect(flash[:alert]).not_to be_empty
+ end
+ end
+ end
+
+ context "when user is not authorized" do
+ let(:unauthorized_user) { create(:user, :confirmed, organization:) }
+ let(:status) { create(:status, component: current_component) }
+ let(:params) do
+ {
+ result_bulk_actions: {
+ decidim_accountability_status_id: status.id,
+ result_ids:
+ }
+ }
+ end
+
+ before do
+ sign_in unauthorized_user
+ end
+
+ it "is not able to perform bulk actions" do
+ post(:update_status, params:)
+
+ expect(response).to have_http_status(:redirect)
+ expect(flash[:alert]).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/spec/shared/manage_child_results_examples.rb b/decidim-accountability/spec/shared/manage_child_results_examples.rb
index d521c83979566..931093159c1fc 100644
--- a/decidim-accountability/spec/shared/manage_child_results_examples.rb
+++ b/decidim-accountability/spec/shared/manage_child_results_examples.rb
@@ -73,7 +73,7 @@
describe "soft delete a result" do
before do
visit current_path
- within ".table-list__actions" do
+ within "tr[data-id='#{result.id}'] .table-list__actions" do
click_on "New result"
end
end
diff --git a/decidim-accountability/spec/shared/manage_results_bulk_actions_examples.rb b/decidim-accountability/spec/shared/manage_results_bulk_actions_examples.rb
new file mode 100644
index 0000000000000..950236f2ae50d
--- /dev/null
+++ b/decidim-accountability/spec/shared/manage_results_bulk_actions_examples.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+shared_examples "when managing results bulk actions as an admin" do
+ context "when in the Results list page" do
+ it "shows a checkbox to select each result" do
+ expect(page).to have_css(".table-list tbody [data-result-checkbox]", count: 2)
+ end
+
+ it "shows a checkbox to (des)select all results" do
+ expect(page).to have_css(".table-list thead [data-select-all]", count: 1)
+ end
+
+ context "when selecting results" do
+ before do
+ page.find_by_id("results_bulk").set(true)
+ end
+
+ it "shows the number of selected results" do
+ expect(page).to have_css("span[data-selected-count]", count: 1)
+ end
+
+ it "shows the bulk actions button" do
+ expect(page).to have_css("#js-bulk-actions-button", count: 1)
+ end
+
+ context "when click the bulk action button" do
+ before do
+ click_on "Actions"
+ end
+
+ it "shows the bulk actions dropdown" do
+ expect(page).to have_css("#js-bulk-actions-dropdown", count: 1)
+ end
+
+ it "shows the change action options" do
+ expect(page).to have_selector(:link_or_button, "Change taxonomies")
+ expect(page).to have_selector(:link_or_button, "Change status")
+ expect(page).to have_selector(:link_or_button, "Change dates")
+ end
+ end
+
+ context "when change taxonomies is selected from actions dropdown" do
+ let!(:taxonomy) { create(:taxonomy, parent: root_taxonomy, organization:) }
+ let(:taxonomy_filter) { create(:taxonomy_filter, root_taxonomy:) }
+ let!(:taxonomy_filter_item) { create(:taxonomy_filter_item, taxonomy_filter:, taxonomy_item: taxonomy) }
+ let(:taxonomy_filter_ids) { [taxonomy_filter.id] }
+
+ before do
+ click_on "Actions"
+ click_on "Change taxonomies"
+ end
+
+ it "changes the taxonomies" do
+ expect(result.taxonomies).to be_empty
+ expect(page).to have_css("#taxonomies_for_filter_#{taxonomy_filter.id}", count: 1)
+ expect(page).to have_selector(:link_or_button, "Change taxonomies")
+ select decidim_sanitize_translated(taxonomy.name), from: "taxonomies_for_filter_#{taxonomy_filter.id}"
+ click_on "Change taxonomies"
+ expect(page).to have_admin_callout "Successfully updated taxonomies #{translated(taxonomy.name)} for results"
+ expect(result.reload.taxonomies.first).to eq(taxonomy)
+ end
+ end
+
+ context "when change status is selected from actions dropdown" do
+ before do
+ click_on "Actions"
+ click_on "Change status"
+ end
+
+ it "changes the status" do
+ select translated(status.name), from: "result_bulk_actions[decidim_accountability_status_id]"
+ click_on "Change status"
+ expect(page).to have_admin_callout "Results status successfully updated"
+ expect(result.reload.status).to eq(status)
+ expect(other_result.reload.status).to eq(status)
+ end
+ end
+
+ context "when change dates is selected from actions dropdown" do
+ before do
+ click_on "Actions"
+ click_on "Change dates"
+ end
+
+ it "changes the dates" do
+ fill_in "result_bulk_actions_start_date_date", with: "01/01/2025"
+ fill_in "result_bulk_actions_end_date_date", with: "02/01/2025"
+ click_on "Change date"
+ expect(page).to have_admin_callout "Results dates successfully updated"
+ expect(result.reload.start_date).to eq(Date.parse("2025-01-01"))
+ expect(result.reload.end_date).to eq(Date.parse("2025-01-02"))
+ end
+ end
+ end
+ end
+end
diff --git a/decidim-accountability/spec/shared/shared_context.rb b/decidim-accountability/spec/shared/shared_context.rb
index 112f5af0bdd57..9466de43b2a5b 100644
--- a/decidim-accountability/spec/shared/shared_context.rb
+++ b/decidim-accountability/spec/shared/shared_context.rb
@@ -2,6 +2,7 @@
RSpec.shared_context "when managing an accountability component" do
let!(:result) { create(:result, scope:, component: current_component) }
+ let!(:other_result) { create(:result, scope:, component: current_component) }
let!(:child_result) { create(:result, scope:, component: current_component, parent: result) }
let!(:status) { create(:status, key: "ongoing", name: { en: "Ongoing" }, component: current_component) }
end
diff --git a/decidim-accountability/spec/system/admin/admin_manages_accountability_spec.rb b/decidim-accountability/spec/system/admin/admin_manages_accountability_spec.rb
index 1ddb4978392c3..bd3997cd21f77 100644
--- a/decidim-accountability/spec/system/admin/admin_manages_accountability_spec.rb
+++ b/decidim-accountability/spec/system/admin/admin_manages_accountability_spec.rb
@@ -20,12 +20,13 @@
let!(:component) { create(:component, manifest:, participatory_space:, settings: { taxonomy_filters: [taxonomy_filter.id] }) }
it_behaves_like "manage results"
+ it_behaves_like "when managing results bulk actions as an admin"
it_behaves_like "export results"
end
describe "child results" do
before do
- within ".table-list__actions" do
+ within "tr[data-id='#{result.id}'] .table-list__actions" do
click_on "New result"
end
end
diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_legacy_foundation.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_legacy_foundation.scss
index 570f7affe83d9..46f7296b8d7f7 100644
--- a/decidim-admin/app/packs/stylesheets/decidim/admin/_legacy_foundation.scss
+++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_legacy_foundation.scss
@@ -388,6 +388,19 @@ li.opens-left > .is-dropdown-submenu {
color: var(--secondary);
}
+.dropdown-pane.dropdown-pane--buttons {
+ li {
+ padding: 0;
+ }
+
+ li button {
+ width: 100%;
+ height: 100%;
+ text-align: left;
+ padding: 1rem;
+ }
+}
+
.dropdown-pane li:hover {
color: #fff;
background-color: var(--secondary);
diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb
index 2293c4843a0b0..d6c282e0aa45b 100644
--- a/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb
+++ b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb
@@ -10,7 +10,7 @@