Merge branch 'release/17.5' into dev
@@ -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 } },
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 61 KiB |
@@ -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)\]
|
||||
|
||||
@@ -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)\]
|
||||
|
||||
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 378 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 587 KiB After Width: | Height: | Size: 459 KiB |
@@ -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 'Delete' on My Meetings page and Meeting index page 'Past' tab \[[#74303](https://community.openproject.org/wp/74303)\]
|
||||
- Bugfix: Cancel occurrence action item is called 'Delete' on My Meetings page and Meeting index page 'Past' 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 "#" 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)\]
|
||||
|
||||
|
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**.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 412 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 42 KiB |
|
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>
|
||||
</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>
|
||||
</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
|
||||
|
||||