From 71658db28b9466bd2dd1f0ad807064c7d99b2ebd Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 10:36:07 +0200 Subject: [PATCH 1/8] fix: vertical menu now correctly highlights active item --- package-lock.json | 101 ++++++++---------- package.json | 22 ++-- .../vertical-main-manu.component.scss | 41 +++++++ .../vertical-main-menu.component.ts | 78 +++++++++++--- src/app/shared/services/menu-item.service.ts | 70 ++++++++---- src/app/shared/sidebar-panelmenu.scss | 46 +------- 6 files changed, 213 insertions(+), 145 deletions(-) diff --git a/package-lock.json b/package-lock.json index 64a0a2bd..b9e780a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,17 +26,17 @@ "@ngrx/router-store": "^18.0.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@onecx/accelerator": "5.13.0", - "@onecx/angular-accelerator": "5.13.0", - "@onecx/angular-auth": "5.13.0", - "@onecx/angular-integration-interface": "5.13.0", - "@onecx/angular-remote-components": "5.13.0", - "@onecx/angular-testing": "5.13.0", - "@onecx/angular-webcomponents": "5.13.0", - "@onecx/integration-interface": "5.13.0", - "@onecx/keycloak-auth": "5.13.0", - "@onecx/portal-integration-angular": "5.13.0", - "@onecx/portal-layout-styles": "5.13.0", + "@onecx/accelerator": "5.19.0", + "@onecx/angular-accelerator": "5.19.0", + "@onecx/angular-auth": "5.19.0", + "@onecx/angular-integration-interface": "5.19.0", + "@onecx/angular-remote-components": "5.19.0", + "@onecx/angular-testing": "5.19.0", + "@onecx/angular-webcomponents": "5.19.0", + "@onecx/integration-interface": "5.19.0", + "@onecx/keycloak-auth": "5.19.0", + "@onecx/portal-integration-angular": "5.19.0", + "@onecx/portal-layout-styles": "5.19.0", "@webcomponents/webcomponentsjs": "^2.8.0", "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", @@ -5821,20 +5821,18 @@ } }, "node_modules/@onecx/accelerator": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-5.13.0.tgz", - "integrity": "sha512-pvO/+Az2+D0I1u1BauLE8DWlkmRXOU6dASomVn/8SSD0zaL2WEaSevPHK13N3WTlBRYGPJ+gHt1CyfHN+mfdOw==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-5.19.0.tgz", + "integrity": "sha512-K/pqn2uZLWZaUV6yXBDTVXUCXXq43LSy5cD96f1sXxH1qGVBu1VpLJm2rlXOUkcTR2OSCTMj1SHya9XHmtEYcw==", "peerDependencies": { "rxjs": "^7.8.1", "tslib": "^2.6.3" } }, "node_modules/@onecx/angular-accelerator": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-accelerator/-/angular-accelerator-5.13.0.tgz", - "integrity": "sha512-qfKHQLzs2QEIjb2KMLokYBeUAFOOV4TW/Idl80y/mi+8adIZwwjCqCxHCtBQ6NhBL7mnCgV3cZaPf5D4qO2Itg==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-accelerator/-/angular-accelerator-5.19.0.tgz", + "integrity": "sha512-0KfN4gNG2rukZpVJ1BBq7VJkD4woZlW3wISe59bs/3sPzyiZwxT2UL/8JtRqYvKdotuE99GBwPJfEUYyBQsE6w==", "dependencies": { "tslib": "^2.3.0" }, @@ -5848,6 +5846,7 @@ "@ngneat/until-destroy": "^10.0.0", "@ngx-translate/core": "^15.0.0", "@onecx/angular-integration-interface": "^5", + "@onecx/angular-remote-components": "^5", "@onecx/integration-interface": "^5", "chart.js": "^4.4.3", "d3-scale-chromatic": "^3.1.0", @@ -5856,10 +5855,9 @@ } }, "node_modules/@onecx/angular-auth": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-auth/-/angular-auth-5.13.0.tgz", - "integrity": "sha512-fhXu38kx4lNHx8FwCPoz3ts3bepbYCta6b+45N1sL9lrVZubpxbwxHazX1v+2DAlUIPlTiqDiFYPP04m/XrtHg==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-auth/-/angular-auth-5.19.0.tgz", + "integrity": "sha512-tGEl8SXAgvs2f+cLIUkeSOe3hcuvsuE54XcQ8X2UyqnU8AzHhIKucRTtXfXDee2FMvyy41NG1mPeO4o4G+XPsA==", "dependencies": { "tslib": "^2.3.0" }, @@ -5875,10 +5873,9 @@ } }, "node_modules/@onecx/angular-integration-interface": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-integration-interface/-/angular-integration-interface-5.13.0.tgz", - "integrity": "sha512-FhMnlh3r1GUMysxyAkJYHBjroiL9kDUb40MMtTqu/0eTXVNQNHCkrDhhj+MzYs0s19WjgdLFuJjrGq3L6h15wA==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-integration-interface/-/angular-integration-interface-5.19.0.tgz", + "integrity": "sha512-IV2DjZrAwyPU6HFf1/goJ/kJ7rp+aQU1byPGvzIZ/4ffTpWTqNm3Bq0CXQtWrWAlu2RHB7G5jzHEVOkRmZ30bQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -5889,10 +5886,9 @@ } }, "node_modules/@onecx/angular-remote-components": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-remote-components/-/angular-remote-components-5.13.0.tgz", - "integrity": "sha512-HMnN/Iy75W1by9GjjwQpxV7+mZWRkQgXxdC+kuyLqHYrHZSKMfJGQJd2qjZjf9y4mYfZRxZs2Xtacg42TN+ptA==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-remote-components/-/angular-remote-components-5.19.0.tgz", + "integrity": "sha512-1AaVi0h5QWwN+fJEecqTmtUWwGctxLnox6cSJotrPtBprCSMvnnUgj/HhJLcsyLEGZ8iTX+V79Cu9m0S2JcMuQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -5901,16 +5897,14 @@ "@angular/common": "^18.0.5", "@angular/core": "^18.0.5", "@ngx-translate/core": "^15.0.0", - "@onecx/angular-accelerator": "^5", "@onecx/integration-interface": "^5", "rxjs": "^7.8.1" } }, "node_modules/@onecx/angular-testing": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-testing/-/angular-testing-5.13.0.tgz", - "integrity": "sha512-bm0ea9pggyTM9l5dOuQ4p/VYOCjXU2/s+j8edND/U/WMGdrgMttsUEwuoreXLrC4BTQYCA+E6iWbZDAgLI7z6Q==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-testing/-/angular-testing-5.19.0.tgz", + "integrity": "sha512-aGtfBqB94xjgOVWayrQ5CbnFdK7WZNQLXlix04ZLTXdXuaAqRG+PoiRD060SIy4msYJRi/mC3dJbCXMvgSrsSQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -5920,10 +5914,9 @@ } }, "node_modules/@onecx/angular-webcomponents": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/angular-webcomponents/-/angular-webcomponents-5.13.0.tgz", - "integrity": "sha512-n7icbAK5plHnH0RWTSPvSaeKLqz6k/7blQLdSdWrcVsC5JPNxvLfr58FIRQIdl9qEgiX+lHkTLOHrA8SkKqirQ==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/angular-webcomponents/-/angular-webcomponents-5.19.0.tgz", + "integrity": "sha512-3ZWl5mwjjBObxAfCMA2aR7MFUfOBXQ/V6s/LIrfVShY9aAbP4SIQIpzmZNbt22m47+hn36gNMWuSYKPIbaSaqA==", "dependencies": { "tslib": "^2.3.0" }, @@ -5938,10 +5931,9 @@ } }, "node_modules/@onecx/integration-interface": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-5.13.0.tgz", - "integrity": "sha512-hIT7Zq4GHps/IhwtWRtuFwiqpKCqEHVVh6HdFqAm3xE//ee6Npq92ArJTsZMoCp5huVhXvAdfs4YGdPglEupfg==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-5.19.0.tgz", + "integrity": "sha512-2dInHzzgHc0MeYGcjilVTQDamJHS6YzC1ouSBbX5QoMXOkkg5d8b+hTFQBhKNPDtW8K4BLhllCLNwuAVMMHxIQ==", "peerDependencies": { "@onecx/accelerator": "^5", "rxjs": "^7.8.1", @@ -5949,10 +5941,9 @@ } }, "node_modules/@onecx/keycloak-auth": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-5.13.0.tgz", - "integrity": "sha512-arLY0yI0PFj+Zm8kNPCMuVv6wHz8rGt7vs7YFtTTM8eS0B4tYcnBseGzjJJQJeQ4TNa2LSt7Aoqn0OvhX+7rjA==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-5.19.0.tgz", + "integrity": "sha512-HHbeL/lWB/vabpwprWnVhW3pSLeie+Hk1CG/eSuLGEDFYtTUDEXenuMdpj2p5JEmuZeacbj5qo4oGWs+P8JILg==", "dependencies": { "tslib": "^2.3.0" }, @@ -5967,10 +5958,9 @@ } }, "node_modules/@onecx/portal-integration-angular": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-5.13.0.tgz", - "integrity": "sha512-H7BjGx2W0HMGFxyxzi/2FvY+StDgPXG+6PHfdWt+BiwwHDxjdhqRmKm4nor01X9GE/6i0zp+PzIYzqXEmuMszA==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-5.19.0.tgz", + "integrity": "sha512-YnyH8kNQurz7t6NnWiLNl8C0Z8+hrCj+E1ORnBi/3N2jOMpd0b1XF2vIYGfXVTrmWN2obMDdhZJ6g5CYSeEm8A==", "dependencies": { "tslib": "^2.3.0" }, @@ -6002,10 +5992,9 @@ } }, "node_modules/@onecx/portal-layout-styles": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-5.13.0.tgz", - "integrity": "sha512-N0sl2BJVHo56lnb+kWWZrksrM1GGGS7AoRgMc1p5SCOL0ilBwQVQHg1MfvjIntUTzo8lY7Dgu5Ea4Mc8viKOsQ==", - "license": "Apache-2.0", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-5.19.0.tgz", + "integrity": "sha512-Mj3HU/qkhoT5AXpXl7cC9X4wKN0hFu+ifgreangfezWgceUmZC792jgxNrO1lKzvAxBRttwC7bQ81LHau46eAQ==", "peerDependencies": { "tslib": "^2.6.3" } diff --git a/package.json b/package.json index 470befcc..86b1ef71 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,17 @@ "@ngrx/router-store": "^18.0.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@onecx/accelerator": "5.13.0", - "@onecx/angular-accelerator": "5.13.0", - "@onecx/angular-auth": "5.13.0", - "@onecx/angular-integration-interface": "5.13.0", - "@onecx/angular-remote-components": "5.13.0", - "@onecx/angular-testing": "5.13.0", - "@onecx/angular-webcomponents": "5.13.0", - "@onecx/integration-interface": "5.13.0", - "@onecx/keycloak-auth": "5.13.0", - "@onecx/portal-integration-angular": "5.13.0", - "@onecx/portal-layout-styles": "5.13.0", + "@onecx/accelerator": "5.19.0", + "@onecx/angular-accelerator": "5.19.0", + "@onecx/angular-auth": "5.19.0", + "@onecx/angular-integration-interface": "5.19.0", + "@onecx/angular-remote-components": "5.19.0", + "@onecx/angular-testing": "5.19.0", + "@onecx/angular-webcomponents": "5.19.0", + "@onecx/integration-interface": "5.19.0", + "@onecx/keycloak-auth": "5.19.0", + "@onecx/portal-integration-angular": "5.19.0", + "@onecx/portal-layout-styles": "5.19.0", "@webcomponents/webcomponentsjs": "^2.8.0", "file-saver": "^2.0.5", "keycloak-angular": "^16.0.1", diff --git a/src/app/remotes/vertical-main-menu/vertical-main-manu.component.scss b/src/app/remotes/vertical-main-menu/vertical-main-manu.component.scss index 5a4d4a5c..d9abf586 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-manu.component.scss +++ b/src/app/remotes/vertical-main-menu/vertical-main-manu.component.scss @@ -1 +1,42 @@ @import './../../shared/sidebar-panelmenu.scss'; + +:host ::ng-deep { + .p-panelmenu { + .p-panelmenu-header { + &.ocx-vertical-menu-active-item { + &:not(:focus-visible):not(:hover) { + > .p-panelmenu-header-content { + .p-panelmenu-header-action { + &:not(:hover):not(:focus) { + background-color: var(--menu-active-item-bg-color); + color: var(--menu-active-item-text-color); + } + } + } + } + } + } + .p-panelmenu-content { + .p-menuitem { + &.ocx-vertical-menu-active-item { + &:not(.p-focus) { + > .p-menuitem-content { + &:not(:hover) { + .p-menuitem-link { + background-color: var(--menu-active-item-bg-color); + color: var(--menu-active-item-text-color); + .p-menuitem-icon { + color: var(--menu-active-item-text-color); + } + .p-menuitem-text { + color: var(--menu-active-item-text-color); + } + } + } + } + } + } + } + } + } +} diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts index 4fac0de3..43d9b5b3 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts @@ -4,6 +4,7 @@ import { Component, Inject, Input, OnInit } from '@angular/core' import { RouterModule } from '@angular/router' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' +import { getLocation } from '@onecx/accelerator' import { AngularRemoteComponentsModule, BASE_URL, @@ -12,6 +13,7 @@ import { ocxRemoteWebcomponent, provideTranslateServiceForRoot } from '@onecx/angular-remote-components' +import { EventsTopic } from '@onecx/integration-interface' import { AppStateService, PortalCoreModule, @@ -20,7 +22,7 @@ import { } from '@onecx/portal-integration-angular' import { MenuItem } from 'primeng/api' import { PanelMenuModule } from 'primeng/panelmenu' -import { Observable, ReplaySubject, map, mergeMap, shareReplay, withLatestFrom } from 'rxjs' +import { BehaviorSubject, ReplaySubject, map, mergeMap, shareReplay, withLatestFrom } from 'rxjs' import { Configuration, MenuItemAPIService } from 'src/app/shared/generated' import { MenuItemService } from 'src/app/shared/services/menu-item.service' import { SharedModule } from 'src/app/shared/shared.module' @@ -57,7 +59,11 @@ import { environment } from 'src/environments/environment' }) @UntilDestroy() export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRemoteWebcomponent, OnInit { - menuItems$: Observable | undefined + eventsTopic$ = new EventsTopic() + + menuItems$: BehaviorSubject = new BehaviorSubject([]) + + activeItemClass = 'ocx-vertical-menu-active-item' constructor( @Inject(BASE_URL) private baseUrl: ReplaySubject, @@ -86,21 +92,59 @@ export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRe } getMenuItems() { - this.menuItems$ = this.appStateService.currentWorkspace$.pipe( - mergeMap((currentWorkspace) => - this.menuItemApiService.getMenuItems({ - getMenuItemsRequest: { - workspaceName: currentWorkspace.workspaceName, - menuKeys: ['main-menu'] + this.appStateService.currentWorkspace$ + .pipe( + mergeMap((currentWorkspace) => + this.menuItemApiService.getMenuItems({ + getMenuItemsRequest: { + workspaceName: currentWorkspace.workspaceName, + menuKeys: ['main-menu'] + } + }) + ), + withLatestFrom(this.userService.lang$), + map(([data, userLang]) => this.menuItemService.constructMenuItems(data?.menu?.[0]?.children, userLang)), + map((menuItems) => { + const bestMatch = this.menuItemService.findActiveItemBestMatch(menuItems, getLocation().applicationPath) + if (bestMatch) { + bestMatch.item.styleClass = this.activeItemClass + for (const item of bestMatch.parents) { + item.expanded = true + } } - }) - ), - withLatestFrom(this.userService.lang$, this.appStateService.currentMfe$.asObservable()), - map(([data, userLang, mfeInfo]) => - this.menuItemService.constructMenuItems(data?.menu?.[0]?.children, userLang, mfeInfo.baseHref) - ), - shareReplay(), - untilDestroyed(this) - ) + return menuItems + }), + map((items) => { + this.menuItemService.flatMenuItems(items).forEach((item) => { + if (item.routerLink) { + item.command = (event) => this.changeActiveItem(event.item) + } + }) + + return items + }), + shareReplay(), + untilDestroyed(this) + ) + .subscribe(this.menuItems$) + } + + private updateItemsByActiveItem(item: MenuItem, activeItem: MenuItem | undefined): MenuItem { + return { + styleClass: item.id === activeItem?.id ? this.activeItemClass : '', + items: item.items?.map((i) => this.updateItemsByActiveItem(i, activeItem)), + command: item.routerLink ? (event) => this.changeActiveItem(event.item) : undefined, + label: item.label, + id: item.id, + icon: item.icon, + routerLink: item.routerLink, + url: item.url, + expanded: item.expanded + } + } + + private changeActiveItem(itemToActivate: MenuItem | undefined) { + const items = this.menuItems$.getValue().map((i) => this.updateItemsByActiveItem(i, itemToActivate)) + this.menuItems$.next(items) } } diff --git a/src/app/shared/services/menu-item.service.ts b/src/app/shared/services/menu-item.service.ts index 702aa46a..e8b6017e 100644 --- a/src/app/shared/services/menu-item.service.ts +++ b/src/app/shared/services/menu-item.service.ts @@ -4,23 +4,61 @@ import { MenuItem } from 'primeng/api' @Injectable({ providedIn: 'root' }) export class MenuItemService { - public constructMenuItems( - userWorkspaceMenuItem: UserWorkspaceMenuItem[] | undefined, - userLang: string, - currentMfePath?: string - ): MenuItem[] { + public constructMenuItems(userWorkspaceMenuItem: UserWorkspaceMenuItem[] | undefined, userLang: string): MenuItem[] { const workspaceMenuItems = userWorkspaceMenuItem?.filter((i) => i) // exclude undefined if (workspaceMenuItems) { workspaceMenuItems.sort((a, b) => (a.position ?? 0) - (b.position ?? 0)) const menuItems = workspaceMenuItems.filter((i) => !i.disabled).map((item) => this.mapMenuItem(item, userLang)) - const mfePath = this.stripPath(currentMfePath) - mfePath && this.expandCurrentMfeMenuItems(menuItems, mfePath) return menuItems } else { return [] } } + findActiveItemBestMatch( + items: MenuItem[], + path: string + ): { item: MenuItem; parents: MenuItem[]; matchedSegments: number } | undefined { + const pathToMatch = this.stripPath(path) + let bestMatch: { item: MenuItem; parents: MenuItem[]; matchedSegments: number } | undefined = undefined + + for (const item of items) { + if (item.routerLink) { + const itemPath = this.stripPath(item.routerLink) + if (itemPath === pathToMatch) { + return { item: item, parents: [], matchedSegments: this.countSegments(this.stripPath(pathToMatch)) } + } else if (itemPath && pathToMatch.includes(itemPath)) { + const matchedSegments = + this.countSegments(pathToMatch) + this.countSegments(this.stripPath(pathToMatch.replace(itemPath, ''))) + if ((bestMatch?.matchedSegments || 0) < matchedSegments) { + bestMatch = { + item: item, + parents: [], + matchedSegments: matchedSegments + } + } + } + } + + if (item.items) { + const bestChildMatch = this.findActiveItemBestMatch(item.items, pathToMatch) + if (bestChildMatch && bestChildMatch.matchedSegments > (bestMatch?.matchedSegments || 0)) { + bestMatch = { + item: bestChildMatch.item, + parents: [...bestChildMatch.parents, item], + matchedSegments: bestChildMatch.matchedSegments + } + } + } + } + + return bestMatch + } + + flatMenuItems(items: MenuItem[]): MenuItem[] { + return items.flatMap((item) => ([] as MenuItem[]).concat(item, this.flatMenuItems(item.items ?? []))) + } + /** Item is never undefined when filtered out in constructMenuItems() */ private mapMenuItem(item: UserWorkspaceMenuItem, userLang: string): MenuItem { const isLocal: boolean = !item.external @@ -40,7 +78,7 @@ export class MenuItemService { icon: item.badge ? 'pi pi-' + item.badge : undefined, routerLink: isLocal ? this.stripBaseHref(item.url) : undefined, url: isLocal ? undefined : this.replaceUrlVariables(item.url), - routerLinkActiveOptions: { exact: true } + styleClass: '' } } @@ -50,20 +88,12 @@ export class MenuItemService { return url?.replace(baseUrl, '') } - private stripPath(path: string | undefined): string | undefined { - return path?.slice(path.at(0) === '/' ? 1 : 0, path.at(-1) === '/' ? -1 : path.length) + private stripPath(path: string): string { + return path.slice(path.at(0) === '/' ? 1 : 0, path.at(-1) === '/' ? -1 : path.length) } - private expandCurrentMfeMenuItems(items: MenuItem[], currentMfePath: string): boolean { - for (const item of items) { - if (this.stripPath(item.routerLink) === currentMfePath) return true - else if (item.items && this.expandCurrentMfeMenuItems(item.items, currentMfePath)) { - item.expanded = true - return true - } - } - - return false + private countSegments(path: string): number { + return path.split('/').length } private replaceUrlVariables(url: string | undefined): string | undefined { diff --git a/src/app/shared/sidebar-panelmenu.scss b/src/app/shared/sidebar-panelmenu.scss index dfe20bf3..72461dca 100644 --- a/src/app/shared/sidebar-panelmenu.scss +++ b/src/app/shared/sidebar-panelmenu.scss @@ -12,11 +12,6 @@ background: var(--menu-item-hover-bg-color); color: var(--menu-item-text-color); } - - &.p-menuitem-link-active:not(:hover) { - background-color: var(--menu-active-item-bg-color); - color: var(--menu-active-item-text-color); - } } } @@ -24,9 +19,7 @@ &:focus-visible { .p-panelmenu-header-content { .p-panelmenu-header-action { - &:not(.p-menuitem-link-active) { - background: var(--menu-item-hover-bg-color); - } + background: var(--menu-item-hover-bg-color); } } } @@ -34,9 +27,7 @@ &:not(.p-highlight):not(.p-disabled) { .p-panelmenu-header-action:focus { - &:not(.p-menuitem-link-active) { - background: var(--menu-item-hover-bg-color); - } + background: var(--menu-item-hover-bg-color); } } @@ -44,11 +35,6 @@ .p-panelmenu-header-content { color: var(--menu-item-text-color); background: var(--menu-item-hover-bg-color); - - &.p-menuitem-link-active { - color: var(--menu-item-text-color); - background: var(--menu-item-hover-bg-color); - } } } @@ -82,22 +68,11 @@ .p-submenu-icon { color: var(--menu-item-text-color); } - - &.p-menuitem-link-active { - background: var(--menu-active-item-bg-color); - color: var(--menu-active-item-text-color); - - .p-menuitem-text, - .p-menuitem-icon, - .p-submenu-icon { - color: var(--menu-active-item-text-color); - } - } } } &.p-highlight { - > .p-menuitem-content:not(:has(.p-menuitem-link-active)) { + > .p-menuitem-content { color: var(--menu-item-text-color); background: var(--menu-item-hover-bg-color); @@ -110,7 +85,7 @@ } } - &.p-focus:not(:has(.p-menuitem-link-active)) { + &.p-focus { > .p-menuitem-content { background: var(--menu-item-hover-bg-color); } @@ -118,7 +93,7 @@ } &:not(.p-highlight):not(.p-disabled) { - &.p-focus:not(:has(.p-menuitem-link-active)) { + &.p-focus { > .p-menuitem-content { color: var(--menu-item-text-color); background: var(--menu-item-hover-bg-color); @@ -141,17 +116,6 @@ .p-submenu-icon { color: var(--menu-item-text-color); } - - &.p-menuitem-link-active { - background: var(--menu-item-hover-bg-color); - color: var(--menu-item-text-color); - - .p-menuitem-text, - .p-menuitem-icon, - .p-submenu-icon { - color: var(--menu-item-text-color); - } - } } } } From f721da572bb28976805ef609ccbfb2d4624508a6 Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 11:45:09 +0200 Subject: [PATCH 2/8] fix: router change reaction to ensure correct active item displayed --- .../vertical-main-menu.component.html | 2 +- .../vertical-main-menu.component.ts | 112 +++++++++++------- 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.html b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.html index 3a05d3be..27789c60 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.html +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.html @@ -1 +1 @@ - + diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts index 838bcb1a..3cb8e2b4 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts @@ -4,7 +4,6 @@ import { Component, Inject, Input, OnInit } from '@angular/core' import { RouterModule } from '@angular/router' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' -import { getLocation } from '@onecx/accelerator' import { AngularRemoteComponentsModule, BASE_URL, @@ -22,12 +21,28 @@ import { } from '@onecx/portal-integration-angular' import { MenuItem } from 'primeng/api' import { PanelMenuModule } from 'primeng/panelmenu' -import { BehaviorSubject, ReplaySubject, map, mergeMap, shareReplay, withLatestFrom } from 'rxjs' +import { + BehaviorSubject, + Observable, + ReplaySubject, + combineLatest, + distinctUntilChanged, + filter, + map, + mergeMap, + shareReplay, + withLatestFrom +} from 'rxjs' import { Configuration, MenuItemAPIService } from 'src/app/shared/generated' import { MenuItemService } from 'src/app/shared/services/menu-item.service' import { SharedModule } from 'src/app/shared/shared.module' import { environment } from 'src/environments/environment' +export interface WorkspaceMenuItems { + workspaceName: string + items: MenuItem[] +} + @Component({ selector: 'app-vertical-main-menu', templateUrl: './vertical-main-menu.component.html', @@ -61,7 +76,9 @@ import { environment } from 'src/environments/environment' export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRemoteWebcomponent, OnInit { eventsTopic$ = new EventsTopic() - menuItems$: BehaviorSubject = new BehaviorSubject([]) + menuItems$: BehaviorSubject = new BehaviorSubject( + undefined + ) activeItemClass = 'ocx-vertical-menu-active-item' @@ -88,52 +105,72 @@ export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRe } ngOnInit(): void { - this.getMenuItems() + combineLatest([ + this.eventsTopic$.asObservable().pipe( + filter((e) => e.type === 'navigated'), + map((e) => (e.payload as any).url), + distinctUntilChanged() + ), + this.getMenuItems() + ]) + .pipe( + map(([url, workspaceItems]) => { + const currentItems = this.menuItems$.getValue() + if (!currentItems || currentItems.workspaceName !== workspaceItems.workspaceName) { + return { + workspaceName: workspaceItems.workspaceName, + items: this.changeActiveItem(url, workspaceItems.items) + } + } else { + return { + workspaceName: currentItems.workspaceName, + items: this.changeActiveItem(url, currentItems.items) + } + } + }) + ) + .subscribe(this.menuItems$) } - getMenuItems() { - this.appStateService.currentWorkspace$ - .pipe( - mergeMap((currentWorkspace) => - this.menuItemApiService.getMenuItems({ + private getMenuItems() { + return this.appStateService.currentWorkspace$.pipe( + mergeMap((currentWorkspace) => + this.menuItemApiService + .getMenuItems({ getMenuItemsRequest: { workspaceName: currentWorkspace.workspaceName, menuKeys: ['main-menu'] } }) - ), - withLatestFrom(this.userService.lang$), - map(([data, userLang]) => this.menuItemService.constructMenuItems(data?.menu?.[0]?.children, userLang)), - map((menuItems) => { - const bestMatch = this.menuItemService.findActiveItemBestMatch(menuItems, getLocation().applicationPath) - if (bestMatch) { - bestMatch.item.styleClass = this.activeItemClass - for (const item of bestMatch.parents) { - item.expanded = true - } - } - return menuItems - }), - map((items) => { - this.menuItemService.flatMenuItems(items).forEach((item) => { - if (item.routerLink) { - item.command = (event) => this.changeActiveItem(event.item) - } - }) + .pipe(map((response) => ({ data: response, workspaceName: currentWorkspace.workspaceName }))) + ), + withLatestFrom(this.userService.lang$), + map( + ([{ data, workspaceName }, userLang]): WorkspaceMenuItems => ({ + workspaceName: workspaceName, + items: this.menuItemService.constructMenuItems(data?.menu?.[0]?.children, userLang) + }) + ), + shareReplay(), + untilDestroyed(this) + ) + } - return items - }), - shareReplay(), - untilDestroyed(this) - ) - .subscribe(this.menuItems$) + private changeActiveItem(url: string, menuItems: MenuItem[]): MenuItem[] { + const bestMatch = this.menuItemService.findActiveItemBestMatch(menuItems, url) + if (bestMatch) { + for (const item of bestMatch.parents) { + item.expanded = true + } + } + const items = menuItems.map((i) => this.updateItemsByActiveItem(i, bestMatch?.item)) + return items } private updateItemsByActiveItem(item: MenuItem, activeItem: MenuItem | undefined): MenuItem { return { styleClass: item.id === activeItem?.id ? this.activeItemClass : '', items: item.items?.map((i) => this.updateItemsByActiveItem(i, activeItem)), - command: item.routerLink ? (event) => this.changeActiveItem(event.item) : undefined, label: item.label, id: item.id, icon: item.icon, @@ -142,9 +179,4 @@ export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRe expanded: item.expanded } } - - private changeActiveItem(itemToActivate: MenuItem | undefined) { - const items = this.menuItems$.getValue().map((i) => this.updateItemsByActiveItem(i, itemToActivate)) - this.menuItems$.next(items) - } } From 8a038873beaf2ef817ce12c9aeb94bc44481e9f5 Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 11:45:36 +0200 Subject: [PATCH 3/8] fix: lint fixes --- .../remotes/vertical-main-menu/vertical-main-menu.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts index 3cb8e2b4..2425f616 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts @@ -23,7 +23,6 @@ import { MenuItem } from 'primeng/api' import { PanelMenuModule } from 'primeng/panelmenu' import { BehaviorSubject, - Observable, ReplaySubject, combineLatest, distinctUntilChanged, From df08470e9f17f2d272428b843481c6cb376a9f6d Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 12:44:50 +0200 Subject: [PATCH 4/8] fix: fix tests --- .../vertical-main-menu.component.spec.ts | 250 ++++++++++++++++++ .../shared/services/menu-item.service.spec.ts | 121 +-------- src/app/shared/services/menu-item.service.ts | 3 +- 3 files changed, 263 insertions(+), 111 deletions(-) diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts index 5435e274..880dd573 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts @@ -136,6 +136,14 @@ describe('OneCXVerticalMainMenuComponent', () => { ) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -181,6 +189,14 @@ describe('OneCXVerticalMainMenuComponent', () => { ) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -222,6 +238,14 @@ describe('OneCXVerticalMainMenuComponent', () => { ) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -263,6 +287,14 @@ describe('OneCXVerticalMainMenuComponent', () => { const router = TestBed.inject(Router) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -303,6 +335,14 @@ describe('OneCXVerticalMainMenuComponent', () => { ) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -371,6 +411,14 @@ describe('OneCXVerticalMainMenuComponent', () => { ) const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '' + } + }) + ) await component.ngOnInit() const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) @@ -385,4 +433,206 @@ describe('OneCXVerticalMainMenuComponent', () => { expect(await secondItemChildren[1].getText()).toEqual('Help Items') expect((await secondItemChildren[1].getChildren()).length).toBe(0) }) + + describe('on router changes', () => { + const baseItems = [ + { + key: 'PORTAL_MAIN_MENU', + name: 'Main Menu', + children: [ + { + key: 'CORE_WELCOME', + name: 'Welcome Page', + url: '/admin/welcome', + position: 0, + external: false, + i18n: {}, + children: [] + }, + { + key: 'CORE_AH_MGMT', + name: 'Announcement & Help', + url: '', + position: 1, + external: false, + i18n: {}, + children: [ + { + key: 'CORE_AH_MGMT_A', + name: 'Announcements', + url: '/admin/announcement', + position: 1, + external: false, + i18n: {}, + children: [] + }, + { + key: 'CORE_AH_MGMT_HI', + name: 'Help Items', + url: '/admin/help', + position: 2, + external: false, + i18n: {}, + children: [] + } + ] + } + ] + } + ] + + it('should expand active item parents', async () => { + const appStateService = TestBed.inject(AppStateService) + spyOn(appStateService.currentWorkspace$, 'asObservable').and.returnValue( + of({ + workspaceName: 'test-workspace' + }) as any + ) + spyOn(appStateService.currentMfe$, 'asObservable').and.returnValue(of({} as any)) + menuItemApiSpy.getMenuItems.and.returnValue( + of({ + workspaceName: 'test-workspace', + menu: baseItems + } as any) + ) + + const { fixture, component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '/admin/help' + } + }) + ) + await component.ngOnInit() + + const menu = await TestbedHarnessEnvironment.harnessForFixture(fixture, PPanelMenuHarness) + const panels = await menu.getAllPanels() + expect(panels.length).toEqual(2) + + expect((await panels[0].getChildren()).length).toBe(0) + const secondItemChildren = await panels[1].getChildren() + expect(secondItemChildren.length).toBe(2) + expect(await secondItemChildren[0].getText()).toEqual('Announcements') + expect(await (await secondItemChildren[0].host()).hasClass(component.activeItemClass)).toBeFalse() + expect(await secondItemChildren[1].getText()).toEqual('Help Items') + expect(await (await secondItemChildren[1].host()).hasClass(component.activeItemClass)).toBeTrue() + + const menuItems = component.menuItems$.getValue() + expect(menuItems?.items.length).toBe(2) + expect(menuItems?.items[0].expanded).toBeFalsy() + expect(menuItems?.items[1].expanded).toBeTrue() + }) + + it('should update items if workspace did not change', async () => { + const appStateService = TestBed.inject(AppStateService) + spyOn(appStateService.currentWorkspace$, 'asObservable').and.returnValue( + of({ + workspaceName: 'test-workspace' + }) as any + ) + spyOn(appStateService.currentMfe$, 'asObservable').and.returnValue(of({} as any)) + menuItemApiSpy.getMenuItems.and.returnValue( + of({ + workspaceName: 'test-workspace', + menu: [ + { + key: 'my-item', + name: 'item-name-1', + url: '/admin/help', + position: 2, + external: false, + i18n: {}, + children: [] + } + ] + } as any) + ) + + const { component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '/admin/help' + } + }) + ) + component.menuItems$.next({ + workspaceName: 'test-workspace', + items: [ + { + id: 'my-item', + items: undefined, + label: 'item-name-2', + routerLink: '/admin/help' + } + ] + }) + await component.ngOnInit() + + const menuItems = component.menuItems$.getValue() + expect(menuItems?.items.length).toBe(1) + expect(menuItems?.items[0].label).toBe('item-name-2') + }) + + it('should overwrite items if workspace has changed', async () => { + const appStateService = TestBed.inject(AppStateService) + spyOn(appStateService.currentWorkspace$, 'asObservable').and.returnValue( + of({ + workspaceName: 'other-workspace' + }) as any + ) + spyOn(appStateService.currentMfe$, 'asObservable').and.returnValue(of({} as any)) + menuItemApiSpy.getMenuItems.and.returnValue( + of({ + workspaceName: 'other-workspace', + menu: [ + { + key: 'PORTAL_MAIN_MENU', + name: 'Main Menu', + children: [ + { + key: 'my-item', + name: 'item-name-1', + url: '/admin/help', + position: 2, + external: false, + i18n: {}, + children: [] + } + ] + } + ] + } as any) + ) + + const { component } = setUp() + spyOn(component.eventsTopic$, 'asObservable').and.returnValue( + of({ + type: 'navigated', + payload: { + url: '/admin/help' + } + }) + ) + component.menuItems$.next({ + workspaceName: 'test-workspace', + items: [ + { + id: 'my-item', + items: undefined, + label: 'item-name-2', + routerLink: '/admin/help' + } + ] + }) + await component.ngOnInit() + + const menuItems = component.menuItems$.getValue() + expect(menuItems?.items.length).toBe(1) + expect(menuItems?.items[0].label).toBe('item-name-1') + }) + }) }) diff --git a/src/app/shared/services/menu-item.service.spec.ts b/src/app/shared/services/menu-item.service.spec.ts index 77321f9c..058c64cf 100644 --- a/src/app/shared/services/menu-item.service.spec.ts +++ b/src/app/shared/services/menu-item.service.spec.ts @@ -89,8 +89,7 @@ describe('MenuItemService', () => { icon: 'pi pi-star', routerLink: '/item1', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined }, { id: '2', @@ -98,8 +97,7 @@ describe('MenuItemService', () => { icon: 'pi pi-check', routerLink: undefined, items: undefined, - url: 'http://external.com', - routerLinkActiveOptions: { exact: true } + url: 'http://external.com' } ] const result = service.constructMenuItems(input, 'en') @@ -138,8 +136,7 @@ describe('MenuItemService', () => { icon: 'pi pi-star', routerLink: '/item1?param=[[DONTREPLACEME]]', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined }, { id: '2', @@ -147,8 +144,7 @@ describe('MenuItemService', () => { icon: 'pi pi-check', routerLink: undefined, items: undefined, - url: 'http://external.com?id=2', - routerLinkActiveOptions: { exact: true } + url: 'http://external.com?id=2' } ] const result = service.constructMenuItems(input, 'en') @@ -189,8 +185,7 @@ describe('MenuItemService', () => { icon: 'pi pi-star', routerLink: '/item1?param=[[DONTREPLACEME]]', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined }, { id: '2', @@ -198,8 +193,7 @@ describe('MenuItemService', () => { icon: 'pi pi-check', routerLink: undefined, items: undefined, - url: 'http://external.com?id=1', - routerLinkActiveOptions: { exact: true } + url: 'http://external.com?id=1' } ] sessionStorage.clear() @@ -241,8 +235,7 @@ describe('MenuItemService', () => { icon: 'pi pi-star', routerLink: '/item1?param=[[DONTREPLACEME]]', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined }, { id: '2', @@ -250,8 +243,7 @@ describe('MenuItemService', () => { icon: 'pi pi-check', routerLink: undefined, items: undefined, - url: 'http://external.com?id=', - routerLinkActiveOptions: { exact: true } + url: 'http://external.com?id=' } ] sessionStorage.clear() @@ -294,8 +286,7 @@ describe('MenuItemService', () => { icon: 'pi pi-star', routerLink: '/item1?param=[[DONTREPLACEME]]', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined }, { id: '2', @@ -303,8 +294,7 @@ describe('MenuItemService', () => { icon: 'pi pi-check', routerLink: undefined, items: undefined, - url: 'http://external.com?id=2&mykey=my-sessionstorage-key', - routerLinkActiveOptions: { exact: true } + url: 'http://external.com?id=2&mykey=my-sessionstorage-key' } ] const result = service.constructMenuItems(input, 'en') @@ -483,103 +473,16 @@ describe('MenuItemService', () => { icon: 'pi pi-child', routerLink: '/child', items: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined } ], - url: undefined, - routerLinkActiveOptions: { exact: true } + url: undefined } ] const result = service.constructMenuItems(input, 'en') expect(result).toEqual(expected) }) - it('should expand parents of current mfe item', () => { - const input: UserWorkspaceMenuItem[] = [ - { - key: '1', - name: 'Parent Item', - position: 1, - disabled: false, - external: false, - children: [ - { - key: '1.1', - name: 'Second parent Item', - position: 1, - disabled: false, - external: false, - children: [ - { - key: '1.1.1', - name: 'Child Item', - position: 1, - disabled: false, - external: false, - url: '/admin/mfe', - children: [] - }, - { - key: '1.1.2', - name: 'Second Child Item', - position: 2, - disabled: false, - external: false, - url: 'admin/otherMfe/', - children: [] - } - ] - } - ] - } - ] - const expected: MenuItem[] = [ - { - id: '1', - label: '', - expanded: true, - icon: undefined, - routerLink: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true }, - items: [ - { - id: '1.1', - label: '', - expanded: true, - icon: undefined, - routerLink: undefined, - url: undefined, - routerLinkActiveOptions: { exact: true }, - items: [ - { - id: '1.1.1', - label: '', - items: undefined, - routerLink: '/admin/mfe', - url: undefined, - icon: undefined, - routerLinkActiveOptions: { exact: true } - }, - { - id: '1.1.2', - label: '', - items: undefined, - routerLink: 'admin/otherMfe/', - url: undefined, - icon: undefined, - routerLinkActiveOptions: { exact: true } - } - ] - } - ] - } - ] - const result = service.constructMenuItems(input, 'en', '/admin/mfe') - expect(result).toEqual(expected) - }) - it('should handle different languages', () => { const input: UserWorkspaceMenuItem[] = [ { diff --git a/src/app/shared/services/menu-item.service.ts b/src/app/shared/services/menu-item.service.ts index e8b6017e..74a0a5d8 100644 --- a/src/app/shared/services/menu-item.service.ts +++ b/src/app/shared/services/menu-item.service.ts @@ -77,8 +77,7 @@ export class MenuItemService { label, icon: item.badge ? 'pi pi-' + item.badge : undefined, routerLink: isLocal ? this.stripBaseHref(item.url) : undefined, - url: isLocal ? undefined : this.replaceUrlVariables(item.url), - styleClass: '' + url: isLocal ? undefined : this.replaceUrlVariables(item.url) } } From d95dd3a29f7180ed7c83c08d62a489164adf1834 Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 13:16:48 +0200 Subject: [PATCH 5/8] fix: sonar code coverage --- .../shared/services/menu-item.service.spec.ts | 84 ++++++++++++++++++- src/app/shared/services/menu-item.service.ts | 7 +- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/app/shared/services/menu-item.service.spec.ts b/src/app/shared/services/menu-item.service.spec.ts index 058c64cf..2ffda7b1 100644 --- a/src/app/shared/services/menu-item.service.spec.ts +++ b/src/app/shared/services/menu-item.service.spec.ts @@ -324,12 +324,49 @@ describe('MenuItemService', () => { external: false, url: '/item1', badge: 'star', - children: [], - i18n: { en: 'Item 1 EN' } + i18n: { en: 'Item 1 EN' }, + children: [ + { + key: '5', + name: 'Item 5', + position: 3, + disabled: false, + external: false, + url: '/item5', + badge: 'check', + children: [], + i18n: { en: 'Item 5 EN' } + }, + { + key: '3', + name: 'Item 3', + position: 1, + disabled: false, + external: true, + badge: 'check', + children: [], + i18n: { en: 'Item 3 EN' } + }, + { + key: '4', + name: 'Item 4', + position: 2, + disabled: false, + external: false, + url: '/item4', + badge: 'check', + children: [], + i18n: { en: 'Item 4 EN' } + } + ] } ] const result = service.constructMenuItems(input, 'en') expect(result[0].id).toBe('1') + expect(result[0].items?.length).toBe(3) + expect(result[0].items?.at(0)?.id).toBe('3') + expect(result[0].items?.at(1)?.id).toBe('4') + expect(result[0].items?.at(2)?.id).toBe('5') expect(result[1].id).toBe('2') }) @@ -502,4 +539,47 @@ describe('MenuItemService', () => { expect(resultEn[0].label).toBe('Item 1 EN') expect(resultFr[0].label).toBe('Item 1 FR') }) + + it('should select closest item as best match', () => { + const items: MenuItem[] = [ + { + label: 'Parent1', + items: [ + { + label: 'Workspace', + routerLink: 'admin/workspace' + }, + { + label: 'User search', + routerLink: 'admin/user/search' + }, + { + label: 'Help', + routerLink: 'admin/help' + } + ] + }, + { + label: 'Parent2', + items: [ + { + label: 'Tenant', + routerLink: 'admin/tenant' + }, + { + label: 'MyPage', + routerLink: 'admin/help/my-page' + } + ] + } + ] + + const result = service.findActiveItemBestMatch(items, '/admin/help/my-page') + + expect(result).toBeDefined() + expect(result?.item.label).toBe('MyPage') + expect(result?.matchedSegments).toEqual(3) + expect(result?.parents.length).toBe(1) + expect(result?.parents[0].label).toBe('Parent2') + }) }) diff --git a/src/app/shared/services/menu-item.service.ts b/src/app/shared/services/menu-item.service.ts index 74a0a5d8..b3dbd1ee 100644 --- a/src/app/shared/services/menu-item.service.ts +++ b/src/app/shared/services/menu-item.service.ts @@ -28,8 +28,7 @@ export class MenuItemService { if (itemPath === pathToMatch) { return { item: item, parents: [], matchedSegments: this.countSegments(this.stripPath(pathToMatch)) } } else if (itemPath && pathToMatch.includes(itemPath)) { - const matchedSegments = - this.countSegments(pathToMatch) + this.countSegments(this.stripPath(pathToMatch.replace(itemPath, ''))) + const matchedSegments = this.countSegments(itemPath) if ((bestMatch?.matchedSegments || 0) < matchedSegments) { bestMatch = { item: item, @@ -55,10 +54,6 @@ export class MenuItemService { return bestMatch } - flatMenuItems(items: MenuItem[]): MenuItem[] { - return items.flatMap((item) => ([] as MenuItem[]).concat(item, this.flatMenuItems(item.items ?? []))) - } - /** Item is never undefined when filtered out in constructMenuItems() */ private mapMenuItem(item: UserWorkspaceMenuItem, userLang: string): MenuItem { const isLocal: boolean = !item.external From 785f814991b2a6d89ee286e72b9d1938b7c67a0c Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 13:36:28 +0200 Subject: [PATCH 6/8] fix: fix sonar issues --- .../vertical-main-menu.component.spec.ts | 16 +++++----- .../vertical-main-menu.component.ts | 5 +-- src/app/shared/services/menu-item.service.ts | 31 +++++++++++-------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts index 880dd573..291502c6 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.spec.ts @@ -140,7 +140,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -193,7 +193,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -242,7 +242,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -291,7 +291,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -339,7 +339,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -379,7 +379,7 @@ describe('OneCXVerticalMainMenuComponent', () => { { key: 'CORE_AH_MGMT', name: 'Announcement & Help', - url: '', + url: 'page-url', position: 1, external: false, i18n: {}, @@ -415,7 +415,7 @@ describe('OneCXVerticalMainMenuComponent', () => { of({ type: 'navigated', payload: { - url: '' + url: 'page-url' } }) ) @@ -452,7 +452,7 @@ describe('OneCXVerticalMainMenuComponent', () => { { key: 'CORE_AH_MGMT', name: 'Announcement & Help', - url: '', + url: 'page-url', position: 1, external: false, i18n: {}, diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts index 2425f616..a4b30883 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts @@ -12,7 +12,7 @@ import { ocxRemoteWebcomponent, provideTranslateServiceForRoot } from '@onecx/angular-remote-components' -import { EventsTopic } from '@onecx/integration-interface' +import { EventsTopic, NavigatedEventPayload } from '@onecx/integration-interface' import { AppStateService, PortalCoreModule, @@ -107,7 +107,8 @@ export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRe combineLatest([ this.eventsTopic$.asObservable().pipe( filter((e) => e.type === 'navigated'), - map((e) => (e.payload as any).url), + map((e) => (e.payload as NavigatedEventPayload).url), + filter((url): url is string => !!url), distinctUntilChanged() ), this.getMenuItems() diff --git a/src/app/shared/services/menu-item.service.ts b/src/app/shared/services/menu-item.service.ts index b3dbd1ee..96d6b3e2 100644 --- a/src/app/shared/services/menu-item.service.ts +++ b/src/app/shared/services/menu-item.service.ts @@ -23,19 +23,12 @@ export class MenuItemService { let bestMatch: { item: MenuItem; parents: MenuItem[]; matchedSegments: number } | undefined = undefined for (const item of items) { - if (item.routerLink) { - const itemPath = this.stripPath(item.routerLink) - if (itemPath === pathToMatch) { - return { item: item, parents: [], matchedSegments: this.countSegments(this.stripPath(pathToMatch)) } - } else if (itemPath && pathToMatch.includes(itemPath)) { - const matchedSegments = this.countSegments(itemPath) - if ((bestMatch?.matchedSegments || 0) < matchedSegments) { - bestMatch = { - item: item, - parents: [], - matchedSegments: matchedSegments - } - } + const segments = this.getMatchedSegments(item, pathToMatch) + if (segments > (bestMatch?.matchedSegments || 0)) { + bestMatch = { + item: item, + parents: [], + matchedSegments: segments } } @@ -54,6 +47,18 @@ export class MenuItemService { return bestMatch } + private getMatchedSegments(item: MenuItem, strippedPath: string): number { + const itemStrippedPath = item.routerLink ? this.stripPath(item.routerLink) : undefined + if (itemStrippedPath && itemStrippedPath === strippedPath) { + return this.countSegments(this.stripPath(strippedPath)) + } else if (itemStrippedPath && strippedPath.includes(itemStrippedPath)) { + const matchedSegments = this.countSegments(itemStrippedPath) + return matchedSegments + } + + return 0 + } + /** Item is never undefined when filtered out in constructMenuItems() */ private mapMenuItem(item: UserWorkspaceMenuItem, userLang: string): MenuItem { const isLocal: boolean = !item.external From 42f069a3d5e0d3309b194f7df5f7b5924d9d0866 Mon Sep 17 00:00:00 2001 From: markuczy Date: Mon, 21 Oct 2024 13:52:10 +0200 Subject: [PATCH 7/8] fix: sonar code coverage --- src/app/shared/services/menu-item.service.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/services/menu-item.service.spec.ts b/src/app/shared/services/menu-item.service.spec.ts index 2ffda7b1..c2a1ff4c 100644 --- a/src/app/shared/services/menu-item.service.spec.ts +++ b/src/app/shared/services/menu-item.service.spec.ts @@ -340,7 +340,6 @@ describe('MenuItemService', () => { { key: '3', name: 'Item 3', - position: 1, disabled: false, external: true, badge: 'check', @@ -551,7 +550,7 @@ describe('MenuItemService', () => { }, { label: 'User search', - routerLink: 'admin/user/search' + routerLink: 'admin/user/search/' }, { label: 'Help', From 302ccc73ccf679276eef39ad5510f11046ac82c0 Mon Sep 17 00:00:00 2001 From: markuczy Date: Tue, 22 Oct 2024 14:37:27 +0200 Subject: [PATCH 8/8] fix: destroy topic on component destroy --- .../vertical-main-menu/vertical-main-menu.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts index a4b30883..c43d411f 100644 --- a/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts +++ b/src/app/remotes/vertical-main-menu/vertical-main-menu.component.ts @@ -1,6 +1,6 @@ import { CommonModule, Location } from '@angular/common' import { HttpClient } from '@angular/common/http' -import { Component, Inject, Input, OnInit } from '@angular/core' +import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core' import { RouterModule } from '@angular/router' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' @@ -72,7 +72,7 @@ export interface WorkspaceMenuItems { ] }) @UntilDestroy() -export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRemoteWebcomponent, OnInit { +export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRemoteWebcomponent, OnInit, OnDestroy { eventsTopic$ = new EventsTopic() menuItems$: BehaviorSubject = new BehaviorSubject( @@ -91,6 +91,9 @@ export class OneCXVerticalMainMenuComponent implements ocxRemoteComponent, ocxRe ) { this.userService.lang$.subscribe((lang) => this.translateService.use(lang)) } + ngOnDestroy(): void { + this.eventsTopic$.destroy() + } @Input() set ocxRemoteComponentConfig(config: RemoteComponentConfig) { this.ocxInitRemoteComponent(config)