From db6cc8ba22b5bbc28c5803ca6474f5687e117df9 Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Thu, 12 Mar 2026 21:53:18 +0100 Subject: [PATCH 1/7] [#72856] Add semantic identifier support to "Copy Project" --- app/components/projects/copy_form_component.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/projects/copy_form_component.html.erb b/app/components/projects/copy_form_component.html.erb index fc8b4d6de6d9..e2c7cc46b658 100644 --- a/app/components/projects/copy_form_component.html.erb +++ b/app/components/projects/copy_form_component.html.erb @@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= - component_wrapper do + component_wrapper(data: identifier_suggestion_data) do settings_primer_form_with(model: target_project, url: copy_project_path(source_project), method: :post) do |f| flex_layout do |container| container.with_row(mb: 3) do @@ -41,6 +41,7 @@ See COPYRIGHT and LICENSE files for more details. render( Primer::Forms::FormList.new( Projects::Settings::NameForm.new(f), + Projects::Settings::EditableIdentifierForm.new(f), Projects::Settings::RelationsForm.new(f, invisible: params[:parent_id].present?), Projects::Settings::CustomFieldsForm.new(f) ) From aeb3d6eef17bd8fa18a67799ffd84a9a28dd41dc Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Fri, 13 Mar 2026 00:54:34 +0100 Subject: [PATCH 2/7] DRY up the suggestion logic --- .../concerns/identifier_suggestion.rb | 45 +++++++++++++++++++ .../projects/copy_form_component.rb | 1 + app/components/projects/new_component.rb | 11 +---- 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 app/components/projects/concerns/identifier_suggestion.rb diff --git a/app/components/projects/concerns/identifier_suggestion.rb b/app/components/projects/concerns/identifier_suggestion.rb new file mode 100644 index 000000000000..c768d332ce52 --- /dev/null +++ b/app/components/projects/concerns/identifier_suggestion.rb @@ -0,0 +1,45 @@ +# 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 Projects + module Concerns + module IdentifierSuggestion + def identifier_suggestion_data + flag_active = OpenProject::FeatureDecisions.semantic_work_package_ids_active? + data = { + controller: "projects--identifier-suggestion", + "projects--identifier-suggestion-mode-value": Project.semantic_alphanumeric_identifier? ? "semantic" : "legacy" + } + data[:"projects--identifier-suggestion-url-value"] = projects_identifier_suggestion_path if flag_active + data + end + end + end +end diff --git a/app/components/projects/copy_form_component.rb b/app/components/projects/copy_form_component.rb index 9d1cefd0d773..86913f685259 100644 --- a/app/components/projects/copy_form_component.rb +++ b/app/components/projects/copy_form_component.rb @@ -33,6 +33,7 @@ class CopyFormComponent < ApplicationComponent include ApplicationHelper include OpPrimer::ComponentHelpers include OpTurbo::Streamable + include Projects::Concerns::IdentifierSuggestion options :source_project, :target_project, :copy_options end diff --git a/app/components/projects/new_component.rb b/app/components/projects/new_component.rb index ee11c72f2ceb..ffe2faeae9d1 100644 --- a/app/components/projects/new_component.rb +++ b/app/components/projects/new_component.rb @@ -33,6 +33,7 @@ class NewComponent < ApplicationComponent include ApplicationHelper include OpPrimer::ComponentHelpers include OpTurbo::Streamable + include Projects::Concerns::IdentifierSuggestion options :project, :template, :step @@ -44,16 +45,6 @@ def step_3_display { display: :none } unless step == 3 end - def identifier_suggestion_data - flag_active = OpenProject::FeatureDecisions.semantic_work_package_ids_active? - data = { - controller: "projects--identifier-suggestion", - "projects--identifier-suggestion-mode-value": Project.semantic_alphanumeric_identifier? ? "semantic" : "legacy" - } - data[:"projects--identifier-suggestion-url-value"] = projects_identifier_suggestion_path if flag_active - data - end - def workspaces_path workspace_type = if Project.workspace_types.key?(project.workspace_type) project.workspace_type From 9147b8508d51ccf58a1d4f82640d5cebd3a34941 Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Mon, 16 Mar 2026 22:56:20 +0100 Subject: [PATCH 3/7] remove the extra copy of identifier_suggestion_data --- .../projects/concerns/identifier_suggestion.rb | 11 ++++++----- app/components/projects/new_component.rb | 11 ----------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/app/components/projects/concerns/identifier_suggestion.rb b/app/components/projects/concerns/identifier_suggestion.rb index c768d332ce52..2c9709531805 100644 --- a/app/components/projects/concerns/identifier_suggestion.rb +++ b/app/components/projects/concerns/identifier_suggestion.rb @@ -32,13 +32,14 @@ module Projects module Concerns module IdentifierSuggestion def identifier_suggestion_data - flag_active = OpenProject::FeatureDecisions.semantic_work_package_ids_active? - data = { + suggestion_mode = Setting::WorkPackageIdentifier.alphanumeric? ? "semantic" : "legacy" + + { controller: "projects--identifier-suggestion", - "projects--identifier-suggestion-mode-value": Project.semantic_alphanumeric_identifier? ? "semantic" : "legacy" + "projects--identifier-suggestion-mode-value": suggestion_mode, + "projects--identifier-suggestion-url-value": projects_identifier_suggestion_path, + "projects--identifier-suggestion-set-name-first-value": I18n.t("js.projects.identifier_suggestion.set_name_first") } - data[:"projects--identifier-suggestion-url-value"] = projects_identifier_suggestion_path if flag_active - data end end end diff --git a/app/components/projects/new_component.rb b/app/components/projects/new_component.rb index d81f75a24c82..ffe2faeae9d1 100644 --- a/app/components/projects/new_component.rb +++ b/app/components/projects/new_component.rb @@ -45,17 +45,6 @@ def step_3_display { display: :none } unless step == 3 end - def identifier_suggestion_data - suggestion_mode = Setting::WorkPackageIdentifier.alphanumeric? ? "semantic" : "legacy" - - { - controller: "projects--identifier-suggestion", - "projects--identifier-suggestion-mode-value": suggestion_mode, - "projects--identifier-suggestion-url-value": projects_identifier_suggestion_path, - "projects--identifier-suggestion-set-name-first-value": I18n.t("js.projects.identifier_suggestion.set_name_first") - } - end - def workspaces_path workspace_type = if Project.workspace_types.key?(project.workspace_type) project.workspace_type From 1f378939eec56c334e82a2895932843e8ab8612d Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Mon, 16 Mar 2026 22:57:51 +0100 Subject: [PATCH 4/7] add a test --- .../projects/copy_form_component_spec.rb | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/spec/components/projects/copy_form_component_spec.rb b/spec/components/projects/copy_form_component_spec.rb index b1933497ebc1..0f0bcf0f6fd5 100644 --- a/spec/components/projects/copy_form_component_spec.rb +++ b/spec/components/projects/copy_form_component_spec.rb @@ -42,4 +42,35 @@ def render_component(**params) it "renders a form" do expect(render_component).to have_css "form" end + + describe "#identifier_suggestion_data" do + it "mounts the Stimulus controller on the wrapper" do + expect(render_component).to have_css("[data-controller='projects--identifier-suggestion']") + end + + it "includes the suggestion URL" do + expect(render_component).to have_css( + "[data-projects--identifier-suggestion-url-value='/projects/identifier_suggestion']" + ) + end + + it "includes the set_name_first translation" do + translation = I18n.t('js.projects.identifier_suggestion.set_name_first') + expect(render_component).to have_css( + "[data-projects--identifier-suggestion-set-name-first-value='#{translation}']" + ) + end + + context "with alphanumeric identifiers", with_settings: { work_packages_identifier: "alphanumeric" } do + it "sets mode to semantic" do + expect(render_component).to have_css("[data-projects--identifier-suggestion-mode-value='semantic']") + end + end + + context "with numeric identifiers", with_settings: { work_packages_identifier: "numeric" } do + it "sets mode to legacy" do + expect(render_component).to have_css("[data-projects--identifier-suggestion-mode-value='legacy']") + end + end + end end From d3ac57c100d7df0ca838b37de1de76aca2d3d47e Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Mon, 16 Mar 2026 23:11:43 +0100 Subject: [PATCH 5/7] rubocop bit --- spec/components/projects/copy_form_component_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/projects/copy_form_component_spec.rb b/spec/components/projects/copy_form_component_spec.rb index 0f0bcf0f6fd5..f545f35bc266 100644 --- a/spec/components/projects/copy_form_component_spec.rb +++ b/spec/components/projects/copy_form_component_spec.rb @@ -55,7 +55,7 @@ def render_component(**params) end it "includes the set_name_first translation" do - translation = I18n.t('js.projects.identifier_suggestion.set_name_first') + translation = I18n.t("js.projects.identifier_suggestion.set_name_first") expect(render_component).to have_css( "[data-projects--identifier-suggestion-set-name-first-value='#{translation}']" ) From c2f35aad932ebc4ac510832e5e83cf0153a28ecb Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Thu, 19 Mar 2026 22:33:41 +0100 Subject: [PATCH 6/7] DRY up component specs --- .../projects/copy_form_component_spec.rb | 33 +++---------------- .../components/projects/new_component_spec.rb | 29 +--------------- .../components/identifier_suggestion_data.rb | 32 ++++++++++++++++++ 3 files changed, 37 insertions(+), 57 deletions(-) create mode 100644 spec/support/shared/components/identifier_suggestion_data.rb diff --git a/spec/components/projects/copy_form_component_spec.rb b/spec/components/projects/copy_form_component_spec.rb index f545f35bc266..a53ba4515d76 100644 --- a/spec/components/projects/copy_form_component_spec.rb +++ b/spec/components/projects/copy_form_component_spec.rb @@ -39,38 +39,13 @@ def render_component(**params) page end + let(:rendered_component) { render_component } + it "renders a form" do - expect(render_component).to have_css "form" + expect(rendered_component).to have_css "form" end describe "#identifier_suggestion_data" do - it "mounts the Stimulus controller on the wrapper" do - expect(render_component).to have_css("[data-controller='projects--identifier-suggestion']") - end - - it "includes the suggestion URL" do - expect(render_component).to have_css( - "[data-projects--identifier-suggestion-url-value='/projects/identifier_suggestion']" - ) - end - - it "includes the set_name_first translation" do - translation = I18n.t("js.projects.identifier_suggestion.set_name_first") - expect(render_component).to have_css( - "[data-projects--identifier-suggestion-set-name-first-value='#{translation}']" - ) - end - - context "with alphanumeric identifiers", with_settings: { work_packages_identifier: "alphanumeric" } do - it "sets mode to semantic" do - expect(render_component).to have_css("[data-projects--identifier-suggestion-mode-value='semantic']") - end - end - - context "with numeric identifiers", with_settings: { work_packages_identifier: "numeric" } do - it "sets mode to legacy" do - expect(render_component).to have_css("[data-projects--identifier-suggestion-mode-value='legacy']") - end - end + it_behaves_like "renders identifier_suggestion_data" end end diff --git a/spec/components/projects/new_component_spec.rb b/spec/components/projects/new_component_spec.rb index b43e6df0ec43..1d93a3427fc0 100644 --- a/spec/components/projects/new_component_spec.rb +++ b/spec/components/projects/new_component_spec.rb @@ -83,33 +83,6 @@ end describe "#identifier_suggestion_data" do - subject(:rendered) { render_inline(described_class.new(project:)) } - - it "mounts the Stimulus controller on the wrapper" do - expect(rendered).to have_css("[data-controller='projects--identifier-suggestion']") - end - - it "includes the suggestion URL" do - expect(rendered).to have_css("[data-projects--identifier-suggestion-url-value='/projects/identifier_suggestion']") - end - - it "includes the set_name_first translation" do - translation = I18n.t("js.projects.identifier_suggestion.set_name_first") - expect(rendered).to have_css( - "[data-projects--identifier-suggestion-set-name-first-value='#{translation}']" - ) - end - - context "with alphanumeric identifiers", with_settings: { work_packages_identifier: "alphanumeric" } do - it "sets mode to semantic" do - expect(rendered).to have_css("[data-projects--identifier-suggestion-mode-value='semantic']") - end - end - - context "with numeric identifiers", with_settings: { work_packages_identifier: "numeric" } do - it "sets mode to legacy" do - expect(rendered).to have_css("[data-projects--identifier-suggestion-mode-value='legacy']") - end - end + it_behaves_like "renders identifier_suggestion_data" end end diff --git a/spec/support/shared/components/identifier_suggestion_data.rb b/spec/support/shared/components/identifier_suggestion_data.rb new file mode 100644 index 000000000000..65bf6a26e4e3 --- /dev/null +++ b/spec/support/shared/components/identifier_suggestion_data.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples "renders identifier_suggestion_data" do + it "mounts the Stimulus controller on the wrapper" do + expect(rendered_component).to have_css("[data-controller='projects--identifier-suggestion']") + end + + it "includes the suggestion URL" do + expect(rendered_component).to have_css( + "[data-projects--identifier-suggestion-url-value='/projects/identifier_suggestion']" + ) + end + + it "includes the set_name_first translation" do + translation = I18n.t("js.projects.identifier_suggestion.set_name_first") + expect(rendered_component).to have_css( + "[data-projects--identifier-suggestion-set-name-first-value='#{translation}']" + ) + end + + context "with alphanumeric identifiers", with_settings: { work_packages_identifier: "alphanumeric" } do + it "sets mode to semantic" do + expect(rendered_component).to have_css("[data-projects--identifier-suggestion-mode-value='semantic']") + end + end + + context "with numeric identifiers", with_settings: { work_packages_identifier: "numeric" } do + it "sets mode to legacy" do + expect(rendered_component).to have_css("[data-projects--identifier-suggestion-mode-value='legacy']") + end + end +end From 717e90064c0bf521255f60e4f554d0aed1cfa583 Mon Sep 17 00:00:00 2001 From: Tomas Hykel Date: Thu, 19 Mar 2026 22:37:03 +0100 Subject: [PATCH 7/7] move a let statement --- spec/components/projects/copy_form_component_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/components/projects/copy_form_component_spec.rb b/spec/components/projects/copy_form_component_spec.rb index a53ba4515d76..6a7537d414b5 100644 --- a/spec/components/projects/copy_form_component_spec.rb +++ b/spec/components/projects/copy_form_component_spec.rb @@ -33,14 +33,13 @@ RSpec.describe Projects::CopyFormComponent, type: :component do let(:source_project) { build_stubbed(:project) } let(:target_project) { Project.new(attributes_for(:project).except(:name)) } + let(:rendered_component) { render_component } def render_component(**params) render_inline(described_class.new(source_project:, target_project:, **params)) page end - let(:rendered_component) { render_component } - it "renders a form" do expect(rendered_component).to have_css "form" end