From 8721ac960676eb6b46ec174fcc1eb8d2e1eb3306 Mon Sep 17 00:00:00 2001 From: David F Date: Thu, 16 Apr 2026 16:59:26 +0200 Subject: [PATCH] Copy workflow to multiple types at once. wp/72383 --- .../workflows/copies/from_types_controller.rb | 16 ++++---- app/forms/workflows/copies/form.rb | 17 +++++++- config/locales/en.yml | 8 ++-- .../copies/from_types_controller_spec.rb | 39 +++++-------------- .../workflows/copies/from_type_spec.rb | 11 ++++-- spec/forms/workflows/copies/form_spec.rb | 7 ++-- 6 files changed, 48 insertions(+), 50 deletions(-) diff --git a/app/controllers/workflows/copies/from_types_controller.rb b/app/controllers/workflows/copies/from_types_controller.rb index a0a61bdc769..557c9380db3 100644 --- a/app/controllers/workflows/copies/from_types_controller.rb +++ b/app/controllers/workflows/copies/from_types_controller.rb @@ -36,7 +36,7 @@ class Workflows::Copies::FromTypesController < ApplicationController before_action :require_admin before_action :set_source_type - before_action :set_target_type + before_action :set_target_types def create if @source_type.nil? @@ -45,18 +45,16 @@ class Workflows::Copies::FromTypesController < ApplicationController scheme: :danger ) @turbo_status = :unprocessable_entity - elsif @target_type.nil? + elsif @target_types.blank? render_flash_message_via_turbo_stream( message: I18n.t(:error_workflow_copy_target), scheme: :danger ) @turbo_status = :unprocessable_entity else - Workflow.eligible_roles.each do |role| - Workflow.copy_one(@source_type, role, @target_type, role) - end - redirect_to edit_workflow_path(@target_type), - notice: t(".notice", type_name: @target_type.name) + Workflow.copy(@source_type, nil, @target_types, Workflow.eligible_roles) + redirect_to edit_workflow_path(@target_types.first), + notice: t(".notice", count: @target_types.size, type_name: @target_types.first.name) return end @@ -69,7 +67,7 @@ class Workflows::Copies::FromTypesController < ApplicationController @source_type = ::Type.find_by(id: params[:workflow_type_id]) end - def set_target_type - @target_type = ::Type.find_by(id: params[:target_type_id]) + def set_target_types + @target_types = ::Type.where(id: params[:target_type_ids]) end end diff --git a/app/forms/workflows/copies/form.rb b/app/forms/workflows/copies/form.rb index b1edc68ad7d..767ab7ca70e 100644 --- a/app/forms/workflows/copies/form.rb +++ b/app/forms/workflows/copies/form.rb @@ -71,8 +71,21 @@ class Workflows::Copies::Form < ApplicationForm "show-when-value-selected-target": "effect" } ) do |from_type| - target_label = helpers.t("workflows.copies.form.target_type_id.label") - from_type.select_list(name: :target_type_id, label: target_label, required: true) do |target_list| + from_type.autocompleter( + name: "target_type_ids", + required: true, + include_blank: false, + label: helpers.t("workflows.copies.form.target_type_ids.label"), + autocomplete_options: { + multiple: true, + decorated: true, + closeOnSelect: false, + appendTo: @append_to, + data: { + "test-selector": "target_types_autocomplete" + } + } + ) do |target_list| @other_types.each do |other_type| target_list.option(label: other_type.name, value: other_type.id) end diff --git a/config/locales/en.yml b/config/locales/en.yml index e20e34024c3..1e4130407fb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1403,8 +1403,8 @@ en: label: "Source role" target_role_ids: label: "Target roles" - target_type_id: - label: "Target type" + target_type_ids: + label: "Target types" mode: from_role: label: "Copy to other roles" @@ -1419,7 +1419,9 @@ en: other: "Successfully copied workflow to %{count} roles." from_types: create: - notice: "Successfully copied workflow to '%{type_name}' type." + notice: + one: "Successfully copied workflow to '%{type_name}' type." + other: "Successfully copied workflow to %{count} types." new: title: "Copy workflow of \"%{source_type}\"" form: diff --git a/spec/controllers/workflows/copies/from_types_controller_spec.rb b/spec/controllers/workflows/copies/from_types_controller_spec.rb index c0df28ef325..15d97a62dc5 100644 --- a/spec/controllers/workflows/copies/from_types_controller_spec.rb +++ b/spec/controllers/workflows/copies/from_types_controller_spec.rb @@ -40,26 +40,11 @@ RSpec.describe Workflows::Copies::FromTypesController do end end - let!(:other_types) do - build_stubbed_list(:type, 2).tap do |stubs| - where_double = instance_double(ActiveRecord::QueryMethods::WhereChain) - not_double = instance_double(ActiveRecord::Relation) - - allow(Type).to receive(:where).and_return(where_double) - allow(where_double).to receive(:not).and_return(not_double) - allow(not_double).to receive(:order).and_return(stubs) - end - end - current_user { build_stubbed(:admin) } describe "#create" do - let!(:target_type) do - other_types.last.tap do |stub| - allow(Type) - .to receive(:find_by).with(id: stub.id.to_s).and_return(stub) - end - end + let!(:target_types) { build_stubbed_list(:type, 2) } + let!(:target_type_ids) { target_types.map { |t| t.id.to_s } } let!(:roles) do build_stubbed_list(:project_role, 2).tap do |stubs| @@ -71,28 +56,24 @@ RSpec.describe Workflows::Copies::FromTypesController do end before do - allow(Workflow).to receive(:copy_one) + allow(Type).to receive(:where).with(id: target_type_ids).and_return(target_types) + allow(Workflow).to receive(:copy) post :create, params: { workflow_type_id: source_type.id.to_s, - target_type_id: target_type.id.to_s + target_type_ids: target_type_ids }, format: :turbo_stream end - it "calls the Workflow.copy_one method on each role" do + it "calls the Workflow.copy method with all target types and eligible roles" do expect(Workflow) - .to have_received(:copy_one).exactly(2).times - expect(Workflow) - .to have_received(:copy_one) - .with(source_type, roles.first, target_type, roles.first) - expect(Workflow) - .to have_received(:copy_one) - .with(source_type, roles.last, target_type, roles.last) + .to have_received(:copy) + .with(source_type, nil, target_types, roles) end it "redirects with a flash notice" do - expect(response).to redirect_to(edit_workflow_path(target_type)) - expect(flash[:notice]).to eq("Successfully copied workflow to '#{target_type.name}' type.") + expect(response).to redirect_to(edit_workflow_path(target_types.first)) + expect(flash[:notice]).to eq("Successfully copied workflow to 2 types.") end end end diff --git a/spec/features/workflows/copies/from_type_spec.rb b/spec/features/workflows/copies/from_type_spec.rb index 93b442e3e6a..05663e85522 100644 --- a/spec/features/workflows/copies/from_type_spec.rb +++ b/spec/features/workflows/copies/from_type_spec.rb @@ -37,18 +37,21 @@ RSpec.describe "Workflow copy from type", :js do current_user { admin } + let(:target_types_autocompleter) { FormFields::Primerized::AutocompleteField.new("target_types", selector: "[data-test-selector='target_types_autocomplete']") } + shared_examples "a copy-to-another-type dialog" do |with_source_role:| - it "permits to select a target type" do + it "permits to select target types" do if with_source_role choose "Copy to another type" end - expect(page).to have_select("Target type", text: types.second.name) - select(types.last.name, from: "Target type") + target_types_autocompleter.select_option types.second.name, types.last.name + target_types_autocompleter.close_autocompleter click_button "Copy" - expect(page).to have_css(".flash-success", text: "Successfully copied workflow to '#{types.last.name}' type.") + expect(page).to have_css(".flash-success", text: "Successfully copied workflow to 2 types.") + expect(page).to have_current_path(edit_workflow_path(types.second)) end end diff --git a/spec/forms/workflows/copies/form_spec.rb b/spec/forms/workflows/copies/form_spec.rb index fdb6614288f..e4395ce4c08 100644 --- a/spec/forms/workflows/copies/form_spec.rb +++ b/spec/forms/workflows/copies/form_spec.rb @@ -45,9 +45,10 @@ RSpec.describe Workflows::Copies::Form, type: :forms do expect(page).to have_field("Copy to other roles", checked: !another_type_at_first) end - it "renders the Target type select list" do - expect(page).to have_select "Target type", required: true, visible: another_type_at_first do |select| - options_text = select.all("option", visible: another_type_at_first).map(&:text) + it "renders the Target types autocompleter" do + data_attributes = "[data-test-selector=\"target_types_autocomplete\"][data-multiple=\"true\"]" + expect(page).to have_css "opce-autocompleter#{data_attributes}", visible: another_type_at_first do |autocompleter| + options_text = JSON.parse(autocompleter["data-items"]).map { |item| item["name"] } expect(options_text).to match_array(other_types.map(&:name)) end end