Skip to content

Commit 530eecc

Browse files
committed
Use displayId in work package URLs for semantic identifier routing
Routes already accept semantic identifiers (e.g. PROJ-7) via WP_ID_URL_PATTERN, but all frontend-generated links still used numeric PKs. This wires displayId into every navigation path so the browser URL bar shows the semantic form when available. Key design decision: data-work-package-id attributes on <a> elements stay numeric — the selection/hover system is keyed by PK. Only the href gets the semantic ID via a new optional routingId parameter on UiStateLinkBuilder. Changes span table links (ID column, linked WP fields, details action), navigation handlers (list view, embedded tables, boards, BCF, calendar), breadcrumbs, tabs, hierarchy, single view, context menu, quickinfo macro, and post-creation redirect. API-only paths (time entries, share, hover cards, progress modal) are deliberately left with numeric IDs — they never appear in the address bar.
1 parent 9372285 commit 530eecc

21 files changed

Lines changed: 120 additions & 35 deletions

File tree

frontend/src/app/features/bim/ifc_models/bcf/list/bcf-list.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,14 @@ export class BcfListComponent extends WorkPackageListViewComponent implements Un
129129
: 'bim.partitioned.show';
130130
// Passing the card param to the new state because the router doesn't keep
131131
// it when going to 'bim.partitioned.show'
132-
const params = { workPackageId, cards, focus };
132+
const routingId = this.resolveRoutingId(workPackageId);
133+
const params = { workPackageId: routingId, cards, focus };
133134

134135
void this.$state.go(stateToGo, params);
135136
}
137+
138+
private resolveRoutingId(workPackageId:string):string {
139+
const wp = this.states.workPackages.get(workPackageId)?.value;
140+
return wp?.displayId ?? workPackageId;
141+
}
136142
}

frontend/src/app/features/boards/board/board-list/board-list.component.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { AuthorisationService } from 'core-app/core/model-auth/model-auth.servic
2626
import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions';
2727
import { WorkPackageCardViewComponent } from 'core-app/features/work-packages/components/wp-card-view/wp-card-view.component';
2828
import { WorkPackageStatesInitializationService } from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service';
29+
import { States } from 'core-app/core/states/states.service';
2930
import { BoardService } from 'core-app/features/boards/board/board.service';
3031
import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
3132
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
@@ -175,6 +176,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
175176
readonly keepTab:KeepTabService,
176177
readonly currentProject:CurrentProjectService,
177178
readonly pathHelper:PathHelperService,
179+
readonly states:States,
178180
) {
179181
super(I18n, injector);
180182
}
@@ -488,17 +490,19 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
488490

489491
openFullViewOnDoubleClick(event:{ workPackageId:string, double:boolean }) {
490492
if (event.double) {
493+
const routingId = this.resolveRoutingId(event.workPackageId);
491494
const projectIdentifier = this.currentProject.identifier;
492-
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, event.workPackageId) + window.location.search;
495+
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, routingId) + window.location.search;
493496
Turbo.visit(link, { action: 'advance' });
494497
}
495498
}
496499

497500
openStateLink(event:{ workPackageId:string; requestedState:string }) {
501+
const routingId = this.resolveRoutingId(event.workPackageId);
498502
if (event.requestedState === 'split') {
499-
this.goToSplitView(event.workPackageId);
503+
this.goToSplitView(routingId);
500504
} else {
501-
this.keepTab.goCurrentShowState(event.workPackageId);
505+
this.keepTab.goCurrentShowState(routingId);
502506
}
503507
}
504508

@@ -509,6 +513,11 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
509513
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
510514
}
511515

516+
private resolveRoutingId(workPackageId:string):string {
517+
const wp = this.states.workPackages.get(workPackageId)?.value;
518+
return wp?.displayId ?? workPackageId;
519+
}
520+
512521
private schema(workPackage:WorkPackageResource) {
513522
return this.schemaCache.of(workPackage);
514523
}

frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
4040
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
4141
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
42+
import { States } from 'core-app/core/states/states.service';
4243

4344
@Component({
4445
selector: 'board-list-container',
@@ -109,6 +110,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
109110
readonly QueryUpdated:QueryUpdatedService,
110111
readonly pathHelper:PathHelperService,
111112
readonly currentProject:CurrentProjectService,
113+
readonly wpStates:States,
112114
) {
113115
super();
114116
}
@@ -134,7 +136,10 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
134136
filter(() => window.location.pathname.includes('/details/')),
135137
).subscribe((selection) => {
136138
// Update split screen
137-
const base = this.pathHelper.boardDetailsPath(this.currentProject.identifier, id, selection.focusedWorkPackage!);
139+
const wpId = selection.focusedWorkPackage!;
140+
const wp = this.wpStates.workPackages.get(wpId)?.value;
141+
const routingId = wp?.displayId ?? wpId;
142+
const base = this.pathHelper.boardDetailsPath(this.currentProject.identifier, id, routingId);
138143
const search = window.location.search;
139144
Turbo.visit(search ? `${base}${search}` : base, { frame: 'content-bodyRight', action: 'advance' });
140145
});

frontend/src/app/features/calendar/op-work-packages-calendar.service.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
uiStateLinkClass,
5454
} from 'core-app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder';
5555
import { debugLog } from 'core-app/shared/helpers/debug_output';
56+
import { States } from 'core-app/core/states/states.service';
5657
import {
5758
WorkPackageViewContextMenu,
5859
} from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
@@ -112,6 +113,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
112113
readonly calendarService:OpCalendarService,
113114
readonly weekdayService:WeekdayService,
114115
readonly dayService:DayResourceService,
116+
readonly states:States,
115117
) {
116118
super();
117119
}
@@ -287,21 +289,28 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
287289
return;
288290
}
289291

292+
const routingId = this.resolveRoutingId(id);
290293
void this.$state.go(
291294
`${splitViewRoute(this.$state)}.tabs`,
292-
{ workPackageId: id, tabIdentifier: 'overview' },
295+
{ workPackageId: routingId, tabIdentifier: 'overview' },
293296
);
294297
}
295298

296299
public openFullView(id:string):void {
297300
this.wpTableSelection.setSelection(id, -1);
298301

302+
const routingId = this.resolveRoutingId(id);
299303
void this.$state.go(
300304
'work-packages.show',
301-
{ workPackageId: id },
305+
{ workPackageId: routingId },
302306
);
303307
}
304308

309+
private resolveRoutingId(workPackageId:string):string {
310+
const wp = this.states.workPackages.get(workPackageId)?.value;
311+
return wp?.displayId ?? workPackageId;
312+
}
313+
305314
public onCardClicked({ workPackageId, event }:{ workPackageId:string, event:MouseEvent }):void {
306315
if (isClickedWithModifier(event)) {
307316
return;

frontend/src/app/features/calendar/wp-calendar/wp-calendar.component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,16 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
342342
}
343343

344344
if (evt.event.extendedProps.workPackage) {
345-
const workPackageId = (evt.event.extendedProps.workPackage as WorkPackageResource).id!;
345+
const wp = evt.event.extendedProps.workPackage as WorkPackageResource;
346346
// Currently the calendar widget is shown on multiple pages,
347347
// but only the calendar module itself is a partitioned query space which can deal with a split screen request
348348
if (this.$state.includes('calendar')) {
349-
this.workPackagesCalendar.openSplitView(workPackageId);
349+
this.workPackagesCalendar.openSplitView(wp.id!);
350350
} else {
351+
const routingId = wp.displayId ?? wp.id!;
351352
void this.$state.go(
352353
'work-packages.show',
353-
{ workPackageId },
354+
{ workPackageId: routingId },
354355
);
355356
}
356357
}

frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class WorkPackageBreadcrumbParentComponent {
115115
}
116116

117117
public parentLink(parent:WorkPackageResource):string {
118-
return this.pathHelper.genericWorkPackagePath(parent.project?.identifier, parent.id!) + window.location.search;
118+
const routingId = parent.displayId ?? parent.id!;
119+
return this.pathHelper.genericWorkPackagePath(parent.project?.identifier, routingId) + window.location.search;
119120
}
120121
}

frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export class WorkPackageBreadcrumbComponent {
6565
}
6666

6767
public ancestorPath(ancestor:WorkPackageResource):string {
68-
return this.pathHelper.genericWorkPackagePath(this.workPackage.project?.identifier, ancestor.id!) + window.location.search;
68+
const routingId = ancestor.displayId ?? ancestor.id!;
69+
return this.pathHelper.genericWorkPackagePath(this.workPackage.project?.identifier, routingId) + window.location.search;
6970
}
7071

7172
public updateActiveInput(val:boolean) {

frontend/src/app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,28 @@ export class UiStateLinkBuilder {
1616
) {
1717
}
1818

19-
public linkToDetails(workPackageId:string, title:string, content:string) {
20-
return this.build(workPackageId, 'split', title, content);
19+
public linkToDetails(workPackageId:string, title:string, content:string, routingId?:string) {
20+
return this.build(workPackageId, 'split', title, content, routingId);
2121
}
2222

23-
public linkToShow(workPackageId:string, title:string, content:string) {
24-
return this.build(workPackageId, 'show', title, content);
23+
public linkToShow(workPackageId:string, title:string, content:string, routingId?:string) {
24+
return this.build(workPackageId, 'show', title, content, routingId);
2525
}
2626

27-
private build(workPackageId:string, state:'show'|'split', title:string, content:string) {
27+
private build(workPackageId:string, state:'show'|'split', title:string, content:string, routingId?:string) {
2828
const a = document.createElement('a');
29-
let tabState:string;
30-
let tabIdentifier:string;
29+
const idForHref = routingId ?? workPackageId;
3130
let href:string;
3231

3332
if (state === 'show') {
3433
const projectIdentifier = this.currentProject.identifier;
35-
href = this.pathHelper.genericWorkPackagePath(projectIdentifier, workPackageId, this.keepTab.currentShowTab) + window.location.search;
34+
href = this.pathHelper.genericWorkPackagePath(projectIdentifier, idForHref, this.keepTab.currentShowTab) + window.location.search;
3635
} else {
3736
const tab = this.keepTab.currentDetailsTab;
3837
href = this.$state.href(
3938
'work-packages.partitioned.list.details.tabs',
4039
{
41-
workPackageId,
40+
workPackageId: idForHref,
4241
tab,
4342
},
4443
);

frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,16 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O
130130

131131
this.editForm?.cancel(false);
132132

133+
const routingId = savedResource.displayId ?? savedResource.id!;
133134
if(this.routedFromAngular && this.successState) {
134-
this.$state.go(this.successState, { workPackageId: savedResource.id })
135+
this.$state.go(this.successState, { workPackageId: routingId })
135136
.then(() => {
136137
this.wpViewFocus.updateFocus(savedResource.id!);
137138
this.notificationService.showSave(savedResource, isInitial);
138139
});
139140
} else {
140141
window.OpenProject.pageState = 'submitted';
141-
Turbo.visit(this.pathHelper.projectWorkPackagePath(savedResource.project.identifier, savedResource.id!) + window.location.search);
142+
Turbo.visit(this.pathHelper.projectWorkPackagePath(savedResource.project.identifier, routingId) + window.location.search);
142143
}
143144

144145
}

frontend/src/app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.directive.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class WorkPackageRelationsHierarchyComponent extends UntilDestroyedMixin
8181
};
8282

8383
ngOnInit() {
84-
this.workPackagePath = this.PathHelper.workPackagePath(this.workPackage.id!);
84+
this.workPackagePath = this.PathHelper.workPackagePath(this.workPackage.displayId ?? this.workPackage.id!);
8585
this.canModifyHierarchy = !!this.workPackage.changeParent;
8686
this.canAddRelation = !!this.workPackage.addRelation;
8787

0 commit comments

Comments
 (0)