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 @@ +
+ <%= decidim_form_for(bulk_actions_form, url: update_dates_results_path, method: :post, html: { class: "form form-defaults w-full" }) do |f| %> +
+ <%= f.hidden_field :result_ids, multiple: true, data: { result_ids_field: true } %> + +
+
+ <%= f.date_field :start_date, label: t(".start_date") %> +
+ +
+ <%= f.date_field :end_date, label: t(".end_date") %> +
+
+ + <%= render partial: "decidim/accountability/admin/results/bulk_actions/submit_buttons", locals: { submit_text: t(".change_dates") } %> +
+ <% end %> +
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 @@ +
+ <%= decidim_form_for(bulk_actions_form, url: update_status_results_path, method: :post, html: { class: "form form-defaults w-full" }) do |f| %> +
+ <%= f.hidden_field :result_ids, multiple: true, data: { result_ids_field: true } %> + +
+ <%= f.select :decidim_accountability_status_id, + options_for_select(statuses.map { |status| [translated_attribute(status.name), status.id] }), + { include_blank: true, label: t(".status") }, + { class: "w-full mt-2" } %> +
+ + <%= render partial: "decidim/accountability/admin/results/bulk_actions/submit_buttons", locals: { submit_text: t(".change_status") } %> + <% end %> +
+
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 @@ +
+ <%= decidim_form_for(bulk_actions_form, url: update_taxonomies_results_path, method: :post, html: { class: "form form-defaults w-full" }) do |f| %> +
+ <%= f.hidden_field :result_ids, multiple: true, data: { result_ids_field: true } %> + + <% current_component_taxonomy_filters.each do |filter| %> +
+ <%= f.select :taxonomies, + options_for_select(taxonomy_items_options_for_filter(filter)), + { include_blank: I18n.t("decidim.taxonomies.prompt"), label: decidim_sanitize_translated(filter.name) }, + { class: "w-full mt-2", id: "taxonomies_for_filter_#{filter.id}", name: "result_bulk_actions[taxonomies][]" } %> +
+ <% end %> + + <%= render partial: "decidim/accountability/admin/results/bulk_actions/submit_buttons", locals: { submit_text: t(".change_taxonomies") } %> + <% end %> +
+
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")) %>
-
+

- <% if parent_result %> - <%= "#{translated_attribute(parent_result.title)} > " %> - <% end %> - <%= t(".title") %> +
+ <% if parent_result %> + <%= "#{translated_attribute(parent_result.title)} > " %> + <% end %> + <%= t(".title") %> + "> +
- <%= export_dropdowns(query) %> - <%= import_dropdown do %> - <% if allowed_to?(:create, :result) && parent_result.nil? %> - <%= link_to new_projects_import_path do %> -
  • - <%= t("actions.import", scope: "decidim.accountability", name: t("models.result.name", scope: "decidim.accountability.admin")) %> -
  • +
    + <%= render partial: "decidim/accountability/admin/results/bulk_actions/dropdown" %> + <%= export_dropdowns(query) %> + <%= import_dropdown do %> + <% if allowed_to?(:create, :result) && parent_result.nil? %> + <%= link_to new_projects_import_path do %> +
  • + <%= t("actions.import", scope: "decidim.accountability", name: t("models.result.name", scope: "decidim.accountability.admin")) %> +
  • + <% end %> <% end %> - <% end %> - <% if allowed_to? :create, :result %> - <%= link_to import_results_path do %> -
  • - <%= t("actions.import_csv", scope: "decidim.accountability") %> -
  • + <% if allowed_to? :create, :result %> + <%= link_to import_results_path do %> +
  • + <%= t("actions.import_csv", scope: "decidim.accountability") %> +
  • + <% end %> <% end %> <% end %> - <% end %> - <%= render partial: "decidim/accountability/admin/shared/subnav" unless parent_result %> - <%= link_to t("actions.new_result", scope: "decidim.accountability"), new_result_path(parent_id: parent_result), class: "button button__sm button__secondary" if allowed_to? :create, :result %> - <%= render partial: "decidim/admin/components/resource_action" %> + <%= render partial: "decidim/accountability/admin/shared/subnav" unless parent_result %> + <%= link_to t("actions.new_result", scope: "decidim.accountability"), new_result_path(parent_id: parent_result), class: "button button__sm button__secondary" if allowed_to? :create, :result %> + <%= render partial: "decidim/admin/components/resource_action" %> +

    + + <%= render partial: "decidim/accountability/admin/results/bulk_actions/taxonomies_form" %> + <%= render partial: "decidim/accountability/admin/results/bulk_actions/status_form" %> + <%= render partial: "decidim/accountability/admin/results/bulk_actions/dates_form" %>
    <%= admin_filter_selector(:results) %> @@ -55,3 +65,5 @@ <% end %>
    <%= 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 @@