diff --git a/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts b/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts index 44fc9d5da8e3..db465109d045 100644 --- a/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts +++ b/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.spec.ts @@ -2,7 +2,7 @@ import { of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; +import { By, Title } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { RouterTestingModule } from '@angular/router/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; @@ -50,6 +50,7 @@ describe('DotContentletWrapperComponent', () => { let dotAddContentletService: DotContentletEditorService; let dotAlertConfirmService: DotAlertConfirmService; let dotRouterService: DotRouterService; + let titleService: Title; let dotIframeService: DotIframeService; beforeEach( @@ -66,6 +67,7 @@ describe('DotContentletWrapperComponent', () => { DotcmsConfigService, LoggerService, StringUtils, + Title, { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, { provide: DotHttpErrorManagerService, @@ -113,8 +115,10 @@ describe('DotContentletWrapperComponent', () => { dotAddContentletService = de.injector.get(DotContentletEditorService); dotAlertConfirmService = de.injector.get(DotAlertConfirmService); dotRouterService = de.injector.get(DotRouterService); + titleService = de.injector.get(Title); dotIframeService = de.injector.get(DotIframeService); + spyOn(titleService, 'setTitle'); spyOn(dotIframeService, 'reload'); spyOn(dotAddContentletService, 'clear'); spyOn(dotAddContentletService, 'load'); @@ -123,6 +127,11 @@ describe('DotContentletWrapperComponent', () => { spyOn(component.custom, 'emit'); }); + afterEach(() => { + component.url = null; + fixture.detectChanges(); + }); + it('should show dot-iframe-dialog', () => { fixture.detectChanges(); dotIframeDialog = de.query(By.css('dot-iframe-dialog')); @@ -304,6 +313,23 @@ describe('DotContentletWrapperComponent', () => { dotIframeDialog.triggerEventHandler('custom', params); expect(dotIframeService.reload).toHaveBeenCalledTimes(1); }); + + it('should set Header and Page title', () => { + const params = { + detail: { + name: 'edit-contentlet-loaded', + data: { + contentType: 'Blog', + pageTitle: 'test' + } + } + }; + spyOn(titleService, 'getTitle').and.returnValue(' - dotCMS platform'); + dotIframeDialog.triggerEventHandler('custom', params); + + expect(component.header).toBe('Blog'); + expect(titleService.setTitle).toHaveBeenCalledWith('test - dotCMS platform'); + }); }); }); }); diff --git a/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.ts b/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.ts index d199ff08a11a..d7bbeee26b1b 100644 --- a/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.ts +++ b/apps/dotcms-ui/src/app/view/components/dot-contentlet-editor/components/dot-contentlet-wrapper/dot-contentlet-wrapper.component.ts @@ -4,6 +4,7 @@ import { DotMessageService } from '@services/dot-message/dot-messages.service'; import { DotAlertConfirmService } from '@services/dot-alert-confirm'; import { DotRouterService } from '@services/dot-router/dot-router.service'; import { DotIframeService } from '@components/_common/iframe/service/dot-iframe/dot-iframe.service'; +import { Title } from '@angular/platform-browser'; export interface DotCMSEditPageEvent { name: string; @@ -49,7 +50,8 @@ export class DotContentletWrapperComponent { private dotAlertConfirmService: DotAlertConfirmService, private dotMessageService: DotMessageService, private dotRouterService: DotRouterService, - private dotIframeService: DotIframeService + private dotIframeService: DotIframeService, + private titleService: Title ) { if (!this.customEventsHandler) { this.customEventsHandler = { @@ -83,6 +85,13 @@ export class DotContentletWrapperComponent { }, 'edit-contentlet-loaded': (e: CustomEvent) => { this.header = e.detail.data.contentType; + this.titleService.setTitle( + `${ + e.detail.data.pageTitle + ? e.detail.data.pageTitle + ' - ' + : this.titleService.getTitle() + } ${this.titleService.getTitle().split(' - ')[1]}` + ); } }; } diff --git a/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts b/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts index 51aea2087e3e..5bf85886627b 100644 --- a/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts +++ b/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.spec.ts @@ -15,6 +15,7 @@ import { DotcmsEventsService, LoginService, Auth } from '@dotcms/dotcms-js'; import { Observable, Subject, of } from 'rxjs'; import { skip } from 'rxjs/operators'; +import { Title } from '@angular/platform-browser'; class RouterMock { _events: Subject = new Subject(); @@ -69,6 +70,16 @@ class DotMenuServiceMock { } } +class TitleServiceMock { + getTitle(): string { + return 'dotCMS platform'; + } + + setTitle(_title: string): void { + /* */ + } +} + class DotcmsEventsServiceMock { _events: Subject = new Subject(); @@ -156,6 +167,7 @@ describe('DotNavigationService', () => { let dotMenuService: DotMenuService; let loginService: LoginService; let router; + let titleService: Title; beforeEach( waitForAsync(() => { @@ -167,6 +179,10 @@ describe('DotNavigationService', () => { provide: DotcmsEventsService, useClass: DotcmsEventsServiceMock }, + { + provide: Title, + useClass: TitleServiceMock + }, { provide: DotMenuService, useClass: DotMenuServiceMock @@ -208,7 +224,9 @@ describe('DotNavigationService', () => { loginService = testbed.inject(LoginService); dotEventService = testbed.inject(DotEventsService); router = testbed.inject(Router); + titleService = testbed.inject(Title); + spyOn(titleService, 'setTitle'); spyOn(dotEventService, 'notify'); spyOn(dotMenuService, 'reloadMenu').and.callThrough(); localStorage.clear(); @@ -326,7 +344,7 @@ describe('DotNavigationService', () => { }); it('should go to first portlet on auth change', () => { - ((loginService as unknown) as LoginServiceMock).triggerNewAuth(baseMockAuth); + (loginService as unknown as LoginServiceMock).triggerNewAuth(baseMockAuth); spyOn(dotMenuService, 'loadMenu').and.returnValue( of([ @@ -364,6 +382,11 @@ describe('DotNavigationService', () => { router.triggerNavigationEnd('/123'); }); + it('should set Page title based on url', () => { + router.triggerNavigationEnd('url/link1'); + expect(titleService.setTitle).toHaveBeenCalledWith('Label 1 - dotCMS platform'); + }); + // TODO: needs to fix this, looks like the dotcmsEventsService instance is different here not sure why. xit('should subscribe to UPDATE_PORTLET_LAYOUTS websocket event', () => { expect(dotcmsEventsService.subscribeTo).toHaveBeenCalledWith('UPDATE_PORTLET_LAYOUTS'); diff --git a/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts b/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts index c0516266b83e..7c571607ce21 100644 --- a/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts +++ b/apps/dotcms-ui/src/app/view/components/dot-navigation/services/dot-navigation.service.ts @@ -13,6 +13,7 @@ import { DotRouterService } from '@services/dot-router/dot-router.service'; import { DotIframeService } from '../../_common/iframe/service/dot-iframe/dot-iframe.service'; import { DotEventsService } from '@services/dot-events/dot-events.service'; import { DotLocalstorageService } from '@services/dot-localstorage/dot-localstorage.service'; +import { Title } from '@angular/platform-browser'; export const replaceSectionsMap = { 'edit-page': 'site-browser' @@ -67,60 +68,59 @@ function isEditPageFromSiteBrowser(menuId: string, previousUrl: string): boolean return menuId === 'edit-page' && previousUrl === '/c/site-browser'; } -const setActiveItems = ({ url, collapsed, menuId, previousUrl }: DotActiveItemsProps) => ( - source: Observable -) => { - let urlId = getTheUrlId(url); - - return source.pipe( - map((m: DotMenu[]) => { - const menus: DotMenu[] = [...m]; - let isActive = false; - - if ( - isEditPageFromSiteBrowser(menuId, previousUrl) || - (isDetailPage(urlId, url) && isMenuActive(menus)) - ) { - return null; - } - - // When user browse using the navigation (Angular Routing) - if (menuId && menuId !== 'edit-page') { - return getActiveMenuFromMenuId({ - menus, - menuId, - collapsed, - url: urlId, - previousUrl - }); - } - - // When user browse using the browser url bar, direct links or reload page - urlId = replaceIdForNonMenuSection(urlId) || urlId; - - for (let i = 0; i < menus.length; i++) { - menus[i].active = false; - menus[i].isOpen = false; +const setActiveItems = + ({ url, collapsed, menuId, previousUrl }: DotActiveItemsProps) => + (source: Observable) => { + let urlId = getTheUrlId(url); + return source.pipe( + map((m: DotMenu[]) => { + const menus: DotMenu[] = [...m]; + let isActive = false; + + if ( + isEditPageFromSiteBrowser(menuId, previousUrl) || + (isDetailPage(urlId, url) && isMenuActive(menus)) + ) { + return null; + } - for (let k = 0; k < menus[i].menuItems.length; k++) { - // Once we activate the first one all the others are close - if (isActive) { - menus[i].menuItems[k].active = false; - } + // When user browse using the navigation (Angular Routing) + if (menuId && menuId !== 'edit-page') { + return getActiveMenuFromMenuId({ + menus, + menuId, + collapsed, + url: urlId, + previousUrl + }); + } - if (!isActive && menus[i].menuItems[k].id === urlId) { - isActive = true; - menus[i].active = true; - menus[i].isOpen = true; - menus[i].menuItems[k].active = true; + // When user browse using the browser url bar, direct links or reload page + urlId = replaceIdForNonMenuSection(urlId) || urlId; + + for (let i = 0; i < menus.length; i++) { + menus[i].active = false; + menus[i].isOpen = false; + + for (let k = 0; k < menus[i].menuItems.length; k++) { + // Once we activate the first one all the others are close + if (isActive) { + menus[i].menuItems[k].active = false; + } + + if (!isActive && menus[i].menuItems[k].id === urlId) { + isActive = true; + menus[i].active = true; + menus[i].isOpen = true; + menus[i].menuItems[k].active = true; + } } } - } - return menus; - }) - ); -}; + return menus; + }) + ); + }; const DOTCMS_MENU_STATUS = 'dotcms.menu.status'; @@ -133,6 +133,7 @@ function getTheUrlId(url: string): string { export class DotNavigationService { private _collapsed$: BehaviorSubject = new BehaviorSubject(true); private _items$: BehaviorSubject = new BehaviorSubject([]); + private _appMainTitle: string; constructor( private dotEventsService: DotEventsService, @@ -142,8 +143,10 @@ export class DotNavigationService { private dotcmsEventsService: DotcmsEventsService, private loginService: LoginService, private router: Router, - private dotLocalstorageService: DotLocalstorageService + private dotLocalstorageService: DotLocalstorageService, + private titleService: Title ) { + this._appMainTitle = this.titleService.getTitle(); const savedMenuStatus = this.dotLocalstorageService.getItem(DOTCMS_MENU_STATUS); this._collapsed$.next(savedMenuStatus === false ? false : true); @@ -153,16 +156,23 @@ export class DotNavigationService { this.onNavigationEnd() .pipe( - switchMap((event: NavigationEnd) => - this.dotMenuService.loadMenu().pipe( + switchMap((event: NavigationEnd) => { + return this.dotMenuService.loadMenu().pipe( + tap((menu: DotMenu[]) => { + const pageTitle = this.getPageCurrentTitle(event.url, menu); + this.titleService.setTitle( + `${pageTitle ? pageTitle + ' - ' : ''} ${this._appMainTitle}` + ); + return menu; + }), setActiveItems({ url: event.url, collapsed: this._collapsed$.getValue(), menuId: this.router.getCurrentNavigation().extras.state?.menuId, previousUrl: this.dotRouterService.previousUrl }) - ) - ), + ); + }), filter((menu) => !!menu) ) .subscribe((menus: DotMenu[]) => { @@ -373,4 +383,17 @@ export class DotNavigationService { private setMenu(menu: DotMenu[]) { this._items$.next(this.addMenuLinks(menu)); } + + private getPageCurrentTitle(url: string, menu: DotMenu[]): string { + let title = ''; + const flattedMenu = menu + .reduce((a, { menuItems }) => [...a, ...menuItems], []) + .reduce((a, { label, menuLink }) => ({ ...a, [menuLink]: label }), {}); + + Object.entries(flattedMenu).forEach(([menuLink, label]: [string, string]) => { + title = url.indexOf(menuLink) >= 0 ? label : title; + }); + + return title; + } }