diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index c20c6849ed..7a285b4461 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -2,7 +2,6 @@ import { EventEmitter, Type } from '@angular/core'; import { Routes } from '@angular/router'; import { Subject } from 'rxjs'; import { eLayoutType } from '../enums/common'; -import { TreeNode } from '../utils'; import { Environment } from './environment'; export namespace ABP { @@ -75,11 +74,6 @@ export namespace ABP { group?: string; } - export interface RouteGroup { - group: string; - items: TreeNode[]; - } - export interface Tab extends Nav { component: Type; } diff --git a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts index 20dc13ddfe..c7acce3aee 100644 --- a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts @@ -1,9 +1,15 @@ import { Injectable, Injector, OnDestroy } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; import { ABP } from '../models/common'; import { OTHERS_GROUP } from '../tokens'; import { pushValueTo } from '../utils/array-utils'; -import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; +import { + BaseTreeNode, + createTreeFromList, + TreeNode, + RouteGroup, + createGroupMap, +} from '../utils/tree-utils'; import { ConfigStateService } from './config-state.service'; import { PermissionService } from './permission.service'; @@ -18,6 +24,8 @@ export abstract class AbstractTreeService[]>([]); private _visible$ = new BehaviorSubject[]>([]); + protected othersGroup: string; + get flat(): T[] { return this._flat$.value; } @@ -51,6 +59,11 @@ export abstract class AbstractTreeService[]): RouteGroup[] | undefined { + const map = createGroupMap(list, this.othersGroup); + return !map ? undefined : Array.from(map, ([key, items]) => ({ group: key, items })); + } + private filterWith(setOrMap: Set | Map): T[] { return this._flat$.value.filter(item => !setOrMap.has(item[this.id])); } @@ -158,6 +171,7 @@ export abstract class AbstractNavTreeService .createOnUpdateStream(state => state) .subscribe(() => this.refresh()); this.permissionService = injector.get(PermissionService); + this.othersGroup = injector.get(OTHERS_GROUP); } protected isGranted({ requiredPolicy }: T): boolean { @@ -182,24 +196,11 @@ export abstract class AbstractNavTreeService @Injectable({ providedIn: 'root' }) export class RoutesService extends AbstractNavTreeService { - private readonly othersGroup: string; - - constructor(injector: Injector) { - super(injector); - this.othersGroup = injector.get(OTHERS_GROUP); + get groupedVisible(): RouteGroup[] | undefined { + return this.createGroupedTree(this.visible); } - get groupedVisible(): ABP.RouteGroup[] | undefined { - const hasGroup = this.visible.some(node => !!node.group); - if (!hasGroup) return; - - const groups = this.visible.reduce((acc, node) => { - const groupName = node.group ?? this.othersGroup; - acc[groupName] ||= []; - acc[groupName].push(node); - return acc; - }, {} as Record[]>); - - return Object.entries(groups).map(([group, items]) => ({ group, items })); + get groupedVisible$(): Observable[] | undefined> { + return this.visible$.pipe(map(visible => this.createGroupedTree(visible))); } } diff --git a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts index ac79c193ac..ab9c6a76ae 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs'; +import { Subject, lastValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { RoutesService } from '../services/routes.service'; import { DummyInjector } from './utils/common.utils'; @@ -47,9 +47,9 @@ describe('Routes Service', () => { it('should add given routes as flat$, tree$, and visible$', async () => { service.add(routes); - const flat = await service.flat$.pipe(take(1)).toPromise(); - const tree = await service.tree$.pipe(take(1)).toPromise(); - const visible = await service.visible$.pipe(take(1)).toPromise(); + const flat = await lastValueFrom(service.flat$.pipe(take(1))); + const tree = await lastValueFrom(service.tree$.pipe(take(1))); + const visible = await lastValueFrom(service.visible$.pipe(take(1))); expect(flat.length).toBe(5); expect(flat[0].name).toBe('baz'); @@ -72,11 +72,37 @@ describe('Routes Service', () => { expect(visible[0].children[0].name).toBe('x'); }); }); + describe('#groupedVisible', () => { - it('should return grouped route list', () => { + it('should return undefined when there are no visible routes', async () => { + service.add(routes); + const result = await lastValueFrom(service.groupedVisible$.pipe(take(1))); + expect(result).toBeUndefined(); + }); + + it( + 'should group visible routes under "' + othersGroup + '" when no group is specified', + async () => { + service.add([ + { path: '/foo', name: 'foo' }, + { path: '/foo/bar', name: 'bar', group: '' }, + { path: '/foo/bar/baz', name: 'baz', group: undefined }, + { path: '/x', name: 'y', group: 'z' }, + ]); + + const result = await lastValueFrom(service.groupedVisible$.pipe(take(1))); + + expect(result[0].group).toBe(othersGroup); + expect(result[0].items[0].name).toBe('foo'); + expect(result[0].items[1].name).toBe('bar'); + expect(result[0].items[2].name).toBe('baz'); + }, + ); + + it('should return grouped route list', async () => { service.add(groupedRoutes); - const tree = service.groupedVisible; + const tree = await lastValueFrom(service.groupedVisible$.pipe(take(1))); expect(tree.length).toBe(3); diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts index bfc938c158..6e1261d823 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -74,6 +74,28 @@ export function createTreeNodeFilterCreator( }; } +export function createGroupMap( + list: TreeNode[], + othersGroupKey: string, +): Map[]> | undefined { + if (!list || !Array.isArray(list) || !list.some(node => Boolean(node.group))) return undefined; + + const mapGroup = new Map[]>(); + + for (const node of list) { + const group = node?.group || othersGroupKey; + if (typeof group !== 'string') { + throw new Error(`Invalid group: ${group}`); + } + + const items = mapGroup.get(group) || []; + items.push(node); + mapGroup.set(group, items); + } + + return mapGroup; +} + export type TreeNode = { [K in keyof T]: T[K]; } & { @@ -82,6 +104,11 @@ export type TreeNode = { parent?: TreeNode; }; +export type RouteGroup = { + readonly group: string; + readonly items: TreeNode[]; +}; + export type NodeKey = number | string | symbol | undefined | null; export type NodeValue any> = F extends undefined