diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts index 43609bedc8ed..39b8f40a5179 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.spec.ts @@ -1086,7 +1086,7 @@ describe('DotEmaShellComponent', () => { expect.objectContaining({ label: expect.any(String), id: '123', - url: 'index' + url: expect.stringContaining('url=index') }) ); }); @@ -1127,7 +1127,7 @@ describe('DotEmaShellComponent', () => { expect.objectContaining({ label: 'Other Page', id: '456', - url: '/other-page' + url: expect.stringContaining('url=%2Fother-page') }) ); }); @@ -1140,7 +1140,7 @@ describe('DotEmaShellComponent', () => { expect(mockGlobalStore.addNewBreadcrumb).toHaveBeenCalledWith( expect.objectContaining({ - url: INITIAL_PAGE_PARAMS.url + url: expect.stringMatching(/^\/dotAdmin\/#.*url=index/) }) ); }); @@ -1161,45 +1161,41 @@ describe('DotEmaShellComponent', () => { expect(() => spectator.detectChanges()).not.toThrow(); }); - it('should not update breadcrumb with stale data while page is loading, then update once loaded', async () => { - // Initialize with the first page fully loaded + it('should replace breadcrumb on navigation, not accumulate stale entries', async () => { + // Page A fully loaded spectator.detectChanges(); await spectator.fixture.whenStable(); spectator.detectChanges(); mockGlobalStore.addNewBreadcrumb.mockClear(); - // Hold the HTTP response so we can inspect state during LOADING + // Hold the response to inspect the LOADING window const pendingRequest$ = new Subject(); jest.spyOn(dotPageApiService, 'get').mockReturnValue(pendingRequest$); - // Navigate to a new page - store.pageLoad({ ...INITIAL_PAGE_PARAMS, url: '/new-page' }); + store.pageLoad({ ...INITIAL_PAGE_PARAMS, url: '/page-b' }); spectator.detectChanges(); - // While status is LOADING, stale pageAsset is present but breadcrumb must not be updated + // While LOADING, stale Page A data is present — breadcrumb must not fire expect(mockGlobalStore.addNewBreadcrumb).not.toHaveBeenCalled(); - // Resolve the HTTP response with the new page's data - const newPageResponse = { + // Resolve with Page B data + const pageBResponse = { ...MOCK_RESPONSE_HEADLESS, - page: { - ...MOCK_RESPONSE_HEADLESS.page, - title: 'New Page Title', - identifier: '999' - } + page: { ...MOCK_RESPONSE_HEADLESS.page, title: 'Page B', identifier: '456' } }; - pendingRequest$.next(newPageResponse); + pendingRequest$.next(pageBResponse); pendingRequest$.complete(); await spectator.fixture.whenStable(); spectator.detectChanges(); - // After loading completes, breadcrumb must reflect the new page + // Called exactly once — with Page B data, never with stale Page A data + expect(mockGlobalStore.addNewBreadcrumb).toHaveBeenCalledTimes(1); expect(mockGlobalStore.addNewBreadcrumb).toHaveBeenCalledWith( expect.objectContaining({ - label: 'New Page Title', - id: '999', - url: '/new-page' + label: 'Page B', + id: '456', + url: expect.stringContaining('url=%2Fpage-b') }) ); }); diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts index 99ca0a1071a2..aa8340076b96 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/dot-ema-shell.component.ts @@ -1,4 +1,4 @@ -import { patchState } from '@ngrx/signals'; +import { patchState, signalMethod } from '@ngrx/signals'; import { Location } from '@angular/common'; import { @@ -11,7 +11,6 @@ import { OnDestroy, OnInit, signal, - untracked, ViewChild } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -34,7 +33,7 @@ import { PageScannerToolType } from '@dotcms/portlets/dot-ema/ui'; import { GlobalStore } from '@dotcms/store'; -import { UVE_MODE } from '@dotcms/types'; +import { DotCMSPage, UVE_MODE } from '@dotcms/types'; import { DotInfoPageComponent, DotMessagePipe, DotNotLicenseComponent, InfoPage } from '@dotcms/ui'; import { EditEmaNavigationBarComponent } from './components/edit-ema-navigation-bar/edit-ema-navigation-bar.component'; @@ -207,24 +206,32 @@ export class DotEmaShellComponent implements OnInit, OnDestroy { this.#updateLocation(cleanedParams); }); - readonly $updateBreadcrumbEffect = effect(() => { + readonly $breadcrumbPage = computed(() => { const page = this.uveStore.pageAsset()?.page; const status = this.uveStore.uveStatus(); - if (page && status === UVE_STATUS.LOADED) { - // untracked: (a) prevents URL-only changes from re-triggering the breadcrumb, - // (b) avoids a TypeError crash when ngOnDestroy calls resetPageParams() (pageParams = null) - // before Angular tears down the effect asynchronously. - const url = untracked(() => this.uveStore.pageParams()?.url); + return page && status === UVE_STATUS.LOADED ? page : null; + }); - this.#globalStore.addNewBreadcrumb({ - label: page.title, - url, - id: `${page.identifier}` - }); - } + readonly $updateBreadcrumb = signalMethod((page) => { + if (!page || !this.uveStore.pageParams()) return; + + const params = this.uveStore.pageFriendlyParams(); + const baseClientHost = this.#activatedRoute.snapshot.data?.uveConfig?.url; + const cleanedParams = normalizeQueryParams(params, baseClientHost); + const urlTree = this.#router.createUrlTree([], { queryParams: cleanedParams }); + + this.#globalStore.addNewBreadcrumb({ + label: page.title, + url: `/dotAdmin/#${urlTree.toString()}`, + id: `${page.identifier}` + }); }); + constructor() { + this.$updateBreadcrumb(this.$breadcrumbPage); + } + ngOnInit(): void { const params = this.#getPageParams(); const viewParams = this.#getViewParams(params.mode);