Skip to content

Commit eb4e946

Browse files
authored
Merge pull request #22719 from opf/feature/73717-adapt-work-package-lists-for-project-based-semantic-work-package-identifiers
Adapt work package lists for semantic identifiers
2 parents e49fe1b + 715cf12 commit eb4e946

9 files changed

Lines changed: 125 additions & 8 deletions

File tree

frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
class="op-ian-item--work-package-id-link spot-link"
1919
[class.spot-link_inactive]="isMobile()"
2020
[attr.title]="workPackage.subject"
21-
[textContent]="'#' + workPackage.id"
21+
[textContent]="workPackage.formattedId"
2222
[attr.href]="fullScreenLink()"
2323
(click)="onLinkClick($event)"
2424
>

frontend/src/app/features/work-packages/components/wp-card-view/wp-single-card/wp-single-card.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
[ngClass]="uiStateLinkClass"
8484
(click)="emitStateLinkClicked($event, workPackage)"
8585
>
86-
#{{workPackage.id}}
86+
{{workPackage.formattedId}}
8787
</a>
8888
<span class="op-wp-single-card--content-subject-line">
8989
@if (showAsInlineCard && showStartDate) {

frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
data-test-selector="op-relation--row-id"
5656
uiSref="work-packages.show.tabs"
5757
[uiParams]="{ workPackageId: relatedWorkPackage.id, tabIdentifier: 'relations' }"
58-
>#{{relatedWorkPackage.id}}</a>
58+
>{{relatedWorkPackage.formattedId}}</a>
5959
<a
6060
class="relation-row--grid-subject"
6161
data-test-selector="op-relation--row-subject"

frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,6 @@ export class LinkedWorkPackageDisplayField extends WorkPackageDisplayField {
7575
}
7676

7777
public get valueString() {
78-
return `#${this.wpId}`;
78+
return this.wpFormattedId;
7979
}
8080
}

frontend/src/app/shared/components/fields/display/field-types/work-package-display-field.module.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
//++
2828

2929
import { DisplayField } from 'core-app/shared/components/fields/display/display-field.module';
30+
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
3031

3132
export class WorkPackageDisplayField extends DisplayField {
3233
public text = {
@@ -57,9 +58,29 @@ export class WorkPackageDisplayField extends DisplayField {
5758
return this.value.href.match(/(\d+)$/)[0];
5859
}
5960

61+
/**
62+
* Returns the work package ID formatted for display.
63+
* Classic mode: `#123` (hash-prefixed), Semantic mode: `PROJ-42` (no prefix).
64+
*
65+
* Delegates to `WorkPackageResource#formattedId` when the linked resource
66+
* is loaded. When unloaded, falls back to the numeric ID extracted from
67+
* the self-link href (which has no `displayId` available).
68+
*/
69+
public get wpFormattedId():string {
70+
const linkedWp = this.value as WorkPackageResource | undefined;
71+
if (linkedWp?.$loaded) {
72+
return linkedWp.formattedId;
73+
}
74+
75+
const id = this.wpId as string | number | null;
76+
if (!id) return '';
77+
78+
return `#${id}`;
79+
}
80+
6081
public get valueString() {
6182
// cannot display the type name easily here as it may not be loaded
62-
return `#${this.wpId} ${this.title}`;
83+
return `${this.wpFormattedId} ${this.title}`;
6384
}
6485

6586
public isEmpty():boolean {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { WorkPackageIdDisplayField } from './wp-id-display-field.module';
2+
import { I18nService } from 'core-app/core/i18n/i18n.service';
3+
import { StateService } from '@uirouter/core';
4+
import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service';
5+
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
6+
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
7+
import { DisplayFieldContext } from 'core-app/shared/components/fields/display/display-field.service';
8+
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
9+
import { IFieldSchema } from 'core-app/shared/components/fields/field.base';
10+
import { Injector } from '@angular/core';
11+
12+
describe('WorkPackageIdDisplayField', () => {
13+
let field:WorkPackageIdDisplayField;
14+
15+
const mockI18n = { t: (key:string) => key };
16+
const mockState = {};
17+
const mockKeepTab = { currentShowTab: 'activity' };
18+
const mockCurrentProject = { identifier: 'my-project' };
19+
const mockPathHelper = {
20+
genericWorkPackagePath: (_proj:string | null, wpId:string, _tab:string) => `/work_packages/${wpId}`,
21+
};
22+
23+
const serviceMap = new Map<unknown, unknown>([
24+
[I18nService, mockI18n],
25+
[StateService, mockState],
26+
[KeepTabService, mockKeepTab],
27+
[CurrentProjectService, mockCurrentProject],
28+
[PathHelperService, mockPathHelper],
29+
]);
30+
31+
function buildField(resourceAttrs:Record<string, unknown> = {}) {
32+
const resource = {
33+
id: '42',
34+
displayId: 'PROJ-7',
35+
...resourceAttrs,
36+
} as unknown as HalResource;
37+
38+
const mockInjector = {
39+
get: (token:unknown, notFoundValue?:unknown) => serviceMap.get(token) ?? notFoundValue ?? {},
40+
} as unknown as Injector;
41+
42+
field = new WorkPackageIdDisplayField('id', {
43+
injector: mockInjector,
44+
container: null,
45+
options: {},
46+
} as unknown as DisplayFieldContext);
47+
48+
field.apply(resource, { type: 'Integer' } as IFieldSchema);
49+
}
50+
51+
describe('valueString', () => {
52+
it('returns the semantic displayId when present on the resource', () => {
53+
buildField({ id: '42', displayId: 'PROJ-7' });
54+
55+
expect(field.valueString).toEqual('PROJ-7');
56+
});
57+
58+
it('falls back to numeric id when displayId is absent', () => {
59+
buildField({ id: '42', displayId: undefined });
60+
61+
expect(field.valueString).toEqual('42');
62+
});
63+
});
64+
65+
describe('render', () => {
66+
it('renders the displayText as visible link content, not the numeric id', () => {
67+
buildField({ id: '42', displayId: 'PROJ-7' });
68+
69+
const container = document.createElement('span');
70+
field.render(container, 'PROJ-7');
71+
72+
const link = container.querySelector('a');
73+
74+
expect(link).toBeTruthy();
75+
expect(link!.textContent).toEqual('PROJ-7');
76+
});
77+
78+
it('uses the numeric id for routing (data-work-package-id)', () => {
79+
buildField({ id: '42', displayId: 'PROJ-7' });
80+
81+
const container = document.createElement('span');
82+
field.render(container, 'PROJ-7');
83+
84+
const link = container.querySelector('a');
85+
86+
expect(link).toBeTruthy();
87+
expect(link!.dataset.workPackageId).toEqual('42');
88+
});
89+
});
90+
});

frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,20 @@ export class WorkPackageIdDisplayField extends IdDisplayField {
4545

4646
private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.$state, this.keepTab, this.currentProject, this.pathHelper);
4747

48+
public get valueString():string {
49+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
50+
return this.resource.displayId ?? this.value?.toString() ?? '';
51+
}
52+
4853
public render(element:HTMLElement, displayText:string):void {
4954
if (!this.value) {
5055
return;
5156
}
5257
const link = this.uiStateBuilder.linkToShow(
58+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
5359
this.value,
5460
displayText,
55-
this.value,
61+
displayText,
5662
);
5763

5864
element.appendChild(link);

frontend/src/app/shared/components/fields/macros/work-package-quickinfo-macro.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[href]="workPackageLink"
1414
[attr.data-work-package-id]="workPackage.id"
1515
[attr.data-hover-card-url]="workPackageHoverCardUrl">
16-
#{{workPackage.id}}:
16+
{{workPackage.formattedId}}:
1717
</a>
1818
<display-field [resource]="workPackage"
1919
[displayFieldOptions]="{ writable: false }"

frontend/src/app/shared/components/time_entries/timer/stop-existing-timer-modal.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
[uiParams]="{ workPackageId: active.entity.id }"
2323
(click)="closeMe()"
2424
>
25-
#{{ active.workPackage.id }}: {{ active.workPackage.name }}
25+
{{ active.workPackage.formattedId }}: {{ active.workPackage.name }}
2626
</a>
2727
</p>
2828

0 commit comments

Comments
 (0)