Merge branch 'release/17.2' into dev
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
%>
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
<!-- Inform about the major features in this section -->
|
||||
Take a look at our release video showing the most important features introduced in OpenProject 17.2.0:
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
<!-- Remove this section if empty, add to it in pull requests linking to tickets and provide information -->
|
||||
**“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.
|
||||
|
||||
<!--more-->
|
||||
|
||||
@@ -33,74 +152,80 @@ release_date: 2026-02-26
|
||||
<!-- Warning: Anything within the below lines will be automatically removed by the release script -->
|
||||
<!-- BEGIN AUTOMATED SECTION -->
|
||||
|
||||
- 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)]
|
||||
|
||||
<!-- END AUTOMATED SECTION -->
|
||||
<!-- Warning: Anything above this line will be automatically removed by the release script -->
|
||||
|
||||
## 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!
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
After Width: | Height: | Size: 146 KiB |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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")
|
||||
|
||||