Merge branch 'release/17.2' into dev

This commit is contained in:
OpenProject Actions CI
2026-03-04 21:52:20 +00:00
45 changed files with 601 additions and 914 deletions
+1 -1
View File
@@ -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"
+3 -3
View File
@@ -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
+1 -45
View File
@@ -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)
+18
View File
@@ -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
+18 -8
View File
@@ -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
-1
View File
@@ -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"
+191 -66
View File
@@ -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:
![Release video of OpenProject 17.2]( UPDATE LINK)
### MCP Server (Enterprise add-on)
[feature: mcp_server ]
OpenProject 17.2 introduces the **MCP Server**, a new Enterprise add-on that lays the foundation for robust integrations between OpenProject and AI systems, including large language models (LLMs), as well as other tools that use the Model Context Protocol (MCP). This server exposes OpenProject's APIv3 resources as MCP-compatible endpoints and enables secure, authenticated access for clients such as LLMs or other MCP clients, opening the door to richer contextual interactions with your project data.
Included in this release are administrative UI support for configuring the MCP Server, infrastructure and metadata endpoints, and integration of MCP authentication with OpenProjects OAuth2 and API key mechanisms, including external OpenID Connect providers. An initial set of MCP tools and resources is provided to surface key entities (projects, work packages, users, etc.), and response formats can be adjusted based on your preferences. With session-cookie and bearer-token support, the MCP Server acts as a secure bridge between your OpenProject instance and external systems that operate via MCP.
![Model context protocol settings in OpenProject administration](openproject_system_guide_new_mcp.png)
See the [**MCP Server documentation**](../../system-admin-guide/integrations/mcp-server/) for setup and examples.
### Reusable meeting templates (Enterprise add-on)
[feature: meeting_templates ]
Preparing meetings often involves recreating the same agenda structure again and again. With OpenProject 17.2, administrators can now define reusable meeting templates that provide a predefined agenda layout for their teams.
These templates make it easy to start meetings with a proven structure instead of building the agenda from scratch each time. Even when meetings are not held regularly, teams can reuse well-designed formats that guide discussions and help ensure that important topics are addressed.
When creating a new one-time meeting, users can choose from the available templates to automatically populate the agenda with the predefined sections and items. This saves time during setup and promotes alignment across teams.
![Meetings templates listed in an OpenProject project](openproject_release_notes_17-2-0_meetings_template.png)
For more details, please refer to the [Meetings documentation](../../user-guide/meetings/one-time-meetings/).
### Allow requiring to be logged in to open external links (Enterprise add-on)
[feature: capture_external_links ]
Building on the external link safety options introduced in OpenProject 17.1, were expanding the protection capabilities in 17.2 to give administrators stronger safeguards for user interactions with links that lead outside of OpenProject.
Administrators can now require users to be logged in before following external links. When this setting is enabled, anyone who is not authenticated will be redirected to the login page before being allowed to continue to the external destination.
![A setting to require users to be logged in before following an external link from OpenProject](openproject_release_notes_17-2-0_external_links_log_in.png)
Read more about [capturing external links in OpenProject](../../system-admin-guide/system-settings/external-links/).
### Project Overview page improvements
OpenProject 17.2 enhances the Project Overview to provide clearer financial insights, easier inline editing, and improved accessibility. Together, these updates make the Overview page a more powerful and inclusive central hub for project information.
#### Updated Overview widgets for Budgets
Project, program, and portfolio managers can now see key financial indicators at a glance. New budget widgets display planned budget, actual costs, spent ratio, and remaining budget, along with visual breakdowns by cost type and recent monthly actuals. Data is automatically aggregated across subprojects where applicable, giving stakeholders a consolidated financial snapshot without leaving the Overview page.
These widgets help teams better understand financial status and trends directly within their project context. Keep in mind that both the Budgets and Time & Costs modules need to be enabled for the widgets to work.
![An example of project budget widget on a project home page in OpenProject](openproject_release_notes_17-2-0_budget_widget.png)
Read more about [budget widgets](../../user-guide/project-home/project-widgets/#budget-widget).
#### Editable project description and project status widgets on a Project view tab
The project description and project status widgets on the Overview tab are now editable inline. Based on your feedback, weve streamlined the experience so authorized users can update content directly where they view it, without switching to another tab.
Note that users without edit permissions will continue to see the content in read-only mode.
![Overview tab of a project home page in OpenProject, showing the project description widget being edited](openproject_system_guide_project_home_overview_tab_edit.png)
#### Improves accessibility of Project Overview and Dashboard Widgets
We have significantly improved the accessibility of widgets on both the Project Overview and Project dashboard pages. Widgets are now fully operable via keyboard, provide clearer structural semantics for screen readers, and follow WCAG 2.1 AA guidelines for focus management, labeling, and navigation order.
These improvements ensure that project information and controls are accessible to all users, including those relying on assistive technologies.
### Comment fields for project attributes
OpenProject 17.2 introduces optional comment fields for project attributes, giving portfolio and project managers additional context behind selected values. Administrators can now enable a dedicated comment field for individual project attributes. This allows users to document the reasoning, assumptions, or background information related to a specific attribute value directly where it is maintained.
Comments are displayed and edited alongside the respective attribute on the Project overview page and follow the same permission logic as the attribute itself. Changes are tracked in the project activity, included in exports, and available via the API. By adding structured context to project metadata, this enhancement improves transparency and supports better governance and decision-making across projects and teams.
![Setting to add a comment text field to a project atttribute in OpenProject administration](openproject_release_notes_17-2-0_project_attributes_comment.png)
Read more about [project attributes in OpenProject](../../user-guide/project-home/project-attributes/).
### PDF export improvements
OpenProject 17.2 enhances PDF exports to provide more exhaustive and reliable reporting.
Work package queries can now include relationship columns in PDF reports. Related work packages are exported as structured tables within the document, making it easier to document complex relationships and dependencies in a clear and shareable format. This ensures that important contextual information is no longer lost when generating formal reports.
In addition, PDF exports now support WebP images embedded in work package descriptions. Images in this modern format are automatically included in the generated document, improving consistency between on-screen content and exported reports.
![Example of a PDF export in OpenProject, showing a work package that includes children and a webp image](openproject_system_guide_pdf_export.png)
Read more about [PDF exports in OpenProject](../../user-guide/work-packages/exporting/#pdf-export).
### UX/UI updates with the Primer design system
OpenProject 17.2 continues the transition to the Primer design system, further unifying the look and feel across the application.
#### Backlogs module update
The Backlogs module has been updated using Primer components. This resulted in a cleaner layout and more consistent interaction patterns, while still preserving familiar functionality such as drag-and-drop and version-based organization. Work packages can now be viewed in a split screen for improved context and efficiency.
![New UX/UI updates for the OpenProject Backlogs module](openproject_system_guide_new_backlog.png)
Read more about [Backlogs](../../user-guide/backlogs-scrum/).
#### Improvements in administration interface
Administrative interfaces for Custom Fields, Versions, and Groups have been further aligned with Primer.
In particular, custom field forms are now consistently styled across all field types. Previously, the appearance varied depending on the type of custom field. This has been unified to provide a clearer and more predictable configuration experience for administrators.
## Important updates and breaking changes
<!-- 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 &quot;Enable REST web service&quot; 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 &quot;priorities&quot; instead of &quot;types&quot; in it&#39;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: &quot;Show attachments in the files tab by default&quot; 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!
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

+14 -14
View File
@@ -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",
+2 -2
View File
@@ -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")