Rename semanticId to displayId, make always present

Replace the conditional `semanticId` API field with `displayId` which is
always present in work package responses. In semantic mode it returns the
project-based identifier (e.g. "PROJ-42"), in classic mode it returns the
numeric ID as a string. This gives API consumers (frontend, mobile) a
single field to read without conditional logic.

- Add `WorkPackage#display_id` method that encapsulates the mode check
- Update both representers (JSON and SQL) to render `displayId` unconditionally
- Update OpenAPI schema documentation
This commit is contained in:
Kabiru Mwenja
2026-04-10 17:18:46 +03:00
parent d39b720e6e
commit 5bbc4e7563
7 changed files with 53 additions and 26 deletions
@@ -76,6 +76,13 @@ module WorkPackage::SemanticIdentifier
end
end
# Returns the user-facing identifier for this work package.
# In semantic mode: the project-based identifier (e.g. "PROJ-42")
# In classic mode: the numeric database ID
def display_id
Setting::WorkPackageIdentifier.semantic_mode_active? ? identifier : id
end
# Allocates the next semantic identifier in the current project and assigns it to the WP.
# Also writes alias rows for every identifier the project has ever used (including "ghost" aliases).
#
@@ -12,11 +12,12 @@ allOf:
description: Work package id
readOnly: true
minimum: 1
semanticId:
displayId:
type: string
description: |-
The project-based semantic identifier for the work package (e.g. PROJ-42).
Only present when semantic mode is enabled.
The user-facing identifier for the work package.
In semantic mode: the project-based identifier (e.g. "PROJ-42").
In classic mode: the numeric ID as a string (e.g. "123").
readOnly: true
lockVersion:
type: integer
@@ -345,11 +345,10 @@ module API
property :id,
render_nil: true
property :semantic_id,
as: :semanticId,
property :display_id,
as: :displayId,
render_nil: true,
getter: ->(*) { identifier },
if: ->(*) { Setting::WorkPackageIdentifier.semantic_mode_active? }
getter: ->(*) { display_id&.to_s }
property :lock_version,
render_nil: true,
@@ -76,9 +76,10 @@ module API
property :id
property :semanticId,
representation: ->(*) { "identifier" },
render_if: ->(*) { Setting::WorkPackageIdentifier.semantic_mode_active? ? "TRUE" : "FALSE" }
property :displayId,
representation: ->(*) {
Setting::WorkPackageIdentifier.semantic_mode_active? ? "identifier" : "id::text"
}
property :subject
@@ -160,16 +160,15 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do
let(:value) { work_package.id }
end
describe "semanticId" do
describe "displayId" do
context "when semantic work package ids are active",
with_flag: { semantic_work_package_ids: true },
with_settings: { work_packages_identifier: "semantic" } do
it { is_expected.to be_json_eql(work_package.identifier.to_json).at_path("semanticId") }
it { is_expected.to be_json_eql(work_package.identifier.to_json).at_path("displayId") }
end
context "when semantic_work_package_ids feature flag is inactive",
with_flag: { semantic_work_package_ids: false } do
it { is_expected.not_to have_json_path("semanticId") }
context "when semantic work package ids are not active" do
it { is_expected.to be_json_eql(work_package.id.to_s.to_json).at_path("displayId") }
end
end
@@ -72,6 +72,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageSqlRepresenter, "rendering" do
{
_type: "WorkPackage",
id: rendered_work_package.id,
displayId: rendered_work_package.id.to_s,
subject: rendered_work_package.subject,
dueDate: rendered_work_package.due_date,
startDate: rendered_work_package.start_date,
@@ -118,6 +119,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageSqlRepresenter, "rendering" do
{
_type: "WorkPackage",
id: rendered_work_package.id,
displayId: rendered_work_package.id.to_s,
subject: rendered_work_package.subject,
date: rendered_work_package.start_date,
_links: {
@@ -156,20 +158,21 @@ RSpec.describe API::V3::WorkPackages::WorkPackageSqlRepresenter, "rendering" do
end
end
context "when semantic work package ids are active",
with_flag: { semantic_work_package_ids: true },
with_settings: { work_packages_identifier: "semantic" } do
let(:project) { create(:project, identifier: "PROJ", types: [type]) }
describe "displayId" do
context "when semantic work package ids are active",
with_flag: { semantic_work_package_ids: true },
with_settings: { work_packages_identifier: "semantic" } do
let(:project) { create(:project, identifier: "PROJ", types: [type]) }
it "includes semanticId" do
expect(json).to be_json_eql("PROJ-1".to_json).at_path("semanticId")
it "returns the semantic identifier" do
expect(json).to be_json_eql("PROJ-1".to_json).at_path("displayId")
end
end
end
context "when semantic_work_package_ids feature flag is inactive",
with_flag: { semantic_work_package_ids: false } do
it "does not include semanticId" do
expect(json).not_to have_json_path("semanticId")
context "when semantic work package ids are not active" do
it "returns the numeric id as a string" do
expect(json).to be_json_eql(rendered_work_package.id.to_s.to_json).at_path("displayId")
end
end
end
end
@@ -136,6 +136,23 @@ RSpec.describe WorkPackage::SemanticIdentifier do
end
end
describe "#display_id" do
context "when semantic mode is active",
with_flag: { semantic_work_package_ids: true },
with_settings: { work_packages_identifier: "semantic" } do
it "returns the semantic identifier" do
expect(work_package.display_id).to eq("MYPROJ-1")
end
end
context "when semantic mode is not active",
with_flag: { semantic_work_package_ids: false } do
it "returns the numeric id" do
expect(work_package.display_id).to eq(work_package.id)
end
end
end
describe "#allocate_and_register_semantic_id" do
let(:project) { create(:project, identifier: "PROJ", wp_sequence_counter: 0) }
let(:target_project) { create(:project, identifier: "OTHER", wp_sequence_counter: 0) }