Merge branch 'release/17.5' into dev

This commit is contained in:
OpenProject Actions CI
2026-05-28 12:18:34 +00:00
78 changed files with 193 additions and 51 deletions
@@ -51,12 +51,24 @@ module ProjectIdentifiers
attr_reader :project
def restore_classic_identifier
generator = ProjectIdentifiers::ClassicIdentifierSuggestionGenerator.new
classic = generator.restore_identifier(project) || generator.suggest_identifier(project.name)
classic_id = identifier_generator.restore_identifier(project) ||
identifier_generator.suggest_identifier(project.name)
# Suppress notifications: this is a background system operation, not a user edit.
Journal::NotificationConfiguration.with(false) do
project.update!(identifier: classic)
project.update!(identifier: classic_id)
rescue ActiveRecord::RecordInvalid => e
handle_update_failure(classic_id, e)
end
end
def handle_update_failure(classic_id, error)
Rails.logger.warn "#{self.class}: Could not set identifier '#{classic_id}' for project #{project.id}; " \
"falling back to a randomized suffix. (#{error.message})"
project.update!(identifier: "project-#{SecureRandom.alphanumeric(5).downcase}")
end
def identifier_generator
@identifier_generator ||= ProjectIdentifiers::ClassicIdentifierSuggestionGenerator.new
end
end
end
@@ -93,7 +93,9 @@ See COPYRIGHT and LICENSE files for more details.
title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_supported_data"),
list: [
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.projects"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.project_ids"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issues"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_ids"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_details"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.custom_fields"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.users"),
@@ -107,8 +109,6 @@ See COPYRIGHT and LICENSE files for more details.
render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new(
title: I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_coming_soon"),
list: [
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.project_ids"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.issue_ids"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.relations"),
I18n.t(:"admin.jira.run.wizard.sections.import_scope.elements.sprints"),
].map { |label| { label:, checked: false } },
+1 -1
View File
@@ -42,7 +42,7 @@ See COPYRIGHT and LICENSE files for more details.
<ul>
<% @work_packages.each do |wp| %>
<li>
<%= link_to(h("#{wp.type} ##{wp.id}"), work_package_path(wp)) %>:
<%= link_to(h("#{wp.type} #{wp.formatted_id}"), work_package_path(wp)) %>:
<%= wp.subject %>
</li>
<% end %>
+2 -2
View File
@@ -148,7 +148,7 @@ en:
custom_field_creation_failed: "Failed to create custom field '%{name}': %{message}"
semantic_identifiers_must_be_enabled:
title: "Project-based semantic identifiers must be enabled."
description: "Jira uses work items identifiers consisting of project key and a sequence number (PRJ-123). OpenProject also supports it, but it needs to be enabled [here](link)."
description: "Jira uses issue identifiers consisting of a project key and a sequence number (PRJ-123). OpenProject also supports it, but it needs to be enabled [here](link)."
blank:
title: "No Jira hosts configured yet"
description: "Configure a Jira host to start importing items from Jira to this OpenProject instance."
@@ -295,7 +295,7 @@ en:
elements:
relations: "Relations between issues"
project_ids: "Project identifiers"
issue_ids: "Issues identifiers"
issue_ids: "Issue identifiers"
sprints: "Sprint assignments"
workflows: "Project-level workflows"
schemes: "Schemas"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+1 -1
View File
@@ -207,7 +207,7 @@ We want to thank Community member [@cheezzz](https://github.com/cheezzz) for con
- Bugfix: Sharing permission dependencies are not migrated \[[#72801](https://community.openproject.org/wp/72801)\]
- Bugfix: Attribute help text not shown on project home page (overview tab) \[[#72807](https://community.openproject.org/wp/72807)\]
- Bugfix: Provide more details when project with identifier exists in OpenProject \[[#72809](https://community.openproject.org/wp/72809)\]
- Bugfix: No line break in table cells after ordered/undordered/task list \[[#72846](https://community.openproject.org/wp/72846)\]
- Bugfix: No line break in table cells after ordered/unordered/task list \[[#72846](https://community.openproject.org/wp/72846)\]
- Bugfix: Right side bar from Overview page is read out before main page content \[[#72850](https://community.openproject.org/wp/72850)\]
- Bugfix: Cannot accept meeting series invite (because newer version of the appointment already exists) \[[#72865](https://community.openproject.org/wp/72865)\]
- Bugfix: Template drop-down is not showing if user starts meeting creation from global meeting index \[[#72873](https://community.openproject.org/wp/72873)\]
+1 -1
View File
@@ -249,7 +249,7 @@ For more information, please see the [GitHub advisory #GHSA-p9gq-hrgh-2645](http
- Bugfix: NoMethodError in Calendar::ICalController#show \[[#71354](https://community.openproject.org/wp/71354)\]
- Bugfix: User cannot create a WP with auto generated subject \[[#72207](https://community.openproject.org/wp/72207)\]
- Bugfix: Backlogs: Not able to navigate through the more menu with arrows \[[#72460](https://community.openproject.org/wp/72460)\]
- Bugfix: Missing feedback (sucess message) on deleting versions \[[#72719](https://community.openproject.org/wp/72719)\]
- Bugfix: Missing feedback (success message) on deleting versions \[[#72719](https://community.openproject.org/wp/72719)\]
- Bugfix: Error 500 when trying to delete a work package with unit costs on a relative URL root \[[#72857](https://community.openproject.org/wp/72857)\]
- Bugfix: SCIM User API returns duplicate records \[[#73431](https://community.openproject.org/wp/73431)\]
- Bugfix: FieldsetGroups are missing descriptions \[[#73501](https://community.openproject.org/wp/73501)\]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 459 KiB

+2 -2
View File
@@ -93,11 +93,11 @@ In these Release Notes, we will give an overview of important feature changes. A
- Bugfix: Lists of work packages should sort correctly by semantic id \[[#74156](https://community.openproject.org/wp/74156)\]
- Bugfix: Automatically converting project identifiers should not lead to usage of reserved keywords \[[#74161](https://community.openproject.org/wp/74161)\]
- Bugfix: Moving work packages after switching to semantic and back should not lead to errors \[[#74192](https://community.openproject.org/wp/74192)\]
- Bugfix: Cancel occurence action item is called &#39;Delete&#39; on My Meetings page and Meeting index page &#39;Past&#39; tab \[[#74303](https://community.openproject.org/wp/74303)\]
- Bugfix: Cancel occurrence action item is called &#39;Delete&#39; on My Meetings page and Meeting index page &#39;Past&#39; tab \[[#74303](https://community.openproject.org/wp/74303)\]
- Bugfix: User cannot restore a cancelled occurrence if series has a deleted WP on the agenda \[[#74304](https://community.openproject.org/wp/74304)\]
- Bugfix: Show default section more clearly when using the section selector for a meeting with no sections \[[#74321](https://community.openproject.org/wp/74321)\]
- Bugfix: Inserting &quot;#&quot; inside text removes content after cursor \[[#74325](https://community.openproject.org/wp/74325)\]
- Bugfix: Сards converting to hash links on copy-paste and DnD \[[#74327](https://community.openproject.org/wp/74327)\]
- Bugfix: Cards converting to hash links on copy-paste and DnD \[[#74327](https://community.openproject.org/wp/74327)\]
- Bugfix: Type colors are not applied correctly at the beginning \[[#74330](https://community.openproject.org/wp/74330)\]
- Bugfix: Impossible to open work packages list from the sidebar after visiting team planner \[[#74331](https://community.openproject.org/wp/74331)\]
- Bugfix: Inconsistent contrast for type colors when switching themes \[[#74332](https://community.openproject.org/wp/74332)\]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 416 KiB

@@ -69,7 +69,7 @@ You can also define a webhook secret shared between GitHub and OpenProject. When
Click **Save**.
![Administration settings to specify webhook secret for GitHub integraiton in OpenProject](openproject-system-guide-github-webhook-secret.png)
![Administration settings to specify webhook secret for GitHub integration in OpenProject](openproject-system-guide-github-webhook-secret.png)
Finally you will need to activate the GitHub module under [Project settings](../../../user-guide/projects/project-settings/modules/) so that all information pulling through from GitHub will be shown in the work packages.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 76 KiB

@@ -1,5 +1,5 @@
<div
class="spot-modal loading-indicator--location"
class="spot-modal wp-table--configuration-modal loading-indicator--location"
data-indicator-name="modal"
>
<div id="spotModalTitle" class="spot-modal--header">{{text.title}}</div>
@@ -2,7 +2,7 @@
<form>
<p [textContent]="text.highlighting_mode.description"></p>
<div class="form--field -full-width">
<div class="form--field">
<div class="form--field-container">
<label class="option-label">
<input type="radio"
@@ -11,10 +11,17 @@
[value]="true"
name="entire_card_switch">
<span [textContent]="text.highlighting_mode.entire_card_by"></span>
&ngsp;
</label>
</div>
</div>
<div class="form--field">
<div class="form--field-container">
<div class="form--select-container">
<select (change)="updateMode($event.target.value)"
id="selected_attribute"
name="selected_attribute"
[disabled]="highlightingMode === 'none'"
class="form--select form--inline-select option-label--select">
<option [textContent]="text.highlighting_mode.type"
[selected]="lastEntireCardAttribute === 'type'"
@@ -23,10 +30,11 @@
[selected]="lastEntireCardAttribute === 'priority'"
value="priority"></option>
</select>
</label>
</div>
</div>
</div>
<div class="form--field -full-width">
<div class="form--field">
<div class="form--field-container">
<label class="option-label">
<input type="radio"
@@ -38,6 +46,5 @@
</label>
</div>
</div>
</form>
</div>
@@ -2,15 +2,19 @@
<form>
<p [textContent]="text.highlighting_mode.description"></p>
<div class="form--field">
<label class="form--label">
<input type="radio"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="inline"
name="highlighting_mode_switch">
<span [textContent]="text.highlighting_mode.inline"></span>&nbsp;
</label>
<div class="form--field-container">
<label class="option-label">
<input type="radio"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="inline"
name="highlighting_mode_switch">
<span [textContent]="text.highlighting_mode.inline"></span>&nbsp;
</label>
</div>
</div>
<div class="form--field">
<div class="form--field-container">
<div class="form--select-container">
<ng-select #highlightedAttributesNgSelect
@@ -32,21 +36,26 @@
</div>
<div class="form--field">
<label class="form--label">
<input type="radio"
[(ngModel)]="entireRowMode"
(change)="updateMode('entire-row')"
[value]="true"
name="entire_row_switch">
<span [textContent]="text.highlighting_mode.entire_row_by"></span>
</label>
<div class="form--field-container">
<label class="option-label">
<input type="radio"
[(ngModel)]="entireRowMode"
(change)="updateMode('entire-row')"
[value]="true"
name="entire_row_switch">
<span [textContent]="text.highlighting_mode.entire_row_by"></span>
</label>
</div>
</div>
<div class="form--field">
<div class="form--field-container">
<div class="form--select-container">
<ng-select #rowHighlightNgSelect
[items]="availableRowHighlightedAttributes"
[(ngModel)]="lastEntireRowAttribute"
[clearable]="false"
[disabled]="highlightingMode === 'inline' || highlightingMode === 'none'"
(open)="onOpen(rowHighlightNgSelect)"
(change)="updateMode($event.value)"
bindLabel="name"
@@ -60,14 +69,16 @@
</div>
<div class="form--field">
<label class="form--label">
<input type="radio"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="none"
name="highlighting_mode_switch">
<span [textContent]="text.highlighting_mode.none"></span>
</label>
<div class="form--field-container">
<label class="option-label">
<input type="radio"
[(ngModel)]="highlightingMode"
(change)="updateMode($event.target.value)"
value="none"
name="highlighting_mode_switch">
<span [textContent]="text.highlighting_mode.none"></span>
</label>
</div>
</div>
</form>
</div>
@@ -87,17 +87,17 @@ RSpec.describe "Work Package boards spec", :js, :selenium do
expect(page).to have_css(".__hl_inline_type_#{type2.id}")
# Highlight whole card by priority
board_page.change_board_highlighting "inline", "Priority"
board_page.change_board_highlighting "Entire card by", "Priority"
expect(page).to have_css(".__hl_background_priority_#{priority.id}")
expect(page).to have_css(".__hl_background_priority_#{priority2.id}")
# Highlight whole card by type
board_page.change_board_highlighting "inline", "Type"
board_page.change_board_highlighting "Entire card by", "Type"
expect(page).to have_css(".__hl_background_type_#{type.id}")
expect(page).to have_css(".__hl_background_type_#{type2.id}")
# Disable highlighting
board_page.change_board_highlighting "none"
board_page.change_board_highlighting "No highlighting"
expect(page).to have_no_css(".__hl_background_type_#{type.id}")
expect(page).to have_no_css(".__hl_background_type_#{type2.id}")
@@ -374,9 +374,8 @@ module Pages
def change_board_highlighting(mode, attribute = nil)
click_dropdown_entry "Configure view"
if attribute.nil?
choose(option: mode)
else
choose mode
unless attribute.nil?
select attribute, from: "selected_attribute"
end
@@ -141,6 +141,39 @@ RSpec.describe WorkPackages::BulkController, with_settings: { journal_aggregatio
it { assert_select "input", attributes: { name: "work_package[parent_id]" } }
end
context "with work package list" do
context "with classic (numeric) identifiers" do
it "displays a hash-prefixed numeric id link for each work package" do
assert_select "ul li a", text: /\A#{Regexp.escape(work_package1.type.to_s)} ##{work_package1.id}\z/
assert_select "ul li a", text: /\A#{Regexp.escape(work_package2.type.to_s)} ##{work_package2.id}\z/
end
end
context "with semantic identifiers" do
let(:semantic_prefix) { "TESTPROJ" }
before do
allow(Setting::WorkPackageIdentifier).to receive_messages(semantic?: true, classic?: false)
work_package1.update_columns(identifier: "#{semantic_prefix}-1", sequence_number: 1)
work_package2.update_columns(identifier: "#{semantic_prefix}-2", sequence_number: 2)
end
it "displays the semantic identifier in each link" do
get :edit, params: { ids: [work_package1.id, work_package2.id] }
assert_select "ul li a", text: /#{semantic_prefix}-1/
assert_select "ul li a", text: /#{semantic_prefix}-2/
end
it "does not display a bare numeric id in the links" do
get :edit, params: { ids: [work_package1.id, work_package2.id] }
assert_select "ul li a", text: /##{work_package1.id}/, count: 0
assert_select "ul li a", text: /##{work_package2.id}/, count: 0
end
end
end
context "custom_field" do
describe "#type" do
it { assert_select "input", attributes: { name: "work_package[custom_field_values][#{custom_field1.id}]" } }
@@ -109,5 +109,38 @@ RSpec.describe ProjectIdentifiers::RevertProjectToClassicService do
expect(project.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
end
end
context "when the classic slug from FriendlyId history is already taken by another project" do
let!(:blocking_project) { create(:project, identifier: "my-app") }
let!(:project) do
create(:project).tap do |p|
p.update_columns(identifier: "MYAPP", wp_sequence_counter: 0)
# Remove p's own initial slug so "my-app" is the only entry in its slug history.
# Without this, the factory slug (newer created_at) would be returned first by
# restore_identifier, the update would succeed, and the conflict path would never fire.
FriendlyId::Slug.where(sluggable_id: p.id, sluggable_type: "Project").delete_all
# blocking_project already owns the "my-app" FriendlyId slug; reassign it so that
# restore_identifier returns "my-app" and project.update! conflicts with blocking_project.
FriendlyId::Slug.where(slug: "my-app", sluggable_type: "Project").update_all(sluggable_id: p.id)
end
end
it "does not raise" do
expect { described_class.new(project).call }.not_to raise_error
end
it "assigns a project-NNNNN fallback identifier" do
described_class.new(project).call
expect(project.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
end
it "logs a warning containing the project id and the conflicting identifier" do
allow(Rails.logger).to receive(:warn)
described_class.new(project).call
expect(Rails.logger).to have_received(:warn)
.with(a_string_including(project.id.to_s, "my-app"))
end
end
end
end
@@ -47,7 +47,7 @@ module Components
choose "Entire row by"
# Open select field
within(page.all(".form--field")[1]) do
within(page.all(".form--field")[3]) do
page.find(".ng-input input").click
end
page.find(".ng-dropdown-panel .ng-option", text: label).click
@@ -59,7 +59,7 @@ module Components
choose "Highlighted attribute(s)"
# Open select field
within(page.all(".form--field")[0]) do
within(page.all(".form--field")[1]) do
page.find(".ng-input input").click
end
@@ -112,6 +112,53 @@ RSpec.describe ProjectIdentifiers::RevertInstanceToClassicIdsJob do
.to match(Projects::Identifier::CLASSIC_FORMAT)
end
end
context "when one project has a conflicting restored identifier" do
# project_before and project_after bracket the conflict project to confirm
# the find_each loop is not aborted — both must be processed.
let!(:project_before) do
create(:project).tap do |p|
p.update_columns(identifier: "BEFORE1")
FriendlyId::Slug.create!(sluggable: p, slug: "project-before")
end
end
# Holds "conflict-slug" as its current identifier, blocking project_conflict
# from reclaiming it.
let!(:blocking_project) { create(:project, identifier: "conflict-slug") }
let!(:project_conflict) do
create(:project).tap do |p|
p.update_columns(identifier: "CONFLICT1")
FriendlyId::Slug.where(sluggable_id: p.id, sluggable_type: "Project").delete_all
FriendlyId::Slug.where(slug: "conflict-slug", sluggable_type: "Project").update_all(sluggable_id: p.id)
end
end
let!(:project_after) do
create(:project).tap do |p|
p.update_columns(identifier: "AFTER1")
FriendlyId::Slug.create!(sluggable: p, slug: "project-after")
end
end
before do
allow(Setting::WorkPackageIdentifier).to receive_messages(classic?: true, semantic?: false)
described_class.new.perform
end
it "still reverts project_before (loop not aborted before the conflict)" do
expect(project_before.reload.identifier).to eq("project-before")
end
it "still reverts project_after (loop not aborted after the conflict)" do
expect(project_after.reload.identifier).to eq("project-after")
end
it "assigns project_conflict a project-NNNNN fallback identifier" do
expect(project_conflict.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
end
end
end
end
end