diff --git a/docs/en/UI/Angular/Modifying-the-Menu.md b/docs/en/UI/Angular/Modifying-the-Menu.md index c59ce0d650..78733ef658 100644 --- a/docs/en/UI/Angular/Modifying-the-Menu.md +++ b/docs/en/UI/Angular/Modifying-the-Menu.md @@ -152,6 +152,21 @@ import { APP_ROUTE_PROVIDER } from './route.provider'; export class AppModule {} ``` +**Note:** +Route items's `name` property is must be a unique key. If there are multiple items with the same name, the last one will be displayed in the menu. If you want to display multiple items with the same name, you can call the `disableFiltering` method of the `RoutesService` to disable the filtering. + +```typescript +import { RoutesService } from '@abp/ng.core'; +import { Component } from '@angular/core'; + +@Component(/* component metadata */) +export class AppComponent { + constructor(private routes: RoutesService) { + routes.disableFiltering(); + } +} +``` + Here is what every property works as: - `path` is the absolute path of the navigation element. @@ -226,7 +241,7 @@ After adding the `routes` property as described above, the navigation menu looks ## How to Patch or Remove a Navigation Element -The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children. +The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children. Also you can use `delete` method to delete the routes with given properties. ```js // this.routes is instance of RoutesService @@ -249,6 +264,9 @@ const newHomeRouteConfig: Partial = { this.routes.add([dashboardRouteConfig]); this.routes.patch('::Menu:Home', newHomeRouteConfig); this.routes.remove(['Your navigation']); + +// or +this.routes.delete({ name: 'Your navigation' }); ``` - Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. 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 c3a1ffe07f..8faab8cedf 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 @@ -26,6 +26,7 @@ export abstract class AbstractTreeService[]>([]); protected othersGroup: string; + private filterRoutesEnabled = true; get flat(): T[] { return this._flat$.value; @@ -90,16 +91,54 @@ export abstract class AbstractTreeService(); - items.forEach(item => map.set(item[this.id], item)); + if (this.filterRoutesEnabled) { + const map = new Map(); + items.forEach(item => map.set(item[this.id], item)); + + const flatItems = this.filterWith(map); + map.forEach(pushValueTo(flatItems)); + + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + return this.publish(flatItems, visibleItems); + } else { + const flatItems = this.flat.concat(items); + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + return this.publish(flatItems, visibleItems); + } + } - const flatItems = this.filterWith(map); - map.forEach(pushValueTo(flatItems)); + delete(params: Partial): T[] { + const willRemoveItems = this.flat.filter(item => { + const keys = Object.keys(params) as Array>; + const isValid = keys.every(key => item[key] === params[key]); - flatItems.sort(this.sort); - const visibleItems = flatItems.filter(item => !this.hide(item)); + return isValid; + }); - return this.publish(flatItems, visibleItems); + if (willRemoveItems?.length) { + willRemoveItems.forEach(item => { + this.delete({ + [this.parentId]: item[this.id], + } as Partial); + }); + + const flatItems = this.flat.filter(item => !willRemoveItems.includes(item)); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + return this.flat; + } + + disableFiltering(): void { + this.filterRoutesEnabled = false; + } + + enableFiltering(): void { + this.filterRoutesEnabled = true; } find(predicate: (item: TreeNode) => boolean, tree = this.tree): TreeNode | null { @@ -145,8 +184,8 @@ export abstract class AbstractTreeService node[key] === params[key]) - ? node - : this.search(params, node.children), + ? node + : this.search(params, node.children), null, ); } @@ -164,7 +203,7 @@ export abstract class AbstractNavTreeService readonly parentId = 'parentName'; readonly hide = (item: T) => item.invisible || !this.isGranted(item); readonly sort = (a: T, b: T) => { - return this.compareFunc(a,b) + return this.compareFunc(a, b); }; constructor(protected injector: Injector) { 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 718b1fc0e2..0c993606b4 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 @@ -172,6 +172,66 @@ describe('Routes Service', () => { }); }); + describe('#disableFiltering', () => { + it('should allow to duplicate routes', () => { + service.disableFiltering(); + + service.add(routes); + + const flat = service.flat; + + expect(flat.length).toBe(routes.length); + }); + + it('should allow to duplicate routes with the same name', () => { + service.disableFiltering(); + + service.add([...routes, { path: '/foo/bar/test', name: 'bar', parentName: 'foo', order: 2 }]); + + const flat = service.flat; + + expect(flat.length).toBe(routes.length + 1); + }); + + it('should allow to routes with the same name but different parentName', () => { + service.disableFiltering(); + + service.add([ + { path: '/foo/bar', name: 'bar', parentName: 'foo', order: 2 }, + { path: '/foo/bar', name: 'bar', parentName: 'baz', order: 1 }, + ]); + + const flat = service.flat; + + expect(flat.length).toBe(2); + }); + }); + + describe('#enableFiltering', () => { + it('should not allow to duplicate routes', () => { + service.disableFiltering(); + + service.add(routes); + + service.enableFiltering(); + + service.add(routes); + + const flat = service.flat; + + expect(flat.length).toBe(5); + }); + + it('should not allow to duplicate routes with the same name', () => { + service.enableFiltering(); + service.add([...routes, { path: '/foo/bar/test', name: 'bar', parentName: 'any', order: 2 }]); + + const flat = service.flat; + + expect(flat.length).toBe(5); + }); + }); + describe('#find', () => { it('should return node found based on query', () => { service.add(routes); @@ -242,6 +302,82 @@ describe('Routes Service', () => { }); }); + describe('#delete', () => { + it('should remove route based on given route', () => { + service.add(routes); + + service.delete({ + name: 'bar', + parentName: 'foo', + }); + + const flat = service.flat; + + expect(flat.length).toBe(2); + + const notFound = service.find(route => route.name === 'bar'); + + expect(notFound).toBe(null); + }); + + it('should remove if more than one route has the same properties', () => { + service.disableFiltering(); + + service.add([ + ...routes, + { + path: '/foo/bar', + name: 'bar', + parentName: 'foo', + invisible: true, + order: 2, + breadcrumbText: 'Bar Breadcrumb', + }, + ]); + + service.delete({ + path: '/foo/bar', + name: 'bar', + parentName: 'foo', + invisible: true, + order: 2, + breadcrumbText: 'Bar Breadcrumb', + }); + + const flat = service.flat; + console.log(flat); + expect(flat.length).toBe(5); + + const notFound = service.search({ + path: '/foo/bar', + name: 'bar', + parentName: 'foo', + invisible: true, + order: 2, + breadcrumbText: 'Bar Breadcrumb', + }); + expect(notFound).toBe(null); + }); + + it("shouldn't remove if there is no route with the given properties", () => { + service.add(routes); + const flatLengthBeforeRemove = service.flat.length; + + service.delete({ + name: 'bar', + parentName: 'baz', + }); + + const flat = service.flat; + + expect(flatLengthBeforeRemove - flat.length).toBe(0); + + const notFound = service.find(route => route.name === 'bar'); + + expect(notFound).not.toBe(null); + }); + }); + describe('#patch', () => { it('should patch propeties of routes based on given routeNames', () => { service['isGranted'] = jest.fn(route => route.requiredPolicy !== 'X');