From 004876b177af3cfb0523a3e1ea6fafd0cfff114f Mon Sep 17 00:00:00 2001 From: David F Date: Tue, 31 Mar 2026 19:33:59 +0200 Subject: [PATCH] Merge workflow copy modes into one unified form. wp/72383 --- .../workflows/page_headers/copy_component.rb | 39 ------ .../workflows/page_headers/edit_component.rb | 26 +--- .../workflows/table_component.html.erb | 10 +- .../workflows/copies/from_roles_controller.rb | 9 +- .../workflows/copies/from_types_controller.rb | 9 +- .../workflows/copies_controller.rb | 45 ++++--- app/forms/workflows/copies/form.rb | 119 +++++++++++++++++ app/forms/workflows/copies/from_role_form.rb | 67 ---------- app/forms/workflows/copies/from_type_form.rb | 46 ------- .../copies/from_types/new.turbo_stream.erb | 55 -------- .../{from_roles => }/new.turbo_stream.erb | 46 +++++-- config/locales/en.yml | 29 +++-- config/routes.rb | 8 +- lookbook/docs/patterns/02-forms.md.erb | 2 +- .../workflows/table_component_spec.rb | 6 +- .../copies/from_roles_controller_spec.rb | 46 ------- .../copies/from_types_controller_spec.rb | 30 ----- .../workflows/copies_controller_spec.rb | 123 ++++++++++++++++++ .../workflows/copies/from_role_spec.rb | 65 +++++---- .../workflows/copies/from_type_spec.rb | 62 +++++---- spec/features/workflows/edit_spec.rb | 26 ++-- spec/features/workflows/index_spec.rb | 20 +-- .../workflows/missing_for_sharing_wp_spec.rb | 3 +- spec/forms/workflows/copies/form_spec.rb | 91 +++++++++++++ .../workflows/copies/from_role_form_spec.rb | 71 ---------- spec/routing/workflows_spec.rb | 10 +- 26 files changed, 509 insertions(+), 554 deletions(-) delete mode 100644 app/components/workflows/page_headers/copy_component.rb rename spec/forms/workflows/copies/from_type_form_spec.rb => app/controllers/workflows/copies_controller.rb (63%) create mode 100644 app/forms/workflows/copies/form.rb delete mode 100644 app/forms/workflows/copies/from_role_form.rb delete mode 100644 app/forms/workflows/copies/from_type_form.rb delete mode 100644 app/views/workflows/copies/from_types/new.turbo_stream.erb rename app/views/workflows/copies/{from_roles => }/new.turbo_stream.erb (51%) create mode 100644 spec/controllers/workflows/copies_controller_spec.rb create mode 100644 spec/forms/workflows/copies/form_spec.rb delete mode 100644 spec/forms/workflows/copies/from_role_form_spec.rb diff --git a/app/components/workflows/page_headers/copy_component.rb b/app/components/workflows/page_headers/copy_component.rb deleted file mode 100644 index 67d040b7a75..00000000000 --- a/app/components/workflows/page_headers/copy_component.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# -- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -# ++ - -module Workflows::PageHeaders - class CopyComponent < BaseComponent - options :title - - def page_breadcrumb - { href: workflows_path, text: t(:label_workflow_plural) } - end - end -end diff --git a/app/components/workflows/page_headers/edit_component.rb b/app/components/workflows/page_headers/edit_component.rb index 80a881cc053..f806afec6b4 100644 --- a/app/components/workflows/page_headers/edit_component.rb +++ b/app/components/workflows/page_headers/edit_component.rb @@ -42,33 +42,19 @@ module Workflows::PageHeaders type.name end - def add_action_buttons(header) # rubocop:disable Metrics/AbcSize + def add_action_buttons(header) header.with_action_button( data: { controller: "async-dialog" }, tag: :a, mobile_icon: :copy, - mobile_label: t(:label_copy_workflow_from_type), + mobile_label: t(:button_copy), size: :medium, - href: new_workflow_copy_from_type_path(type), - aria: { label: helpers.t(:label_copy_workflow_from_type) }, - title: helpers.t(:label_copy_workflow_from_type) + href: new_workflow_copy_path(type, source_role_id: role&.id), + aria: { label: helpers.t(:button_copy) }, + title: helpers.t(:button_copy) ) do |button| button.with_leading_visual_icon(icon: :copy) - t(:label_copy_workflow_from_type) - end - - header.with_action_button( - data: { controller: "async-dialog" }, - tag: :a, - mobile_icon: :copy, - mobile_label: t(:label_copy_workflow_from_role), - size: :medium, - href: new_workflow_copy_from_role_path(type, source_role_id: role&.id), - aria: { label: helpers.t(:label_copy_workflow_from_role) }, - title: helpers.t(:label_copy_workflow_from_role) - ) do |button| - button.with_leading_visual_icon(icon: :copy) - t(:label_copy_workflow_from_role) + t(:button_copy) end end diff --git a/app/components/workflows/table_component.html.erb b/app/components/workflows/table_component.html.erb index fd6215dd142..e6a8fd2dae2 100644 --- a/app/components/workflows/table_component.html.erb +++ b/app/components/workflows/table_component.html.erb @@ -54,16 +54,10 @@ See COPYRIGHT and LICENSE files for more details. item.with_leading_visual_icon(icon: :pencil) end menu.with_divider - menu.with_item(label:t("label_copy_workflow_from_type"), + menu.with_item(label: t("button_copy"), content_arguments: { data: { controller: "async-dialog" }}, tag: :a, - href: new_workflow_copy_from_type_path(type)) do |item| - item.with_leading_visual_icon(icon: :copy) - end - menu.with_item(label:t("label_copy_workflow_from_role"), - content_arguments: { data: { controller: "async-dialog" }}, - tag: :a, - href: new_workflow_copy_from_role_path(type)) do |item| + href: new_workflow_copy_path(type)) do |item| item.with_leading_visual_icon(icon: :copy) end end diff --git a/app/controllers/workflows/copies/from_roles_controller.rb b/app/controllers/workflows/copies/from_roles_controller.rb index 3fa0b5b7fe1..d53b33e12cc 100644 --- a/app/controllers/workflows/copies/from_roles_controller.rb +++ b/app/controllers/workflows/copies/from_roles_controller.rb @@ -37,10 +37,7 @@ class Workflows::Copies::FromRolesController < ApplicationController before_action :set_source_type before_action :set_source_role - before_action :set_all_roles - before_action :set_target_roles, only: %i[create] - - def new; end + before_action :set_target_roles def create if @source_type.nil? || @source_role.nil? @@ -74,10 +71,6 @@ class Workflows::Copies::FromRolesController < ApplicationController @source_role = eligible_roles.find_by(id: params[:source_role_id]) end - def set_all_roles - @all_roles = eligible_roles - end - def set_target_roles @target_roles = eligible_roles.find_by(id: params[:target_role_ids]) end diff --git a/app/controllers/workflows/copies/from_types_controller.rb b/app/controllers/workflows/copies/from_types_controller.rb index dbd71907069..0b0a74a2a1d 100644 --- a/app/controllers/workflows/copies/from_types_controller.rb +++ b/app/controllers/workflows/copies/from_types_controller.rb @@ -36,10 +36,7 @@ class Workflows::Copies::FromTypesController < ApplicationController before_action :require_admin before_action :set_source_type - before_action :set_other_types - before_action :set_target_type, only: %i[create] - - def new; end + before_action :set_target_type def create if @source_type.nil? @@ -71,10 +68,6 @@ class Workflows::Copies::FromTypesController < ApplicationController @source_type = ::Type.find(params[:workflow_type_id]) end - def set_other_types - @other_types = ::Type.where.not(id: @source_type.id).order(:position) - end - def set_target_type @target_type = ::Type.find(params[:target_type_id]) end diff --git a/spec/forms/workflows/copies/from_type_form_spec.rb b/app/controllers/workflows/copies_controller.rb similarity index 63% rename from spec/forms/workflows/copies/from_type_form_spec.rb rename to app/controllers/workflows/copies_controller.rb index 235a0cdd825..27226ca429f 100644 --- a/spec/forms/workflows/copies/from_type_form_spec.rb +++ b/app/controllers/workflows/copies_controller.rb @@ -27,25 +27,40 @@ # # See COPYRIGHT and LICENSE files for more details. #++ -# -require "spec_helper" -RSpec.describe Workflows::Copies::FromTypeForm, type: :forms do - include_context "with rendered form" +class Workflows::CopiesController < ApplicationController + include OpTurbo::ComponentStream - let(:model) { false } - let(:params) { { source_type:, other_types: } } - let(:source_type) { create(:type) } - let(:other_types) { create_list(:type, 4) } + layout "admin" - it "renders the Target type select list" do - expect(page).to have_select "Target type", required: true do |select| - options_text = select.all("option").map(&:text) - expect(options_text).to match_array(other_types.map(&:name)) - end + before_action :require_admin + + before_action :set_source_type + before_action :set_source_role + before_action :set_other_types + before_action :set_all_roles + + def new; end + + private + + def set_source_type + @source_type = ::Type.find(params[:workflow_type_id]) end - it "renders submit button" do - expect(page).to have_button "Copy", class: "Button--primary" + def set_source_role + @source_role = eligible_roles.find_by(id: params[:source_role_id]) + end + + def set_other_types + @other_types = ::Type.where.not(id: @source_type.id).order(:position) + end + + def set_all_roles + @all_roles = eligible_roles + end + + def eligible_roles + @eligible_roles ||= Workflow.eligible_roles end end diff --git a/app/forms/workflows/copies/form.rb b/app/forms/workflows/copies/form.rb new file mode 100644 index 00000000000..b1edc68ad7d --- /dev/null +++ b/app/forms/workflows/copies/form.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class Workflows::Copies::Form < ApplicationForm + def initialize(source_type:, source_role:, other_types:, all_roles:, append_to: nil) + super() + @source_type = source_type + @source_role = source_role + @other_types = other_types + @all_roles = all_roles + @append_to = append_to + end + + form do |copy| + another_type_at_first = @source_role.nil? + copy.advanced_radio_button_group(name: :mode) do |radio_group| + radio_group.radio_button( + value: "from_type", + checked: another_type_at_first, + label: helpers.t("workflows.copies.form.mode.from_type.label"), + caption: helpers.t("workflows.copies.form.mode.from_type.caption"), + data: { + target_name: "mode", + "show-when-value-selected-target": "cause" + } + ) + radio_group.radio_button( + value: "from_role", + checked: !another_type_at_first, + label: helpers.t("workflows.copies.form.mode.from_role.label"), + caption: helpers.t("workflows.copies.form.mode.from_role.caption"), + data: { + target_name: "mode", + "show-when-value-selected-target": "cause" + } + ) + end + + copy.group( + hidden: !another_type_at_first, + data: { + target_name: "mode", + value: "from_type", + "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| + @other_types.each do |other_type| + target_list.option(label: other_type.name, value: other_type.id) + end + end + end + + copy.group( + hidden: another_type_at_first, + data: { + target_name: "mode", + value: "from_role", + "show-when-value-selected-target": "effect" + } + ) do |from_role| + source_label = helpers.t("workflows.copies.form.source_role_id.label") + required = another_type_at_first + disabled = !another_type_at_first + from_role.select_list(name: :source_role_id, label: source_label, required:, disabled:) do |source_role_list| + @all_roles.each do |role| + source_role_list.option(label: role.name, value: role.id, selected: role == @source_role) + end + end + from_role.autocompleter( + name: "target_role_ids", + required: true, + include_blank: false, + label: helpers.t("workflows.copies.form.target_role_ids.label"), + autocomplete_options: { + multiple: true, + decorated: true, + closeOnSelect: false, + appendTo: @append_to, + data: { + "test-selector": "target_roles_autocomplete" + } + } + ) do |target_list| + @all_roles.each do |role| + target_list.option(label: role.name, value: role.id) + end + end + end + end +end diff --git a/app/forms/workflows/copies/from_role_form.rb b/app/forms/workflows/copies/from_role_form.rb deleted file mode 100644 index 614bd6b61ef..00000000000 --- a/app/forms/workflows/copies/from_role_form.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class Workflows::Copies::FromRoleForm < ApplicationForm - def initialize(source_type:, source_role:, all_roles:, append_to: nil) - super() - @source_type = source_type - @source_role = source_role - @all_roles = all_roles - @append_to = append_to - end - - form do |copy| - source_label = helpers.t("workflows.copies.from_role_form.source_role") - copy.select_list(name: :source_role_id, label: source_label, required: true) do |source_role_list| - @all_roles.each do |role| - source_role_list.option(label: role.name, value: role.id, selected: role == @source_role) - end - end - copy.autocompleter( - name: "target_role_ids", - required: true, - include_blank: false, - label: helpers.t("workflows.copies.from_role_form.target_roles"), - autocomplete_options: { - multiple: true, - decorated: true, - closeOnSelect: false, - appendTo: @append_to, - data: { - "test-selector": "target_roles_autocomplete" - } - } - ) do |target_list| - @all_roles.each do |role| - target_list.option(label: role.name, value: role.id) - end - end - end -end diff --git a/app/forms/workflows/copies/from_type_form.rb b/app/forms/workflows/copies/from_type_form.rb deleted file mode 100644 index 94e127ee541..00000000000 --- a/app/forms/workflows/copies/from_type_form.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class Workflows::Copies::FromTypeForm < ApplicationForm - def initialize(source_type:, other_types:) - super() - @source_type = source_type - @other_types = other_types - end - - form do |copy| - target_label = helpers.t("workflows.copies.from_type_form.target_type") - copy.select_list(name: :target_type_id, label: target_label, required: true) do |target_list| - @other_types.each do |other_type| - target_list.option(label: other_type.name, value: other_type.id) - end - end - end -end diff --git a/app/views/workflows/copies/from_types/new.turbo_stream.erb b/app/views/workflows/copies/from_types/new.turbo_stream.erb deleted file mode 100644 index 23433c90f0b..00000000000 --- a/app/views/workflows/copies/from_types/new.turbo_stream.erb +++ /dev/null @@ -1,55 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> -<% title = t(".title", source_type: @source_type.name) %> - -<%= turbo_stream.dialog do - render(Primer::Alpha::Dialog.new(title:)) do |d| - d.with_header(variant: :large) - d.with_body do - settings_primer_form_with(url: workflow_copy_from_type_path(@source_type), id: "copy_from_type") do |f| - render(Primer::Alpha::Banner.new(scheme: :warning, mb: 4)) do |banner| - t(".warning") - end + - render(Workflows::Copies::FromTypeForm.new(f, source_type: @source_type, other_types: @other_types)) - end - end - d.with_footer do - render(Primer::Beta::Button.new( - scheme: :primary, - tag: :button, - type: :submit, - form: "copy_from_type", - data: { turbo: true } - )) do - t(:button_copy) - end - end - end -end -%> diff --git a/app/views/workflows/copies/from_roles/new.turbo_stream.erb b/app/views/workflows/copies/new.turbo_stream.erb similarity index 51% rename from app/views/workflows/copies/from_roles/new.turbo_stream.erb rename to app/views/workflows/copies/new.turbo_stream.erb index e5324abade7..61b1d45f89d 100644 --- a/app/views/workflows/copies/from_roles/new.turbo_stream.erb +++ b/app/views/workflows/copies/new.turbo_stream.erb @@ -26,27 +26,51 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<% title = t(".title", source_type: @source_type.name) %> - -<% dialog_id = "copy_from_type_dialog" %> <%= turbo_stream.dialog do - render(Primer::Alpha::Dialog.new(title:, size: :medium_portrait, id: dialog_id)) do |d| + title = t(".title", source_type: @source_type.name) + dialog_id = "copy_from_type_dialog" + another_type_at_first = @source_role.nil? + + render(Primer::Alpha::Dialog.new(title:, size: :large, id: dialog_id, data: { controller: "show-when-value-selected" })) do |d| d.with_header(variant: :large) d.with_body(classes: "workflow-status-dialog-body") do - settings_primer_form_with(url: workflow_copy_from_role_path(@source_type), id: "copy_from_role") do |f| - render(Primer::Alpha::Banner.new(scheme: :warning, mb: 4)) do |banner| - t(".warning") - end + - render(Workflows::Copies::FromRoleForm.new(f, source_type: @source_type, source_role: @source_role, all_roles: @all_roles, append_to: "##{dialog_id}")) + settings_primer_form_with(id: "copy_form") do |f| + render(Workflows::Copies::Form.new(f, source_type: @source_type, source_role: @source_role, other_types: @other_types, all_roles: @all_roles, append_to: "##{dialog_id}")) end end d.with_footer do + render(Primer::Beta::Button.new(data: { "close-dialog-id": dialog_id })) { "Cancel" } + + render(Primer::Beta::Button.new( + hidden: !another_type_at_first, scheme: :primary, tag: :button, type: :submit, - form: "copy_from_role", - data: { turbo: true } + form: "copy_form", + formaction: workflow_copy_from_type_path(@source_type), + data: { + target_name: "mode", + value: "from_type", + "show-when-value-selected-target": "effect", + turbo: true + } + )) do + t(:button_copy) + end + + + render(Primer::Beta::Button.new( + hidden: another_type_at_first, + scheme: :primary, + tag: :button, + type: :submit, + form: "copy_form", + formaction: workflow_copy_from_role_path(@source_type, source_role_id: @source_role&.id), + data: { + target_name: "mode", + value: "from_role", + "show-when-value-selected-target": "effect", + turbo: true + } )) do t(:button_copy) end diff --git a/config/locales/en.yml b/config/locales/en.yml index 3b541e39d3e..4691f73b87b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1393,19 +1393,22 @@ en: workflows: copies: - from_role_form: - source_role: "Source role" - target_roles: "Target roles" - from_roles: - new: - title: "Copy workflow of \"%{source_type}\" between roles" - warning: "This action will replace existing workflows of the selected target roles." - from_type_form: - target_type: "Target type" - from_types: - new: - title: "Copy workflow from \"%{source_type}\" to another type" - warning: "This action will replace existing workflows of all the roles of the selected target type." + form: + source_role_id: + label: "Source role" + target_role_ids: + label: "Target roles" + target_type_id: + label: "Target type" + mode: + from_role: + label: "Copy to other roles" + caption: "Copy the current workflow to one or more roles inside the same work package type. If the selected role has already a workflow the current one will be overwritten." + from_type: + label: "Copy to another type" + caption: "Copy the current workflow to another work packages type. If the selected type has already a workflow the current one will be overwritten." + new: + title: "Copy workflow of \"%{source_type}\"" form: matrix_caption: "Workflow matrix" matrix_caption_assignee: "Workflow matrix for assignee" diff --git a/config/routes.rb b/config/routes.rb index f0d80617304..65a5492f7ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -804,9 +804,11 @@ Rails.application.routes.draw do end resources :workflows, only: %i[index edit update], param: :type_id do - resource :copy, only: %i[], module: "workflows/copies" do - resource :from_type, only: %i[new create] - resource :from_role, only: %i[new create] + resource :copy, only: %i[new], module: :workflows do + scope module: :copies do + resource :from_type, only: %i[create] + resource :from_role, only: %i[create] + end end collection do get :status_dialog diff --git a/lookbook/docs/patterns/02-forms.md.erb b/lookbook/docs/patterns/02-forms.md.erb index 63c317a9891..7c21e040755 100644 --- a/lookbook/docs/patterns/02-forms.md.erb +++ b/lookbook/docs/patterns/02-forms.md.erb @@ -137,7 +137,7 @@ primer_form_with( data: { target_name: "frequency", value: "foo" - "show-when-value-selected-target": "cause" + "show-when-value-selected-target": "effect" } end diff --git a/spec/components/workflows/table_component_spec.rb b/spec/components/workflows/table_component_spec.rb index 034044bfad5..7869ae64599 100644 --- a/spec/components/workflows/table_component_spec.rb +++ b/spec/components/workflows/table_component_spec.rb @@ -65,14 +65,12 @@ RSpec.describe Workflows::TableComponent, type: :component do expect(rendered_component).to have_css("li", text: types.first.name) do |row| expect(row).to have_link(types.first.name, href: edit_workflow_path(types.first)) expect(row).to have_link("Edit", href: edit_workflow_path(types.first)) - expect(row).to have_link("Copy to another type", href: new_workflow_copy_from_type_path(types.first)) - expect(row).to have_link("Copy to other roles", href: new_workflow_copy_from_role_path(types.first)) + expect(row).to have_link("Copy", href: new_workflow_copy_path(types.first)) end expect(rendered_component).to have_css("li", text: types.second.name) do |row| expect(row).to have_link(types.second.name, href: edit_workflow_path(types.second)) expect(row).to have_link("Edit", href: edit_workflow_path(types.second)) - expect(row).to have_link("Copy to another type", href: new_workflow_copy_from_type_path(types.second)) - expect(row).to have_link("Copy to other roles", href: new_workflow_copy_from_role_path(types.second)) + expect(row).to have_link("Copy", href: new_workflow_copy_path(types.second)) end end end diff --git a/spec/controllers/workflows/copies/from_roles_controller_spec.rb b/spec/controllers/workflows/copies/from_roles_controller_spec.rb index 301834bc845..4e0f8d17dd5 100644 --- a/spec/controllers/workflows/copies/from_roles_controller_spec.rb +++ b/spec/controllers/workflows/copies/from_roles_controller_spec.rb @@ -53,58 +53,12 @@ RSpec.describe Workflows::Copies::FromRolesController do build_stubbed_list(:project_role, 2) end - let!(:source_role) { nil } - before do allow(eligible_roles).to receive(:find_by).and_return(source_role) end current_user { build_stubbed(:admin) } - describe "#new" do - let(:params) do - { workflow_type_id: source_type.id.to_s, source_role_id: source_role&.id } - end - - before do - get :new, params: - end - - it "is a success" do - expect(response) - .to have_http_status(:ok) - end - - it "renders the correct template" do - expect(response) - .to render_template :new - end - - it "assigns the source type" do - expect(assigns[:source_type]) - .to eq source_type - end - - it "does not assign any source role" do - expect(assigns[:source_role]) - .to be_nil - end - - it "assigns the eligible roles" do - expect(assigns[:all_roles]) - .to match_array(all_roles) - end - - describe "when the source role is specified" do - let!(:source_role) { all_roles.sample } - - it "assigns the source role" do - expect(assigns[:source_role]) - .to eq source_role - end - end - end - describe "#create" do let!(:source_role) { all_roles.sample } let!(:target_roles) do diff --git a/spec/controllers/workflows/copies/from_types_controller_spec.rb b/spec/controllers/workflows/copies/from_types_controller_spec.rb index b933691a7b4..a529d8703ec 100644 --- a/spec/controllers/workflows/copies/from_types_controller_spec.rb +++ b/spec/controllers/workflows/copies/from_types_controller_spec.rb @@ -53,36 +53,6 @@ RSpec.describe Workflows::Copies::FromTypesController do current_user { build_stubbed(:admin) } - describe "#new" do - let(:params) do - { workflow_type_id: source_type.id.to_s } - end - - before do - get :new, params: - end - - it "is a success" do - expect(response) - .to have_http_status(:ok) - end - - it "renders the correct template" do - expect(response) - .to render_template :new - end - - it "assigns the source_type" do - expect(assigns[:source_type]) - .to eq source_type - end - - it "assigns the source_role" do - expect(assigns[:other_types]) - .to match_array(other_types) - end - end - describe "#create" do let!(:target_type) do other_types.last.tap do |stub| diff --git a/spec/controllers/workflows/copies_controller_spec.rb b/spec/controllers/workflows/copies_controller_spec.rb new file mode 100644 index 00000000000..97863bb1621 --- /dev/null +++ b/spec/controllers/workflows/copies_controller_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe Workflows::CopiesController do + let!(:source_type) do + build_stubbed(:type) do |stub| + allow(Type) + .to receive(:find) + .with(stub.id.to_s) + .and_return(stub) + 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 + + let!(:eligible_roles) do + instance_double(ActiveRecord::Relation, to_a: all_roles).tap do |relation| + allow(Role) + .to receive(:where) + .with(type: ProjectRole.name) + .and_return(relation) + end + end + + let!(:all_roles) do + build_stubbed_list(:project_role, 2) + end + + let!(:source_role) { nil } + + before do + allow(eligible_roles).to receive(:find_by).and_return(source_role) + end + + current_user { build_stubbed(:admin) } + + describe "#new" do + let(:params) do + { workflow_type_id: source_type.id.to_s, source_role_id: source_role&.id } + end + + before do + get :new, params:, format: :turbo_stream + end + + it "is a success" do + expect(response) + .to have_http_status(:ok) + end + + it "renders the correct template" do + expect(response) + .to render_template :new + end + + it "assigns the source type" do + expect(assigns[:source_type]) + .to eq source_type + end + + it "assigns the other types" do + expect(assigns[:other_types]) + .to match_array(other_types) + end + + it "does not assign any source role" do + expect(assigns[:source_role]) + .to be_nil + end + + it "assigns the eligible roles" do + expect(assigns[:all_roles]) + .to match_array(all_roles) + end + + describe "when the source role is specified" do + let!(:source_role) { all_roles.sample } + + it "assigns the source role" do + expect(assigns[:source_role]) + .to eq source_role + end + end + end +end diff --git a/spec/features/workflows/copies/from_role_spec.rb b/spec/features/workflows/copies/from_role_spec.rb index 454ef8f6ce4..098176499f0 100644 --- a/spec/features/workflows/copies/from_role_spec.rb +++ b/spec/features/workflows/copies/from_role_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -RSpec.describe "Workflow copy from role" do +RSpec.describe "Workflow copy from role", :js do let!(:type) { create(:type) } let!(:roles) { create_list(:project_role, 3) } let(:admin) { create(:admin) } @@ -39,45 +39,42 @@ RSpec.describe "Workflow copy from role" do current_user { admin } - before do - visit new_workflow_copy_from_role_path(type) + shared_examples "a copy-to-other-roles dialog" do |with_source_role:| + it "permits to select a source role and target source roles" do + unless with_source_role + choose "Copy to other roles" + + expect(page).to have_select("Source role", text: roles.first.name) + select(roles.last.name, from: "Source role") + end + + target_roles_autocompleter.select_option roles.first.name, roles.second.name + target_roles_autocompleter.close_autocompleter + + click_button "Copy" + + expect(page).to have_css(".flash-success", text: "Successful update.") + end end - it "permits to select a source role and target source roles", :js do - expect(page).to have_select("Source role", text: roles.first.name) - select(roles.last.name, from: "Source role") + describe "from the workflows index page" do + before do + visit workflows_path + within "li", text: type.name do + find("button[aria-haspopup=true]").click + click_link "Copy" + end + end - target_roles_autocompleter.select_option roles.first.name, roles.second.name - target_roles_autocompleter.close_autocompleter - - click_button "Copy" - - expect(page).to have_css(".flash-success", text: "Successful update.") + it_behaves_like "a copy-to-other-roles dialog", with_source_role: false end - it "allows to go back to Workflow index page" do - visit workflows_path - within "li", text: type.name do - click_link "Copy to other roles" + describe "from the workflows edit page" do + before do + visit edit_workflow_path(type) + click_link "Copy" end - within ".Banner--warning" do - click_link "Cancel" - end - - expect(page).to have_heading "Workflow" - expect(page).to have_current_path(workflows_path) - end - - it "allows to go back to Workflow edit page" do - visit edit_workflow_path(type) - click_link "Copy to other roles" - - within ".Banner--warning" do - click_link "Cancel" - end - - expect(page).to have_heading type.name - expect(page).to have_current_path(edit_workflow_path(type)) + it_behaves_like "a copy-to-other-roles dialog", with_source_role: true end end diff --git a/spec/features/workflows/copies/from_type_spec.rb b/spec/features/workflows/copies/from_type_spec.rb index 1be1e0de9b3..9a8a522a709 100644 --- a/spec/features/workflows/copies/from_type_spec.rb +++ b/spec/features/workflows/copies/from_type_spec.rb @@ -30,48 +30,46 @@ require "spec_helper" -RSpec.describe "Workflow copy from type" do - let(:types) { create_list(:type, 3) } - let(:type) { types.first } +RSpec.describe "Workflow copy from type", :js do + let!(:types) { create_list(:type, 3) } + let!(:type) { types.first } let(:admin) { create(:admin) } current_user { admin } - before do - visit new_workflow_copy_from_type_path(type) + shared_examples "a copy-to-another-type dialog" do |with_source_role:| + it "permits to select a target type" 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") + + click_button "Copy" + + expect(page).to have_css(".flash-success", text: "Successful update.") + end end - it "permits to select another type", :js do - expect(page).to have_select("Target type", text: types.second.name) - select(types.last.name, from: "Target type") - click_button "Copy" + describe "from the workflows index page" do + before do + visit workflows_path + within "li", text: type.name do + find("button[aria-haspopup=true]").click + click_link "Copy" + end + end - expect(page).to have_css(".flash-success", text: "Successful update.") + it_behaves_like "a copy-to-another-type dialog", with_source_role: false end - it "allows to go back to Workflow index page" do - visit workflows_path - within "li", text: type.name do - click_link "Copy to another type" + describe "from the workflows edit page" do + before do + visit edit_workflow_path(type) + click_link "Copy" end - within ".Banner--warning" do - click_link "Cancel" - end - - expect(page).to have_heading "Workflow" - expect(page).to have_current_path(workflows_path) - end - - it "allows to go back to Workflow edit page" do - visit edit_workflow_path(type) - click_link "Copy to another type" - - within ".Banner--warning" do - click_link "Cancel" - end - - expect(page).to have_heading type.name - expect(page).to have_current_path(edit_workflow_path(type)) + it_behaves_like "a copy-to-another-type dialog", with_source_role: true end end diff --git a/spec/features/workflows/edit_spec.rb b/spec/features/workflows/edit_spec.rb index 15d43953ab9..dca86ecfd80 100644 --- a/spec/features/workflows/edit_spec.rb +++ b/spec/features/workflows/edit_spec.rb @@ -372,24 +372,6 @@ RSpec.describe "Workflow edit" do end end - it "allows navigating to Workflow copy-from-type page" do - within ".PageHeader-actions" do - click_on "Copy to another type" - end - - expect(page).to have_heading "Copy workflow" - expect(page).to have_current_path(new_workflow_copy_from_type_path(type)) - end - - it "allows navigating to Workflow copy-from-role page" do - within ".PageHeader-actions" do - click_on "Copy to other roles" - end - - expect(page).to have_heading "Copy workflow" - expect(page).to have_current_path(new_workflow_copy_from_role_path(type, source_role_id: role.id)) - end - context "with status dialog", :js do before do visit_workflow_edit(role:) @@ -579,4 +561,12 @@ RSpec.describe "Workflow edit" do expect(page).to have_text("Add statuses to start configuring workflows for this role") end end + + it "allows navigating to any Copy page", :js do + within ".PageHeader-actions" do + click_on "Copy" + end + + expect(page).to have_heading "Copy workflow" + end end diff --git a/spec/features/workflows/index_spec.rb b/spec/features/workflows/index_spec.rb index ae897e7d7ac..fc92f23562a 100644 --- a/spec/features/workflows/index_spec.rb +++ b/spec/features/workflows/index_spec.rb @@ -74,34 +74,18 @@ RSpec.describe "Workflows index" do expect(page).to have_current_path(edit_workflow_path(some_type)) end - it "allows navigating to any copy-from-type page" do + it "allows navigating to any Copy page", :js do expect(page).to have_heading("Workflows") some_type = types.sample within "ul.Box-list" do within "li", text: some_type.name do find("button[aria-haspopup=true]").click - click_link "Copy to another type" + click_link "Copy" end end expect(page).to have_heading "Copy workflow" - expect(page).to have_current_path(new_workflow_copy_from_type_path(some_type)) - end - - it "allows navigating to any copy-from-role page" do - expect(page).to have_heading("Workflows") - - some_type = types.sample - within "ul.Box-list" do - within "li", text: some_type.name do - find("button[aria-haspopup=true]").click - click_link "Copy to other roles" - end - end - - expect(page).to have_heading "Copy workflow" - expect(page).to have_current_path(new_workflow_copy_from_role_path(some_type)) end it "allows navigating to Workflow summary page" do diff --git a/spec/features/workflows/missing_for_sharing_wp_spec.rb b/spec/features/workflows/missing_for_sharing_wp_spec.rb index d8cb7a18ad6..37f170d6f77 100644 --- a/spec/features/workflows/missing_for_sharing_wp_spec.rb +++ b/spec/features/workflows/missing_for_sharing_wp_spec.rb @@ -68,8 +68,9 @@ RSpec.describe "Configuring the workflow for work package sharing", :js, # On the copy workflow form, select the already existing workflow for copying within "li", text: type.name do find("button[aria-haspopup=true]").click - click_link "Copy to other roles" + click_link "Copy" end + choose "Copy to other roles" select role.name, from: "source_role_id" target_roles_autocompleter.select_option work_package_role.name target_roles_autocompleter.close_autocompleter diff --git a/spec/forms/workflows/copies/form_spec.rb b/spec/forms/workflows/copies/form_spec.rb new file mode 100644 index 00000000000..fdb6614288f --- /dev/null +++ b/spec/forms/workflows/copies/form_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ +# +require "spec_helper" + +RSpec.describe Workflows::Copies::Form, type: :forms do + include_context "with rendered form" + + let(:model) { false } + let(:params) { { source_type:, source_role:, other_types:, all_roles: } } + let(:source_type) { create(:type) } + let(:other_types) { create_list(:type, 4) } + let(:all_roles) { create_list(:project_role, 4) } + + shared_examples "a copy form with conditional fields" do |another_type_at_first:| + it "renders radio buttons to choose the mode" do + expect(page).to have_field("Copy to another type", checked: another_type_at_first) + 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) + expect(options_text).to match_array(other_types.map(&:name)) + end + end + + it "renders the Source role select list" do + required = another_type_at_first + disabled = visible = !another_type_at_first + expect(page).to have_select "Source role", required:, disabled:, visible: do |select| + options_text = select.all("option", visible: !another_type_at_first).map(&:text) + expect(options_text).to match_array(all_roles.map(&:name)) + end + end + + it "renders the Target roles autocompleter" do + data_attributes = "[data-test-selector=\"target_roles_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(all_roles.map(&:name)) + end + end + end + + describe "when the source role is not specified" do + let(:source_role) { nil } + + it_behaves_like "a copy form with conditional fields", another_type_at_first: true + end + + describe "when the source role is specified" do + let(:source_role) { all_roles.sample } + + it_behaves_like "a copy form with conditional fields", another_type_at_first: false + + it "renders the Source role select list with read-only source" do + expect(page).to have_select "Source role", disabled: true do |select| + selected_option_text = select.all("option[selected=selected]").map(&:text) + expect(selected_option_text).to contain_exactly(source_role.name) + end + end + end +end diff --git a/spec/forms/workflows/copies/from_role_form_spec.rb b/spec/forms/workflows/copies/from_role_form_spec.rb deleted file mode 100644 index ba44ea6ef23..00000000000 --- a/spec/forms/workflows/copies/from_role_form_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ -# -require "spec_helper" - -RSpec.describe Workflows::Copies::FromRoleForm, type: :forms do - include_context "with rendered form" - - let(:model) { false } - let(:params) { { source_type:, source_role:, all_roles: } } - let(:source_type) { create(:type) } - let(:all_roles) { create_list(:project_role, 4) } - let(:source_role) { nil } - - it "renders the Source role select list" do - expect(page).to have_select "Source role", required: true do |select| - options_text = select.all("option").map(&:text) - expect(options_text).to match_array(all_roles.map(&:name)) - end - end - - it "renders the Target roles autocompleter" do - data_attributes = "[data-test-selector=\"target_roles_autocomplete\"][data-multiple=\"true\"]" - expect(page).to have_css "opce-autocompleter#{data_attributes}" do |autocompleter| - options_text = JSON.parse(autocompleter["data-items"]).map { |item| item["name"] } - expect(options_text).to match_array(all_roles.map(&:name)) - end - end - - it "renders submit button" do - expect(page).to have_button "Copy", class: "Button--primary" - end - - describe "when the source type is specified" do - let(:source_role) { all_roles.sample } - - it "renders the Source role select list with selected source" do - expect(page).to have_select "Source role", required: true do |select| - selected_option_text = select.all("option[selected=selected]").map(&:text) - expect(selected_option_text).to contain_exactly(source_role.name) - end - end - end -end diff --git a/spec/routing/workflows_spec.rb b/spec/routing/workflows_spec.rb index ca468aee45c..22bcdd23710 100644 --- a/spec/routing/workflows_spec.rb +++ b/spec/routing/workflows_spec.rb @@ -36,16 +36,14 @@ RSpec.describe "workflows routes" do it { expect(get("/workflows/42/edit")).to route_to("workflows#edit", type_id: "42") } it { expect(patch("/workflows/42")).to route_to("workflows#update", type_id: "42") } - it { expect(get("/workflows/42/copy/from_type/new")).to route_to("workflows/copies/from_types#new", workflow_type_id: "42") } - it { expect(post("/workflows/42/copy/from_type")).to route_to("workflows/copies/from_types#create", workflow_type_id: "42") } - - it { expect(get("/workflows/42/copy/from_role/new")).to route_to("workflows/copies/from_roles#new", workflow_type_id: "42") } + it { expect(get("/workflows/42/copy/new")).to route_to("workflows/copies#new", workflow_type_id: "42") } it do - expect(get("/workflows/42/copy/from_role/new?source_role_id=23")) - .to route_to("workflows/copies/from_roles#new", workflow_type_id: "42", source_role_id: "23") + expect(get("/workflows/42/copy/new?source_role_id=23")) + .to route_to("workflows/copies#new", workflow_type_id: "42", source_role_id: "23") end + it { expect(post("/workflows/42/copy/from_type")).to route_to("workflows/copies/from_types#create", workflow_type_id: "42") } it { expect(post("/workflows/42/copy/from_role")).to route_to("workflows/copies/from_roles#create", workflow_type_id: "42") } it { expect(get("/workflows/summary")).to route_to("workflows/summaries#show") }