diff --git a/app/components/workflows/blankslate_component.html.erb b/app/components/workflows/blankslate_component.html.erb index da7a8b2ae1c..6947ea1984f 100644 --- a/app/components/workflows/blankslate_component.html.erb +++ b/app/components/workflows/blankslate_component.html.erb @@ -31,8 +31,9 @@ See COPYRIGHT and LICENSE files for more details. render(Primer::Beta::Blankslate.new(border: true)) do |blankslate| blankslate.with_heading(tag: :h2).with_content(t("admin.workflows.blankslate.title")) blankslate.with_description_content(t("admin.workflows.blankslate.description")) + # TODO: pass all roles once BlankslateComponent accepts roles: and StatusDialogComponent supports multi-role natively. blankslate.with_primary_action( - href: helpers.status_dialog_workflow_tab_path(@type, @tab, role_id: @role.id), + href: helpers.status_dialog_workflow_tab_path(@type, @tab, role_ids: [@role.id]), scheme: :secondary, data: { controller: "async-dialog" } ) do |button| diff --git a/app/components/workflows/status_form_component.html.erb b/app/components/workflows/status_form_component.html.erb index d9bf8ef27b7..59394c0a330 100644 --- a/app/components/workflows/status_form_component.html.erb +++ b/app/components/workflows/status_form_component.html.erb @@ -28,8 +28,9 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= + # TODO: pass all roles once StatusFormComponent and confirm_statuses support multi-role natively. primer_form_with( - url: helpers.confirm_statuses_workflow_tab_path(@type, @tab, role_id: @role.id), + url: helpers.confirm_statuses_workflow_tab_path(@type, @tab, role_ids: [@role.id]), method: :post, id: FORM_ID, data: { turbo_frame: "workflow-table" } diff --git a/app/components/workflows/status_matrix_form_component.html.erb b/app/components/workflows/status_matrix_form_component.html.erb index 93522276a2f..c6aa018b334 100644 --- a/app/components/workflows/status_matrix_form_component.html.erb +++ b/app/components/workflows/status_matrix_form_component.html.erb @@ -32,19 +32,29 @@ See COPYRIGHT and LICENSE files for more details. render Primer::OpenProject::SubHeader.new do |subheader| if @type && @available_roles.any? subheader.with_filter_component do - render(Primer::Alpha::ActionMenu.new(select_variant: :single)) do |menu| - menu.with_show_button(scheme: :secondary) do |button| + render( + Primer::Alpha::SelectPanel.new( + select_variant: :multiple, + fetch_strategy: :local, + title: t("admin.workflows.role_selector.title"), + data: data_attributes + ) + ) do |panel| + panel.with_show_button(scheme: :secondary) do |button| button.with_trailing_visual_icon(icon: :"triangle-down") - @role ? t("admin.workflows.role_selector.label", role: @role.name) : t("admin.workflows.role_selector.no_role") + if @roles.many? + t("admin.workflows.role_selector.roles", count: @roles.size) + elsif @role + t("admin.workflows.role_selector.label", role: @role.name) + else + t("admin.workflows.role_selector.no_role") + end end @available_roles.each do |available_role| - menu.with_item( + panel.with_item( label: available_role.name, - active: available_role == @role, - tag: :a, - href: helpers.edit_workflow_tab_path(@type, @tab, role_id: available_role.id), - content_arguments: { data: { "admin--workflow-checkbox-state-confirmation-trigger": "click", - turbo_action: "advance" } } + active: @roles.include?(available_role), + item_id: available_role.id ) end end @@ -56,7 +66,9 @@ See COPYRIGHT and LICENSE files for more details. scheme: :secondary, leading_icon: :plus, label: t("admin.workflows.status_button"), - href: helpers.status_dialog_workflow_tab_path(@type, @tab, role_id: @role&.id, status_ids: @statuses.pluck(:id).presence), + # TODO: status_dialog and StatusDialogComponent currently work with a single role (@role = @roles.first); + # update when they support multi-role natively. + href: helpers.status_dialog_workflow_tab_path(@type, @tab, role_ids: @roles.map(&:id), status_ids: @statuses.pluck(:id).presence), data: { controller: "async-dialog" } ) do t("admin.workflows.status_button") @@ -76,7 +88,9 @@ See COPYRIGHT and LICENSE files for more details. } ) do %> <%= hidden_field_tag "type_id", @type.id %> - <%= hidden_field_tag "role_id", @role.id %> + <% @roles.each do |role| %> + <%= hidden_field_tag "role_ids[]", role.id %> + <% end %> <%= hidden_field_tag "tab", @tab %> <%= helpers.render_tabs helpers.workflow_tabs(@type) %> @@ -94,7 +108,7 @@ See COPYRIGHT and LICENSE files for more details. Primer::OpenProject::FeedbackDialog.new( title: t("admin.workflows.leave_confirmation.title"), data: { - "admin--workflow-checkbox-state-target": "confirmationDialog", + "admin--workflow-checkbox-state-target": "confirmationDialog" } ) ) do |dialog| diff --git a/app/components/workflows/status_matrix_form_component.rb b/app/components/workflows/status_matrix_form_component.rb index 14c3ade5fdb..bc3365aaff1 100644 --- a/app/components/workflows/status_matrix_form_component.rb +++ b/app/components/workflows/status_matrix_form_component.rb @@ -33,14 +33,26 @@ module Workflows include OpTurbo::Streamable include OpPrimer::ComponentHelpers - def initialize(tab:, role:, type:, available_roles:, statuses:, has_status_changes:) + def initialize(tab:, roles:, type:, available_roles:, statuses:, has_status_changes:) super @tab = tab - @role = role + @roles = roles + @role = @roles.first @type = type @available_roles = available_roles @statuses = statuses @has_status_changes = has_status_changes end + + private + + def data_attributes + { + controller: "admin--workflow-role-select", + "admin--workflow-role-select-base-url-value": helpers.edit_workflow_path(@type), + "admin--workflow-role-select-current-role-ids-value": @roles.map(&:id).join(","), + "admin--workflow-role-select-admin--workflow-checkbox-state-outlet": "#workflow_form" + } + end end end diff --git a/app/components/workflows/status_removal_danger_dialog_component.html.erb b/app/components/workflows/status_removal_danger_dialog_component.html.erb index c2b020ff5b7..8a8ccd8e578 100644 --- a/app/components/workflows/status_removal_danger_dialog_component.html.erb +++ b/app/components/workflows/status_removal_danger_dialog_component.html.erb @@ -46,7 +46,8 @@ See COPYRIGHT and LICENSE files for more details. # The reason this is done here is because the submit is not a DELETE, and GET form submissions # strip url params dialog.with_additional_details do - concat(hidden_field_tag(:role_id, @role.id)) + # TODO: pass all roles once StatusRemovalDangerDialogComponent supports multi-role natively. + concat(hidden_field_tag("role_ids[]", @role.id)) @status_ids.each { |id| concat(hidden_field_tag("status_ids[]", id)) } end end diff --git a/app/controllers/workflows/tabs_controller.rb b/app/controllers/workflows/tabs_controller.rb index 94c9805c22a..6c476c890b5 100644 --- a/app/controllers/workflows/tabs_controller.rb +++ b/app/controllers/workflows/tabs_controller.rb @@ -38,11 +38,11 @@ class Workflows::TabsController < ApplicationController before_action :set_type before_action :set_tab before_action :set_eligible_roles - before_action :set_role + before_action :set_roles def edit unless turbo_frame_request? - redirect_to edit_workflow_path(@type, role_id: params[:role_id], tab: @tab) + redirect_to edit_workflow_path(@type, role_ids: params[:role_ids], tab: @tab) return end @@ -53,12 +53,19 @@ class Workflows::TabsController < ApplicationController end end - def update - call = Workflows::BulkUpdateService - .new(role: @role, type: @type, tab: @tab) - .call(permitted_status_params) + def update # rubocop:disable Metrics/AbcSize + success = false + Workflow.transaction do + success = true + @roles.each do |role| + result = Workflows::BulkUpdateService.new(role:, type: @type, tab: @tab) + .call(permitted_status_params) + success = false unless result.success? + end + raise ActiveRecord::Rollback unless success + end - if call.success? + if success render_flash_message_via_turbo_stream( message: I18n.t(:notice_successful_update), scheme: :success @@ -69,7 +76,7 @@ class Workflows::TabsController < ApplicationController update_via_turbo_stream( component: Workflows::StatusMatrixFormComponent.new( tab: @tab, - role: @role, + roles: @roles, type: @type, available_roles: @eligible_roles, statuses:, @@ -125,7 +132,7 @@ class Workflows::TabsController < ApplicationController update_via_turbo_stream( component: Workflows::StatusMatrixFormComponent.new( tab: @tab, - role: @role, + roles: @roles, type: @type, available_roles: @eligible_roles, statuses:, @@ -150,8 +157,11 @@ class Workflows::TabsController < ApplicationController @eligible_roles = Workflow.eligible_roles.order(:builtin, :position) end - def set_role - @role = @eligible_roles.find(params[:role_id]) + def set_roles + @roles = @eligible_roles.where(id: params[:role_ids]) + # TODO: remove @role once the matrix form and all dependent components + # (dialogs, status selectors, page headers) work natively with @roles (multi-role). + @role = @roles.first end def statuses_for_form diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 82b03b15118..fb40fead193 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -38,7 +38,7 @@ class WorkflowsController < ApplicationController before_action :find_types, only: %i[index] before_action :find_type, only: %i[edit] - before_action :find_optional_role, only: %i[edit] + before_action :find_optional_roles, only: %i[edit] def index; end @@ -64,8 +64,13 @@ class WorkflowsController < ApplicationController @type = ::Type.find(params[:type_id]) end - def find_optional_role - @role = eligible_roles.find_by(id: params[:role_id]) || eligible_roles.order(:builtin, :position).first + def find_optional_roles + ordered = eligible_roles.order(:builtin, :position) + @roles = ordered.where(id: params[:role_ids]) + @roles = [ordered.first] if @roles.empty? + # TODO: remove @role once the matrix form and all dependent components + # (dialogs, status selectors, page headers) work natively with @roles (multi-role). + @role = @roles.first end def eligible_roles diff --git a/app/helpers/workflow_helper.rb b/app/helpers/workflow_helper.rb index a56b291fce3..61c7007adb0 100644 --- a/app/helpers/workflow_helper.rb +++ b/app/helpers/workflow_helper.rb @@ -37,7 +37,7 @@ module WorkflowHelper ].map do |tab| tab.merge( partial: "workflows/form", - path: edit_workflow_tab_path(type, tab[:name], params.permit(:role_id)), + path: edit_workflow_tab_path(type, tab[:name], params.permit(role_ids: [])), data: { "admin--workflow-checkbox-state-confirmation-trigger": "click", turbo_frame: "workflow-table", turbo_action: "advance" } diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb index 49db69fe7cc..26a2de62642 100644 --- a/app/views/workflows/edit.html.erb +++ b/app/views/workflows/edit.html.erb @@ -31,6 +31,6 @@ See COPYRIGHT and LICENSE files for more details. <%= render Workflows::PageHeaders::EditComponent.new(@type, role: @role, tabs: workflow_tabs(@type)) %> <% end %> -<% if @type && @role %> - <%= turbo_frame_tag "workflow-table", src: edit_workflow_tab_path(@type, @current_tab, role_id: @role.id, status_ids: params[:status_ids]) %> +<% if @type && @roles.any? %> + <%= turbo_frame_tag "workflow-table", src: edit_workflow_tab_path(@type, @current_tab, role_ids: @roles.map(&:id), status_ids: params[:status_ids]) %> <% end %> diff --git a/app/views/workflows/summaries/show.html.erb b/app/views/workflows/summaries/show.html.erb index 034123f66d9..f5ed8bcff9e 100644 --- a/app/views/workflows/summaries/show.html.erb +++ b/app/views/workflows/summaries/show.html.erb @@ -61,7 +61,7 @@ See COPYRIGHT and LICENSE files for more details.