diff --git a/Gemfile b/Gemfile index 0404e6b2c3b..34559dcbc41 100644 --- a/Gemfile +++ b/Gemfile @@ -433,4 +433,4 @@ end gem "openproject-octicons", "~>19.32.0" gem "openproject-octicons_helper", "~>19.32.0" -gem "openproject-primer_view_components", "~>0.81.1" +gem "openproject-primer_view_components", "~>0.82.0" diff --git a/Gemfile.lock b/Gemfile.lock index 31e2e1ee4ef..5a5f7dca92a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -895,7 +895,7 @@ GEM actionview openproject-octicons (= 19.32.1) railties - openproject-primer_view_components (0.81.1) + openproject-primer_view_components (0.82.0) actionview (>= 7.2.0) activesupport (>= 7.2.0) openproject-octicons (>= 19.30.1) @@ -1679,7 +1679,7 @@ DEPENDENCIES openproject-octicons (~> 19.32.0) openproject-octicons_helper (~> 19.32.0) openproject-openid_connect! - openproject-primer_view_components (~> 0.81.1) + openproject-primer_view_components (~> 0.82.0) openproject-recaptcha! openproject-reporting! openproject-storages! @@ -2059,7 +2059,7 @@ CHECKSUMS openproject-octicons (19.32.1) sha256=32253f3256ad4e1aec36442558ce140623c01e5241d9b90f6eb6d317f462781e openproject-octicons_helper (19.32.1) sha256=7676059927ae940170fb13d62f88b885985a3f0d483e1bb246475afcffd90f8f openproject-openid_connect (1.0.0) - openproject-primer_view_components (0.81.1) sha256=ebda313d71f4c7e82b9a31e0d54b1e2b5ac3776816161fb61517ced1d945587d + openproject-primer_view_components (0.82.0) sha256=c3d61578d26e6fa6e4bfaf520345de01cc09b487c9841de78494017aadd65ae2 openproject-recaptcha (1.0.0) openproject-reporting (1.0.0) openproject-storages (1.0.0) 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 0c6adf5d3a5..04e3527d8da 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 @@ -34,10 +34,11 @@ module Admin::Import::Jira::ImportRuns attr_reader :title, :list, :system_arguments - def initialize(title:, list:, **system_arguments) + def initialize(title:, list:, show_icon: true, **system_arguments) super() @title = title @list = list + @show_icon = show_icon @system_arguments = system_arguments end @@ -57,14 +58,16 @@ module Admin::Import::Jira::ImportRuns end def render_item(item) - concat(render( - Primer::Beta::Octicon.new( - icon: item[:checked] ? :"check-circle" : :"x-circle", - color: item[:checked] ? :success : :danger - ) - )) + if @show_icon + concat(render( + Primer::Beta::Octicon.new( + icon: item[:checked] ? :"check-circle" : :"x-circle", + color: item[:checked] ? :success : :danger + ) + )) + end if item[:url].present? - concat(render(Primer::Beta::Link.new(href: item[:url], ml: 1)) { item[:label] }) + concat(render(Primer::Beta::Link.new(href: item[:url], ml: 1, target: "_blank")) { item[:label] }) else concat(render(Primer::Beta::Text.new(ml: 1)) { item[:label] }) end diff --git a/app/components/admin/import/jira/import_runs/wizard_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_component.html.erb index 0879776cb73..2247f5f10fc 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 @@ -41,14 +41,7 @@ See COPYRIGHT and LICENSE files for more details. 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: :semibold)) { - I18n.t(:"admin.jira.run.wizard.groups.groups_and_users.title") - } - end - component.with_row do - render(Admin::Import::Jira::ImportRuns::WizardStepGroupsAndUsersComponent.new(model)) - end + component.with_row(scheme: :neutral, color: :muted, align_items: :center) do render(Primer::Beta::Text.new(font_weight: :semibold)) { I18n.t(:"admin.jira.run.wizard.groups.configuration.title") @@ -57,6 +50,7 @@ See COPYRIGHT and LICENSE files for more details. 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: :semibold)) { I18n.t(:"admin.jira.run.wizard.groups.confirming.title") @@ -65,6 +59,7 @@ See COPYRIGHT and LICENSE files for more details. 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: :semibold)) { I18n.t(:"admin.jira.run.wizard.groups.review.title") 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 56fd776d70c..d8e3eb0918c 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 @@ -51,7 +51,8 @@ See COPYRIGHT and LICENSE files for more details. 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 + list: import_selection, + show_icon: false )) end if model.in_state?(:projects_meta_done) @@ -80,7 +81,8 @@ See COPYRIGHT and LICENSE files for more details. 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 + list: import_selection, + show_icon: false )) end end diff --git a/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.html.erb b/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.html.erb deleted file mode 100644 index 9ba63de92ae..00000000000 --- a/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.html.erb +++ /dev/null @@ -1,113 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<%= flex_layout do |box| - if model.status_before?(:groups_and_users_init) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "BEFORE" - }) - end - elsif model.in_state?(:groups_and_users_init) - 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_groups_and_users'), - data: { turbo_stream: true } - )) { - "FETCH GROUPS AND USERS" - } - end - elsif model.in_state?(:groups_and_users_fetching) - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - "FETCHING_GROUPS_AND_USERS: #{model.cursor}" - )) - end - elsif model.in_state?(:groups_and_users_fetching_error) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "FETCHING_ERROR" - }) - end - elsif model.in_state?(:groups_and_users_fetching_done) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "FETCHING_DONE" - }) - 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: 'import_groups_and_users'), - data: { turbo_stream: true } - )) { - "IMPORT GROUPS AND USERS" - } - end - elsif model.in_state?(:groups_and_users_importing) - box.with_row(mt: 3) do - render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new( - "IMPORTING_GROUPS_AND_USERS: #{model.cursor}" - )) - end - elsif model.in_state?(:groups_and_users_importing_error) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "IMPORTING_ERROR" - }) - end - elsif model.in_state?(:groups_and_users_importing_done) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "IMPORTING_DONE" - }) - 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: 'import_scope'), - data: { turbo_stream: true } - )) { - "IMPORT SCOPE" - }) - end - elsif model.status_after?(:groups_and_users_importing_done) - box.with_row do - concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) { - "AFTER" - }) - end - end -end -%> diff --git a/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.rb b/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.rb deleted file mode 100644 index 6107c75feac..00000000000 --- a/app/components/admin/import/jira/import_runs/wizard_step_groups_and_users_component.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Admin::Import::Jira::ImportRuns - class WizardStepGroupsAndUsersComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - 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 90cd8cea76a..18f794a2204 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 @@ -42,7 +42,7 @@ See COPYRIGHT and LICENSE files for more details. }) end end - if model.in_state?(:import_scope) + if model.in_state?(:instance_meta_done) box.with_row(mb: 3, mt: 3) do render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert)) { I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_info") 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 9b803975d4c..3d7c72a82db 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 @@ -39,7 +39,7 @@ module Admin::Import::Jira::ImportRuns issues_label(available_issues_count), statuses_label(available_statuses_count), types_label(available_types_count), - I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.users") + users_label(available_users_count) ].map { |label| { label:, checked: true } } end @@ -83,5 +83,9 @@ module Admin::Import::Jira::ImportRuns 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/controllers/admin/import/jira/import_runs_controller.rb b/app/controllers/admin/import/jira/import_runs_controller.rb index 70f123cfd5d..af64a16a9fc 100644 --- a/app/controllers/admin/import/jira/import_runs_controller.rb +++ b/app/controllers/admin/import/jira/import_runs_controller.rb @@ -36,12 +36,8 @@ module Admin::Import::Jira layout "admin" VALID_STEPS = %i[ - init fetch_instance_meta - fetch_groups_and_users - import_groups_and_users fetch_projects_meta - import_scope configure import revert @@ -111,10 +107,6 @@ module Admin::Import::Jira end end - def init - @jira_import.transition_to!(:initial) - end - def fetch_instance_meta @jira_import.transition_to!(:instance_meta_fetching) end @@ -145,22 +137,10 @@ module Admin::Import::Jira .first end - def import_scope - @jira_import.transition_to!(:import_scope) - end - def configure @jira_import.transition_to!(:configuring) end - def fetch_groups_and_users - @jira_import.transition_to!(:groups_and_users_fetching) - end - - def import_groups_and_users - @jira_import.transition_to!(:groups_and_users_importing) - end - def revert @jira_import.transition_to!(:reverting) end diff --git a/app/models/import/jira_import_state_machine.rb b/app/models/import/jira_import_state_machine.rb index c9e31175dbf..1986293dd2b 100644 --- a/app/models/import/jira_import_state_machine.rb +++ b/app/models/import/jira_import_state_machine.rb @@ -39,20 +39,6 @@ module Import state :instance_meta_error state :instance_meta_done - state :groups_and_users_init - - state :groups_and_users_fetching - state :groups_and_users_fetching_cancelling - state :groups_and_users_fetching_cancelled - state :groups_and_users_fetching_error - state :groups_and_users_fetching_done - - state :groups_and_users_importing - state :groups_and_users_importing_cancelling - state :groups_and_users_importing_cancelled - state :groups_and_users_importing_error - state :groups_and_users_importing_done - state :import_scope state :configuring state :projects_meta_fetching @@ -73,19 +59,7 @@ module Import transition from: INITIAL, to: [INSTANCE_META_FETCHING] transition from: INSTANCE_META_FETCHING, to: [INSTANCE_META_DONE, INSTANCE_META_ERROR] transition from: INSTANCE_META_ERROR, to: [INSTANCE_META_FETCHING] - transition from: INSTANCE_META_DONE, to: [GROUPS_AND_USERS_INIT] - transition from: GROUPS_AND_USERS_INIT, to: [GROUPS_AND_USERS_FETCHING] - transition from: GROUPS_AND_USERS_FETCHING, to: [GROUPS_AND_USERS_FETCHING_ERROR, - GROUPS_AND_USERS_FETCHING_CANCELLING, - GROUPS_AND_USERS_FETCHING_DONE] - transition from: GROUPS_AND_USERS_FETCHING_CANCELLING, to: [GROUPS_AND_USERS_FETCHING_CANCELLED] - transition from: GROUPS_AND_USERS_FETCHING_ERROR, to: [GROUPS_AND_USERS_FETCHING] - transition from: GROUPS_AND_USERS_FETCHING_DONE, to: [GROUPS_AND_USERS_IMPORTING] - transition from: GROUPS_AND_USERS_IMPORTING, to: [GROUPS_AND_USERS_IMPORTING_ERROR, - GROUPS_AND_USERS_IMPORTING_DONE] - transition from: GROUPS_AND_USERS_IMPORTING_ERROR, to: [GROUPS_AND_USERS_IMPORTING] - transition from: GROUPS_AND_USERS_IMPORTING_DONE, to: [IMPORT_SCOPE] - transition from: IMPORT_SCOPE, to: [CONFIGURING] + transition from: INSTANCE_META_DONE, to: [CONFIGURING, INSTANCE_META_FETCHING] transition from: CONFIGURING, to: [PROJECTS_META_FETCHING] transition from: PROJECTS_META_FETCHING, to: [PROJECTS_META_DONE, PROJECTS_META_ERROR] transition from: PROJECTS_META_ERROR, to: [PROJECTS_META_FETCHING] @@ -98,22 +72,6 @@ module Import transition from: REVERT_CANCELLED, to: [REVERTING] transition from: REVERT_ERROR, to: [REVERTING] - after_transition(to: :groups_and_users_fetching) do |jira_import, _transition| - Import::JiraFetchGroupsAndUsersJob.perform_later(jira_import.id) - end - - after_transition(to: :groups_and_users_importing) do |jira_import, _transition| - Import::JiraImportGroupsAndUsersJob.perform_later(jira_import.id) - end - - after_transition(to: :groups_and_users_fetching_done) do |jira_import, _transition| - jira_import.update_column(:cursor, nil) - end - - after_transition(to: :groups_and_users_importing_done) do |jira_import, _transition| - jira_import.update_column(:cursor, nil) - end - after_transition(to: :reverted) do |jira_import, _transition| jira_import.update_column(:cursor, nil) end @@ -140,8 +98,6 @@ module Import [ INSTANCE_META_FETCHING, PROJECTS_META_FETCHING, - GROUPS_AND_USERS_FETCHING, - GROUPS_AND_USERS_IMPORTING, IMPORTING, REVERTING ].include?(current_state) diff --git a/app/services/import/jira_client.rb b/app/services/import/jira_client.rb index 63b31132690..ae1d6c00ea5 100644 --- a/app/services/import/jira_client.rb +++ b/app/services/import/jira_client.rb @@ -77,6 +77,10 @@ module Import get("/rest/api/2/serverInfo") end + def applicationrole + get("/rest/api/2/applicationrole") + end + def all_cluster_nodes get("/rest/api/2/cluster/nodes") end @@ -104,6 +108,16 @@ module Import get("/rest/api/2/project/type") end + def project_versions(project_id_or_key:, + start_at: 0, + max_results: 100) + get("/rest/api/2/project/#{project_id_or_key}/version", + params: { + startAt: start_at, + maxResults: max_results + }) + end + def issue_types get("/rest/api/2/issuetype") end @@ -185,6 +199,10 @@ module Import get("/rest/api/2/user", params: { key:, expand: "groups" }) end + def user_by_username(username:) + get("/rest/api/2/user", params: { username:, expand: "groups" }) + end + def groups(query: "", max_results: 1000) get("/rest/api/2/groups/picker", params: { query:, maxResults: max_results }) end diff --git a/app/views/admin/import/jira/instances/edit.html.erb b/app/views/admin/import/jira/instances/edit.html.erb index b5d57f63d19..003428b6c64 100644 --- a/app/views/admin/import/jira/instances/edit.html.erb +++ b/app/views/admin/import/jira/instances/edit.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<% html_title t(:label_administration), Jira.model_name, @jira.url %> +<% html_title t(:label_administration), Import::Jira.model_name, @jira.url %> <%= render(Primer::OpenProject::PageHeader.new) do |header| diff --git a/app/workers/import/jira_fetch_and_import_projects_job.rb b/app/workers/import/jira_fetch_and_import_projects_job.rb index ee7de1ba5e8..37a0e178364 100644 --- a/app/workers/import/jira_fetch_and_import_projects_job.rb +++ b/app/workers/import/jira_fetch_and_import_projects_job.rb @@ -30,15 +30,205 @@ module Import class JiraFetchAndImportProjectsJob < ApplicationJob + include Import::JiraOpenProjectReferenceCreation + def perform(jira_import_id) jira_import = Import::JiraImport.find(jira_import_id) + Import::JiraFetchProjectsJob.perform_now(jira_import_id) - Import::JiraImportProjectsJob.perform_now(jira_import_id) + fetch_and_save_users_data(jira_import) + + Journal::NotificationConfiguration.with(false) do + import_users(jira_import) + Import::JiraImportProjectsJob.perform_now(jira_import_id) + end jira_import.transition_to!(:imported) rescue StandardError => e jira_import&.transition_to!(:import_error, error: e.message) jira_import&.update!(job_id: nil, error: e.message) end + + private + + def fetch_and_save_users_data(jira_import) + user_keys, mention_usernames = collect_user_to_import + resolve_mention_user_keys(mention_usernames, user_keys, jira_import.client) + upsert_data = build_users_upsert_data(user_keys, jira_import) + Import::JiraUser.upsert_all(upsert_data, unique_by: %i[jira_id jira_user_key]) + end + + def collect_user_to_import + user_keys = Set.new + mention_usernames = Set.new + JiraIssue.find_each do |issue| + collect_user_keys_from_issue(user_keys, mention_usernames, issue) + end + [user_keys, mention_usernames] + end + + def collect_user_keys_from_issue(user_keys, mention_usernames, issue) + payload = issue.payload["fields"] + collect_field_user_keys(user_keys, mention_usernames, payload) + collect_comment_user_keys(user_keys, mention_usernames, payload) + collect_changelog_user_keys(user_keys, issue) + end + + def collect_field_user_keys(user_keys, mention_usernames, payload) + payload + .slice("creator", "reporter", "assignee") + .each_value { |v| user_keys << v["key"] if v.present? } + collect_markup_mentions(payload["description"], mention_usernames) + end + + def collect_comment_user_keys(user_keys, mention_usernames, payload) + payload.dig("comment", "comments").each do |c| + user_keys << c.dig("author", "key") + collect_markup_mentions(c["body"], mention_usernames) + end + end + + def resolve_mention_user_keys(mention_usernames, user_keys, jira_client) + mention_usernames.compact.each do |username| + user = jira_client.user_by_username(username:) + user_keys << user["key"] if user.present? + end + end + + def collect_changelog_user_keys(user_keys, issue) + (issue.payload.dig("changelog", "histories") || []).each do |entry| + user_keys << entry.dig("author", "key") if entry.dig("author", "key").present? + end + end + + def collect_markup_mentions(text, mention_usernames) + return if text.blank? + + ast = JiraWikiMarkup::Parser.new(text).parse + collect_mentions_from_node(ast, mention_usernames) + end + + # rubocop:disable Metrics/AbcSize + def collect_mentions_from_node(node, mention_usernames) + case node + when JiraWikiMarkup::Nodes::Mention + mention_usernames << node.username + when JiraWikiMarkup::Nodes::List + node.items.each { |item| collect_mentions_from_node(item, mention_usernames) } + when JiraWikiMarkup::Nodes::ListItem + node.children.each { |child| collect_mentions_from_node(child, mention_usernames) } + collect_mentions_from_node(node.sublist, mention_usernames) if node.sublist + else + return unless node.respond_to?(:children) + + node.children.each { |child| collect_mentions_from_node(child, mention_usernames) } + end + end + # rubocop:enable Metrics/AbcSize + + def build_users_upsert_data(user_keys, jira_import) + jira_client = jira_import.client + updated_at = Time.zone.now + created_at = updated_at + user_keys.compact.map do |jira_user_key| + # here we send a direct user request to get group memberships + # which are not returned by users_search endpoint + jira_user_by_key = jira_client.user_by_key(key: jira_user_key) + { + payload: jira_user_by_key, + jira_id: jira_import.jira_id, + jira_import_id: jira_import.id, + jira_user_key:, + created_at:, + updated_at: + } + end + end + + def import_users(jira_import) + Import::JiraUser.where(jira_import_id: jira_import.id).find_each do |jira_user| + import_user(jira_user, jira_import) + end + end + + # rubocop:disable Metrics/PerceivedComplexity + # rubocop:disable Metrics/AbcSize + def import_user(jira_user, jira_import) + call = Users::CreateService + .new(user: User.system) + .call(jira_user.to_op_attributes) + + call.on_success do |_result| + create_reference!( + op_leg: call.result, + jira_leg: jira_user, + jira_import:, + uses_existing: false + ) + end + call.on_failure do |_result| + if call.errors.find { |error| error.type == :taken }.present? + user = jira_user.try_to_find_existing_op_users.first + if user.present? + create_reference!( + op_leg: user, + jira_leg: jira_user, + jira_import:, + uses_existing: true + ) + else + raise "Existing User is expected to be found, because there was an email " \ + "or login collision. See attributes: #{jira_user.to_op_attributes}" + end + else + raise call.message + end + end + + jira_user_groups = jira_user.payload["groups"]["items"].pluck("name") + + jira_user_groups.each do |group_name| + call = Groups::CreateService + .new(user: User.system) + .call(name: group_name) + call.on_success do |result| + group = result.result + create_reference!( + op_leg: group, + jira_leg: nil, + jira_import:, + uses_existing: false + ) + end + call.on_failure do |_result| + if call.errors.find { |error| error.type == :taken }.present? + group = Group.where(name: group_name).first + if group.present? + create_reference!( + op_leg: group, + jira_leg: nil, + jira_import:, + uses_existing: true + ) + else + raise "Existing Group is expected to be found. Group name: #{group_name}" + end + else + raise call.message + end + end + member_id = Import::JiraOpenProjectReference.where( + jira_import_id: jira_import.id, + jira_entity_id: jira_user.id, + jira_entity_class: jira_user.class.to_s + ).pick(:op_entity_id) + group = Group.find_by!(name: group_name) + Groups::AddUsersService + .new(group, current_user: User.system) + .call(ids: [member_id], send_notifications: false) + end + end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize end end diff --git a/app/workers/import/jira_fetch_groups_and_users_job.rb b/app/workers/import/jira_fetch_groups_and_users_job.rb deleted file mode 100644 index b7f54581e39..00000000000 --- a/app/workers/import/jira_fetch_groups_and_users_job.rb +++ /dev/null @@ -1,142 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Import - class JiraFetchGroupsAndUsersJob < ApplicationJob - include JobIteration::Iteration - - class GroupMembersEnumerator - def initialize(jira_client, group_name:, cursor:, page_size: 30) - @jira_client = jira_client - @group_name = group_name - @page = @jira_client.group_members(group_name:, start_at: cursor, max_results: page_size) - # Jira DC has it is own page limit configuration. - # Therefore it makes sense to respect it. - server_page_size = @page["maxResults"] - @page_size = if server_page_size == page_size - page_size - else - server_page_size - end - @cursor = cursor || 0 - end - - def to_enumerator - to_enum(:each).lazy - end - - private - - def each - loop do - yield @page["values"], @page["startAt"] - - @cursor += @page_size - @page = @jira_client.group_members(group_name: @group_name, start_at: @cursor, max_results: @page_size) - # Jira DC has it is own page limit configuration. - # Therefore it makes sense to respect it. - server_page_size = @page["maxResults"] - @page_size = if @page_size == server_page_size - @page_size - else - server_page_size - end - - break if @page["isLast"] - end - end - end - - on_complete do |job| - jira_import = Import::JiraImport.find(job.arguments.first) - jira_import.transition_to!(:groups_and_users_fetching_done) - end - - around_iterate do |job, block| - block.call - jira_import = Import::JiraImport.find(job.arguments.first) - jira_import.update_column(:cursor, cursor_position) - end - - rescue_from(StandardError) do |e| - jira_import = Import::JiraImport.find(arguments.first) - jira_import.transition_to!(:groups_and_users_fetching_error, - job_id: job_id, - error_backtrace: e.backtrace, - error: e.message) - end - - # rubocop:disable Metrics/AbcSize - def build_enumerator(jira_import_id, cursor:) - jira_import = Import::JiraImport.find(jira_import_id) - group_names = jira_import.client.groups["groups"].pluck("name") - enumerator_builder.nested( - [ - ->(cursor) { enumerator_builder.array(group_names, cursor:) }, - ->(group_name, cursor) { - enumerator_builder.wrap( - enumerator_builder, - GroupMembersEnumerator.new( - jira_import.client, - group_name:, - page_size: 30, - cursor: - ).to_enumerator - ) - } - ], - cursor: - ) - end - # rubocop:enable Metrics/AbcSize - - def each_iteration(users_batch, jira_import_id) - jira_import = Import::JiraImport.find(jira_import_id) - jira_client = jira_import.client - updated_at = Time.zone.now - created_at = updated_at - users_upsert_data = users_batch.map do |jira_user| - jira_user_key = jira_user.fetch("key") - # here we send a direct user request to get group memberships - # which are not returned by users_search endpoint - jira_user_by_key = jira_client.user_by_key(key: jira_user_key) - { - payload: jira_user_by_key, - jira_id: jira_import.jira_id, - jira_import_id: jira_import.id, - jira_user_key:, - created_at:, - updated_at: - } - end - Import::JiraUser.upsert_all(users_upsert_data, unique_by: %i[jira_id jira_user_key]) - end - end -end diff --git a/app/workers/import/jira_import_groups_and_users_job.rb b/app/workers/import/jira_import_groups_and_users_job.rb deleted file mode 100644 index 879d8f27375..00000000000 --- a/app/workers/import/jira_import_groups_and_users_job.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Import - class JiraImportGroupsAndUsersJob < ApplicationJob - include JobIteration::Iteration - include Import::JiraOpenProjectReferenceCreation - - on_complete do |job| - jira_import = Import::JiraImport.find(job.arguments.first) - jira_import.transition_to!(:groups_and_users_importing_done) - end - - around_iterate do |job, block| - block.call - jira_import = Import::JiraImport.find(job.arguments.first) - jira_import.update_column(:cursor, cursor_position) - end - - rescue_from(StandardError) do |e| - jira_import = Import::JiraImport.find(arguments.first) - jira_import.transition_to!(:groups_and_users_importing_error, - job_id: job_id, - error_backtrace: e.backtrace, - error: e.message) - end - - def build_enumerator(jira_import_id, cursor:) - jira_import = Import::JiraImport.find(jira_import_id) - cursor ||= jira_import.cursor.to_i - enumerator_builder.active_record_on_records( - Import::JiraUser.where(jira_import_id:), - cursor: - ) - end - - # rubocop:disable Metrics/PerceivedComplexity - # rubocop:disable Metrics/AbcSize - def each_iteration(jira_user, jira_import_id) - jira_import = Import::JiraImport.find(jira_import_id) - call = Users::CreateService - .new(user: User.system) - .call(jira_user.to_op_attributes) - call.on_success do |_result| - create_reference!( - op_leg: call.result, - jira_leg: jira_user, - jira_import:, - uses_existing: false - ) - end - call.on_failure do |_result| - if call.errors.find { |error| error.type == :taken }.present? - user = jira_user.try_to_find_existing_op_users.first - if user.present? - create_reference!( - op_leg: user, - jira_leg: jira_user, - jira_import:, - uses_existing: true - ) - else - raise "Existing User is expected to be found, because there was an email " \ - "or login collision. See attributes: #{jira_user.to_op_attributes}" - end - else - raise call.message - end - end - - jira_user_groups = jira_user.payload["groups"]["items"].pluck("name") - - jira_user_groups.each do |group_name| - call = Groups::CreateService - .new(user: User.system) - .call(name: group_name) - call.on_success do |result| - group = result.result - create_reference!( - op_leg: group, - jira_leg: nil, - jira_import:, - uses_existing: false - ) - end - call.on_failure do |_result| - if call.errors.find { |error| error.type == :taken }.present? - group = Group.where(name: group_name).first - if group.present? - create_reference!( - op_leg: group, - jira_leg: nil, - jira_import:, - uses_existing: true - ) - else - raise "Existing Group is expected to be found. Group name: #{group_name}" - end - else - raise call.message - end - end - member_id = Import::JiraOpenProjectReference.where( - jira_import_id:, - jira_entity_id: jira_user.id, - jira_entity_class: jira_user.class.to_s - ).pick(:op_entity_id) - group = Group.find_by!(name: group_name) - Groups::AddUsersService - .new(group, current_user: User.system) - .call(ids: [member_id], send_notifications: false) - end - end - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/AbcSize - end -end diff --git a/app/workers/import/jira_import_projects_job.rb b/app/workers/import/jira_import_projects_job.rb index b3e7db86396..bb845d51f2f 100644 --- a/app/workers/import/jira_import_projects_job.rb +++ b/app/workers/import/jira_import_projects_job.rb @@ -172,14 +172,10 @@ module Import ### WORK PACKAGE # required because otherwise project.types does not include type and then wp creation fails. project.reload - author_name = jira_issue.payload.dig("fields", "creator", "name") - author = if author_name.present? - User.find_by!(login: author_name) - end - assignee_name = jira_issue.payload.dig("fields", "assignee", "name") - assigned_to = if assignee_name.present? - User.find_by!(login: assignee_name) - end + author_key = jira_issue.payload.dig("fields", "creator", "key") + author = find_user(author_key, jira_import) + assignee_key = jira_issue.payload.dig("fields", "assignee", "key") + assigned_to = find_user(assignee_key, jira_import) members = [author, assigned_to] members.uniq! members.compact! @@ -246,6 +242,20 @@ module Import private + def find_user(jira_user_key, jira_import) + return if jira_user_key.blank? + + jira_user = Import::JiraUser.find_by(jira_user_key:, jira_import:) + if jira_user + JiraOpenProjectReference.find_by!( + jira_entity_class: "Import::JiraUser", + jira_entity_id: jira_user.id + ).op_leg + else + raise "Import::JiraUser with jira_user_key #{jira_user_key} not found!" + end + end + def convert_rich_text(description) return "" if description.blank? diff --git a/app/workers/import/jira_instance_meta_data_job.rb b/app/workers/import/jira_instance_meta_data_job.rb index 66c5bac2c00..b5502b3606f 100644 --- a/app/workers/import/jira_instance_meta_data_job.rb +++ b/app/workers/import/jira_instance_meta_data_job.rb @@ -50,7 +50,6 @@ module Import available = collect_metadata(client) jira_import.update!(job_id: nil, available:, error: nil) jira_import.transition_to!(:instance_meta_done) - jira_import.transition_to!(:groups_and_users_init) rescue StandardError => e jira_import&.transition_to!(:instance_meta_error, error: e.message) jira_import&.update!(job_id: nil, error: e.message) @@ -60,6 +59,9 @@ module Import issue_types_count = client.issue_types_count statuses_count = client.statuses_count issues_count = client.issues_count + users_count = client.applicationrole.inject(0) do |users_count, application| + users_count + application["userCount"] + end projects = client.projects.map do |project| { "id" => project["id"], "key" => project["key"], "name" => project["name"] } end @@ -69,6 +71,7 @@ module Import "total_issues" => issues_count, "total_statuses" => statuses_count, "total_issue_types" => issue_types_count, + "total_users" => users_count, "server_info" => server_info } end diff --git a/config/locales/en.yml b/config/locales/en.yml index d1de7803c0c..90f567babd1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -250,7 +250,6 @@ en: button_select: "Select projects" label_selected_data: "Selected data for import" label_progress: "Fetching data from Jira..." - label_importing: "Currently importing" elements: relations: "Relations between issues" workflows: "Project-level workflows" diff --git a/docs/release-notes/17-2-0/README.md b/docs/release-notes/17-2-0/README.md index 01a3582c481..e17bfa0e2eb 100644 --- a/docs/release-notes/17-2-0/README.md +++ b/docs/release-notes/17-2-0/README.md @@ -3,12 +3,12 @@ title: OpenProject 17.2.0 sidebar_navigation: title: 17.2.0 release_version: 17.2.0 -release_date: 2026-02-26 +release_date: 2026-03-11 --- # OpenProject 17.2.0 - Release date: 2026-02-26 + Release date: 2026-03-11 We released OpenProject [OpenProject 17.2.0](https://community.openproject.org/versions/2246). The release contains several bug fixes and we recommend updating to the newest version. @@ -20,11 +20,130 @@ release_date: 2026-02-26 ## Important feature changes - +Take a look at our release video showing the most important features introduced in OpenProject 17.2.0: + +![Release video of OpenProject 17.2]( UPDATE LINK) + +### MCP Server (Enterprise add-on) + +[feature: mcp_server ] + +OpenProject 17.2 introduces the **MCP Server**, a new Enterprise add-on that lays the foundation for robust integrations between OpenProject and AI systems, including large language models (LLMs), as well as other tools that use the Model Context Protocol (MCP). This server exposes OpenProject's APIv3 resources as MCP-compatible endpoints and enables secure, authenticated access for clients such as LLMs or other MCP clients, opening the door to richer contextual interactions with your project data. + +Included in this release are administrative UI support for configuring the MCP Server, infrastructure and metadata endpoints, and integration of MCP authentication with OpenProject’s OAuth2 and API key mechanisms, including external OpenID Connect providers. An initial set of MCP tools and resources is provided to surface key entities (projects, work packages, users, etc.), and response formats can be adjusted based on your preferences. With session-cookie and bearer-token support, the MCP Server acts as a secure bridge between your OpenProject instance and external systems that operate via MCP. + +![Model context protocol settings in OpenProject administration](openproject_system_guide_new_mcp.png) + +See the [**MCP Server documentation**](../../system-admin-guide/integrations/mcp-server/) for setup and examples. + +### Reusable meeting templates (Enterprise add-on) + +[feature: meeting_templates ] + +Preparing meetings often involves recreating the same agenda structure again and again. With OpenProject 17.2, administrators can now define reusable meeting templates that provide a predefined agenda layout for their teams. + +These templates make it easy to start meetings with a proven structure instead of building the agenda from scratch each time. Even when meetings are not held regularly, teams can reuse well-designed formats that guide discussions and help ensure that important topics are addressed. + +When creating a new one-time meeting, users can choose from the available templates to automatically populate the agenda with the predefined sections and items. This saves time during setup and promotes alignment across teams. + +![Meetings templates listed in an OpenProject project](openproject_release_notes_17-2-0_meetings_template.png) + +For more details, please refer to the [Meetings documentation](../../user-guide/meetings/one-time-meetings/). + +### Allow requiring to be logged in to open external links (Enterprise add-on) + +[feature: capture_external_links ] + +Building on the external link safety options introduced in OpenProject 17.1, we’re expanding the protection capabilities in 17.2 to give administrators stronger safeguards for user interactions with links that lead outside of OpenProject. + +Administrators can now require users to be logged in before following external links. When this setting is enabled, anyone who is not authenticated will be redirected to the login page before being allowed to continue to the external destination. + +![A setting to require users to be logged in before following an external link from OpenProject](openproject_release_notes_17-2-0_external_links_log_in.png) + +Read more about [capturing external links in OpenProject](../../system-admin-guide/system-settings/external-links/). + +### Project Overview page improvements + +OpenProject 17.2 enhances the Project Overview to provide clearer financial insights, easier inline editing, and improved accessibility. Together, these updates make the Overview page a more powerful and inclusive central hub for project information. + +#### Updated Overview widgets for Budgets + +Project, program, and portfolio managers can now see key financial indicators at a glance. New budget widgets display planned budget, actual costs, spent ratio, and remaining budget, along with visual breakdowns by cost type and recent monthly actuals. Data is automatically aggregated across subprojects where applicable, giving stakeholders a consolidated financial snapshot without leaving the Overview page. + +These widgets help teams better understand financial status and trends directly within their project context. Keep in mind that both the Budgets and Time & Costs modules need to be enabled for the widgets to work. + +![An example of project budget widget on a project home page in OpenProject](openproject_release_notes_17-2-0_budget_widget.png) + +Read more about [budget widgets](../../user-guide/project-home/project-widgets/#budget-widget). + +#### Editable project description and project status widgets on a Project view tab + +The project description and project status widgets on the Overview tab are now editable inline. Based on your feedback, we’ve streamlined the experience so authorized users can update content directly where they view it, without switching to another tab. + +Note that users without edit permissions will continue to see the content in read-only mode. + +![Overview tab of a project home page in OpenProject, showing the project description widget being edited](openproject_system_guide_project_home_overview_tab_edit.png) + +#### Improves accessibility of Project Overview and Dashboard Widgets + +We have significantly improved the accessibility of widgets on both the Project Overview and Project dashboard pages. Widgets are now fully operable via keyboard, provide clearer structural semantics for screen readers, and follow WCAG 2.1 AA guidelines for focus management, labeling, and navigation order. + +These improvements ensure that project information and controls are accessible to all users, including those relying on assistive technologies. + +### Comment fields for project attributes + +OpenProject 17.2 introduces optional comment fields for project attributes, giving portfolio and project managers additional context behind selected values. Administrators can now enable a dedicated comment field for individual project attributes. This allows users to document the reasoning, assumptions, or background information related to a specific attribute value directly where it is maintained. + +Comments are displayed and edited alongside the respective attribute on the Project overview page and follow the same permission logic as the attribute itself. Changes are tracked in the project activity, included in exports, and available via the API. By adding structured context to project metadata, this enhancement improves transparency and supports better governance and decision-making across projects and teams. + +![Setting to add a comment text field to a project atttribute in OpenProject administration](openproject_release_notes_17-2-0_project_attributes_comment.png) + +Read more about [project attributes in OpenProject](../../user-guide/project-home/project-attributes/). + +### PDF export improvements + +OpenProject 17.2 enhances PDF exports to provide more exhaustive and reliable reporting. + +Work package queries can now include relationship columns in PDF reports. Related work packages are exported as structured tables within the document, making it easier to document complex relationships and dependencies in a clear and shareable format. This ensures that important contextual information is no longer lost when generating formal reports. + +In addition, PDF exports now support WebP images embedded in work package descriptions. Images in this modern format are automatically included in the generated document, improving consistency between on-screen content and exported reports. + +![Example of a PDF export in OpenProject, showing a work package that includes children and a webp image](openproject_system_guide_pdf_export.png) + +Read more about [PDF exports in OpenProject](../../user-guide/work-packages/exporting/#pdf-export). + +### UX/UI updates with the Primer design system + +OpenProject 17.2 continues the transition to the Primer design system, further unifying the look and feel across the application. + +#### Backlogs module update + +The Backlogs module has been updated using Primer components. This resulted in a cleaner layout and more consistent interaction patterns, while still preserving familiar functionality such as drag-and-drop and version-based organization. Work packages can now be viewed in a split screen for improved context and efficiency. + +![New UX/UI updates for the OpenProject Backlogs module](openproject_system_guide_new_backlog.png) + +Read more about [Backlogs](../../user-guide/backlogs-scrum/). + + +#### Improvements in administration interface + +Administrative interfaces for Custom Fields, Versions, and Groups have been further aligned with Primer. + +In particular, custom field forms are now consistently styled across all field types. Previously, the appearance varied depending on the type of custom field. This has been unified to provide a clearer and more predictable configuration experience for administrators. + ## Important updates and breaking changes - +**“Enable REST web service” renamed** +The system setting previously labeled “Enable REST web service” is now called “Enable API tokens”. This is a naming change only and does not affect functionality. + +**“Status” boards renamed to “Kanban” boards** +To better reflect their purpose and common terminology, Status boards are now called Kanban boards. Existing boards and configurations remain unchanged. + +**Improved OAuth token security for document collaboration** +OAuth tokens used for collaborative document editing (BlockNote ↔ Hocuspocus) now have shorter lifetimes and are automatically refreshed. This enhances security while keeping the editing experience unchanged. +**API tokens usable as Bearer token** +Newly generated API tokens can directly be used as Bearer tokens and do not need to be presented as basic auth credentials with the username `apikey`. This is intended to make usage of our APIs easier. The previously existing basic auth flow is still supported. @@ -33,74 +152,80 @@ release_date: 2026-02-26 -- Feature: Reusable meeting templates for meeting agendas \[[#35642](https://community.openproject.org/wp/35642)\] -- Feature: Export relationship columns in PDF report \[[#66000](https://community.openproject.org/wp/66000)\] -- Feature: Overview widget for Budgets \[[#66124](https://community.openproject.org/wp/66124)\] -- Feature: Comment fields for project attributes \[[#66343](https://community.openproject.org/wp/66343)\] -- Feature: Make project description and status widget editable on Overview tab \[[#67690](https://community.openproject.org/wp/67690)\] -- Feature: Implement token refreshing and reduce token expiration time \[[#68460](https://community.openproject.org/wp/68460)\] -- Feature: Display custom field type on form \[[#68524](https://community.openproject.org/wp/68524)\] -- Feature: MCP Server Infrastructure and Metadata Endpoint \[[#68683](https://community.openproject.org/wp/68683)\] -- Feature: Integrate MCP Authentication with OpenProject OAuth2 \[[#68685](https://community.openproject.org/wp/68685)\] -- Feature: Provide initial set of MCP Tools \[[#68686](https://community.openproject.org/wp/68686)\] -- Feature: Expose OpenProject APIv3 Entities as MCP Resources \[[#68689](https://community.openproject.org/wp/68689)\] -- Feature: Add Admin Page for MCP Configuration \[[#68690](https://community.openproject.org/wp/68690)\] -- Feature: Standardized inplace edit fields based on Primer \[[#68832](https://community.openproject.org/wp/68832)\] -- Feature: Add enterprise banner for MCP server \[[#70086](https://community.openproject.org/wp/70086)\] -- Feature: Primerize Custom Field forms \[[#70292](https://community.openproject.org/wp/70292)\] -- Feature: Support WebP images in PDF exports \[[#70333](https://community.openproject.org/wp/70333)\] -- Feature: Use autocompleters in Admin/Backlogs page \[[#71069](https://community.openproject.org/wp/71069)\] -- Feature: Improve Accessibility of Project Overview and Dashboard Widgets \[[#71075](https://community.openproject.org/wp/71075)\] -- Feature: Allow to use API Keys as Bearer tokens \[[#71147](https://community.openproject.org/wp/71147)\] -- Feature: Allow requiring to be logged in for external links \[[#71624](https://community.openproject.org/wp/71624)\] -- Feature: Primerize versions project settings \[[#71641](https://community.openproject.org/wp/71641)\] -- Feature: Primerize groups administration \[[#71642](https://community.openproject.org/wp/71642)\] -- Feature: Rename "Enable REST web service" setting \[[#71886](https://community.openproject.org/wp/71886)\] -- Feature: Reduce page size of MCP responses \[[#71977](https://community.openproject.org/wp/71977)\] -- Feature: Allow to configure MCP tool response volume \[[#71978](https://community.openproject.org/wp/71978)\] -- Feature: Allow authentication to MCP endpoint via session cookie \[[#72253](https://community.openproject.org/wp/72253)\] -- Feature: Enable Column Sorting on Versions Overview \[[#72354](https://community.openproject.org/wp/72354)\] -- Feature: MCP Server as a bridge between OpenProject and LLMs \[[#62781](https://community.openproject.org/wp/62781)\] -- Bugfix: Children column on WP list cannot be expanded \[[#64491](https://community.openproject.org/wp/64491)\] -- Bugfix: DPA/AVV cannot be downloaded \[[#67323](https://community.openproject.org/wp/67323)\] -- Bugfix: BlockNote: Color for text not applied from the block side menu \[[#67507](https://community.openproject.org/wp/67507)\] -- Bugfix: Mobile web: When deep linking to a comment the comment is not fully scrolled into view \[[#68221](https://community.openproject.org/wp/68221)\] -- Bugfix: Updating the activity anchor URL without a page load does not highlight the relevant target element \[[#68262](https://community.openproject.org/wp/68262)\] -- Bugfix: Documents index page: pagination per page options overflow on mobile \[[#68533](https://community.openproject.org/wp/68533)\] -- Bugfix: Changing the filter on the activity tab with a large number of comments and slow network/compute lacks loading state while waiting for request completion \[[#68878](https://community.openproject.org/wp/68878)\] -- Bugfix: Flickering spec ./modules/meeting/spec/features/structured\_meetings/work\_package\_meetings\_tab\_spec.rb:392 \[[#68952](https://community.openproject.org/wp/68952)\] -- Bugfix: Clicking work package tabs triggers page reload and flickering \[[#69210](https://community.openproject.org/wp/69210)\] -- Bugfix: Label for the admin document types reflects "priorities" instead of "types" in it's messaging \[[#69304](https://community.openproject.org/wp/69304)\] -- Bugfix: Infinite SAML Seeding Loop Causing Disk Space Exhaustion \[[#69339](https://community.openproject.org/wp/69339)\] -- Bugfix: "Show attachments in the files tab by default" potentially overwrites the setting for existing project \[[#69991](https://community.openproject.org/wp/69991)\] -- Bugfix: Fix accessibility errors found by ERB Lint \[[#70166](https://community.openproject.org/wp/70166)\] -- Bugfix: Missing list items when using checkboxes in tables \[[#70537](https://community.openproject.org/wp/70537)\] -- Bugfix: Documents: when document content exceeds vertical height, the cursor does not scroll into view unless there is content typed \[[#70791](https://community.openproject.org/wp/70791)\] -- Bugfix: Helm-Chart: Allow user to provide service specific annotations \[[#71055](https://community.openproject.org/wp/71055)\] -- Bugfix: Activity tab overflows with long names \[[#71106](https://community.openproject.org/wp/71106)\] -- Bugfix: Multi-user custom field requires clicking twice in order to be in focus \[[#71135](https://community.openproject.org/wp/71135)\] -- Bugfix: Status translation issue on status widget \[[#71137](https://community.openproject.org/wp/71137)\] -- Bugfix: Unnecessary empty journals on dragging work packages with automatic subjects \[[#71421](https://community.openproject.org/wp/71421)\] -- Bugfix: Sending mails via sendmail does not work \[[#71447](https://community.openproject.org/wp/71447)\] -- Bugfix: Error Content-Security-Policy with Hocuspocus integration due to URL scheme misconfiguration \[[#71888](https://community.openproject.org/wp/71888)\] -- Bugfix: BlockNote Extension: Click on WP title opens new tab and redirects the current tab \[[#71898](https://community.openproject.org/wp/71898)\] -- Bugfix: Connection error on successive navigation to and from a document \[[#71901](https://community.openproject.org/wp/71901)\] -- Bugfix: Impossible to search for archived projects, page reverts to active projects list on its own \[[#71971](https://community.openproject.org/wp/71971)\] -- Bugfix: Remove presenter field/participants references in onetime templates \[[#72222](https://community.openproject.org/wp/72222)\] -- Bugfix: Space is too small for placeholder text in Backlogs module \[[#72366](https://community.openproject.org/wp/72366)\] -- Bugfix: Missing caption in new template dialog \[[#72375](https://community.openproject.org/wp/72375)\] -- Bugfix: Wrong wording in Enterprise on-prem support token input field \[[#72459](https://community.openproject.org/wp/72459)\] +- Bugfix: Children column on WP list cannot be expanded [[#64491](https://community.openproject.org/wp/64491)] +- Bugfix: DPA/AVV cannot be downloaded [[#67323](https://community.openproject.org/wp/67323)] +- Bugfix: BlockNote: Color for text not applied from the block side menu [[#67507](https://community.openproject.org/wp/67507)] +- Bugfix: Mobile web: When deep linking to a comment the comment is not fully scrolled into view [[#68221](https://community.openproject.org/wp/68221)] +- Bugfix: Updating the activity anchor URL without a page load does not highlight the relevant target element [[#68262](https://community.openproject.org/wp/68262)] +- Bugfix: Documents index page: pagination per page options overflow on mobile [[#68533](https://community.openproject.org/wp/68533)] +- Bugfix: Changing the filter on the activity tab with a large number of comments and slow network/compute lacks loading state while waiting for request completion [[#68878](https://community.openproject.org/wp/68878)] +- Bugfix: Flickering spec ./modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb:392 [[#68952](https://community.openproject.org/wp/68952)] +- Bugfix: Clicking work package tabs triggers page reload and flickering [[#69210](https://community.openproject.org/wp/69210)] +- Bugfix: Label for the admin document types reflects "priorities" instead of "types" in its messaging [[#69304](https://community.openproject.org/wp/69304)] +- Bugfix: Infinite SAML Seeding Loop Causing Disk Space Exhaustion [[#69339](https://community.openproject.org/wp/69339)] +- Bugfix: "Show attachments in the files tab by default" potentially overwrites the setting for existing project [[#69991](https://community.openproject.org/wp/69991)] +- Bugfix: Fix accessibility errors found by ERB Lint [[#70166](https://community.openproject.org/wp/70166)] +- Bugfix: Missing list items when using checkboxes in tables [[#70537](https://community.openproject.org/wp/70537)] +- Bugfix: Documents: when document content exceeds vertical height, the cursor does not scroll into view unless there is content typed [[#70791](https://community.openproject.org/wp/70791)] +- Bugfix: Helm-Chart: Allow user to provide service specific annotations [[#71055](https://community.openproject.org/wp/71055)] +- Bugfix: Activity tab overflows with long names [[#71106](https://community.openproject.org/wp/71106)] +- Bugfix: Multi-user custom field requires clicking twice in order to be in focus [[#71135](https://community.openproject.org/wp/71135)] +- Bugfix: Status translation issue on status widget [[#71137](https://community.openproject.org/wp/71137)] +- Bugfix: Unnecessary empty journals on dragging work packages with automatic subjects [[#71421](https://community.openproject.org/wp/71421)] +- Bugfix: Sending mails via sendmail does not work [[#71447](https://community.openproject.org/wp/71447)] +- Bugfix: Error Content-Security-Policy with Hocuspocus integration due to URL scheme misconfiguration [[#71888](https://community.openproject.org/wp/71888)] +- Bugfix: BlockNote Extension: Click on WP title opens new tab and redirects the current tab [[#71898](https://community.openproject.org/wp/71898)] +- Bugfix: Connection error on successive navigation to and from a document [[#71901](https://community.openproject.org/wp/71901)] +- Bugfix: Impossible to search for archived projects, page reverts to active projects list on its own [[#71971](https://community.openproject.org/wp/71971)] +- Bugfix: Remove presenter field/participants references in onetime templates [[#72222](https://community.openproject.org/wp/72222)] +- Bugfix: Space is too small for placeholder text in Backlogs module [[#72366](https://community.openproject.org/wp/72366)] +- Bugfix: Missing caption in new template dialog [[#72375](https://community.openproject.org/wp/72375)] +- Bugfix: Wrong wording in Enterprise on-prem support token input field [[#72459](https://community.openproject.org/wp/72459)] +- Feature: Reusable meeting templates for meeting agendas [[#35642](https://community.openproject.org/wp/35642)] +- Feature: Primerized Backlogs list [[#57688](https://community.openproject.org/wp/57688)] +- Feature: Export relationship columns in PDF report [[#66000](https://community.openproject.org/wp/66000)] +- Feature: Overview widget for Budgets [[#66124](https://community.openproject.org/wp/66124)] +- Feature: Comment fields for project attributes [[#66343](https://community.openproject.org/wp/66343)] +- Feature: Make project description and status widget editable on Overview tab [[#67690](https://community.openproject.org/wp/67690)] +- Feature: Implement token refreshing and reduce token expiration time [[#68460](https://community.openproject.org/wp/68460)] +- Feature: Display custom field type on form [[#68524](https://community.openproject.org/wp/68524)] +- Feature: MCP Server Infrastructure and Metadata Endpoint [[#68683](https://community.openproject.org/wp/68683)] +- Feature: Integrate MCP Authentication with OpenProject OAuth2 [[#68685](https://community.openproject.org/wp/68685)] +- Feature: Provide initial set of MCP Tools [[#68686](https://community.openproject.org/wp/68686)] +- Feature: Expose OpenProject APIv3 Entities as MCP Resources [[#68689](https://community.openproject.org/wp/68689)] +- Feature: Add Admin Page for MCP Configuration [[#68690](https://community.openproject.org/wp/68690)] +- Feature: Standardized inplace edit fields based on Primer [[#68832](https://community.openproject.org/wp/68832)] +- Feature: Add enterprise banner for MCP server [[#70086](https://community.openproject.org/wp/70086)] +- Feature: Primerize Custom Field forms [[#70292](https://community.openproject.org/wp/70292)] +- Feature: Support WebP images in PDF exports [[#70333](https://community.openproject.org/wp/70333)] +- Feature: Rename status boards to kanban boards [[#70911](https://community.openproject.org/wp/70911)] +- Feature: Use autocompleters in Admin/Backlogs page [[#71069](https://community.openproject.org/wp/71069)] +- Feature: Improve Accessibility of Project Overview and Dashboard Widgets [[#71075](https://community.openproject.org/wp/71075)] +- Feature: Allow to use API Keys as Bearer tokens [[#71147](https://community.openproject.org/wp/71147)] +- Feature: Allow requiring to be logged in for external links [[#71624](https://community.openproject.org/wp/71624)] +- Feature: Primerize versions project settings [[#71641](https://community.openproject.org/wp/71641)] +- Feature: Primerize groups administration [[#71642](https://community.openproject.org/wp/71642)] +- Feature: Rename "Enable REST web service" setting [[#71886](https://community.openproject.org/wp/71886)] +- Feature: Reduce page size of MCP responses [[#71977](https://community.openproject.org/wp/71977)] +- Feature: Allow to configure MCP tool response volume [[#71978](https://community.openproject.org/wp/71978)] +- Feature: Allow authentication to MCP endpoint via session cookie [[#72253](https://community.openproject.org/wp/72253)] +- Feature: Enable Column Sorting on Versions Overview [[#72354](https://community.openproject.org/wp/72354)] +- Feature: Add "beta" label in MCP Admin settings headline [[#72511](https://community.openproject.org/wp/72511)] +- Feature: MCP Server as a bridge between OpenProject and LLMs [[#62781](https://community.openproject.org/wp/62781)] ## Contributions -A very special thank you goes to our sponsors for this release. +A very special thank you goes to our sponsors for this release. UPDATE +Your support, alongside the efforts of our amazing Community, helps drive these innovations. + Also a big thanks to our Community members for reporting bugs and helping us identify and provide fixes. Special thanks for reporting and finding bugs go to Holger Schantin, Stefan Weiberg, Jure Uršič, Natalie Stettner, Romain Besson. -Last but not least, we are very grateful for our very engaged translation contributors on Crowdin, who translated quite a few OpenProject strings! -Would you like to help out with translations yourself? -Then take a look at our translation guide and find out exactly how you can contribute. -It is very much appreciated! +Last but not least, we are very grateful for our very engaged translation contributors on Crowdin, who translated quite a few OpenProject strings! This release we would like to particularly thank the following users: + +UPDATE + +Would you like to help out with translations yourself? Then take a look at our [translation guide](../../contributions-guide/translate-openproject/) and find out exactly how you can contribute. It is very much appreciated! diff --git a/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_budget_widget.png b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_budget_widget.png new file mode 100644 index 00000000000..03d248d4e39 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_budget_widget.png differ diff --git a/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_external_links_log_in.png b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_external_links_log_in.png new file mode 100644 index 00000000000..fbacd8e7e33 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_external_links_log_in.png differ diff --git a/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_meetings_template.png b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_meetings_template.png new file mode 100644 index 00000000000..87aa3176a59 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_meetings_template.png differ diff --git a/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_project_attributes_comment.png b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_project_attributes_comment.png new file mode 100644 index 00000000000..f864de8c7d0 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_release_notes_17-2-0_project_attributes_comment.png differ diff --git a/docs/release-notes/17-2-0/openproject_system_guide_new_backlog.png b/docs/release-notes/17-2-0/openproject_system_guide_new_backlog.png new file mode 100644 index 00000000000..aa94c7b6ef9 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_system_guide_new_backlog.png differ diff --git a/docs/release-notes/17-2-0/openproject_system_guide_new_mcp.png b/docs/release-notes/17-2-0/openproject_system_guide_new_mcp.png new file mode 100644 index 00000000000..4de3c622477 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_system_guide_new_mcp.png differ diff --git a/docs/release-notes/17-2-0/openproject_system_guide_pdf_export.png b/docs/release-notes/17-2-0/openproject_system_guide_pdf_export.png new file mode 100644 index 00000000000..da9890151d3 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_system_guide_pdf_export.png differ diff --git a/docs/release-notes/17-2-0/openproject_system_guide_project_home_overview_tab_edit.png b/docs/release-notes/17-2-0/openproject_system_guide_project_home_overview_tab_edit.png new file mode 100644 index 00000000000..0047c5b02c7 Binary files /dev/null and b/docs/release-notes/17-2-0/openproject_system_guide_project_home_overview_tab_edit.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1a1292ff47..99d59e8be1b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -56,12 +56,12 @@ "@ng-select/ng-select": "^20.1.0", "@ngneat/content-loader": "^7.0.0", "@openproject/octicons-angular": "^19.32.0", - "@openproject/primer-view-components": "^0.81.1", + "@openproject/primer-view-components": "^0.82.0", "@openproject/reactivestates": "^3.0.1", "@primer/css": "^22.1.0", "@primer/live-region-element": "^0.8.0", "@primer/primitives": "^11.3.2", - "@primer/view-components": "npm:@openproject/primer-view-components@^0.81.1", + "@primer/view-components": "npm:@openproject/primer-view-components@^0.82.0", "@rails/request.js": "^0.0.13", "@stimulus-components/auto-submit": "^6.0.0", "@stimulus-components/reveal": "^5.0.0", @@ -7361,9 +7361,9 @@ } }, "node_modules/@openproject/primer-view-components": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.81.1.tgz", - "integrity": "sha512-4jrN87j9/T83D4oFDpAAUweXKLvi2E5Wzyh5ifZc4dK9467AWReMNcek2gIN/Ane15NIMmlei+kYjW7jrofCMw==", + "version": "0.82.0", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.82.0.tgz", + "integrity": "sha512-i7rXa5Fsf1IY//txycIUQjzhIBSGGpL8g76AQvrEzPUQ54okFt8xLmUzqBWzQiU4MWpsPYLTc19ZXK5mvfQAbw==", "license": "MIT", "dependencies": { "@github/auto-check-element": "^6.0.0", @@ -7765,9 +7765,9 @@ }, "node_modules/@primer/view-components": { "name": "@openproject/primer-view-components", - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.81.1.tgz", - "integrity": "sha512-4jrN87j9/T83D4oFDpAAUweXKLvi2E5Wzyh5ifZc4dK9467AWReMNcek2gIN/Ane15NIMmlei+kYjW7jrofCMw==", + "version": "0.82.0", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.82.0.tgz", + "integrity": "sha512-i7rXa5Fsf1IY//txycIUQjzhIBSGGpL8g76AQvrEzPUQ54okFt8xLmUzqBWzQiU4MWpsPYLTc19ZXK5mvfQAbw==", "license": "MIT", "dependencies": { "@github/auto-check-element": "^6.0.0", @@ -30245,9 +30245,9 @@ } }, "@openproject/primer-view-components": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.81.1.tgz", - "integrity": "sha512-4jrN87j9/T83D4oFDpAAUweXKLvi2E5Wzyh5ifZc4dK9467AWReMNcek2gIN/Ane15NIMmlei+kYjW7jrofCMw==", + "version": "0.82.0", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.82.0.tgz", + "integrity": "sha512-i7rXa5Fsf1IY//txycIUQjzhIBSGGpL8g76AQvrEzPUQ54okFt8xLmUzqBWzQiU4MWpsPYLTc19ZXK5mvfQAbw==", "requires": { "@github/auto-check-element": "^6.0.0", "@github/auto-complete-element": "^3.8.0", @@ -30440,9 +30440,9 @@ "integrity": "sha512-/8EDh3MmF9cbmrLETFmIuNFIdvpSCkvBlx6zzD8AZ4dZ5UYExQzFj8QAtIrRtCFJ2ZmW5QrtrPR3+JVb8KEDpg==" }, "@primer/view-components": { - "version": "npm:@openproject/primer-view-components@0.81.1", - "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.81.1.tgz", - "integrity": "sha512-4jrN87j9/T83D4oFDpAAUweXKLvi2E5Wzyh5ifZc4dK9467AWReMNcek2gIN/Ane15NIMmlei+kYjW7jrofCMw==", + "version": "npm:@openproject/primer-view-components@0.82.0", + "resolved": "https://registry.npmjs.org/@openproject/primer-view-components/-/primer-view-components-0.82.0.tgz", + "integrity": "sha512-i7rXa5Fsf1IY//txycIUQjzhIBSGGpL8g76AQvrEzPUQ54okFt8xLmUzqBWzQiU4MWpsPYLTc19ZXK5mvfQAbw==", "requires": { "@github/auto-check-element": "^6.0.0", "@github/auto-complete-element": "^3.8.0", diff --git a/frontend/package.json b/frontend/package.json index 183c87a58dd..87b5571ab20 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -111,12 +111,12 @@ "@ng-select/ng-select": "^20.1.0", "@ngneat/content-loader": "^7.0.0", "@openproject/octicons-angular": "^19.32.0", - "@openproject/primer-view-components": "^0.81.1", + "@openproject/primer-view-components": "^0.82.0", "@openproject/reactivestates": "^3.0.1", "@primer/css": "^22.1.0", "@primer/live-region-element": "^0.8.0", "@primer/primitives": "^11.3.2", - "@primer/view-components": "npm:@openproject/primer-view-components@^0.81.1", + "@primer/view-components": "npm:@openproject/primer-view-components@^0.82.0", "@rails/request.js": "^0.0.13", "@stimulus-components/auto-submit": "^6.0.0", "@stimulus-components/reveal": "^5.0.0", diff --git a/frontend/src/assets/sass/backlogs/_master_backlog.sass b/frontend/src/assets/sass/backlogs/_master_backlog.sass index 159b6dd9ae2..6500946d453 100644 --- a/frontend/src/assets/sass/backlogs/_master_backlog.sass +++ b/frontend/src/assets/sass/backlogs/_master_backlog.sass @@ -45,25 +45,6 @@ $op-backlogs-header--points-min-width-narrow: 2rem .op-backlogs-header--menu margin-left: var(--stack-gap-normal) -.op-backlogs-collapsible - display: flex - flex-wrap: wrap - align-items: center - column-gap: var(--stack-gap-normal) - row-gap: var(--base-size-4) - flex: 1 - - &--title-line - display: flex - align-items: center - gap: var(--stack-gap-condensed) - flex: 1 - min-width: fit-content - - &--description - display: inline - white-space: nowrap - .op-backlogs-story display: grid grid-template-columns: var(--control-xsmall-size) 1fr minmax($op-backlogs-header--points-min-width, max-content) auto @@ -105,32 +86,47 @@ $op-backlogs-header--points-min-width-narrow: 2rem flex: 1 1 100% overflow: hidden +//------------------ Header form responsive styling ----------------------- // Note: Using hardcoded values because Sass doesn't interpolate variables in // @container query conditions. -// Note: 655px is between $breakpoint-sm and $breakpoint-md. This was found to -// be a sensible value after initial testing with different viewports. -@container backlogsListsContainer (min-width: 655px) +// Note: 1102px is twice the small breakpoint plus the 16px gap. Since we show two boxes next to each other, +// we thus assure that the form wraps at the small breakpoint. +@container backlogsListsContainer (min-width: 1102px) .op-backlogs-header-form .FormControl-spacingWrapper flex-direction: row column-gap: 0.5rem + flex-wrap: wrap & > :first-child - flex: 1 1 auto - min-width: 33% + flex: 1 1 33% +@container backlogsListsContainer (min-width: 1550px) + .op-backlogs-header-form + .FormControl-spacingWrapper + & > :first-child + flex-basis: 25% + +//------------------ Header responsive styling ----------------------- +// Note: 1143px is matching the the breakpoint we use within PVC for the container query. +// Thus the layout switches at the exact same point. +@container backlogsListsContainer (max-width: 1443px) + .op-backlogs-header + grid-template-rows: 1fr auto + grid-template-areas: "collapsible points menu" "collapsible . . " + align-items: baseline + + &--menu + align-self: flex-start + // Unfortunately, the invisible button style bites us here again. + margin-top: -6px + + +//------------------ Mobile responsive styling ----------------------- @container backlogsListsContainer (max-width: 654px) .op-backlogs-header grid-template-columns: 1fr minmax($op-backlogs-header--points-min-width-narrow, max-content) auto - .op-backlogs-collapsible - flex-direction: column - align-items: flex-start - - &--description - [data-collapsed] & - display: none - .op-backlogs-points-label display: none diff --git a/frontend/src/global_styles/content/_widget_box.sass b/frontend/src/global_styles/content/_widget_box.sass index 7e848e6e1dc..e80094c4eb6 100644 --- a/frontend/src/global_styles/content/_widget_box.sass +++ b/frontend/src/global_styles/content/_widget_box.sass @@ -44,6 +44,10 @@ $widget-box--enumeration-width: 20px &_half-width flex-basis: calc(25% - var(--stack-gap-normal)) + min-width: 200px + + @media screen and (max-width: $breakpoint-lg) + flex-basis: calc(50% - var(--stack-gap-normal)) &_full-width flex-basis: 100% @@ -188,6 +192,7 @@ $widget-box--enumeration-width: 20px font-weight: var(--base-text-weight-bold) color: var(--fgColor-default) !important padding: 0 !important + @include text-shortener .toolbar--editable-toolbar height: 20px diff --git a/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb b/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb index ffe6cc10a50..6ce78e50a6d 100644 --- a/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb +++ b/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb @@ -1,8 +1,8 @@ -<%= render(Primer::Beta::BorderBox.new) do |component| %> +<%= render(Primer::Beta::BorderBox.new(list_id: "border-box-list")) do |component| %> <% component.with_header do %> <%= render(Primer::OpenProject::FlexLayout.new(align_items: :flex_start)) do |flex| %> <%= flex.with_column(flex: 1, pt: 1) do %> - <%= render(Primer::OpenProject::BorderBox::CollapsibleHeader.new(box: component)) do |header| + <%= render(Primer::OpenProject::BorderBox::CollapsibleHeader.new(collapsible_id: "border-box-list")) do |header| header.with_count(count: 3) header.with_title { "Collapsible title" } header.with_description { "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor" } diff --git a/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb index 9d6f47b1518..394c4c6b894 100644 --- a/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb +++ b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb @@ -33,10 +33,10 @@ See COPYRIGHT and LICENSE files for more details. <% grid.with_area(:collapsible) do %> <%= render( - Backlogs::CollapsibleComponent.new( + Primer::OpenProject::BorderBox::CollapsibleHeader.new( collapsible_id: "#{dom_id(backlog)}-list", - toggle_label: t(".label_toggle_backlog", name: sprint.name), - collapsed: + collapsed:, + multi_line: false ) ) do |collapsible| collapsible.with_title { sprint.name } diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.html.erb b/modules/backlogs/app/components/backlogs/collapsible_component.html.erb deleted file mode 100644 index efe6818b35a..00000000000 --- a/modules/backlogs/app/components/backlogs/collapsible_component.html.erb +++ /dev/null @@ -1,61 +0,0 @@ -<%# -- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++# %> - -<%= render(Primer::BaseComponent.new(**@system_arguments)) do %> - <%= - render( - Primer::BaseComponent.new( - tag: :div, - role: "button", - tabindex: 0, - classes: "op-backlogs-collapsible", - aria: { - label: @toggle_label, - controls: @collapsible_id, - expanded: !@collapsed - }, - data: { - target: "collapsible-header.triggerElement", - action: "click:collapsible-header#toggle keydown:collapsible-header#toggleViaKeyboard" - } - ) - ) do - %> - <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--title-line")) do %> - <%= title %> - <%= count %> - <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--toggle")) do %> - <%= render(Primer::Beta::Octicon.new(icon: "chevron-up", hidden: @collapsed, data: { target: "collapsible-header.arrowUp" })) %> - <%= render(Primer::Beta::Octicon.new(icon: "chevron-down", hidden: !@collapsed, data: { target: "collapsible-header.arrowDown" })) %> - <% end %> - <% end %> - - <%= description %> - <% end %> -<% end %> diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.rb b/modules/backlogs/app/components/backlogs/collapsible_component.rb deleted file mode 100644 index 3ab04940271..00000000000 --- a/modules/backlogs/app/components/backlogs/collapsible_component.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Backlogs - class CollapsibleComponent < Primer::Component - include OpPrimer::ComponentHelpers - - renders_one :title, ->(**system_arguments) { - system_arguments[:classes] = class_names( - system_arguments[:classes], - "op-backlogs-collapsible--title", - "Box-title" - ) - - Primer::Beta::Truncate.new(tag: :h3, **system_arguments) - } - - renders_one :count, ->(**system_arguments) { - system_arguments[:mr] ||= 2 - system_arguments[:scheme] ||= :primary - system_arguments[:classes] = class_names( - system_arguments[:classes], - "op-backlogs-collapsible--count" - ) - - Primer::Beta::Counter.new(**system_arguments) - } - - renders_one :description, ->(**system_arguments) { - system_arguments[:color] ||= :subtle - system_arguments[:hidden] = @collapsed - system_arguments[:classes] = class_names( - system_arguments[:classes], - "op-backlogs-collapsible--description" - ) - - Primer::Beta::Text.new(**system_arguments) - } - - def initialize(collapsible_id:, toggle_label:, collapsed: false, **system_arguments) - super() - - @collapsible_id = collapsible_id - @toggle_label = toggle_label - @collapsed = collapsed - - @system_arguments = deny_tag_argument(**system_arguments) - @system_arguments[:tag] = :"collapsible-header" - @system_arguments[:classes] = class_names( - system_arguments[:classes], - "CollapsibleHeader", - "CollapsibleHeader--collapsed" => @collapsed - ) - if @collapsed - @system_arguments[:data] = merge_data( - @system_arguments, { - data: { collapsed: @collapsed } - } - ) - end - end - end -end diff --git a/modules/backlogs/app/controllers/rb_application_controller.rb b/modules/backlogs/app/controllers/rb_application_controller.rb index 91f08ef4deb..83906ae31a7 100644 --- a/modules/backlogs/app/controllers/rb_application_controller.rb +++ b/modules/backlogs/app/controllers/rb_application_controller.rb @@ -46,7 +46,7 @@ class RbApplicationController < ApplicationController # because of strong params, we want to pluck this variable out right now, # otherwise it causes issues where we are doing `attributes=`. if (@sprint_id = params.delete(:sprint_id)) - @sprint = Sprint.visible.where(project: @project).find(@sprint_id) + @sprint = Sprint.visible.apply_to(@project).find(@sprint_id) end end diff --git a/modules/backlogs/app/forms/backlogs/backlog_header_form.rb b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb index 65625de8dfd..11b07fffed6 100644 --- a/modules/backlogs/app/forms/backlogs/backlog_header_form.rb +++ b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb @@ -45,6 +45,8 @@ module Backlogs f.group(layout: :horizontal) do |dates| dates.single_date_picker( name: :start_date, + input_width: :xsmall, + full_width: false, label: attribute_name(:start_date), placeholder: attribute_name(:start_date), visually_hide_label: true, @@ -52,6 +54,8 @@ module Backlogs ) dates.single_date_picker( name: :effective_date, + input_width: :xsmall, + full_width: false, label: attribute_name(:effective_date), placeholder: attribute_name(:effective_date), visually_hide_label: true, diff --git a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb index 90ce9199be1..41d6d5e128d 100644 --- a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb +++ b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb @@ -48,30 +48,60 @@ RSpec.describe RbStoriesController do end describe "PUT #move" do - let(:other_sprint) { create(:sprint, name: "Sprint 2", project:) } + context "with a version from the same project" do + let(:other_sprint) { create(:sprint, name: "Sprint 2", project:) } - it "responds with success", :aggregate_failures do - put :move, params: { - project_id: project.id, - sprint_id: sprint.id, - id: story.id, - target_id: other_sprint.id, - position: 1 - }, - format: :turbo_stream + it "responds with success", :aggregate_failures do + put :move, params: { + project_id: project.id, + sprint_id: sprint.id, + id: story.id, + target_id: other_sprint.id, + position: 1 + }, + format: :turbo_stream - expect(response).to be_successful - expect(response).to have_http_status :ok - expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" - expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_sprint.id}" - expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" - expect(assigns(:project)).to eq(project) - expect(assigns(:sprint)).to eq(sprint) - expect(assigns(:story)).to eq(story) - expect(assigns(:backlog)).to be_a(Backlog) + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:story)).to eq(story) + expect(assigns(:backlog)).to be_a(Backlog) + end + end + + context "with a version from another project" do + let(:other_project) { create(:project) } + let(:other_sprint) { create(:sprint, name: "Sprint 2", project: other_project, sharing: "system") } + let(:story) { create(:story, status:, version: other_sprint, project:) } + + it "responds with success", :aggregate_failures do + put :move, params: { + project_id: project.id, + sprint_id: other_sprint.id, + id: story.id, + target_id: sprint.id, + position: 1 + }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_sprint.id}" + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(other_sprint) + expect(assigns(:story)).to eq(story) + expect(assigns(:backlog)).to be_a(Backlog) + end end context "when service call fails" do + let(:other_sprint) { create(:sprint, name: "Sprint 2", project:) } let(:service_result) { ServiceResult.failure(message: "Something went wrong") } before do diff --git a/modules/backlogs/spec/support/pages/backlogs.rb b/modules/backlogs/spec/support/pages/backlogs.rb index a368f1ab6ea..a269ba432d4 100644 --- a/modules/backlogs/spec/support/pages/backlogs.rb +++ b/modules/backlogs/spec/support/pages/backlogs.rb @@ -115,7 +115,7 @@ module Pages def fold_backlog(backlog) within_backlog(backlog) do - find(:button, accessible_name: "Collapse/Expand #{backlog.name}").click + find(:button, aria: { controls: "backlog_#{backlog.id}-list" }).click end end diff --git a/modules/costs/app/components/costs/widgets/actual_costs.rb b/modules/costs/app/components/costs/widgets/actual_costs.rb index 551d7003185..10ad507e3a7 100644 --- a/modules/costs/app/components/costs/widgets/actual_costs.rb +++ b/modules/costs/app/components/costs/widgets/actual_costs.rb @@ -40,7 +40,9 @@ module Costs def initialize(...) super - @aggregated_costs = Costs::AggregatedCosts.new(project:, current_user:, date_range: Date.current.all_year) + start_date = Date.current.beginning_of_month - 11.months + end_date = Date.current.end_of_month + @aggregated_costs = Costs::AggregatedCosts.new(project:, current_user:, date_range: start_date..end_date) end def render? diff --git a/modules/meeting/app/components/meeting_sections/backlogs/header_component.html.erb b/modules/meeting/app/components/meeting_sections/backlogs/header_component.html.erb index 54f48c444a0..95f702004f8 100644 --- a/modules/meeting/app/components/meeting_sections/backlogs/header_component.html.erb +++ b/modules/meeting/app/components/meeting_sections/backlogs/header_component.html.erb @@ -4,7 +4,8 @@ flex.with_column(flex: 1, pt: 1) do render( Primer::OpenProject::BorderBox::CollapsibleHeader.new( - box: @box, collapsed: @collapsed + collapsible_id: "#{dom_id(@backlog)}-list", + collapsed: @collapsed ) ) do |header| header.with_title(font_weight: :bold) { @backlog.title } diff --git a/modules/meeting/app/components/meeting_sections/backlogs/header_component.rb b/modules/meeting/app/components/meeting_sections/backlogs/header_component.rb index e45eaf87e80..d76d0b4123e 100644 --- a/modules/meeting/app/components/meeting_sections/backlogs/header_component.rb +++ b/modules/meeting/app/components/meeting_sections/backlogs/header_component.rb @@ -34,12 +34,11 @@ module MeetingSections include OpTurbo::Streamable include OpPrimer::ComponentHelpers - def initialize(backlog:, collapsed:, current_occurrence:, box: nil) + def initialize(backlog:, collapsed:, current_occurrence:) super @backlog = backlog @meeting = backlog.meeting - @box = box @current_occurrence = current_occurrence # When a specific collapsed state is needed, collapsed is passed in as either true or false diff --git a/modules/meeting/app/components/meeting_sections/show_component.html.erb b/modules/meeting/app/components/meeting_sections/show_component.html.erb index 53eff3ec45d..85f01caadc0 100644 --- a/modules/meeting/app/components/meeting_sections/show_component.html.erb +++ b/modules/meeting/app/components/meeting_sections/show_component.html.erb @@ -1,13 +1,13 @@ <%= component_wrapper(class: "op-meeting-section-container", data: { test_selector: "meeting-section-container-#{@meeting_section.id}" }.merge(draggable_item_config)) do - render(border_box_container(mt: 3, data: drag_and_drop_target_config)) do |component| + render(border_box_container(mt: 3, data: drag_and_drop_target_config, list_id: "#{dom_id(@meeting_section)}-list")) do |component| if render_section_wrapper? component.with_header(font_weight: :bold, pl: 0) do render(MeetingSections::HeaderComponent.new(meeting_section: @meeting_section, state: @state)) end elsif render_backlog? component.with_header do - render(MeetingSections::Backlogs::HeaderComponent.new(backlog: @meeting_section, box: component, collapsed: @collapsed, current_occurrence: @current_occurrence)) + render(MeetingSections::Backlogs::HeaderComponent.new(backlog: @meeting_section, collapsed: @collapsed, current_occurrence: @current_occurrence)) end end if render_new_button_in_section? 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 1c436b926c3..bc8074c4c42 100644 --- a/spec/controllers/admin/import/jira/import_runs_controller_spec.rb +++ b/spec/controllers/admin/import/jira/import_runs_controller_spec.rb @@ -39,45 +39,26 @@ RSpec.describe Admin::Import::Jira::ImportRunsController do # Walks a JiraImport through the state machine to reach the target state. # All after_transition job callbacks are stubbed. def transition_to_state(jira_import, target_state) - gu_prefix = %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_done - groups_and_users_importing groups_and_users_importing_done import_scope] + prefix = %w[instance_meta_fetching instance_meta_done] paths = { "initial" => [], "instance_meta_fetching" => %w[instance_meta_fetching], "instance_meta_error" => %w[instance_meta_fetching instance_meta_error], "instance_meta_done" => %w[instance_meta_fetching instance_meta_done], - "groups_and_users_init" => %w[instance_meta_fetching instance_meta_done groups_and_users_init], - "groups_and_users_fetching" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching], - "groups_and_users_fetching_error" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_error], - "groups_and_users_fetching_done" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_done], - "groups_and_users_importing" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_done - groups_and_users_importing], - "groups_and_users_importing_error" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_done - groups_and_users_importing groups_and_users_importing_error], - "groups_and_users_importing_done" => %w[instance_meta_fetching instance_meta_done groups_and_users_init - groups_and_users_fetching groups_and_users_fetching_done - groups_and_users_importing groups_and_users_importing_done], - "import_scope" => gu_prefix, - "configuring" => gu_prefix + %w[configuring], - "projects_meta_fetching" => gu_prefix + %w[configuring projects_meta_fetching], - "projects_meta_error" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_error], - "projects_meta_done" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done], - "importing" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done importing], - "import_error" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done importing import_error], - "imported" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported], - "completed" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported completed], - "reverting" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported reverting], - "revert_error" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done - importing imported reverting revert_error], - "reverted" => gu_prefix + %w[configuring projects_meta_fetching projects_meta_done - importing imported reverting reverted] + "configuring" => prefix + %w[configuring], + "projects_meta_fetching" => prefix + %w[configuring projects_meta_fetching], + "projects_meta_error" => prefix + %w[configuring projects_meta_fetching projects_meta_error], + "projects_meta_done" => prefix + %w[configuring projects_meta_fetching projects_meta_done], + "importing" => prefix + %w[configuring projects_meta_fetching projects_meta_done importing], + "import_error" => prefix + %w[configuring projects_meta_fetching projects_meta_done importing import_error], + "imported" => prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported], + "completed" => prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported completed], + "reverting" => prefix + %w[configuring projects_meta_fetching projects_meta_done importing imported reverting], + "revert_error" => prefix + %w[configuring projects_meta_fetching projects_meta_done + importing imported reverting revert_error], + "reverted" => prefix + %w[configuring projects_meta_fetching projects_meta_done + importing imported reverting reverted] } steps = paths.fetch(target_state.to_s) @@ -90,8 +71,6 @@ RSpec.describe Admin::Import::Jira::ImportRunsController do allow(Import::JiraProjectsMetaDataJob).to receive(:perform_later).and_return(double(job_id: "job-stub")) allow(Import::JiraFetchAndImportProjectsJob).to receive(:perform_later).and_return(double(job_id: "job-stub")) allow(Import::JiraRevertImportJob).to receive(:perform_later).and_return(double(job_id: "job-stub")) - allow(Import::JiraFetchGroupsAndUsersJob).to receive(:perform_later).and_return(double(job_id: "job-stub")) - allow(Import::JiraImportGroupsAndUsersJob).to receive(:perform_later).and_return(double(job_id: "job-stub")) end context "when user is not an admin" do @@ -169,57 +148,18 @@ RSpec.describe Admin::Import::Jira::ImportRunsController do end end - context "when step is fetch_groups_and_users" do - before { transition_to_state(jira_import, "groups_and_users_init") } + context "when step is fetch_instance_meta from instance_meta_error" do + before { transition_to_state(jira_import, "instance_meta_error") } - it "transitions to groups_and_users_fetching and triggers the job" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "fetch_groups_and_users" }, format: :turbo_stream - expect(jira_import.current_state).to eq("groups_and_users_fetching") - expect(Import::JiraFetchGroupsAndUsersJob).to have_received(:perform_later).with(jira_import.id) - end - end - - context "when step is fetch_groups_and_users from groups_and_users_fetching_error" do - before { transition_to_state(jira_import, "groups_and_users_fetching_error") } - - it "retries groups and users fetching" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "fetch_groups_and_users" }, format: :turbo_stream - expect(jira_import.current_state).to eq("groups_and_users_fetching") - expect(Import::JiraFetchGroupsAndUsersJob).to have_received(:perform_later).with(jira_import.id).twice - end - end - - context "when step is import_groups_and_users" do - before { transition_to_state(jira_import, "groups_and_users_fetching_done") } - - it "transitions to groups_and_users_importing and triggers the job" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "import_groups_and_users" }, format: :turbo_stream - expect(jira_import.current_state).to eq("groups_and_users_importing") - expect(Import::JiraImportGroupsAndUsersJob).to have_received(:perform_later).with(jira_import.id) - end - end - - context "when step is import_groups_and_users from groups_and_users_importing_error" do - before { transition_to_state(jira_import, "groups_and_users_importing_error") } - - it "retries groups and users importing" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "import_groups_and_users" }, format: :turbo_stream - expect(jira_import.current_state).to eq("groups_and_users_importing") - expect(Import::JiraImportGroupsAndUsersJob).to have_received(:perform_later).with(jira_import.id).twice - end - end - - context "when step is import_scope" do - before { transition_to_state(jira_import, "groups_and_users_importing_done") } - - it "transitions to import_scope" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "import_scope" }, format: :turbo_stream - expect(jira_import.current_state).to eq("import_scope") + it "retries instance meta fetching" do + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "fetch_instance_meta" }, format: :turbo_stream + expect(jira_import.current_state).to eq("instance_meta_fetching") + expect(Import::JiraInstanceMetaDataJob).to have_received(:perform_later).with(jira_import.id).twice end end context "when step is configure" do - before { transition_to_state(jira_import, "import_scope") } + before { transition_to_state(jira_import, "instance_meta_done") } it "transitions to configuring" do post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "configure" }, format: :turbo_stream @@ -277,6 +217,16 @@ RSpec.describe Admin::Import::Jira::ImportRunsController do end end + context "when step is revert from import_error" do + before { transition_to_state(jira_import, "import_error") } + + it "transitions to reverting and triggers the job" do + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "revert" }, format: :turbo_stream + expect(jira_import.current_state).to eq("reverting") + expect(Import::JiraRevertImportJob).to have_received(:perform_later).with(jira_import.id) + end + end + context "when step is revert from revert_error" do before { transition_to_state(jira_import, "revert_error") } @@ -326,7 +276,7 @@ RSpec.describe Admin::Import::Jira::ImportRunsController do before { transition_to_state(jira_import, "imported") } it "handles the transition error via turbo_stream" do - post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "init" }, format: :turbo_stream + post :continue, params: { jira_id: jira.id, id: jira_import.id, step: "fetch_instance_meta" }, format: :turbo_stream expect(jira_import.current_state).to eq("imported") expect(response).to have_http_status(:ok) expect(response.media_type).to eq("text/vnd.turbo-stream.html")