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..3666762d34a4 --- /dev/null +++ b/app/components/admin/import/jira/import_runs/imported_stats_banner_component.html.erb @@ -0,0 +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 %> 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/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/row_component.rb b/app/components/admin/import/jira/import_runs/row_component.rb index 155a9b093ab7..43a26e9aa97a 100644 --- a/app/components/admin/import/jira/import_runs/row_component.rb +++ b/app/components/admin/import/jira/import_runs/row_component.rb @@ -34,58 +34,74 @@ 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 + + render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state)) end - def status - render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state)) + 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) + 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 - (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 - [edit_button] - # buttons = [] - # buttons.push(remove_button) if model.deletable? - # buttons.push(edit_button) - # buttons + [ + action_menu + ] 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) + 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 ) - ) + 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 - 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) - } - ) - ) + 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/select_projects/list_header_component.html.erb b/app/components/admin/import/jira/import_runs/select_projects/list_header_component.html.erb index b2fce231c382..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, mt: 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"), - 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(:button_check_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(:button_uncheck_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 bbc026ef1dbd..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 @@ -11,20 +11,18 @@ "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)) do + I18n.t(:"admin.jira.run.wizard.select_projects.info") + end + ) 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| @@ -39,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( @@ -55,7 +55,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/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..f4c7743cd68e --- /dev/null +++ b/app/components/admin/import/jira/import_runs/side_panel/status_component.html.erb @@ -0,0 +1,22 @@ +<%= + 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..efcfb1bbc5a6 --- /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/components/admin/import/jira/import_runs/table_component.rb b/app/components/admin/import/jira/import_runs/table_component.rb index 65f07fd072b7..e25e8df3d0bb 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,8 @@ module Admin::Import::Jira::ImportRuns class TableComponent < OpPrimer::BorderBoxTableComponent - columns :id, :status, :projects, :last_changed + columns :id, :creator, :projects, :last_changed + main_column :id def initialize(jira:, **) @jira = jira @@ -56,7 +57,7 @@ def has_header? def headers [ [:id, { caption: I18n.t(:"admin.jira.run.title") }], - [:status, { caption: Import::JiraImport.human_attribute_name(:status) }], + [: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/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..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,47 +29,38 @@ 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)) { - I18n.t(:"admin.jira.run.wizard.groups.fetch.title") - } + 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)) 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") - } + 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(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, - 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 9eab87083f7b..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 @@ -28,60 +28,100 @@ 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 - )) - end - if model.in_state?(:projects_meta_done) box.with_row do - 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( + 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) + 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_info") + 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 0352a6e442c9..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,46 +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(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(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { - 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 end - end - end - if model.in_state?(:initial) - box.with_row(mb: 3) do - render(Primer::Beta::Text.new) { - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.description") - } - end - box.with_row do - 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") + 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 - 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( - I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_progress") - )) - end - 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..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,85 +28,115 @@ 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") - }) - if model.status_before?(:instance_meta_done) - 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") - }) - 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 - 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( - 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 - end - if model.in_state?(:configuring) 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: '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") - } + 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)) 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 - elsif 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 - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_progress") - )) - end - end -end -%> + 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/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..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,104 +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") - }) - 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 - )) - 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, - 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, - 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") }) - end - end - end -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/app/components/admin/import/jira/row_component.rb b/app/components/admin/import/jira/row_component.rb index 432152459537..3a0bacea575f 100644 --- a/app/components/admin/import/jira/row_component.rb +++ b/app/components/admin/import/jira/row_component.rb @@ -49,5 +49,35 @@ def time_ago(date_time) def added helpers.format_date(model.created_at) end + + 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 005a85dcc83b..76cf07f256b3 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) @@ -44,11 +44,16 @@ def has_header? rows.any? end + def has_actions? + true + end + 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/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/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/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/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/app/views/admin/import/jira/import_runs/show.html.erb b/app/views/admin/import/jira/import_runs/show.html.erb index 100b42f094a2..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,42 +30,35 @@ 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}" } 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}" ] ) - 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/app/views/admin/import/jira/instances/index.html.erb b/app/views/admin/import/jira/instances/index.html.erb index 6f9936abe5ae..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,73 +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, - )) + 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, - )) + 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 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/config/locales/en.yml b/config/locales/en.yml index 419b3db5c681..48a53b82b276 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. @@ -1188,9 +1191,11 @@ en: environment variable. columns: added: "Added" + creator: "Creator" label_ago: "%{amount} ago" last_change: "Last change" projects: "Projects" + server_url: "Server URL" configuration: new: "New configuration" title: "Jira configuration" @@ -1205,18 +1210,24 @@ 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: + 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" 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" @@ -1256,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" @@ -1300,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" @@ -1307,9 +1313,12 @@ 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" groups_and_users: @@ -1318,7 +1327,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." @@ -1327,6 +1335,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" @@ -1334,6 +1356,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" @@ -1348,19 +1373,21 @@ 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." 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" - 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}" 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 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 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..e09715ebcced --- /dev/null +++ b/spec/features/admin/import/jira/import_runs_spec.rb @@ -0,0 +1,76 @@ +# 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) } + shared_let(:author) { create(:user, firstname: "Jane", lastname: "Doe") } + let!(:jira) { create(:jira, name: "My Jira") } + let!(:jira_import) do + create(:jira_import, + jira:, + author:, + projects: [{ "name" => "Project Alpha" }, { "name" => "Project Beta" }]) + end + + 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 + + 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 + + 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 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 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