Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@angular/core';
import { UIRouterGlobals } from '@uirouter/core';
import { States } from 'core-app/core/states/states.service';
import { resolveRoutingId } from 'core-app/features/work-packages/helpers/resolve-routing-id';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { DragAndDropService } from 'core-app/shared/helpers/drag-and-drop/drag-and-drop.service';
Expand Down Expand Up @@ -129,8 +130,13 @@ export class BcfListComponent extends WorkPackageListViewComponent implements Un
: 'bim.partitioned.show';
// Passing the card param to the new state because the router doesn't keep
// it when going to 'bim.partitioned.show'
const params = { workPackageId, cards, focus };
const routingId = this.resolveRoutingId(workPackageId);
const params = { workPackageId: routingId, cards, focus };

void this.$state.go(stateToGo, params);
}

private resolveRoutingId(workPackageId:string):string {
return resolveRoutingId(this.states, workPackageId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { AuthorisationService } from 'core-app/core/model-auth/model-auth.servic
import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions';
import { WorkPackageCardViewComponent } from 'core-app/features/work-packages/components/wp-card-view/wp-card-view.component';
import { WorkPackageStatesInitializationService } from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service';
import { States } from 'core-app/core/states/states.service';
import { resolveRoutingId } from 'core-app/features/work-packages/helpers/resolve-routing-id';
import { BoardService } from 'core-app/features/boards/board/board.service';
import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
Expand Down Expand Up @@ -175,6 +177,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
readonly keepTab:KeepTabService,
readonly currentProject:CurrentProjectService,
readonly pathHelper:PathHelperService,
readonly states:States,
) {
super(I18n, injector);
}
Expand Down Expand Up @@ -488,17 +491,19 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni

openFullViewOnDoubleClick(event:{ workPackageId:string, double:boolean }) {
if (event.double) {
const routingId = this.resolveRoutingId(event.workPackageId);
const projectIdentifier = this.currentProject.identifier;
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, event.workPackageId) + window.location.search;
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, routingId) + window.location.search;
Turbo.visit(link, { action: 'advance' });
}
}

openStateLink(event:{ workPackageId:string; requestedState:string }) {
const routingId = this.resolveRoutingId(event.workPackageId);
if (event.requestedState === 'split') {
this.goToSplitView(event.workPackageId);
this.goToSplitView(routingId);
} else {
this.keepTab.goCurrentShowState(event.workPackageId);
this.keepTab.goCurrentShowState(routingId);
}
}

Expand All @@ -509,6 +514,10 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
}

private resolveRoutingId(workPackageId:string):string {
return resolveRoutingId(this.states, workPackageId);
}

private schema(workPackage:WorkPackageResource) {
return this.schemaCache.of(workPackage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
uiStateLinkClass,
} from 'core-app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder';
import { debugLog } from 'core-app/shared/helpers/debug_output';
import { States } from 'core-app/core/states/states.service';
import { resolveRoutingId } from 'core-app/features/work-packages/helpers/resolve-routing-id';
import {
WorkPackageViewContextMenu,
} from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
Expand Down Expand Up @@ -112,6 +114,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
readonly calendarService:OpCalendarService,
readonly weekdayService:WeekdayService,
readonly dayService:DayResourceService,
readonly states:States,
) {
super();
}
Expand Down Expand Up @@ -287,21 +290,27 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
return;
}

const routingId = this.resolveRoutingId(id);
void this.$state.go(
`${splitViewRoute(this.$state)}.tabs`,
{ workPackageId: id, tabIdentifier: 'overview' },
{ workPackageId: routingId, tabIdentifier: 'overview' },
);
}

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

const routingId = this.resolveRoutingId(id);
void this.$state.go(
'work-packages.show',
{ workPackageId: id },
{ workPackageId: routingId },
);
}

private resolveRoutingId(workPackageId:string):string {
return resolveRoutingId(this.states, workPackageId);
}

public onCardClicked({ workPackageId, event }:{ workPackageId:string, event:MouseEvent }):void {
if (isClickedWithModifier(event)) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,15 +342,16 @@ export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implement
}

if (evt.event.extendedProps.workPackage) {
const workPackageId = (evt.event.extendedProps.workPackage as WorkPackageResource).id!;
const wp = evt.event.extendedProps.workPackage as WorkPackageResource;
// Currently the calendar widget is shown on multiple pages,
// but only the calendar module itself is a partitioned query space which can deal with a split screen request
if (this.$state.includes('calendar')) {
this.workPackagesCalendar.openSplitView(workPackageId);
this.workPackagesCalendar.openSplitView(wp.id!);
} else {
const routingId = wp.displayId ?? wp.id!;
void this.$state.go(
'work-packages.show',
{ workPackageId },
{ workPackageId: routingId },
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ export class WorkPackageSingleCardComponent extends UntilDestroyedMixin implemen
this.untilDestroyed(),
map(() => {
if (this.selectedWhenOpen) {
return this.uiRouterGlobals.params.workPackageId === this.workPackage.id;
// Route param may be semantic ("PROJ-7") or numeric ("42").
// Compare against both id and displayId to handle both modes.
const routeWpId = this.uiRouterGlobals.params.workPackageId;
return routeWpId === this.workPackage.id || routeWpId === this.workPackage.displayId;
}

return this.wpTableSelection.isSelected(this.workPackage.id!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,16 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O

this.editForm?.cancel(false);

const routingId = savedResource.displayId ?? savedResource.id!;
if(this.routedFromAngular && this.successState) {
this.$state.go(this.successState, { workPackageId: savedResource.id })
this.$state.go(this.successState, { workPackageId: routingId })
.then(() => {
this.wpViewFocus.updateFocus(savedResource.id!);
this.notificationService.showSave(savedResource, isInitial);
});
} else {
window.OpenProject.pageState = 'submitted';
Turbo.visit(this.pathHelper.projectWorkPackagePath(savedResource.project.identifier, savedResource.id!) + window.location.search);
Turbo.visit(this.pathHelper.projectWorkPackagePath(savedResource.project.identifier, routingId) + window.location.search);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora
import {
KeepTabService,
} from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service';
import { States } from 'core-app/core/states/states.service';
import { resolveRoutingId } from 'core-app/features/work-packages/helpers/resolve-routing-id';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { firstValueFrom } from 'rxjs';
import { QueryRequestParams } from 'core-app/features/work-packages/components/wp-query/url-params-helper';
Expand Down Expand Up @@ -63,6 +65,8 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo

@InjectField() keepTab:KeepTabService;

@InjectField() states:States;

// Cache the form promise
private formPromise:Promise<QueryFormResource|undefined>|undefined;

Expand Down Expand Up @@ -190,15 +194,17 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo

handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) {
if (event.double) {
const routingId = this.resolveRoutingId(event.workPackageId);
const projectIdentifier = this.currentProject.identifier;
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, event.workPackageId) + window.location.search;
const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, routingId) + window.location.search;
Turbo.visit(link, { action: 'advance' });
}
}

openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) {
const routingId = this.resolveRoutingId(event.workPackageId);
const params = {
workPackageId: event.workPackageId,
workPackageId: routingId,
focus: true,
};

Expand All @@ -208,4 +214,8 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo
this.keepTab.goCurrentShowState(params.workPackageId);
}
}

private resolveRoutingId(workPackageId:string):string {
return resolveRoutingId(this.states, workPackageId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { States } from 'core-app/core/states/states.service';

/**
* Resolve a numeric work package ID to its semantic routing ID (e.g. "PROJ-42").
* Falls back to the input ID if the WP is not in cache or has no displayId —
* this is a best-effort lookup, not a guarantee. The URL just shows the
* numeric ID temporarily until the WP is cached.
*
* Used in navigation handlers where only the numeric PK is available from
* data-work-package-id attributes, but the URL should show the semantic form.
*/
export function resolveRoutingId(states:States, workPackageId:string):string {
const wp = states.workPackages.get(workPackageId)?.value;
return wp?.displayId ?? workPackageId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import { KeepTabService } from 'core-app/features/work-packages/components/wp-si
import { WorkPackageViewBaselineService } from '../wp-view-base/view-services/wp-view-baseline.service';
import { combineLatest } from 'rxjs';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { States } from 'core-app/core/states/states.service';
import { resolveRoutingId } from 'core-app/features/work-packages/helpers/resolve-routing-id';

@Component({
selector: 'wp-list-view',
Expand Down Expand Up @@ -85,6 +87,7 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
readonly wpTableBaseline = inject(WorkPackageViewBaselineService);
readonly pathHelper = inject(PathHelperService);
readonly states = inject(States);

text = {
jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.pagination'),
Expand Down Expand Up @@ -178,8 +181,9 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
}

openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) {
const routingId = this.resolveRoutingId(event.workPackageId);
const params = {
workPackageId: event.workPackageId,
workPackageId: routingId,
focus: true,
};

Expand All @@ -203,7 +207,12 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements
}

private openInFullView(workPackageId:string) {
const routingId = this.resolveRoutingId(workPackageId);
const projectIdentifier = this.CurrentProject.identifier;
window.location.href = this.pathHelper.genericWorkPackagePath(projectIdentifier, workPackageId) + window.location.search;
window.location.href = this.pathHelper.genericWorkPackagePath(projectIdentifier, routingId) + window.location.search;
}

private resolveRoutingId(workPackageId:string):string {
return resolveRoutingId(this.states, workPackageId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,40 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp
ngOnInit():void {
this.observeWorkPackage();

const wpId = (this.$state.params.workPackageId || this.workPackageId) as string;
this.wpTableFocus.updateFocus(wpId, false);

if (this.wpTableSelection.isEmpty) {
this.wpTableSelection.setRowState(wpId, true);
}

this.wpTableFocus.whenChanged()
.pipe(
this.untilDestroyed(),
)
.subscribe((newId) => {
const idSame = wpId.toString() === newId.toString();
const currentId = this.workPackage?.id ?? this.workPackageId;
const idSame = currentId.toString() === newId.toString();
if (!idSame && this.$state.includes(`${this.baseRoute}.details`)) {
this.$state.go(
(this.$state.current.name!),
{ workPackageId: newId, focus: false },
);
}
});
this.recentItemsService.add(wpId);
}

/**
* Set focus, selection, and recent-items after the WP has loaded.
*
* Intentionally deferred from ngOnInit because the route param
* (this.workPackageId) may be a semantic identifier like "PROJ-7",
* but focus/selection services are keyed by numeric PK. By the time
* init() runs, this.workPackage.id is guaranteed to be the numeric PK.
*/
protected override init():void {
super.init();
const numericId = this.workPackage.id!;
this.wpTableFocus.updateFocus(numericId, false);

if (this.wpTableSelection.isEmpty) {
this.wpTableSelection.setRowState(numericId, true);
}

this.recentItemsService.add(numericId);
}

get activeTabComponent():Type<TabComponent>|undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin {
/**
* Observe changes of work package and re-run initialization.
* Needs to be run explicitly by descendants.
*
* Note: this.workPackageId may be a semantic identifier (e.g. "PROJ-7")
* from the route param. The API resolves it correctly, but the cache key
* would be "PROJ-7" while list queries cache the same WP under "42".
* After the first load we normalize to the numeric PK to prevent
* dual cache entries.
*/
protected observeWorkPackage():void {
this
Expand All @@ -150,6 +156,13 @@ export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin {
.requireAndStream()
.pipe(this.untilDestroyed())
.subscribe((wp:WorkPackageResource) => {
// Normalize semantic route param (e.g. "PROJ-7") to numeric PK
// for cache coherence — downstream code uses this.workPackageId
// as a cache key, and the canonical key is always numeric.
if (this.workPackageId !== wp.id && wp.id) {
this.workPackageId = wp.id;
}

if (!this.workPackage) {
this.workPackage = wp;
this.init();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { UrlParamsHelperService } from 'core-app/features/work-packages/componen
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { HalDeletedEvent, HalEventsService } from 'core-app/features/hal/services/hal-events.service';
import { States } from 'core-app/core/states/states.service';

@Injectable()
export class WorkPackageService {
Expand All @@ -47,7 +48,8 @@ export class WorkPackageService {
private readonly UrlParamsHelper:UrlParamsHelperService,
private readonly toastService:ToastService,
private readonly I18n:I18nService,
private readonly halEvents:HalEventsService) {
private readonly halEvents:HalEventsService,
private readonly states:States) {
}

public performBulkDelete(ids:string[], defaultHandling:boolean) {
Expand All @@ -68,8 +70,11 @@ export class WorkPackageService {

ids.forEach((id) => this.halEvents.push({ _type: 'WorkPackage', id }, { eventType: 'deleted' } as HalDeletedEvent));

const routeWpId = this.$state.params.workPackageId as string;
const wp = this.states.workPackages.get(routeWpId)?.value;
const numericId = wp?.id ?? routeWpId;
if (this.$state.includes('**.list.details.**')
&& ids.includes(this.$state.params.workPackageId)) {
&& ids.includes(numericId)) {
this.$state.go('work-packages.partitioned.list', this.$state.params);
}
})
Expand Down
Loading
Loading