From e6a49517cd010c8b6a59f96c120ef21062beac7a Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Tue, 2 Jun 2026 16:30:53 +0300 Subject: [PATCH 1/3] Render semantic ID autofix preview via BorderBoxTableComponent Long identifiers overflowed into the neighbouring column. Replace the hand-rolled flex table with the design-system BorderBoxTableComponent, which handles column spacing, wrapping, and responsive stacking; the project and previous-identifier columns wrap as main columns. --- .../identifier_autofix_row_component.rb | 70 ++++++++++++++++ ...ntifier_autofix_section_component.html.erb | 75 ++--------------- .../identifier_autofix_section_component.rb | 15 ---- .../identifier_autofix_table_component.rb | 80 +++++++++++++++++++ config/locales/en.yml | 1 + 5 files changed, 157 insertions(+), 84 deletions(-) create mode 100644 app/components/work_packages/admin/settings/identifier_autofix_row_component.rb create mode 100644 app/components/work_packages/admin/settings/identifier_autofix_table_component.rb diff --git a/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb b/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb new file mode 100644 index 000000000000..18828eb95d8d --- /dev/null +++ b/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb @@ -0,0 +1,70 @@ +# 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 WorkPackages + module Admin + module Settings + class IdentifierAutofixRowComponent < OpPrimer::BorderBoxRowComponent + def project + render(Primer::Beta::Link.new(href: project_path(model[:project]))) { model[:project].name } + end + + def previous_identifier + flex_layout(direction: :column) do |col| + col.with_row { render(Primer::Beta::Text.new) { model[:current_identifier] } } + if (label = error_label).present? + col.with_row do + render(Primer::Beta::Text.new(color: :danger, font_size: :small)) { label } + end + end + end + end + + def autofixed_suggestion + model[:suggested_identifier] + end + + # The sequence number is derived deterministically from the identifier so it looks + # varied across projects but is stable across renders. Range: 1–500. + def example_work_package_id + identifier = model[:suggested_identifier] + "#{identifier}-#{(identifier.bytes.sum % 500) + 1}" + end + + private + + def error_label + I18n.t("admin.settings.work_packages_identifier.autofix_preview.error_#{model[:error_reason]}", + default: "") + end + end + end + end +end diff --git a/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb b/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb index a9ba02a04458..f77b45f98241 100644 --- a/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb +++ b/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb @@ -38,72 +38,9 @@ end %> -<%= - render(border_box_container(mb: 3)) do |component| - component.with_header(font_weight: :bold) do - flex_layout do |header| - header.with_column(flex: 1) do - render(Primer::Beta::Text.new(font_weight: :semibold)) do - I18n.t("admin.settings.work_packages_identifier.box_header.label_project") - end - end - header.with_column(flex: 1) do - render(Primer::Beta::Text.new(font_weight: :semibold)) do - I18n.t("admin.settings.work_packages_identifier.box_header.label_previous_identifier") - end - end - header.with_column(flex: 1) do - render(Primer::Beta::Text.new(font_weight: :semibold)) do - I18n.t("admin.settings.work_packages_identifier.box_header.label_autofixed_suggestion") - end - end - header.with_column(flex: 1) do - render(Primer::Beta::Text.new(font_weight: :semibold)) do - I18n.t("admin.settings.work_packages_identifier.box_header.label_example_work_package_id") - end - end - end - end - - displayed.each do |entry| - component.with_row do - flex_layout(align_items: :center) do |row| - row.with_column(flex: 1) do - render(Primer::Beta::Link.new(href: project_path(entry[:project]))) do - entry[:project].name - end - end - row.with_column(flex: 1) do - flex_layout(direction: :column) do |col| - col.with_row do - render(Primer::Beta::Text.new) { entry[:current_identifier] } - end - col.with_row do - render(Primer::Beta::Text.new(color: :danger, font_size: :small)) do - error_label(entry[:error_reason]) - end - end - end - end - row.with_column(flex: 1) do - render(Primer::Beta::Text.new) { entry[:suggested_identifier] } - end - row.with_column(flex: 1) do - render(Primer::Beta::Text.new) { sample_wp_id(entry[:suggested_identifier]) } - end - end - end - end - - if remaining_count.positive? - component.with_row do - render(Primer::Beta::Text.new(color: :muted)) do - I18n.t( - "admin.settings.work_packages_identifier.autofix_preview.remaining_projects", - count: remaining_count - ) - end - end - end - end -%> +<%= render( + WorkPackages::Admin::Settings::IdentifierAutofixTableComponent.new( + rows: displayed, + remaining_count: + ) + ) %> diff --git a/app/components/work_packages/admin/settings/identifier_autofix_section_component.rb b/app/components/work_packages/admin/settings/identifier_autofix_section_component.rb index 7ca1d1acc621..8dbeea1b6d67 100644 --- a/app/components/work_packages/admin/settings/identifier_autofix_section_component.rb +++ b/app/components/work_packages/admin/settings/identifier_autofix_section_component.rb @@ -32,8 +32,6 @@ module WorkPackages module Admin module Settings class IdentifierAutofixSectionComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - DISPLAY_COUNT = ProjectIdentifiers::IdentifierAutofix::PreviewQuery::DISPLAY_COUNT def initialize(projects_data:, total_count: projects_data.size) @@ -46,19 +44,6 @@ def initialize(projects_data:, total_count: projects_data.size) private attr_reader :total_count, :displayed, :remaining_count - - def error_label(error_reason) - I18n.t("admin.settings.work_packages_identifier.autofix_preview.error_#{error_reason}", - default: "") - end - - # Produces a realistic-looking example work package ID for the preview table. - # The sequence number is derived deterministically from the identifier so it looks - # varied across projects but is stable across renders. Range: 1–500. - def sample_wp_id(identifier) - n = (identifier.bytes.sum % 500) + 1 - "#{identifier}-#{n}" - end end end end diff --git a/app/components/work_packages/admin/settings/identifier_autofix_table_component.rb b/app/components/work_packages/admin/settings/identifier_autofix_table_component.rb new file mode 100644 index 000000000000..d912f24f5ec0 --- /dev/null +++ b/app/components/work_packages/admin/settings/identifier_autofix_table_component.rb @@ -0,0 +1,80 @@ +# 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 WorkPackages + module Admin + module Settings + class IdentifierAutofixTableComponent < OpPrimer::BorderBoxTableComponent + columns :project, :previous_identifier, :autofixed_suggestion, :example_work_package_id + # Project and previous identifier hold the long content; spanning two grid columns lets + # them wrap instead of truncating, while the short handle columns stay compact. + main_column :project, :previous_identifier + mobile_labels :previous_identifier, :autofixed_suggestion, :example_work_package_id + + def initialize(rows:, remaining_count: 0, **) + super(rows:, **) + @remaining_count = remaining_count + end + + def row_class + IdentifierAutofixRowComponent + end + + def mobile_title + header(:table_title) + end + + def headers + [ + [:project, { caption: header(:label_project) }], + [:previous_identifier, { caption: header(:label_previous_identifier) }], + [:autofixed_suggestion, { caption: header(:label_autofixed_suggestion) }], + [:example_work_package_id, { caption: header(:label_example_work_package_id) }] + ] + end + + def has_footer? + @remaining_count.positive? + end + + def footer + I18n.t("admin.settings.work_packages_identifier.autofix_preview.remaining_projects", + count: @remaining_count) + end + + private + + def header(key) + I18n.t("admin.settings.work_packages_identifier.box_header.#{key}") + end + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 2786c28db4fe..ce93f6db9a83 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -452,6 +452,7 @@ en: OpenProject can automatically update these so that they are valid as in the examples below. Click on 'Convert identifiers' to update identifiers for all projects in this manner and enable project-based semantic identifiers. box_header: + table_title: Projects with identifiers to update label_project: Project label_previous_identifier: Previous identifier label_autofixed_suggestion: Future identifier From 23736a12bdd83f8f7f8e1803909a3da614c43e13 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Tue, 2 Jun 2026 17:19:41 +0300 Subject: [PATCH 2/3] Add bottom spacing below the autofix preview table The Convert identifiers button sat flush against the preview table. The table now carries its own bottom margin (mb: 3, via Primer::Box), matching the spacing the previous hand-rolled box had. --- .../identifier_autofix_section_component.html.erb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb b/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb index f77b45f98241..3386b800f4cf 100644 --- a/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb +++ b/app/components/work_packages/admin/settings/identifier_autofix_section_component.html.erb @@ -38,9 +38,11 @@ end %> -<%= render( - WorkPackages::Admin::Settings::IdentifierAutofixTableComponent.new( - rows: displayed, - remaining_count: - ) - ) %> +<%= render(Primer::Box.new(mb: 3)) do %> + <%= render( + WorkPackages::Admin::Settings::IdentifierAutofixTableComponent.new( + rows: displayed, + remaining_count: + ) + ) %> +<% end %> From 284e62abbf13800278d1b54bb2e33d9bbc0a4c1f Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Wed, 3 Jun 2026 11:27:28 +0300 Subject: [PATCH 3/3] Switch to inline message component as the more conventional structure Discussed in UX meeting an accepted: https://community.openproject.org/meetings/10359#outcome-3654 --- .../admin/settings/identifier_autofix_row_component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb b/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb index 18828eb95d8d..2cca49038bb3 100644 --- a/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb +++ b/app/components/work_packages/admin/settings/identifier_autofix_row_component.rb @@ -41,7 +41,7 @@ def previous_identifier col.with_row { render(Primer::Beta::Text.new) { model[:current_identifier] } } if (label = error_label).present? col.with_row do - render(Primer::Beta::Text.new(color: :danger, font_size: :small)) { label } + render(Primer::OpenProject::InlineMessage.new(scheme: :critical, size: :small)) { label } end end end