From 29fce212a87e68436d233ab71aa69711e4665db9 Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 14:53:47 +0200 Subject: [PATCH 01/22] [JIM-133] Jira Migrator UI refinements https://community.openproject.org/wp/JIM-133 --- app/views/admin/import/jira/instances/index.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/admin/import/jira/instances/index.html.erb b/app/views/admin/import/jira/instances/index.html.erb index 6f9936abe5ae..9799f86b3fec 100644 --- a/app/views/admin/import/jira/instances/index.html.erb +++ b/app/views/admin/import/jira/instances/index.html.erb @@ -112,7 +112,8 @@ See COPYRIGHT and LICENSE files for more details. I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.sprints"), ].map { |label| { label:, checked: false } }, h: :full, - )) + icon: :"op-flag" + )) end flex.with_column(flex: 1, ml: 1) do render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( @@ -123,6 +124,7 @@ See COPYRIGHT and LICENSE files for more details. I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.schemes") ].map { |label| { label:, checked: false } }, h: :full, + icon: :"arrow-right" )) end end From 24f2fb545e85c335d8593d32abcee1e5bd985397 Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 15:02:06 +0200 Subject: [PATCH 02/22] add server_url column to Jira configuration table --- app/components/admin/import/jira/row_component.rb | 4 ++++ app/components/admin/import/jira/table_component.rb | 5 +++-- config/locales/en.yml | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/components/admin/import/jira/row_component.rb b/app/components/admin/import/jira/row_component.rb index 432152459537..4e0e32ce9fe6 100644 --- a/app/components/admin/import/jira/row_component.rb +++ b/app/components/admin/import/jira/row_component.rb @@ -49,5 +49,9 @@ def time_ago(date_time) def added helpers.format_date(model.created_at) end + + def server_url + model.url + end end end diff --git a/app/components/admin/import/jira/table_component.rb b/app/components/admin/import/jira/table_component.rb index 005a85dcc83b..531c1497f9f2 100644 --- a/app/components/admin/import/jira/table_component.rb +++ b/app/components/admin/import/jira/table_component.rb @@ -30,7 +30,7 @@ module Admin::Import::Jira class TableComponent < OpPrimer::BorderBoxTableComponent - columns :name, :last_change, :added + columns :name, :last_change, :added, :server_url def mobile_title Import::Jira.model_name.human(count: 2) @@ -48,7 +48,8 @@ def headers [ [:name, { caption: Import::Jira.human_attribute_name(:name) }], [:last_change, { caption: I18n.t(:"admin.jira.columns.last_change") }], - [:added, { caption: I18n.t(:"admin.jira.columns.added") }] + [:added, { caption: I18n.t(:"admin.jira.columns.added") }], + [:server_url, { caption: I18n.t(:"admin.jira.columns.server_url") }] ] end diff --git a/config/locales/en.yml b/config/locales/en.yml index 419b3db5c681..dbb20c24f1e7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1191,6 +1191,7 @@ en: label_ago: "%{amount} ago" last_change: "Last change" projects: "Projects" + server_url: "Server URL" configuration: new: "New configuration" title: "Jira configuration" From 3c990e4d970b1edf4b29bf3873bccd5a2e2ac217 Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 15:12:20 +0200 Subject: [PATCH 03/22] add action menu & column to Jira configuration table --- .../admin/import/jira/row_component.rb | 26 +++++++++++++++++++ .../admin/import/jira/table_component.rb | 4 +++ config/locales/en.yml | 3 +++ 3 files changed, 33 insertions(+) diff --git a/app/components/admin/import/jira/row_component.rb b/app/components/admin/import/jira/row_component.rb index 4e0e32ce9fe6..3a0bacea575f 100644 --- a/app/components/admin/import/jira/row_component.rb +++ b/app/components/admin/import/jira/row_component.rb @@ -53,5 +53,31 @@ def added def server_url model.url end + + def button_links + [ + action_menu + ] + end + + def action_menu + render(Primer::Alpha::ActionMenu.new) do |menu| + menu.with_show_button( + scheme: :invisible, + size: :small, + icon: :"kebab-horizontal", + "aria-label": t(:button_actions), + tooltip_direction: :w + ) + menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.actions.button_open"), + content_arguments: { tag: :a, href: admin_import_jira_path(model) }) do |item| + item.with_leading_visual_icon(icon: :sliders) + end + menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.actions.button_edit"), + content_arguments: { tag: :a, href: edit_admin_import_jira_path(model) }) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + end + end end end diff --git a/app/components/admin/import/jira/table_component.rb b/app/components/admin/import/jira/table_component.rb index 531c1497f9f2..76cf07f256b3 100644 --- a/app/components/admin/import/jira/table_component.rb +++ b/app/components/admin/import/jira/table_component.rb @@ -44,6 +44,10 @@ def has_header? rows.any? end + def has_actions? + true + end + def headers [ [:name, { caption: Import::Jira.human_attribute_name(:name) }], diff --git a/config/locales/en.yml b/config/locales/en.yml index dbb20c24f1e7..87c3ee32ac0c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1165,6 +1165,9 @@ en: jemalloc_allocator: Jemalloc memory allocator jira: + actions: + button_edit: "Edit configuration" + button_open: "Open runs" banner: contribution_callout: > Please, help us improve the Jira Migrator with your feedback and private data donations. You can [join the development community](link) of the Jira Migrator. From 0d997a9a3927f40590b0765cd14a4159e59e09ab Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 16:14:27 +0200 Subject: [PATCH 04/22] Replace Test configuration button with text below PAT input "You can test this configuration by clicking here" and add a Cancel button --- app/forms/admin/import/jira/form.rb | 33 ++++++++++++++----- config/locales/en.yml | 2 +- .../admin/import/jira/instances_spec.rb | 12 +++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/forms/admin/import/jira/form.rb b/app/forms/admin/import/jira/form.rb index daee81f7c8e7..9e008bc42676 100644 --- a/app/forms/admin/import/jira/form.rb +++ b/app/forms/admin/import/jira/form.rb @@ -50,7 +50,7 @@ class Form < ApplicationForm if model.persisted? && model.personal_access_token.present? client_form.html_content do render(Primer::BaseComponent.new(tag: :div, classes: "FormControl")) do - render( + input_wrap = render( Primer::OpenProject::FlexLayout.new( align_items: :flex_end, classes: "FormControl-input-wrap FormControl-input-width--large" @@ -86,6 +86,12 @@ class Form < ApplicationForm ) end end + + caption = render(Primer::BaseComponent.new(tag: :span, classes: "FormControl-caption")) do + test_connection_caption + end + + helpers.safe_join([input_wrap, caption]) end end @@ -103,11 +109,12 @@ class Form < ApplicationForm required: !model.persisted?, input_width: :large, autocomplete: "off", + caption: test_connection_caption, data: { "admin--jira-configuration-form-target": "tokenInput" } ) end - client_form.group(layout: :horizontal) do |button_group| + client_form.group(layout: :horizontal, mt: 1, mb: 2) do |button_group| button_group.submit( name: :submit, label: model.persisted? ? I18n.t("admin.jira.form.button_save") : I18n.t("admin.jira.form.button_add"), @@ -116,16 +123,24 @@ class Form < ApplicationForm ) button_group.button( - name: :test, - label: I18n.t("admin.jira.form.button_test"), + name: :cancel, + label: I18n.t(:button_cancel), scheme: :default, - type: :button, - data: { - "admin--jira-configuration-form-target": "button", - action: "click->admin--jira-configuration-form#testConnection" - } + tag: :a, + href: url_helpers.admin_import_jira_index_path ) end end + + private + + def test_connection_caption + helpers.link_translate( + "admin.jira.form.test_configuration_caption", + links: { test: "#" }, + external: false, + data: { action: "click->admin--jira-configuration-form#testConnection" } + ) + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 87c3ee32ac0c..4e9d116da540 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1209,13 +1209,13 @@ en: button_add: "Add configuration" button_delete_token: "Delete token" button_save: "Save configuration" - button_test: "Test configuration" delete_token_confirm: "Are you sure you want to delete the token? This will disable the Jira connection." fields: name: "Name" personal_access_token: "Personal Access Token" url: "Jira Server/Data Center URL" label_testing: "Testing configuration..." + test_configuration_caption: "You can test this configuration by [clicking here](test)" run: blank: description: "Create an import run to start importing information from this Jira instance" diff --git a/spec/features/admin/import/jira/instances_spec.rb b/spec/features/admin/import/jira/instances_spec.rb index 993dbd18c5f8..067987afe80c 100644 --- a/spec/features/admin/import/jira/instances_spec.rb +++ b/spec/features/admin/import/jira/instances_spec.rb @@ -38,6 +38,17 @@ describe "new form" do before { visit new_admin_import_jira_path } + it "offers testing the configuration via a caption link instead of a button" do + expect(page).to have_no_button("Test configuration") + expect(page).to have_link("clicking here") + end + + it "cancels back to the Jira import overview" do + click_on "Cancel" + + expect(page).to have_current_path(admin_import_jira_index_path) + end + it "does not restore form values when navigating back after a successful creation" do fill_in "Name", with: "My Jira" fill_in "Jira Server/Data Center URL", with: "https://jira.example.com" @@ -88,6 +99,7 @@ it "shows the masked token with a delete button when a token is present" do expect(page).to have_field("Personal Access Token", with: "*********", disabled: true) expect(page).to have_css("[href='/admin/import/jira/9/delete_token']") + expect(page).to have_link("clicking here") end it "deletes the token and shows the token input field" do From 1d00913978edbf86e2a3031f360d550ed0ee5c6a Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 16:37:17 +0200 Subject: [PATCH 05/22] Replace the button "Edit configuration" with an action menu popup --- .../admin/import/jira/instances/show.html.erb | 22 +++++--- .../admin/import/jira/import_runs_spec.rb | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 spec/features/admin/import/jira/import_runs_spec.rb diff --git a/app/views/admin/import/jira/instances/show.html.erb b/app/views/admin/import/jira/instances/show.html.erb index e34bdc1d4902..3661b31c0e9d 100644 --- a/app/views/admin/import/jira/instances/show.html.erb +++ b/app/views/admin/import/jira/instances/show.html.erb @@ -43,12 +43,22 @@ See COPYRIGHT and LICENSE files for more details. header.with_description do I18n.t(:"admin.jira.run.index.description") end - header.with_action_button( - mobile_icon: nil, - mobile_label: nil, - tag: :a, - href: edit_admin_import_jira_path(@jira) - ) { I18n.t(:"admin.jira.run.index.button_edit_configuration") } + header.with_action_menu( + menu_arguments: { anchor_align: :end }, + button_arguments: { + icon: "kebab-horizontal", + "aria-label": I18n.t(:button_actions), + test_selector: "jira-configuration-action-menu" + } + ) do |menu| + menu.with_item( + scheme: :default, + label: I18n.t(:"admin.jira.run.index.button_edit_configuration"), + content_arguments: { tag: :a, href: edit_admin_import_jira_path(@jira) } + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + end end %> diff --git a/spec/features/admin/import/jira/import_runs_spec.rb b/spec/features/admin/import/jira/import_runs_spec.rb new file mode 100644 index 000000000000..7150fdf010e7 --- /dev/null +++ b/spec/features/admin/import/jira/import_runs_spec.rb @@ -0,0 +1,52 @@ +# 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 "Jira import runs page", :js do + shared_let(:admin) { create(:admin) } + let!(:jira) { create(:jira, name: "My Jira") } + + current_user { admin } + + before { visit admin_import_jira_path(jira) } + + describe "header action menu" do + it "opens the edit configuration entry from the actions menu" do + expect(page).to have_no_link("Edit configuration") + + page.find_test_selector("jira-configuration-action-menu").click + + within "anchored-position" do + expect(page).to have_link("Edit configuration", href: edit_admin_import_jira_path(jira)) + end + end + end +end From b7c163e7ef5d2f7eb5ac6c07899f5baf43370f97 Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 16:50:32 +0200 Subject: [PATCH 06/22] add action menu to import runs table, remove status column, place status behind id text --- .../import/jira/import_runs/row_component.rb | 59 ++++++++----------- .../jira/import_runs/table_component.rb | 3 +- config/locales/en.yml | 3 + 3 files changed, 28 insertions(+), 37 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/row_component.rb b/app/components/admin/import/jira/import_runs/row_component.rb index 155a9b093ab7..320d1b3d9aa8 100644 --- a/app/components/admin/import/jira/import_runs/row_component.rb +++ b/app/components/admin/import/jira/import_runs/row_component.rb @@ -34,15 +34,13 @@ def id render( Primer::Beta::Link.new( href: admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - font_weight: :bold + font_weight: :bold, + mr: 1 ) ) do "#{I18n.t('admin.jira.run.title')} ##{model.id}" - end - end - - def status - render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state)) + end + + render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state)) end def last_changed @@ -54,38 +52,29 @@ def projects end def button_links - [edit_button] - # buttons = [] - # buttons.push(remove_button) if model.deletable? - # buttons.push(edit_button) - # buttons - end - - def edit_button - render( - Primer::Beta::IconButton.new( - icon: :pencil, - tag: :a, - href: admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - "aria-label": I18n.t(:button_edit) - ) - ) + [ + action_menu + ] end - def remove_button - render( - Primer::Beta::IconButton.new( - icon: :trash, - scheme: :danger, - tag: :a, - href: remove_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - "aria-label": I18n.t(:button_delete), - data: { - turbo_method: :delete, - turbo_confirm: I18n.t(:text_are_you_sure) - } + def action_menu + render(Primer::Alpha::ActionMenu.new) do |menu| + menu.with_show_button( + scheme: :invisible, + size: :small, + icon: :"kebab-horizontal", + "aria-label": t(:button_actions), + tooltip_direction: :w ) - ) + menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.run.actions.button_edit"), + content_arguments: { tag: :a, href: admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) }) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.run.actions.button_open_history"), + content_arguments: { tag: :a, href: history_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) }) do |item| + item.with_leading_visual_icon(icon: :history) + end + end end end end diff --git a/app/components/admin/import/jira/import_runs/table_component.rb b/app/components/admin/import/jira/import_runs/table_component.rb index 65f07fd072b7..c5655106ed31 100644 --- a/app/components/admin/import/jira/import_runs/table_component.rb +++ b/app/components/admin/import/jira/import_runs/table_component.rb @@ -30,7 +30,7 @@ module Admin::Import::Jira::ImportRuns class TableComponent < OpPrimer::BorderBoxTableComponent - columns :id, :status, :projects, :last_changed + columns :id, :projects, :last_changed def initialize(jira:, **) @jira = jira @@ -56,7 +56,6 @@ def has_header? def headers [ [:id, { caption: I18n.t(:"admin.jira.run.title") }], - [:status, { caption: Import::JiraImport.human_attribute_name(:status) }], [:projects, { caption: I18n.t(:"admin.jira.columns.projects") }], [:last_changed, { caption: I18n.t(:"admin.jira.columns.last_change") }] ] diff --git a/config/locales/en.yml b/config/locales/en.yml index 4e9d116da540..5d591e80b344 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1217,6 +1217,9 @@ en: label_testing: "Testing configuration..." test_configuration_caption: "You can test this configuration by [clicking here](test)" run: + actions: + button_edit: "Edit run" + button_open_history: "Open run history" blank: description: "Create an import run to start importing information from this Jira instance" title: "No import runs set up yet" From 383abc416ac7005ee8a2bb460a070705c38ddaf4 Mon Sep 17 00:00:00 2001 From: as-op Date: Thu, 23 Jul 2026 16:56:41 +0200 Subject: [PATCH 07/22] add creator column to jira import runs table --- .../admin/import/jira/import_runs/row_component.rb | 4 ++++ .../admin/import/jira/import_runs/table_component.rb | 3 ++- config/locales/en.yml | 1 + spec/features/admin/import/jira/import_runs_spec.rb | 10 ++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/components/admin/import/jira/import_runs/row_component.rb b/app/components/admin/import/jira/import_runs/row_component.rb index 320d1b3d9aa8..13667e96bd12 100644 --- a/app/components/admin/import/jira/import_runs/row_component.rb +++ b/app/components/admin/import/jira/import_runs/row_component.rb @@ -43,6 +43,10 @@ def id render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state)) end + def creator + render(Users::AvatarComponent.new(user: model.author, size: :mini, link: true, show_name: true)) + end + def last_changed helpers.format_time(model.updated_at) end diff --git a/app/components/admin/import/jira/import_runs/table_component.rb b/app/components/admin/import/jira/import_runs/table_component.rb index c5655106ed31..b94445fc98fa 100644 --- a/app/components/admin/import/jira/import_runs/table_component.rb +++ b/app/components/admin/import/jira/import_runs/table_component.rb @@ -30,7 +30,7 @@ module Admin::Import::Jira::ImportRuns class TableComponent < OpPrimer::BorderBoxTableComponent - columns :id, :projects, :last_changed + columns :id, :creator, :projects, :last_changed def initialize(jira:, **) @jira = jira @@ -56,6 +56,7 @@ def has_header? def headers [ [:id, { caption: I18n.t(:"admin.jira.run.title") }], + [:creator, { caption: I18n.t(:"admin.jira.columns.creator") }], [:projects, { caption: I18n.t(:"admin.jira.columns.projects") }], [:last_changed, { caption: I18n.t(:"admin.jira.columns.last_change") }] ] diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d591e80b344..aa448c12c15e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1191,6 +1191,7 @@ en: environment variable. columns: added: "Added" + creator: "Creator" label_ago: "%{amount} ago" last_change: "Last change" projects: "Projects" diff --git a/spec/features/admin/import/jira/import_runs_spec.rb b/spec/features/admin/import/jira/import_runs_spec.rb index 7150fdf010e7..e832737fa49b 100644 --- a/spec/features/admin/import/jira/import_runs_spec.rb +++ b/spec/features/admin/import/jira/import_runs_spec.rb @@ -32,7 +32,9 @@ RSpec.describe "Jira import runs page", :js do shared_let(:admin) { create(:admin) } + shared_let(:author) { create(:user, firstname: "Jane", lastname: "Doe") } let!(:jira) { create(:jira, name: "My Jira") } + let!(:jira_import) { create(:jira_import, jira:, author:) } current_user { admin } @@ -49,4 +51,12 @@ end end end + + describe "creator column" do + it "shows the import run author next to the avatar" do + expect(page).to have_text("Creator") + expect(page).to have_text("Jane Doe") + expect(page).to have_css(".op-avatar") + end + end end From 3288006076cd5f22bf2dcc14aa50e905da2abef8 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 09:17:37 +0200 Subject: [PATCH 08/22] Add a "..." button behind the projects in the Projects column which shows all projects --- .../import/jira/import_runs/row_component.rb | 5 ++++- .../admin/import/jira/import_runs_spec.rb | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/row_component.rb b/app/components/admin/import/jira/import_runs/row_component.rb index 13667e96bd12..bc2ce1ed42b5 100644 --- a/app/components/admin/import/jira/import_runs/row_component.rb +++ b/app/components/admin/import/jira/import_runs/row_component.rb @@ -52,7 +52,10 @@ def last_changed end def projects - (model.projects || []).pluck("name").join(", ") + names = (model.projects || []).pluck("name").join(", ") + return names if names.blank? + + render(OpPrimer::ExpandableTextComponent.new(flex: 1)) { names } end def button_links diff --git a/spec/features/admin/import/jira/import_runs_spec.rb b/spec/features/admin/import/jira/import_runs_spec.rb index e832737fa49b..e09715ebcced 100644 --- a/spec/features/admin/import/jira/import_runs_spec.rb +++ b/spec/features/admin/import/jira/import_runs_spec.rb @@ -34,7 +34,12 @@ shared_let(:admin) { create(:admin) } shared_let(:author) { create(:user, firstname: "Jane", lastname: "Doe") } let!(:jira) { create(:jira, name: "My Jira") } - let!(:jira_import) { create(:jira_import, jira:, author:) } + let!(:jira_import) do + create(:jira_import, + jira:, + author:, + projects: [{ "name" => "Project Alpha" }, { "name" => "Project Beta" }]) + end current_user { admin } @@ -59,4 +64,13 @@ expect(page).to have_css(".op-avatar") end end + + describe "projects column" do + it "renders the project list inside an expandable text with an ellipsis expander" do + within "[data-controller='expandable-text']" do + expect(page).to have_text("Project Alpha, Project Beta") + expect(page).to have_button(class: "ellipsis-expander", visible: :all) + end + end + end end From 4fb79b6bc0d3b9150d093fa92a064cfe6de3b363 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 11:03:06 +0200 Subject: [PATCH 09/22] Move header status into new sidepanel * Add a section title: "Run Status" last change date time e.g "23/10/2023 11:15 PM" + badge when the run before review mode: Link clock icon + "Open the run history" + open in new tab icon * Remove the badge and history link * Add hash in front of the run number "Import run #323" --- .../import/jira/import_runs/row_component.rb | 38 ++++++++++++---- .../side_panel/status_component.html.erb | 21 +++++++++ .../side_panel/status_component.rb | 41 ++++++++++++++++++ .../import_runs/side_panel_component.html.erb | 11 +++++ .../jira/import_runs/side_panel_component.rb | 43 +++++++++++++++++++ ...streamable_status_badge_component.html.erb | 5 ++- .../streamable_status_badge_component.rb | 12 +++++- .../jira/import_runs/component_streams.rb | 2 +- .../import/jira/import_runs/show.html.erb | 43 ++++++++----------- config/locales/en.yml | 3 ++ 10 files changed, 180 insertions(+), 39 deletions(-) create mode 100644 app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb create mode 100644 app/components/admin/import/jira/import_runs/side_panel/status_component.rb create mode 100644 app/components/admin/import/jira/import_runs/side_panel_component.html.erb create mode 100644 app/components/admin/import/jira/import_runs/side_panel_component.rb diff --git a/app/components/admin/import/jira/import_runs/row_component.rb b/app/components/admin/import/jira/import_runs/row_component.rb index bc2ce1ed42b5..43a26e9aa97a 100644 --- a/app/components/admin/import/jira/import_runs/row_component.rb +++ b/app/components/admin/import/jira/import_runs/row_component.rb @@ -48,7 +48,15 @@ def creator end def last_changed - helpers.format_time(model.updated_at) + helpers.format_time(last_changed_date) + end + + def last_changed_date + if model.last_transition + model.last_transition.created_at + else + model.updated_at + end end def projects @@ -73,15 +81,27 @@ def action_menu "aria-label": t(:button_actions), tooltip_direction: :w ) - menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.run.actions.button_edit"), - content_arguments: { tag: :a, href: admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) }) do |item| - item.with_leading_visual_icon(icon: :pencil) - end - menu.with_item(scheme: :default, label: I18n.t(:"admin.jira.run.actions.button_open_history"), - content_arguments: { tag: :a, href: history_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) }) do |item| - item.with_leading_visual_icon(icon: :history) - end + add_menu_item(menu, label: :"admin.jira.run.actions.button_edit", icon: :pencil, href: run_path) + add_menu_item(menu, label: :"admin.jira.run.actions.button_open_history", icon: :history, href: run_history_path) end end + + private + + def add_menu_item(menu, label:, icon:, href:) + menu.with_item(scheme: :default, + label: I18n.t(label), + content_arguments: { tag: :a, href: }) do |item| + item.with_leading_visual_icon(icon:) + end + end + + def run_path + admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) + end + + def run_history_path + history_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id) + end end end diff --git a/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb b/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb new file mode 100644 index 000000000000..ba685344bf01 --- /dev/null +++ b/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb @@ -0,0 +1,21 @@ +<%= + render(Primer::OpenProject::SidePanel::Section.new) do |section| + section.with_title { I18n.t(:"admin.jira.run.side_panel.run_status") } + flex_layout do |flex| + flex.with_row do + render(Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(jira_import)) + end + flex.with_row(mt: 3) do + render( + Primer::Beta::Link.new( + href: history_admin_import_jira_run_path(jira_id: jira_import.jira.id, id: jira_import.id), + target: "_blank" + )) do |link| + link.with_leading_visual_icon(icon: :clock) + link.with_trailing_visual_icon(icon: :"link-external") + I18n.t(:"admin.jira.run.side_panel.open_history") + end + end + end + end +%> diff --git a/app/components/admin/import/jira/import_runs/side_panel/status_component.rb b/app/components/admin/import/jira/import_runs/side_panel/status_component.rb new file mode 100644 index 000000000000..b8f4ca06adad --- /dev/null +++ b/app/components/admin/import/jira/import_runs/side_panel/status_component.rb @@ -0,0 +1,41 @@ +# 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 Admin::Import::Jira::ImportRuns + module SidePanel + class StatusComponent < ApplicationComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + alias_method :jira_import, :model + end + end +end diff --git a/app/components/admin/import/jira/import_runs/side_panel_component.html.erb b/app/components/admin/import/jira/import_runs/side_panel_component.html.erb new file mode 100644 index 000000000000..fa17c7c18480 --- /dev/null +++ b/app/components/admin/import/jira/import_runs/side_panel_component.html.erb @@ -0,0 +1,11 @@ +<%= + component_wrapper do + render(Primer::OpenProject::SidePanel.new(spacious: true)) do |panel| + [ + Admin::Import::Jira::ImportRuns::SidePanel::StatusComponent.new(@jira_import) + ].each do |component| + panel.with_section(component) + end + end + end +%> diff --git a/app/components/admin/import/jira/import_runs/side_panel_component.rb b/app/components/admin/import/jira/import_runs/side_panel_component.rb new file mode 100644 index 000000000000..b01f0ece4841 --- /dev/null +++ b/app/components/admin/import/jira/import_runs/side_panel_component.rb @@ -0,0 +1,43 @@ +# 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 Admin::Import::Jira::ImportRuns + class SidePanelComponent < ApplicationComponent + include ApplicationHelper + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + def initialize(jira_import) + super() + + @jira_import = jira_import + end + end +end diff --git a/app/components/admin/import/jira/import_runs/streamable_status_badge_component.html.erb b/app/components/admin/import/jira/import_runs/streamable_status_badge_component.html.erb index 43b609d6dde1..695240248f7f 100644 --- a/app/components/admin/import/jira/import_runs/streamable_status_badge_component.html.erb +++ b/app/components/admin/import/jira/import_runs/streamable_status_badge_component.html.erb @@ -1,5 +1,6 @@ <%= - component_wrapper(tag: :span) do - render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(@status, **@system_arguments)) + component_wrapper(class: "d-flex flex-items-center") do + concat(render(Primer::Beta::Text.new(mr: 2)) { helpers.format_time(last_changed_date) }) + concat(render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(@jira_import.current_state, **@system_arguments))) end %> diff --git a/app/components/admin/import/jira/import_runs/streamable_status_badge_component.rb b/app/components/admin/import/jira/import_runs/streamable_status_badge_component.rb index e5f82cd9abad..b316ef10272c 100644 --- a/app/components/admin/import/jira/import_runs/streamable_status_badge_component.rb +++ b/app/components/admin/import/jira/import_runs/streamable_status_badge_component.rb @@ -32,11 +32,19 @@ module Admin::Import::Jira::ImportRuns class StreamableStatusBadgeComponent < ApplicationComponent include OpTurbo::Streamable - def initialize(status, **system_arguments) + def initialize(jira_import, **system_arguments) super - @status = status + @jira_import = jira_import @system_arguments = system_arguments end + + def last_changed_date + if @jira_import.last_transition + @jira_import.last_transition.created_at + else + @jira_import.updated_at + end + end end end diff --git a/app/controllers/concerns/admin/import/jira/import_runs/component_streams.rb b/app/controllers/concerns/admin/import/jira/import_runs/component_streams.rb index 3eba184f4352..5c3b0328a0cc 100644 --- a/app/controllers/concerns/admin/import/jira/import_runs/component_streams.rb +++ b/app/controllers/concerns/admin/import/jira/import_runs/component_streams.rb @@ -43,7 +43,7 @@ def stream_wizard method: "morph" ) update_via_turbo_stream( - component: ::Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.current_state), + component: ::Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import), method: "morph" ) render turbo_stream: resolve_turbo_streams diff --git a/app/views/admin/import/jira/import_runs/show.html.erb b/app/views/admin/import/jira/import_runs/show.html.erb index 100b42f094a2..e067b305367f 100644 --- a/app/views/admin/import/jira/import_runs/show.html.erb +++ b/app/views/admin/import/jira/import_runs/show.html.erb @@ -30,14 +30,7 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), Import::JiraImport.model_name, @jira_import.id %> <%= render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title do - render(Primer::BaseComponent.new(tag: :span, display: :flex, align_items: :center, style: "line-height: 1")) do - concat(render(Primer::Beta::Text.new(mr: 2)) { - "#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id.to_s}" - }) - concat(render(Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.current_state))) - end - end + header.with_title { "#{I18n.t(:"admin.jira.run.title")} ##{@jira_import.id.to_s}" } header.with_breadcrumbs( [ { href: admin_index_path, text: t(:label_administration) }, @@ -47,25 +40,25 @@ See COPYRIGHT and LICENSE files for more details. "#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id.to_s}" ] ) - header.with_description(underlined_links: false) do - render( - Primer::Beta::Link.new( - href: history_admin_import_jira_run_path(jira_id: @jira_import.jira.id, id: @jira_import.id), - underline: false, - )) do |link| - I18n.t(:"admin.jira.run.history") - end - end end %> + <%= - render Primer::BaseComponent.new( - tag: :div, - data: { - controller: "admin--jira-import", - "admin--jira-import-url-value": continue_admin_import_jira_run_path(jira_id: @jira.id, id: @jira_import.id) - } - ) do - render(Admin::Import::Jira::ImportRuns::WizardComponent.new(@jira_import)) + render(Primer::Alpha::Layout.new(stacking_breakpoint: :md)) do |content| + content.with_main do + render Primer::BaseComponent.new( + tag: :div, + data: { + controller: "admin--jira-import", + "admin--jira-import-url-value": continue_admin_import_jira_run_path(jira_id: @jira.id, id: @jira_import.id) + } + ) do + render(Admin::Import::Jira::ImportRuns::WizardComponent.new(@jira_import)) + end + end + + content.with_sidebar(row_placement: :start, col_placement: :end) do + render(Admin::Import::Jira::ImportRuns::SidePanelComponent.new(@jira_import)) + end end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index aa448c12c15e..6cd3aee86244 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1225,6 +1225,9 @@ en: description: "Create an import run to start importing information from this Jira instance" title: "No import runs set up yet" history: "History" + side_panel: + run_status: "Run status" + open_history: "Open the run history" import_blocked_error: "Another Jira import run is currently in progress or awaiting review. Please complete or revert it before starting a new import." index: button_edit_configuration: "Edit configuration" From 6353786f9be20feccbcda474e8b4d85980d9f509 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 11:11:00 +0200 Subject: [PATCH 10/22] Combine wizard sections "Get base data" + "Configure import" into "Configure import" "Confirm and import" + "Review import" into "Run and review import" --- .../jira/import_runs/wizard_component.html.erb | 16 ++-------------- config/locales/en.yml | 10 ++-------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/wizard_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_component.html.erb index c62d113be3e2..98016e362167 100644 --- a/app/components/admin/import/jira/import_runs/wizard_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_component.html.erb @@ -35,36 +35,24 @@ See COPYRIGHT and LICENSE files for more details. ) do |component| component.with_row(scheme: :neutral, color: :muted, align_items: :center) do render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.groups.fetch.title") + I18n.t(:"admin.jira.run.wizard.groups.configuration.title") } end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepFetchDataComponent.new(model)) end - - component.with_row(scheme: :neutral, color: :muted, align_items: :center) do - render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.groups.configuration.title") - } - end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepImportScopeComponent.new(model)) end component.with_row(scheme: :neutral, color: :muted, align_items: :center) do render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.groups.confirming.title") + I18n.t(:"admin.jira.run.wizard.groups.run_and_review.title") } end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepConfirmImportComponent.new(model)) end - - component.with_row(scheme: :neutral, color: :muted, align_items: :center) do - render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.groups.review.title") - } - end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepReviewComponent.new( model, diff --git a/config/locales/en.yml b/config/locales/en.yml index 6cd3aee86244..56e85cf2d16c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1267,14 +1267,8 @@ en: groups: configuration: title: "Configure import" - confirming: - title: "Confirm and import" - fetch: - title: "Get base data" - groups_and_users: - title: "Groups and Users" - review: - title: "Review import" + run_and_review: + title: "Run and review import" import_dialog: confirm: "I understand and have a backup" confirm_button: "Start import" From a4c5b4208f3e8d35af9bc3ca1df9f41434a4ceb3 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 12:55:07 +0200 Subject: [PATCH 11/22] move the info box into the previous wizard section, update fetch_data section layout --- .../import_runs/info_list_box_component.rb | 9 ++- .../wizard_step_fetch_data_component.html.erb | 77 ++++++++++++++++--- .../wizard_step_fetch_data_component.rb | 49 ++++++++++++ ...izard_step_import_scope_component.html.erb | 54 +++++-------- .../wizard_step_import_scope_component.rb | 48 ------------ config/locales/en.yml | 4 +- 6 files changed, 143 insertions(+), 98 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/info_list_box_component.rb b/app/components/admin/import/jira/import_runs/info_list_box_component.rb index 7a289d5a2c5e..41ac4626eee4 100644 --- a/app/components/admin/import/jira/import_runs/info_list_box_component.rb +++ b/app/components/admin/import/jira/import_runs/info_list_box_component.rb @@ -46,7 +46,14 @@ def initialize(title:, list:, subtitle: nil, show_icon: true, icon: :"dot-fill", end def call - render(OpPrimer::InsetBoxComponent.new(border: false, **system_arguments)) do + render( + Primer::Box.new( + border: true, + border_radius: 2, + p: 4, + **system_arguments + ) + ) do flex_layout do |flex| flex.with_row(mb: 1) do render(Primer::Beta::Text.new(font_weight: :bold)) { title } diff --git a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb index 0352a6e442c9..68a8e943aab6 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb @@ -31,43 +31,96 @@ See COPYRIGHT and LICENSE files for more details. box.with_row do flex_layout do |row| row.with_column(flex: 1) do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title") - }) - if model.status_equal_or_after?(:instance_meta_done) + concat(content_tag(:div, class: "d-flex flex-items-center") do + concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title") + }) + if model.status_equal_or_after?(:configuring) + concat(render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) { + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done") + }) + end + end) + if model.status_equal_or_after?(:configuring) concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done") + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done_description", server_info: model.jira.url) }) end end end end - if model.in_state?(:initial) + if model.status_before?(:configuring) box.with_row(mb: 3) do render(Primer::Beta::Text.new) { I18n.t(:"admin.jira.run.wizard.sections.fetch_data.description") } end + end + if model.in_state?(:initial) box.with_row do - render(Primer::Beta::Button.new( + concat(render(Primer::Beta::Button.new( scheme: :primary, tag: :a, size: :medium, href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'fetch_instance_meta'), data: { turbo_stream: true } - )) do + )) { I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_fetch") - end + }) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + )) { + I18n.t(:button_cancel) + }) end elsif model.in_state?(:instance_meta_error) box.with_row(mt: 3) do render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'fetch_instance_meta')) end elsif model.in_state?(:instance_meta_fetching) - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( + box.with_row do + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium + )) do |button| + button.with_leading_visual_icon(icon: :sync, animation: :rotate) I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_progress") - )) + end) + end + elsif model.in_state?(:instance_meta_done) + box.with_row(mb: 3) do + flex_layout do |flex| + flex.with_column(flex: 1, mr: 1) do + render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_available_server_data", server_info: model.jira.name), + list: import_stats_available, + show_icon: true, + icon: :"check-circle", + icon_color: :success, + h: :full + )) + end + end + end + box.with_row do + concat(render(Primer::Beta::Button.new( + scheme: :primary, + size: :medium, + tag: :a, + href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'configure'), + data: { turbo_stream: true } + )) { + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_continue") + }) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + )) { + I18n.t(:button_cancel) + }) end end end %> diff --git a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.rb b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.rb index 17a9bfdf189e..6a7d89627025 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.rb +++ b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.rb @@ -31,5 +31,54 @@ module Admin::Import::Jira::ImportRuns class WizardStepFetchDataComponent < ApplicationComponent include OpPrimer::ComponentHelpers + include Admin::Import::Jira::ImportRunsHelper + + def server_info + info = model.available["server_info"] + return nil unless info + + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + safe_join([ + info["serverTitle"], + " ", + info["version"], + " ", + render(Primer::Beta::Link.new(href: model.jira.url, target: "_blank")) do |link| + link.with_trailing_visual_icon(icon: :"link-external") + info["baseUrl"] + end + ]) + end + end + + def import_stats_available + [ + projects_label(available_projects_count), + issues_label(available_issues_count), + statuses_label(available_statuses_count), + types_label(available_types_count), + users_label(available_users_count) + ].compact.map { |label| { label:, checked: true } } + end + + def available_projects_count + model.available["projects"]&.count + end + + def available_issues_count + model.available["total_issues"] + end + + def available_statuses_count + model.available["total_statuses"] + end + + def available_types_count + model.available["total_issue_types"] + end + + def available_users_count + model.available["total_users"] + end end end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb index 95d38a053dcc..762418746575 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb @@ -32,7 +32,7 @@ See COPYRIGHT and LICENSE files for more details. concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.title") }) - if model.status_before?(:instance_meta_done) + if model.status_before?(:configuring) concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption") }) @@ -42,38 +42,14 @@ See COPYRIGHT and LICENSE files for more details. }) end end - if model.in_state?(:instance_meta_done) - box.with_row(mb: 3) do - flex_layout do |flex| - flex.with_column(flex: 1, mr: 1) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_available_server_data", server_info: model.jira.name), - subtitle: server_info, - list: import_stats_available, - h: :full - )) - end - end - end - box.with_row do - render(Primer::Beta::Button.new( - scheme: :primary, - size: :medium, - tag: :a, - href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'configure'), - data: { turbo_stream: true } - )) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue") - } - end - elsif model.in_state?(:configuring, :projects_meta_error) - box.with_row(mb: 3, mt: 3) do + if model.in_state?(:configuring, :projects_meta_error) + box.with_row(mt: 1, mb: 3) do render(Primer::Beta::Text.new) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_import") } end - box.with_row(mb: 3) do - render(Primer::Beta::Button.new( + box.with_row(mb: 1) do + concat(render(Primer::Beta::Button.new( size: :medium, tag: :a, href: admin_import_jira_run_select_projects_path(jira_id: model.jira.id, run_id: model.id), @@ -81,11 +57,9 @@ See COPYRIGHT and LICENSE files for more details. )) do |component| component.with_trailing_visual_counter(count: selected_projects_count) I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") - end - end - if model.in_state?(:configuring) - box.with_row do - render(Primer::Beta::Button.new( + end) + if model.in_state?(:configuring) + concat(render(Primer::Beta::Button.new( scheme: :primary, size: :medium, tag: :a, @@ -94,9 +68,17 @@ See COPYRIGHT and LICENSE files for more details. disabled: model.projects.nil? || model.projects.empty? )) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue") - } + }) end - elsif model.in_state?(:projects_meta_error) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + )) { + I18n.t(:button_cancel) + }) + end + if model.in_state?(:projects_meta_error) box.with_row do render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'fetch_projects_meta')) end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.rb b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.rb index 47f998a86547..011055ca94ef 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.rb +++ b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.rb @@ -33,56 +33,8 @@ class WizardStepImportScopeComponent < ApplicationComponent include OpPrimer::ComponentHelpers include Admin::Import::Jira::ImportRunsHelper - def import_stats_available - [ - projects_label(available_projects_count), - issues_label(available_issues_count), - statuses_label(available_statuses_count), - types_label(available_types_count), - users_label(available_users_count) - ].compact.map { |label| { label:, checked: true } } - end - - def server_info - info = model.available["server_info"] - return nil unless info - - render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do - safe_join([ - info["serverTitle"], - " ", - info["version"], - " ", - render(Primer::Beta::Link.new(href: model.jira.url, target: "_blank")) do |link| - link.with_trailing_visual_icon(icon: :"link-external") - info["baseUrl"] - end - ]) - end - end - def selected_projects_count model.projects&.count || 0 end - - def available_projects_count - model.available["projects"]&.count - end - - def available_issues_count - model.available["total_issues"] - end - - def available_statuses_count - model.available["total_statuses"] - end - - def available_types_count - model.available["total_issue_types"] - end - - def available_users_count - model.available["total_users"] - end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 56e85cf2d16c..6e048446edc5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1314,9 +1314,12 @@ en: fetch_data: button_fetch: "Check available data" caption_done: "Completed" + caption_done_description: "Meta data fetched from %{server_info}" description: "Check what data is available for import in the host Jira instance." label_progress: "Fetching data from Jira..." title: "Fetch instance meta data" + button_continue: "Continue to scope selection" + label_available_server_data: "Available data on %{server_info}" groups_and_users: title: "Groups and Users" import_result: @@ -1353,7 +1356,6 @@ en: sprints: "Sprint assignments" users: "Involved users and groups" workflows: "Project-level workflows" - label_available_server_data: "Available data on %{server_info}" label_coming_later: "Coming later" label_coming_soon: "Coming soon (Q3 2026)" label_import: "Select which projects you would like to import." From 383ca7458e442c5489baa51c76acb1447032937d Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 13:03:20 +0200 Subject: [PATCH 12/22] import run table: declare main_column --- app/components/admin/import/jira/import_runs/table_component.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/admin/import/jira/import_runs/table_component.rb b/app/components/admin/import/jira/import_runs/table_component.rb index b94445fc98fa..e25e8df3d0bb 100644 --- a/app/components/admin/import/jira/import_runs/table_component.rb +++ b/app/components/admin/import/jira/import_runs/table_component.rb @@ -31,6 +31,7 @@ module Admin::Import::Jira::ImportRuns class TableComponent < OpPrimer::BorderBoxTableComponent columns :id, :creator, :projects, :last_changed + main_column :id def initialize(jira:, **) @jira = jira From 559847c45226809b1f7445e815160470886fea2a Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 13:28:50 +0200 Subject: [PATCH 13/22] update import_scope section layout --- .../wizard_step_fetch_data_component.html.erb | 2 +- ...izard_step_import_scope_component.html.erb | 32 +++++++++++++++---- config/locales/en.yml | 5 ++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb index 68a8e943aab6..8f8cad7b3fc0 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb @@ -32,7 +32,7 @@ See COPYRIGHT and LICENSE files for more details. flex_layout do |row| row.with_column(flex: 1) do concat(content_tag(:div, class: "d-flex flex-items-center") do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { + concat(render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title") }) if model.status_equal_or_after?(:configuring) diff --git a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb index 762418746575..182f3e499e11 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb @@ -29,16 +29,25 @@ See COPYRIGHT and LICENSE files for more details. <%= flex_layout do |box| box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.title") - }) + + concat(content_tag(:div, class: "d-flex flex-items-center") do + concat(render(Primer::Beta::Text.new(font_weight: :bold)) { + I18n.t(:"admin.jira.run.wizard.sections.import_scope.title") + }) + if model.status_equal_or_after?(:projects_meta_done) + concat(render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) { + I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done") + }) + end + end) + if model.status_before?(:configuring) concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption") }) elsif model.status_equal_or_after?(:projects_meta_done) concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done") + I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done_description", count: selected_projects_count) }) end end @@ -85,9 +94,20 @@ See COPYRIGHT and LICENSE files for more details. end elsif model.in_state?(:projects_meta_fetching) box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( + concat(render(Primer::Beta::Button.new( + size: :medium, + disabled: true + )) do |component| + component.with_trailing_visual_counter(count: selected_projects_count) + I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") + end) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium + )) do |button| + button.with_leading_visual_icon(icon: :sync, animation: :rotate) I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_progress") - )) + end) end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 6e048446edc5..062546a993c6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1342,6 +1342,9 @@ en: button_select_projects: "Select projects to import" caption: "Choose what you want to import into OpenProject" caption_done: "Completed" + caption_done_description: + one: "1 project selected as scope" + other: "%{count} projects selected as scope" description: "Select what data you want to import from the available data fetched from the host Jira instance." elements: custom_fields: "A subset of custom fields" @@ -1360,7 +1363,7 @@ en: label_coming_soon: "Coming soon (Q3 2026)" label_import: "Select which projects you would like to import." label_info: "Please note that this import tool is in beta and cannot import all types of data. Here is a summary of what the host Jira instance offers for import and what this tool is able to import right now." - label_progress: "Fetching data from Jira..." + label_progress: "Fetching data" label_selected_data: "Selected data for import" label_supported_data: "Supported data" title: "Import scope" From c766a0bd77d83c1ab4424ec4dca2f316e4c92a7a Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 13:34:43 +0200 Subject: [PATCH 14/22] update confirm_import section layout --- .../wizard_step_confirm_import_component.html.erb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb index 9eab87083f7b..62d180fc5057 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb @@ -52,18 +52,27 @@ See COPYRIGHT and LICENSE files for more details. render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( title: I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_available_data"), list: import_selection, - show_icon: true + show_icon: true, + icon: :"check-circle", + icon_color: :success )) end if model.in_state?(:projects_meta_done) box.with_row do - render(Primer::Beta::Button.new( + concat(render(Primer::Beta::Button.new( scheme: :primary, tag: :a, size: :medium, href: import_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), data: { controller: "async-dialog" } - )) { I18n.t(:"admin.jira.run.wizard.sections.confirm_import.button_start") } + )) { I18n.t(:"admin.jira.run.wizard.sections.confirm_import.button_start") }) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + )) { + I18n.t(:button_cancel) + }) end elsif model.in_state?(:import_error) box.with_row do From 801ee0e9f2b4faf2fcbd14d38e3132250b85ea69 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 14:51:59 +0200 Subject: [PATCH 15/22] update select projects layout * Move filter input and actions into modal body * add blue info dialog "Only projects where the Jira token owner is member are selectable as import scope." * Number button text: "Select" instead of "Continue" * "Select all" and "Unselect all" instead of "Check all" and "Uncheck all" --- .../list_header_component.html.erb | 10 +++---- .../select_projects/modal_component.html.erb | 26 ++++++++----------- config/locales/en.yml | 9 ++++--- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb b/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb index b2fce231c382..df6f9f5355d5 100644 --- a/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb +++ b/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb @@ -1,12 +1,12 @@ <%= component_wrapper do - flex_layout(align_items: :center, mt: 2, flex_wrap: :wrap) do |flex| + flex_layout(align_items: :center, mb: 2, flex_wrap: :wrap) do |flex| flex.with_column(flex: 1) do render( Primer::Alpha::TextField.new( name: :filter, value: filter, - label: I18n.t(:"admin.jira.run.wizard.select_dialog.filter_projects"), - placeholder: I18n.t(:"admin.jira.run.wizard.select_dialog.filter_projects"), + label: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), + placeholder: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), visually_hide_label: true, input_width: :large, style: "min-width: 250px;", @@ -25,7 +25,7 @@ ) ) do |button| button.with_leading_visual_icon(icon: :"check-circle") - I18n.t(:button_check_all) + I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") end end flex.with_column do @@ -38,7 +38,7 @@ ) ) do |button| button.with_leading_visual_icon(icon: :"x-circle") - I18n.t(:button_uncheck_all) + I18n.t(:"admin.jira.run.wizard.select_projects.button_unselect_all") end end end diff --git a/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb b/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb index bbc026ef1dbd..01a4648ceeef 100644 --- a/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb +++ b/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb @@ -11,20 +11,16 @@ "keep-open-on-submit": true } ) - ) do |d| - d.with_header( - show_divider: true, - variant: :medium - ) do - concat(render(Primer::Beta::Text.new(font_weight: :bold)) do - I18n.t(:"admin.jira.run.wizard.select_projects.title") - end) + ) do |dialog| + dialog.with_header(show_divider: true) + dialog.with_body(classes: "Overlay-body_autocomplete_height") do + concat(render(Primer::Alpha::Banner.new(scheme: :default, icon: :info, mb: 3)) { + I18n.t(:"admin.jira.run.wizard.select_projects.info") + }) concat(render(list_header_component)) - end - d.with_body(classes: "Overlay-body_autocomplete_height") do concat(render(list_component)) end - d.with_footer( + dialog.with_footer( show_divider: true ) do component_collection do |modal_footer| @@ -41,9 +37,9 @@ modal_footer.with_component( content_tag(:div, data: { "admin--jira-projects-target": "submitButton" }) { render(Admin::Import::Jira::ImportRuns::SelectProjects::ModalSubmitComponent.new( - jira_import: @jira_import, - count: selected_count - )) + jira_import: @jira_import, + count: selected_count + )) } ) modal_footer.with_component( @@ -55,7 +51,7 @@ ) ) do |spinner_button| spinner_button.with_trailing_visual_icon(icon: :sync, animation: :rotate, style: "min-width: 2rem") - I18n.t(:button_continue) + I18n.t(:"admin.jira.run.wizard.select_projects.button_select") end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 062546a993c6..51ab99f46ff8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1367,10 +1367,13 @@ en: label_selected_data: "Selected data for import" label_supported_data: "Supported data" title: "Import scope" - select_dialog: - filter_projects: "Filter by text" select_projects: - title: "Select projects" + button_select: "Select" + button_select_all: "Select all" + button_unselect_all: "Unselect all" + filter_projects: "Filter by text" + info: "Only projects where the Jira token owner is member are selectable as import scope." + title: "Select projects to import" test: api_error: "Jira API returned error status %{status}. Please check your Jira instance URL and API token." connection_error: "Connection error: %{message}" From fec57118e0326d3e66bdecef6b64fe98540431b1 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 15:24:33 +0200 Subject: [PATCH 16/22] refactor SelectProjectsDialog controller concerns * clicking "Continue to scope selection" moves to next section AND automatically opens the project selection dialog --- .../import_runs/select_projects_controller.rb | 97 +----------- .../import/jira/import_runs_controller.rb | 8 +- .../import_runs/select_projects_dialog.rb | 138 ++++++++++++++++++ .../jira/import_runs_controller_spec.rb | 24 ++- 4 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 app/controllers/concerns/admin/import/jira/import_runs/select_projects_dialog.rb diff --git a/app/controllers/admin/import/jira/import_runs/select_projects_controller.rb b/app/controllers/admin/import/jira/import_runs/select_projects_controller.rb index 950f0e17ff85..fc32b0478671 100644 --- a/app/controllers/admin/import/jira/import_runs/select_projects_controller.rb +++ b/app/controllers/admin/import/jira/import_runs/select_projects_controller.rb @@ -32,22 +32,13 @@ module Admin::Import::Jira::ImportRuns class SelectProjectsController < ApplicationController include OpTurbo::ComponentStream include ComponentStreams + include SelectProjectsDialog before_action :require_admin before_action :find_jira_and_jira_import - PER_PAGE = 20 - def show - respond_with_dialog( - Admin::Import::Jira::ImportRuns::SelectProjects::ModalComponent.new( - jira_import: @jira_import, - list_header_component: project_list_header_component, - list_component: project_list_component, - list_footer_component: project_list_footer_component, - selected_count: selected_ids.size - ) - ) + respond_with_dialog(select_projects_modal_component) end def update @@ -108,12 +99,6 @@ def find_jira_and_jira_import init_session if action_name == "show" end - def init_session - session[:selected_ids] = @jira_import.project_ids - session[:project_page] = 1 - session[:project_filter] = nil - end - def respond_with_modal_components(with_footer: false) respond_to do |format| format.turbo_stream do @@ -147,83 +132,5 @@ def respond_with_counter_component end end end - - def set_page(page) - session[:project_page] = page - end - - def set_filter(filter) - session[:project_filter] = filter.blank? ? nil : filter.to_s.strip - end - - def set_selection_ids(new_selections) - session[:selected_ids] = new_selections.map(&:to_s).compact_blank.uniq - end - - def selected_ids - session[:selected_ids] || [] - end - - def project_filter - session[:project_filter] - end - - def page - [[(session[:project_page] || 1).to_i, 1].compact.max, total_pages].min - end - - def total_pages - (filtered_projects.size.to_f / PER_PAGE).ceil - end - - def available_projects - @jira_import.available&.dig("projects") || [] - end - - def filtered_projects - return available_projects if project_filter.blank? - - query = project_filter.downcase - available_projects.select do |project| - project["name"].to_s.downcase.include?(query) || - project["key"].to_s.downcase.include?(query) - end - end - - def paginated_projects - start_index = (page - 1) * PER_PAGE - filtered_projects.slice(start_index, PER_PAGE) || [] - end - - def project_list_header_component - Admin::Import::Jira::ImportRuns::SelectProjects::ListHeaderComponent.new( - jira_import: @jira_import, - filter: project_filter - ) - end - - def project_list_component - Admin::Import::Jira::ImportRuns::SelectProjects::ListComponent.new( - jira_import: @jira_import, - selected_ids: selected_ids, - projects: paginated_projects - ) - end - - def project_list_footer_component - Admin::Import::Jira::ImportRuns::SelectProjects::ListFooterComponent.new( - jira_import: @jira_import, - page: page, - total_pages: total_pages - ) - end - - def project_list_counter_component - count = selected_ids.count - Admin::Import::Jira::ImportRuns::SelectProjects::ModalSubmitComponent.new( - jira_import: @jira_import, - count: - ) - end end end diff --git a/app/controllers/admin/import/jira/import_runs_controller.rb b/app/controllers/admin/import/jira/import_runs_controller.rb index be29a0482a45..2cd58eba75f3 100644 --- a/app/controllers/admin/import/jira/import_runs_controller.rb +++ b/app/controllers/admin/import/jira/import_runs_controller.rb @@ -32,6 +32,7 @@ module Admin::Import::Jira class ImportRunsController < ApplicationController include OpTurbo::ComponentStream include ImportRuns::ComponentStreams + include ImportRuns::SelectProjectsDialog layout "admin" @@ -58,8 +59,11 @@ def create end def continue - change_step(params[:step]) unless @jira_import.status_running? - stream_wizard + step = params[:step] + change_step(step) unless @jira_import.status_running? + stream_wizard do + open_select_projects_dialog if step.present? && step.to_sym == :configure && @jira_import.in_state?(:configuring) + end rescue StandardError => e handle_error(e) end diff --git a/app/controllers/concerns/admin/import/jira/import_runs/select_projects_dialog.rb b/app/controllers/concerns/admin/import/jira/import_runs/select_projects_dialog.rb new file mode 100644 index 000000000000..e53562ed0764 --- /dev/null +++ b/app/controllers/concerns/admin/import/jira/import_runs/select_projects_dialog.rb @@ -0,0 +1,138 @@ +# 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 Admin::Import::Jira::ImportRuns + module SelectProjectsDialog + extend ActiveSupport::Concern + + PER_PAGE = 20 + + private + + def open_select_projects_dialog + init_session + dialog_via_turbo_stream(component: select_projects_modal_component) + end + + def select_projects_modal_component + Admin::Import::Jira::ImportRuns::SelectProjects::ModalComponent.new( + jira_import: @jira_import, + list_header_component: project_list_header_component, + list_component: project_list_component, + list_footer_component: project_list_footer_component, + selected_count: selected_ids.size + ) + end + + def init_session + session[:selected_ids] = @jira_import.project_ids + session[:project_page] = 1 + session[:project_filter] = nil + end + + def set_page(page) + session[:project_page] = page + end + + def set_filter(filter) + session[:project_filter] = filter.blank? ? nil : filter.to_s.strip + end + + def set_selection_ids(new_selections) + session[:selected_ids] = new_selections.map(&:to_s).compact_blank.uniq + end + + def selected_ids + session[:selected_ids] || [] + end + + def project_filter + session[:project_filter] + end + + def page + [[(session[:project_page] || 1).to_i, 1].compact.max, total_pages].min + end + + def total_pages + (filtered_projects.size.to_f / PER_PAGE).ceil + end + + def available_projects + @jira_import.available&.dig("projects") || [] + end + + def filtered_projects + return available_projects if project_filter.blank? + + query = project_filter.downcase + available_projects.select do |project| + project["name"].to_s.downcase.include?(query) || + project["key"].to_s.downcase.include?(query) + end + end + + def paginated_projects + start_index = (page - 1) * PER_PAGE + filtered_projects.slice(start_index, PER_PAGE) || [] + end + + def project_list_header_component + Admin::Import::Jira::ImportRuns::SelectProjects::ListHeaderComponent.new( + jira_import: @jira_import, + filter: project_filter + ) + end + + def project_list_component + Admin::Import::Jira::ImportRuns::SelectProjects::ListComponent.new( + jira_import: @jira_import, + selected_ids: selected_ids, + projects: paginated_projects + ) + end + + def project_list_footer_component + Admin::Import::Jira::ImportRuns::SelectProjects::ListFooterComponent.new( + jira_import: @jira_import, + page: page, + total_pages: total_pages + ) + end + + def project_list_counter_component + count = selected_ids.count + Admin::Import::Jira::ImportRuns::SelectProjects::ModalSubmitComponent.new( + jira_import: @jira_import, + count: + ) + end + end +end diff --git a/spec/controllers/admin/import/jira/import_runs_controller_spec.rb b/spec/controllers/admin/import/jira/import_runs_controller_spec.rb index 14cbd00d75db..6d38b432e8cd 100644 --- a/spec/controllers/admin/import/jira/import_runs_controller_spec.rb +++ b/spec/controllers/admin/import/jira/import_runs_controller_spec.rb @@ -168,12 +168,29 @@ def transition_to_state(jira_import, target_state) end context "when step is configure" do - before { transition_to_state(jira_import, "instance_meta_done") } + before do + transition_to_state(jira_import, "instance_meta_done") + jira_import.update!(available: { "projects" => [{ "id" => "10001", "name" => "Project One", "key" => "PROJ1" }] }) + end it "transitions to configuring" do post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "configure" }, format: :turbo_stream expect(jira_import.current_state).to eq("configuring") end + + it "opens the select projects dialog in the same response" do + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "configure" }, format: :turbo_stream + expect(response.media_type).to eq("text/vnd.turbo-stream.html") + expect(response.body).to include(Admin::Import::Jira::ImportRuns::SelectProjects::ModalComponent::MODAL_ID) + end + + it "initializes the select projects session" do + jira_import.update!(projects: [{ "id" => "10001", "name" => "Project One", "key" => "PROJ1" }]) + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "configure" }, format: :turbo_stream + expect(session[:selected_ids]).to eq(%w[10001]) + expect(session[:project_page]).to eq(1) + expect(session[:project_filter]).to be_nil + end end context "when step is fetch_projects_meta" do @@ -184,6 +201,11 @@ def transition_to_state(jira_import, target_state) expect(jira_import.current_state).to eq("projects_meta_fetching") expect(Import::JiraProjectsMetaDataJob).to have_received(:perform_later).with(jira_import.id) end + + it "does not open the select projects dialog" do + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "fetch_projects_meta" }, format: :turbo_stream + expect(response.body).not_to include(Admin::Import::Jira::ImportRuns::SelectProjects::ModalComponent::MODAL_ID) + end end context "when step is fetch_projects_meta from projects_meta_error" do From 066cd52f601ce6b5e2c6b31c7266228a91492cb2 Mon Sep 17 00:00:00 2001 From: as-op Date: Mon, 27 Jul 2026 15:49:29 +0200 Subject: [PATCH 17/22] update review section layout --- .../wizard_step_review_component.html.erb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb index f835d8c539c3..bea2fbed913c 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb @@ -85,6 +85,10 @@ See COPYRIGHT and LICENSE files for more details. concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { I18n.t(:"admin.jira.run.wizard.sections.import_result.caption") }) + elsif model.in_state?(:imported) + concat(render(Primer::Beta::Text.new(mt: 1, tag: :div)) { + I18n.t(:"admin.jira.run.wizard.sections.import_result.preview_description") + }) end end if model.in_state?(:imported) @@ -104,11 +108,6 @@ See COPYRIGHT and LICENSE files for more details. end end if model.in_state?(:imported) - box.with_row(mb: 3) do - render(Primer::Alpha::Banner.new(scheme: :default, icon: :info)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.preview_description") - } - end box.with_row do concat(render(Primer::Beta::Button.new( tag: :a, @@ -122,9 +121,17 @@ See COPYRIGHT and LICENSE files for more details. scheme: :default, tag: :a, size: :medium, + color: :danger, href: revert_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), data: { controller: "async-dialog" } )) { I18n.t(:"admin.jira.run.wizard.sections.import_result.button_revert") }) + concat(render(Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + )) { + I18n.t(:button_cancel) + }) end end end From 21a041bfa45c02bf39770c435c6d488c840afa52 Mon Sep 17 00:00:00 2001 From: as-op Date: Tue, 28 Jul 2026 11:51:15 +0200 Subject: [PATCH 18/22] Add import status component --- .../imported_stats_banner_component.html.erb | 12 +++ .../imported_stats_banner_component.rb | 45 +++++++++++ .../wizard_step_review_component.html.erb | 14 +--- .../wizard_step_review_component.rb | 30 ++++++- config/locales/en.yml | 15 +++- .../imported_stats_banner_component_spec.rb | 81 +++++++++++++++++++ 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb create mode 100644 app/components/admin/import/jira/import_runs/imported_stats_banner_component.rb create mode 100644 spec/components/admin/import/jira/import_runs/imported_stats_banner_component_spec.rb diff --git a/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb new file mode 100644 index 000000000000..4826bc7bb0b3 --- /dev/null +++ b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb @@ -0,0 +1,12 @@ +<%= render(Primer::Box.new(bg: :success, border: true, border_color: :success, border_radius: 3, p: 4)) do + concat(render(Primer::Box.new(display: :flex, justify_content: :space_between, align_items: :center, mb: 3)) do + concat(render(Primer::Beta::Heading.new(tag: :h3, font_size: 3)) { title }) + concat(action) if action? + end) + concat(render(Primer::Box.new(display: :flex, flex_wrap: :wrap, classes: "gap-3")) do + stats.each do |stat| + card = render(Users::WorkingHours::StatCardComponent.new(label: stat[:label], value: stat[:value], subtitle: stat[:subtitle])) + concat(stat[:url].present? ? render(Primer::Beta::Link.new(href: stat[:url], target: "_blank", underline: false, display: :flex, flex: 1)) { card } : card) + end + end) +end %> diff --git a/app/components/admin/import/jira/import_runs/imported_stats_banner_component.rb b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.rb new file mode 100644 index 000000000000..f71b99544c2a --- /dev/null +++ b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.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 Admin::Import::Jira::ImportRuns + class ImportedStatsBannerComponent < ApplicationComponent + renders_one :action + + attr_reader :title, :stats + + # stats: an array of { label:, value:, subtitle:, url: } hashes, one per card. + # url is optional; when present the whole card links to it. + def initialize(title:, stats:, **) + super(nil, **) + @title = title + @stats = Array(stats) + end + end +end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb index bea2fbed913c..806b0012c172 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb @@ -91,19 +91,11 @@ See COPYRIGHT and LICENSE files for more details. }) end end - if model.in_state?(:imported) - box.with_row(mt: 3) do - render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.info") - } - end - end if model.in_state?(:imported, :finalizing_done) box.with_row(mt: 3, mb: 3) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_result.label_results"), - list: imported_data, - show_icon: false + render(Admin::Import::Jira::ImportRuns::ImportedStatsBannerComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.import_result.stats.title"), + stats: imported_stats )) end end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_review_component.rb b/app/components/admin/import/jira/import_runs/wizard_step_review_component.rb index d2e123e61fbc..99869e14279f 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_review_component.rb +++ b/app/components/admin/import/jira/import_runs/wizard_step_review_component.rb @@ -33,13 +33,27 @@ class WizardStepReviewComponent < ApplicationComponent include OpPrimer::ComponentHelpers include Admin::Import::Jira::ImportRunsHelper - def imported_data + def imported_stats [ - { label: projects_label(imported_projects.count), checked: true, url: imported_projects_url }, - { label: work_packages_label(imported_work_packages.count), checked: true, url: imported_work_packages_url } + stat_entry(:projects, imported_projects.count, url: imported_projects_url), + stat_entry(:work_packages, imported_work_packages.count, url: imported_work_packages_url), + stat_entry(:users_and_groups, imported_users_and_groups.count), + stat_entry(:other_settings, imported_other_settings.count) ].compact end + def stat_entry(key, value, url: nil) + return if value.zero? + + scope = "admin.jira.run.wizard.sections.import_result.stats.#{key}" + { + label: I18n.t(:"#{scope}.label"), + value:, + subtitle: I18n.t(:"#{scope}.subtitle"), + url: + } + end + def imported_projects @imported_projects ||= Import::JiraOpenProjectReference .where(jira_import: model, op_entity_class: "Project", uses_existing: false) @@ -72,5 +86,15 @@ def imported_work_packages_url helpers.work_packages_path(query_props: { f: [{ n: "project", o: "=", v: project_ids }] }.to_json) end end + + def imported_users_and_groups + @imported_users_and_groups ||= Import::JiraOpenProjectReference + .where(jira_import: model, op_entity_class: %w[User Group], uses_existing: false) + end + + def imported_other_settings + @imported_other_settings ||= Import::JiraOpenProjectReference + .where(jira_import: model, op_entity_class: %w[Type Status IssuePriority WorkPackageCustomField], uses_existing: false) + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 51ab99f46ff8..114cfcf44425 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1326,7 +1326,6 @@ en: button_done: "Approve import" button_revert: "Revert import" caption: "Review import run or revert import" - info: "Import run was successful." label_finalize_import: "Approve import" label_finalizing: "Approving import..." label_finalizing_done: "Import approved." @@ -1335,6 +1334,20 @@ en: label_revert_progress: "Reverting import..." label_reverted: "Import reverted." preview_description: 'The imported data is currently in review mode. Click "Approve import" to make the import permanent or "Revert import" to undo all changes made in this import run.' + stats: + other_settings: + label: "Other settings" + subtitle: "Including types and status" + projects: + label: "Projects" + subtitle: "Selected as scope" + title: "Data imported" + users_and_groups: + label: "Users & groups" + subtitle: "Involved in imported projects " + work_packages: + label: "Work packages" + subtitle: "With attachments" title: "Import run results" import_scope: button_continue: "Continue" diff --git a/spec/components/admin/import/jira/import_runs/imported_stats_banner_component_spec.rb b/spec/components/admin/import/jira/import_runs/imported_stats_banner_component_spec.rb new file mode 100644 index 000000000000..cfdeecc696a3 --- /dev/null +++ b/spec/components/admin/import/jira/import_runs/imported_stats_banner_component_spec.rb @@ -0,0 +1,81 @@ +# 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 "rails_helper" + +RSpec.describe Admin::Import::Jira::ImportRuns::ImportedStatsBannerComponent, type: :component do + let(:stats) do + [ + { label: "Projects", value: 3, subtitle: "Imported into OpenProject" }, + { label: "Work packages", value: 2748, subtitle: "With attachments" } + ] + end + + it "renders the title on a success-colored panel" do + render_inline(described_class.new(title: "Data imported", stats:)) + + expect(page).to have_css(".color-bg-success") + expect(page).to have_css("h3", text: "Data imported") + end + + it "renders one stat card per entry with label, value and subtitle" do + render_inline(described_class.new(title: "Data imported", stats:)) + + expect(page).to have_text("Projects") + expect(page).to have_text("3") + expect(page).to have_text("Work packages") + expect(page).to have_text("2748") + expect(page).to have_text("With attachments") + end + + it "links a tile to its url, opening in a new tab, when url is present" do + linked_stats = [ + { label: "Projects", value: 3, subtitle: "Imported into OpenProject", url: "/projects" }, + { label: "Work packages", value: 2748, subtitle: "With attachments" } + ] + render_inline(described_class.new(title: "Data imported", stats: linked_stats)) + + expect(page).to have_css('a[href="/projects"][target="_blank"]', text: "Projects") + expect(page).to have_no_css("a", text: "Work packages") + end + + it "does not link tiles when url is blank" do + render_inline(described_class.new(title: "Data imported", stats:)) + + expect(page).to have_no_css("a") + end + + it "renders no stat cards when stats is empty" do + render_inline(described_class.new(title: "Data imported", stats: [])) + + expect(page).to have_css("h3", text: "Data imported") + expect(page).to have_no_text("Projects") + end +end From 3a3cce4a0ff726693e8cbea409b7752c4ccf2117 Mon Sep 17 00:00:00 2001 From: as-op Date: Tue, 28 Jul 2026 12:12:23 +0200 Subject: [PATCH 19/22] obey eslint --- .../imported_stats_banner_component.html.erb | 26 +- .../list_header_component.html.erb | 86 +++---- .../select_projects/modal_component.html.erb | 22 +- .../side_panel/status_component.html.erb | 3 +- .../import_runs/side_panel_component.html.erb | 2 +- .../import_runs/wizard_component.html.erb | 25 +- ...ard_step_confirm_import_component.html.erb | 146 ++++++----- .../wizard_step_fetch_data_component.html.erb | 212 +++++++++------- ...izard_step_import_scope_component.html.erb | 190 ++++++++------- .../wizard_step_review_component.html.erb | 226 ++++++++++-------- .../import/jira/import_runs/show.html.erb | 4 +- .../import/jira/instances/index.html.erb | 135 ++++++----- 12 files changed, 608 insertions(+), 469 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb index 4826bc7bb0b3..3666762d34a4 100644 --- a/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb +++ b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb @@ -1,12 +1,16 @@ <%= render(Primer::Box.new(bg: :success, border: true, border_color: :success, border_radius: 3, p: 4)) do - concat(render(Primer::Box.new(display: :flex, justify_content: :space_between, align_items: :center, mb: 3)) do - concat(render(Primer::Beta::Heading.new(tag: :h3, font_size: 3)) { title }) - concat(action) if action? - end) - concat(render(Primer::Box.new(display: :flex, flex_wrap: :wrap, classes: "gap-3")) do - stats.each do |stat| - card = render(Users::WorkingHours::StatCardComponent.new(label: stat[:label], value: stat[:value], subtitle: stat[:subtitle])) - concat(stat[:url].present? ? render(Primer::Beta::Link.new(href: stat[:url], target: "_blank", underline: false, display: :flex, flex: 1)) { card } : card) - end - end) -end %> + concat( + render(Primer::Box.new(display: :flex, justify_content: :space_between, align_items: :center, mb: 3)) do + concat(render(Primer::Beta::Heading.new(tag: :h3, font_size: 3)) { title }) + concat(action) if action? + end + ) + concat( + render(Primer::Box.new(display: :flex, flex_wrap: :wrap, classes: "gap-3")) do + stats.each do |stat| + card = render(Users::WorkingHours::StatCardComponent.new(label: stat[:label], value: stat[:value], subtitle: stat[:subtitle])) + concat(stat[:url].present? ? render(Primer::Beta::Link.new(href: stat[:url], target: "_blank", underline: false, display: :flex, flex: 1)) { card } : card) + end + end + ) + end %> diff --git a/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb b/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb index df6f9f5355d5..de5558b5c6d9 100644 --- a/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb +++ b/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb @@ -1,45 +1,45 @@ <%= component_wrapper do - flex_layout(align_items: :center, mb: 2, flex_wrap: :wrap) do |flex| - flex.with_column(flex: 1) do - render( - Primer::Alpha::TextField.new( - name: :filter, - value: filter, - label: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), - placeholder: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), - visually_hide_label: true, - input_width: :large, - style: "min-width: 250px;", - leading_visual: { icon: :search }, - data: { action: "input->admin--jira-projects#filterProjects" } - ) - ) - end - flex.with_column do - render( - Primer::Beta::Button.new( - scheme: :invisible, - tag: :a, - href: check_all_url, - data: { action: "click->admin--jira-projects#checkAll" } - ) - ) do |button| - button.with_leading_visual_icon(icon: :"check-circle") - I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") + flex_layout(align_items: :center, mb: 2, flex_wrap: :wrap) do |flex| + flex.with_column(flex: 1) do + render( + Primer::Alpha::TextField.new( + name: :filter, + value: filter, + label: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), + placeholder: I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects"), + visually_hide_label: true, + input_width: :large, + style: "min-width: 250px;", + leading_visual: { icon: :search }, + data: { action: "input->admin--jira-projects#filterProjects" } + ) + ) + end + flex.with_column do + render( + Primer::Beta::Button.new( + scheme: :invisible, + tag: :a, + href: check_all_url, + data: { action: "click->admin--jira-projects#checkAll" } + ) + ) do |button| + button.with_leading_visual_icon(icon: :"check-circle") + I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") + end + end + flex.with_column do + render( + Primer::Beta::Button.new( + scheme: :invisible, + tag: :a, + href: uncheck_all_url, + data: { action: "click->admin--jira-projects#uncheckAll" } + ) + ) do |button| + button.with_leading_visual_icon(icon: :"x-circle") + I18n.t(:"admin.jira.run.wizard.select_projects.button_unselect_all") + end + end end - end - flex.with_column do - render( - Primer::Beta::Button.new( - scheme: :invisible, - tag: :a, - href: uncheck_all_url, - data: { action: "click->admin--jira-projects#uncheckAll" } - ) - ) do |button| - button.with_leading_visual_icon(icon: :"x-circle") - I18n.t(:"admin.jira.run.wizard.select_projects.button_unselect_all") - end - end - end -end %> + end %> diff --git a/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb b/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb index 01a4648ceeef..38893c56ccdb 100644 --- a/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb +++ b/app/components/admin/import/jira/import_runs/select_projects/modal_component.html.erb @@ -14,9 +14,11 @@ ) do |dialog| dialog.with_header(show_divider: true) dialog.with_body(classes: "Overlay-body_autocomplete_height") do - concat(render(Primer::Alpha::Banner.new(scheme: :default, icon: :info, mb: 3)) { - I18n.t(:"admin.jira.run.wizard.select_projects.info") - }) + concat( + render(Primer::Alpha::Banner.new(scheme: :default, icon: :info, mb: 3)) do + I18n.t(:"admin.jira.run.wizard.select_projects.info") + end + ) concat(render(list_header_component)) concat(render(list_component)) end @@ -35,12 +37,14 @@ I18n.t(:button_cancel) end modal_footer.with_component( - content_tag(:div, data: { "admin--jira-projects-target": "submitButton" }) { - render(Admin::Import::Jira::ImportRuns::SelectProjects::ModalSubmitComponent.new( - jira_import: @jira_import, - count: selected_count - )) - } + content_tag(:div, data: { "admin--jira-projects-target": "submitButton" }) do + render( + Admin::Import::Jira::ImportRuns::SelectProjects::ModalSubmitComponent.new( + jira_import: @jira_import, + count: selected_count + ) + ) + end ) modal_footer.with_component( Primer::Beta::Button.new( diff --git a/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb b/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb index ba685344bf01..f4c7743cd68e 100644 --- a/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb +++ b/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb @@ -10,7 +10,8 @@ Primer::Beta::Link.new( href: history_admin_import_jira_run_path(jira_id: jira_import.jira.id, id: jira_import.id), target: "_blank" - )) do |link| + ) + ) do |link| link.with_leading_visual_icon(icon: :clock) link.with_trailing_visual_icon(icon: :"link-external") I18n.t(:"admin.jira.run.side_panel.open_history") diff --git a/app/components/admin/import/jira/import_runs/side_panel_component.html.erb b/app/components/admin/import/jira/import_runs/side_panel_component.html.erb index fa17c7c18480..efcfb1bbc5a6 100644 --- a/app/components/admin/import/jira/import_runs/side_panel_component.html.erb +++ b/app/components/admin/import/jira/import_runs/side_panel_component.html.erb @@ -3,7 +3,7 @@ render(Primer::OpenProject::SidePanel.new(spacious: true)) do |panel| [ Admin::Import::Jira::ImportRuns::SidePanel::StatusComponent.new(@jira_import) - ].each do |component| + ].each do |component| panel.with_section(component) end end diff --git a/app/components/admin/import/jira/import_runs/wizard_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_component.html.erb index 98016e362167..3aeb206837db 100644 --- a/app/components/admin/import/jira/import_runs/wizard_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_component.html.erb @@ -29,14 +29,15 @@ See COPYRIGHT and LICENSE files for more details. <%= component_wrapper do - render(border_box_container( - data: { "admin--jira-import-target": model.status_running? ? 'poll' : "finished" } - ) + render( + border_box_container( + data: { "admin--jira-import-target": model.status_running? ? "poll" : "finished" } + ) ) do |component| component.with_row(scheme: :neutral, color: :muted, align_items: :center) do - render(Primer::Beta::Text.new(font_weight: :bold)) { + render(Primer::Beta::Text.new(font_weight: :bold)) do I18n.t(:"admin.jira.run.wizard.groups.configuration.title") - } + end end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepFetchDataComponent.new(model)) @@ -46,18 +47,20 @@ See COPYRIGHT and LICENSE files for more details. end component.with_row(scheme: :neutral, color: :muted, align_items: :center) do - render(Primer::Beta::Text.new(font_weight: :bold)) { + render(Primer::Beta::Text.new(font_weight: :bold)) do I18n.t(:"admin.jira.run.wizard.groups.run_and_review.title") - } + end end component.with_row do render(Admin::Import::Jira::ImportRuns::WizardStepConfirmImportComponent.new(model)) end component.with_row do - render(Admin::Import::Jira::ImportRuns::WizardStepReviewComponent.new( - model, - import_selection: import_selection - )) + render( + Admin::Import::Jira::ImportRuns::WizardStepReviewComponent.new( + model, + import_selection: import_selection + ) + ) end end end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb index 62d180fc5057..17609348db7f 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb @@ -28,69 +28,89 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= flex_layout do |box| - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.title") - }) - if model.status_before?(:projects_meta_done) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption") - }) - elsif model.status_equal_or_after?(:imported) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done") - }) - end - end - if model.in_state?(:projects_meta_done, :import_error) - box.with_row(mb: 3) do - render(Primer::Beta::Text.new) { - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.description") - } - end - box.with_row(mb: 3) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_available_data"), - list: import_selection, - show_icon: true, - icon: :"check-circle", - icon_color: :success - )) - end - if model.in_state?(:projects_meta_done) box.with_row do - concat(render(Primer::Beta::Button.new( - scheme: :primary, - tag: :a, - size: :medium, - href: import_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - data: { controller: "async-dialog" } - )) { I18n.t(:"admin.jira.run.wizard.sections.confirm_import.button_start") }) - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium, - href: admin_import_jira_path(model.jira) - )) { - I18n.t(:button_cancel) - }) + concat( + render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.title") + end + ) + if model.status_before?(:projects_meta_done) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption") + end + ) + elsif model.status_equal_or_after?(:imported) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done") + end + ) + end end - elsif model.in_state?(:import_error) - box.with_row do - render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'import')) + if model.in_state?(:projects_meta_done, :import_error) + box.with_row(mb: 3) do + render(Primer::Beta::Text.new) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.description") + end + end + box.with_row(mb: 3) do + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_available_data"), + list: import_selection, + show_icon: true, + icon: :"check-circle", + icon_color: :success + ) + ) + end + if model.in_state?(:projects_meta_done) + box.with_row do + concat( + render( + Primer::Beta::Button.new( + scheme: :primary, + tag: :a, + size: :medium, + href: import_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), + data: { controller: "async-dialog" } + ) + ) { I18n.t(:"admin.jira.run.wizard.sections.confirm_import.button_start") } + ) + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + ) + ) do + I18n.t(:button_cancel) + end + ) + end + elsif model.in_state?(:import_error) + box.with_row do + render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, "import")) + end + end + elsif model.in_state?(:importing) + box.with_row(mt: 3, mb: 3) do + render( + Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_progress") + ) + ) + end + box.with_row(mb: 3) do + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_import_data"), + list: import_selection, + show_icon: true + ) + ) + end end - end - elsif model.in_state?(:importing) - box.with_row(mt: 3, mb: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_progress") - )) - end - box.with_row(mb: 3) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_import_data"), - list: import_selection, - show_icon: true - )) - end - end -end %> + end %> diff --git a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb index 8f8cad7b3fc0..4e704b507d51 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_fetch_data_component.html.erb @@ -28,99 +28,129 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= flex_layout do |box| - box.with_row do - flex_layout do |row| - row.with_column(flex: 1) do - concat(content_tag(:div, class: "d-flex flex-items-center") do - concat(render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title") - }) - if model.status_equal_or_after?(:configuring) - concat(render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done") - }) + box.with_row do + flex_layout do |row| + row.with_column(flex: 1) do + concat( + content_tag(:div, class: "d-flex flex-items-center") do + concat( + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title") + end + ) + if model.status_equal_or_after?(:configuring) + concat( + render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done") + end + ) + end + end + ) + if model.status_equal_or_after?(:configuring) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done_description", server_info: model.jira.url) + end + ) + end end - end) - if model.status_equal_or_after?(:configuring) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done_description", server_info: model.jira.url) - }) end end - end - end - if model.status_before?(:configuring) - box.with_row(mb: 3) do - render(Primer::Beta::Text.new) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.description") - } - end - end - if model.in_state?(:initial) - box.with_row do - concat(render(Primer::Beta::Button.new( - scheme: :primary, - tag: :a, - size: :medium, - href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'fetch_instance_meta'), - data: { turbo_stream: true } - )) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_fetch") - }) - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium, - href: admin_import_jira_path(model.jira) - )) { - I18n.t(:button_cancel) - }) - end - elsif model.in_state?(:instance_meta_error) - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'fetch_instance_meta')) - end - elsif model.in_state?(:instance_meta_fetching) - box.with_row do - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium - )) do |button| - button.with_leading_visual_icon(icon: :sync, animation: :rotate) - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_progress") - end) - end - elsif model.in_state?(:instance_meta_done) - box.with_row(mb: 3) do - flex_layout do |flex| - flex.with_column(flex: 1, mr: 1) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_available_server_data", server_info: model.jira.name), - list: import_stats_available, - show_icon: true, - icon: :"check-circle", - icon_color: :success, - h: :full - )) + if model.status_before?(:configuring) + box.with_row(mb: 3) do + render(Primer::Beta::Text.new) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.description") + end + end + end + if model.in_state?(:initial) + box.with_row do + concat( + render( + Primer::Beta::Button.new( + scheme: :primary, + tag: :a, + size: :medium, + href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: "fetch_instance_meta"), + data: { turbo_stream: true } + ) + ) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_fetch") + end + ) + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + ) + ) do + I18n.t(:button_cancel) + end + ) + end + elsif model.in_state?(:instance_meta_error) + box.with_row(mt: 3) do + render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, "fetch_instance_meta")) + end + elsif model.in_state?(:instance_meta_fetching) + box.with_row do + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium + ) + ) do |button| + button.with_leading_visual_icon(icon: :sync, animation: :rotate) + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_progress") + end + ) + end + elsif model.in_state?(:instance_meta_done) + box.with_row(mb: 3) do + flex_layout do |flex| + flex.with_column(flex: 1, mr: 1) do + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_available_server_data", server_info: model.jira.name), + list: import_stats_available, + show_icon: true, + icon: :"check-circle", + icon_color: :success, + h: :full + ) + ) + end + end + end + box.with_row do + concat( + render( + Primer::Beta::Button.new( + scheme: :primary, + size: :medium, + tag: :a, + href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: "configure"), + data: { turbo_stream: true } + ) + ) do + I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_continue") + end + ) + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + ) + ) do + I18n.t(:button_cancel) + end + ) end end - end - box.with_row do - concat(render(Primer::Beta::Button.new( - scheme: :primary, - size: :medium, - tag: :a, - href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'configure'), - data: { turbo_stream: true } - )) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_continue") - }) - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium, - href: admin_import_jira_path(model.jira) - )) { - I18n.t(:button_cancel) - }) - end - end -end %> + end %> diff --git a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb index 182f3e499e11..ebbf2ca4ac7a 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_import_scope_component.html.erb @@ -28,87 +28,115 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= flex_layout do |box| - box.with_row do - - concat(content_tag(:div, class: "d-flex flex-items-center") do - concat(render(Primer::Beta::Text.new(font_weight: :bold)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.title") - }) - if model.status_equal_or_after?(:projects_meta_done) - concat(render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done") - }) - end - end) + box.with_row do + concat( + content_tag(:div, class: "d-flex flex-items-center") do + concat( + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.title") + end + ) + if model.status_equal_or_after?(:projects_meta_done) + concat( + render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done") + end + ) + end + end + ) - if model.status_before?(:configuring) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption") - }) - elsif model.status_equal_or_after?(:projects_meta_done) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done_description", count: selected_projects_count) - }) - end - end - if model.in_state?(:configuring, :projects_meta_error) - box.with_row(mt: 1, mb: 3) do - render(Primer::Beta::Text.new) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_import") - } - end - box.with_row(mb: 1) do - concat(render(Primer::Beta::Button.new( - size: :medium, - tag: :a, - href: admin_import_jira_run_select_projects_path(jira_id: model.jira.id, run_id: model.id), - data: { controller: "async-dialog" } - )) do |component| - component.with_trailing_visual_counter(count: selected_projects_count) - I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") - end) - if model.in_state?(:configuring) - concat(render(Primer::Beta::Button.new( - scheme: :primary, - size: :medium, - tag: :a, - href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: 'fetch_projects_meta'), - data: { turbo_stream: true }, - disabled: model.projects.nil? || model.projects.empty? - )) { - I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue") - }) + if model.status_before?(:configuring) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption") + end + ) + elsif model.status_equal_or_after?(:projects_meta_done) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done_description", count: selected_projects_count) + end + ) + end end - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium, - href: admin_import_jira_path(model.jira) - )) { - I18n.t(:button_cancel) - }) - end - if model.in_state?(:projects_meta_error) - box.with_row do - render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'fetch_projects_meta')) + if model.in_state?(:configuring, :projects_meta_error) + box.with_row(mt: 1, mb: 3) do + render(Primer::Beta::Text.new) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_import") + end + end + box.with_row(mb: 1) do + concat( + render( + Primer::Beta::Button.new( + size: :medium, + tag: :a, + href: admin_import_jira_run_select_projects_path(jira_id: model.jira.id, run_id: model.id), + data: { controller: "async-dialog" } + ) + ) do |component| + component.with_trailing_visual_counter(count: selected_projects_count) + I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") + end + ) + if model.in_state?(:configuring) + concat( + render( + Primer::Beta::Button.new( + scheme: :primary, + size: :medium, + tag: :a, + href: continue_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id, step: "fetch_projects_meta"), + data: { turbo_stream: true }, + disabled: model.projects.blank? + ) + ) do + I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue") + end + ) + end + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + ) + ) do + I18n.t(:button_cancel) + end + ) + end + if model.in_state?(:projects_meta_error) + box.with_row do + render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, "fetch_projects_meta")) + end + end + elsif model.in_state?(:projects_meta_fetching) + box.with_row(mt: 3) do + concat( + render( + Primer::Beta::Button.new( + size: :medium, + disabled: true + ) + ) do |component| + component.with_trailing_visual_counter(count: selected_projects_count) + I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") + end + ) + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium + ) + ) do |button| + button.with_leading_visual_icon(icon: :sync, animation: :rotate) + I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_progress") + end + ) + end end - end - elsif model.in_state?(:projects_meta_fetching) - box.with_row(mt: 3) do - concat(render(Primer::Beta::Button.new( - size: :medium, - disabled: true - )) do |component| - component.with_trailing_visual_counter(count: selected_projects_count) - I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select") - end) - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium - )) do |button| - button.with_leading_visual_icon(icon: :sync, animation: :rotate) - I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_progress") - end) - end - end -end -%> + end %> diff --git a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb index 806b0012c172..337fd8a79d0d 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_review_component.html.erb @@ -28,103 +28,131 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= flex_layout do |box| - if model.in_state?(:reverting, :reverted, :revert_error) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_revert") - }) - end - case model.current_state.to_sym - when :reverting - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_revert_progress") - )) + if model.in_state?(:reverting, :reverted, :revert_error) + box.with_row do + concat( + render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_revert") + end + ) + end + case model.current_state.to_sym + when :reverting + box.with_row(mt: 3) do + render( + Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_revert_progress") + ) + ) + end + when :reverted + box.with_row(mt: 3) do + render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_reverted") + end + end + when :revert_error + box.with_row(mt: 3) do + render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, "revert")) + end + end + elsif model.in_state?(:finalizing, :finalizing_done, :finalizing_error) + box.with_row do + concat( + render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalize_import") + end + ) + end + case model.current_state.to_sym + when :finalizing + box.with_row(mt: 3) do + render( + Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalizing") + ) + ) + end + when :finalizing_done + box.with_row(mt: 3) do + render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalizing_done") + end + end + when :finalizing_error + box.with_row(mt: 3) do + render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, "finalize")) + end + end + else + box.with_row do + concat( + render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.title") + end + ) + if model.status_before?(:imported) + concat( + render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.caption") + end + ) + elsif model.in_state?(:imported) + concat( + render(Primer::Beta::Text.new(mt: 1, tag: :div)) do + I18n.t(:"admin.jira.run.wizard.sections.import_result.preview_description") + end + ) + end + end + if model.in_state?(:imported, :finalizing_done) + box.with_row(mt: 3, mb: 3) do + render( + Admin::Import::Jira::ImportRuns::ImportedStatsBannerComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.import_result.stats.title"), + stats: imported_stats + ) + ) + end + end + if model.in_state?(:imported) + box.with_row do + concat( + render( + Primer::Beta::Button.new( + tag: :a, + scheme: :primary, + size: :medium, + mr: 2, + href: finalize_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), + data: { controller: "async-dialog" } + ) + ) { I18n.t(:"admin.jira.run.wizard.sections.import_result.button_done") } + ) + concat( + render( + Primer::Beta::Button.new( + scheme: :default, + tag: :a, + size: :medium, + color: :danger, + href: revert_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), + data: { controller: "async-dialog" } + ) + ) { I18n.t(:"admin.jira.run.wizard.sections.import_result.button_revert") } + ) + concat( + render( + Primer::Beta::Button.new( + tag: :a, + size: :medium, + href: admin_import_jira_path(model.jira) + ) + ) do + I18n.t(:button_cancel) + end + ) + end + end end - when :reverted - box.with_row(mt: 3) do - render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_reverted") - } - end - when :revert_error - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'revert')) - end - end - elsif model.in_state?(:finalizing, :finalizing_done, :finalizing_error) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalize_import") - }) - end - case model.current_state.to_sym - when :finalizing - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalizing") - )) - end - when :finalizing_done - box.with_row(mt: 3) do - render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.label_finalizing_done") - } - end - when :finalizing_error - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'finalize')) - end - end - else - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.title") - }) - if model.status_before?(:imported) - concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.caption") - }) - elsif model.in_state?(:imported) - concat(render(Primer::Beta::Text.new(mt: 1, tag: :div)) { - I18n.t(:"admin.jira.run.wizard.sections.import_result.preview_description") - }) - end - end - if model.in_state?(:imported, :finalizing_done) - box.with_row(mt: 3, mb: 3) do - render(Admin::Import::Jira::ImportRuns::ImportedStatsBannerComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_result.stats.title"), - stats: imported_stats - )) - end - end - if model.in_state?(:imported) - box.with_row do - concat(render(Primer::Beta::Button.new( - tag: :a, - scheme: :primary, - size: :medium, - mr: 2, - href: finalize_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - data: { controller: "async-dialog" } - )) { I18n.t(:"admin.jira.run.wizard.sections.import_result.button_done") }) - concat(render(Primer::Beta::Button.new( - scheme: :default, - tag: :a, - size: :medium, - color: :danger, - href: revert_modal_admin_import_jira_run_path(jira_id: model.jira.id, id: model.id), - data: { controller: "async-dialog" } - )) { I18n.t(:"admin.jira.run.wizard.sections.import_result.button_revert") }) - concat(render(Primer::Beta::Button.new( - tag: :a, - size: :medium, - href: admin_import_jira_path(model.jira) - )) { - I18n.t(:button_cancel) - }) - end - end - end -end %> + end %> diff --git a/app/views/admin/import/jira/import_runs/show.html.erb b/app/views/admin/import/jira/import_runs/show.html.erb index e067b305367f..5cf76023fffc 100644 --- a/app/views/admin/import/jira/import_runs/show.html.erb +++ b/app/views/admin/import/jira/import_runs/show.html.erb @@ -30,14 +30,14 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), Import::JiraImport.model_name, @jira_import.id %> <%= render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { "#{I18n.t(:"admin.jira.run.title")} ##{@jira_import.id.to_s}" } + header.with_title { "#{I18n.t(:"admin.jira.run.title")} ##{@jira_import.id}" } header.with_breadcrumbs( [ { href: admin_index_path, text: t(:label_administration) }, { href: admin_import_path, text: t(:"admin.import.title") }, { href: admin_import_jira_index_path, text: I18n.t(:"admin.jira.title") }, { href: admin_import_jira_path(@jira), text: @jira.name || @jira.url }, - "#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id.to_s}" + "#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id}" ] ) end diff --git a/app/views/admin/import/jira/instances/index.html.erb b/app/views/admin/import/jira/instances/index.html.erb index 9799f86b3fec..424052dcc39b 100644 --- a/app/views/admin/import/jira/instances/index.html.erb +++ b/app/views/admin/import/jira/instances/index.html.erb @@ -37,7 +37,8 @@ See COPYRIGHT and LICENSE files for more details. [ { href: admin_index_path, text: t(:label_administration) }, { href: admin_import_path, text: t(:"admin.import.title") }, - t(:"admin.jira.title")] + t(:"admin.jira.title") + ] ) end %> @@ -57,75 +58,95 @@ See COPYRIGHT and LICENSE files for more details. <%= flex_layout(direction: :column) do |container| container.with_row do - render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert, mb: 3)) { - concat(render(Primer::Beta::Text.new(tag: :p, font_weight: :bold)) { + render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert, mb: 3)) do + concat( + render(Primer::Beta::Text.new(tag: :p, font_weight: :bold)) do I18n.t(:"admin.jira.banner.title") - }) - concat(render(Primer::Beta::Text.new(tag: :p, font_weight: :normal)) { - I18n.t(:"admin.jira.banner.description") - }) - concat(render(Primer::Beta::Text.new(tag: :p, font_weight: :normal)) { - link_translate("admin.jira.banner.contribution_callout", - links: { link: %i[jira_migrator_project] }, - external: true) - }) - } + end + ) + concat( + render(Primer::Beta::Text.new(tag: :p, font_weight: :normal)) do + I18n.t(:"admin.jira.banner.description") + end + ) + concat( + render(Primer::Beta::Text.new(tag: :p, font_weight: :normal)) do + link_translate( + "admin.jira.banner.contribution_callout", + links: { link: %i[jira_migrator_project] }, + external: true + ) + end + ) + end end unless Setting::WorkPackageIdentifier.semantic? container.with_row do - render(Primer::Alpha::Banner.new(scheme: :danger, icon: :stop, mb: 3)) { - concat(render(Primer::Beta::Text.new(tag: :p, font_weight: :bold)) { - I18n.t("admin.jira.errors.semantic_identifiers_must_be_enabled.title") - }) - concat(render(Primer::Beta::Text.new(tag: :p)) { - link_translate("admin.jira.errors.semantic_identifiers_must_be_enabled.description", - links: { link: admin_settings_work_packages_identifier_path }, - external: true) - }) - } + render(Primer::Alpha::Banner.new(scheme: :danger, icon: :stop, mb: 3)) do + concat( + render(Primer::Beta::Text.new(tag: :p, font_weight: :bold)) do + I18n.t("admin.jira.errors.semantic_identifiers_must_be_enabled.title") + end + ) + concat( + render(Primer::Beta::Text.new(tag: :p)) do + link_translate( + "admin.jira.errors.semantic_identifiers_must_be_enabled.description", + links: { link: admin_settings_work_packages_identifier_path }, + external: true + ) + end + ) + end end end container.with_row(pb: 3) do flex_layout do |flex| flex.with_column(flex: 1, mr: 1) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_supported_data"), - list: [ - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.projects"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.project_ids"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issues"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_ids"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_details"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.custom_fields"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.users"), - ].map { |label| { label:, checked: true } }, - h: :full, - icon: :"check-circle", - icon_color: :success - )) + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_supported_data"), + list: [ + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.projects"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.project_ids"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issues"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_ids"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_details"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.custom_fields"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.users") + ].map { |label| { label:, checked: true } }, + h: :full, + icon: :"check-circle", + icon_color: :success + ) + ) end flex.with_column(flex: 1, ml: 1) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_coming_soon"), - list: [ - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.relations"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.sprints"), - ].map { |label| { label:, checked: false } }, - h: :full, - icon: :"op-flag" - )) + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_coming_soon"), + list: [ + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.relations"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.sprints") + ].map { |label| { label:, checked: false } }, + h: :full, + icon: :"op-flag" + ) + ) end flex.with_column(flex: 1, ml: 1) do - render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( - title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_coming_later"), - list: [ - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.workflows"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.permissions"), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.schemes") - ].map { |label| { label:, checked: false } }, - h: :full, - icon: :"arrow-right" - )) + render( + Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new( + title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_coming_later"), + list: [ + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.workflows"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.permissions"), + I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.schemes") + ].map { |label| { label:, checked: false } }, + h: :full, + icon: :"arrow-right" + ) + ) end end end From bf9a561b16223cb896a6eb0ee3593c68f9adaea5 Mon Sep 17 00:00:00 2001 From: Andrej <77627197+as-op@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:14:08 +0200 Subject: [PATCH 20/22] sort en.yml --- config/locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 114cfcf44425..dd2bb1637ea5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1312,14 +1312,14 @@ en: label_users_import_explanation: "Users that are involved in selected projects (group memberships included)" title: "Import data" fetch_data: + button_continue: "Continue to scope selection" button_fetch: "Check available data" caption_done: "Completed" caption_done_description: "Meta data fetched from %{server_info}" description: "Check what data is available for import in the host Jira instance." + label_available_server_data: "Available data on %{server_info}" label_progress: "Fetching data from Jira..." title: "Fetch instance meta data" - button_continue: "Continue to scope selection" - label_available_server_data: "Available data on %{server_info}" groups_and_users: title: "Groups and Users" import_result: From af668d5ecbf52e82b28637e64bb059a7fc95753c Mon Sep 17 00:00:00 2001 From: as-op Date: Tue, 28 Jul 2026 13:54:41 +0200 Subject: [PATCH 21/22] update test with changed label content --- .../admin/import/jira/select_projects_spec.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/spec/features/admin/import/jira/select_projects_spec.rb b/spec/features/admin/import/jira/select_projects_spec.rb index ea49c45731ff..1279b605d66f 100644 --- a/spec/features/admin/import/jira/select_projects_spec.rb +++ b/spec/features/admin/import/jira/select_projects_spec.rb @@ -54,7 +54,7 @@ end let(:modal_id) { Admin::Import::Jira::ImportRuns::SelectProjects::ModalComponent::MODAL_ID } - let(:filter_label) { I18n.t(:"admin.jira.run.wizard.select_dialog.filter_projects") } + let(:filter_label) { I18n.t(:"admin.jira.run.wizard.select_projects.filter_projects") } before do allow(Import::JiraInstanceMetaDataJob).to receive(:perform_later) @@ -140,12 +140,12 @@ def clear_filter before { open_select_projects_modal } it "checks and unchecks all visible projects" do - click_on I18n.t(:button_check_all) + click_on I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") expect(page).to have_field("Project Alpha", checked: true) expect(page).to have_field("Project Beta", checked: true) expect(page).to have_field("Gamma Project", checked: true) - click_on I18n.t(:button_uncheck_all) + click_on I18n.t(:"admin.jira.run.wizard.select_projects.button_unselect_all") expect(page).to have_field("Project Alpha", checked: false) expect(page).to have_field("Project Beta", checked: false) expect(page).to have_field("Gamma Project", checked: false) @@ -154,16 +154,16 @@ def clear_filter it "scopes bulk check and uncheck to visible filtered projects" do fill_in filter_label, with: "Alpha" expect(page).to have_no_field("Project Beta") - click_on I18n.t(:button_check_all) + click_on I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") clear_filter expect(page).to have_field("Project Alpha", checked: true) expect(page).to have_field("Project Beta", checked: false) expect(page).to have_field("Gamma Project", checked: false) - click_on I18n.t(:button_check_all) + click_on I18n.t(:"admin.jira.run.wizard.select_projects.button_select_all") fill_in filter_label, with: "Alpha" expect(page).to have_no_field("Project Beta") - click_on I18n.t(:button_uncheck_all) + click_on I18n.t(:"admin.jira.run.wizard.select_projects.button_unselect_all") clear_filter expect(page).to have_field("Project Alpha", checked: false) expect(page).to have_field("Project Beta", checked: true) @@ -213,7 +213,9 @@ def clear_filter it "discards changes when cancelled" do check "Project Alpha" - click_on I18n.t(:button_cancel) + within("##{modal_id}") do + click_on I18n.t(:button_cancel) + end expect(page).to have_no_css("##{modal_id}[open]") expect(jira_import.reload.projects).to be_empty end From e965c8295442e68d586ef83bd6ca13dbe41e492a Mon Sep 17 00:00:00 2001 From: as-op Date: Tue, 28 Jul 2026 14:54:59 +0200 Subject: [PATCH 22/22] update confirm_import section layout --- ...izard_step_confirm_import_component.html.erb | 17 ++++++++++++++--- config/locales/en.yml | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb index 17609348db7f..784e73d1b9c2 100644 --- a/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb +++ b/app/components/admin/import/jira/import_runs/wizard_step_confirm_import_component.html.erb @@ -30,8 +30,19 @@ See COPYRIGHT and LICENSE files for more details. <%= flex_layout do |box| box.with_row do concat( - render(Primer::Beta::Text.new(font_weight: :bold, tag: :div)) do - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.title") + content_tag(:div, class: "d-flex flex-items-center") do + concat( + render(Primer::Beta::Text.new(font_weight: :bold)) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.title") + end + ) + if model.status_equal_or_after?(:imported) + concat( + render(Primer::Beta::Label.new(size: :medium, font_weight: :bold, color: :success, ml: 1)) do + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done") + end + ) + end end ) if model.status_before?(:projects_meta_done) @@ -43,7 +54,7 @@ See COPYRIGHT and LICENSE files for more details. elsif model.status_equal_or_after?(:imported) concat( render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do - I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done") + I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done_info") end ) end diff --git a/config/locales/en.yml b/config/locales/en.yml index dd2bb1637ea5..48a53b82b276 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1305,6 +1305,7 @@ en: button_start: "Start import" caption: "Review your import settings and start the import" caption_done: "Completed" + caption_done_info: "All data has been imported" description: "You are about to start an import run with the following settings." label_available_data: "Data to be imported" label_import_data: "Currently importing"