Browse Source

Merge pull request #15938 from abpframework/feat/15935

Grouped menu feature @abp/ng.core
pull/16148/head^2
Masum ULU 3 years ago
committed by GitHub
parent
commit
ad0bb87f4e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      docs/en/UI/Angular/Modifying-the-Menu.md
  2. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  3. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  4. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  5. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  6. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  7. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json
  8. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  9. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  10. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  11. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  12. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  13. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  14. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  15. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  16. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  17. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  18. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  19. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  20. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  21. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json
  22. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json
  23. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json
  24. 1
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json
  25. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json
  26. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json
  27. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json
  28. 5
      npm/ng-packs/packages/core/src/lib/core.module.ts
  29. 2
      npm/ng-packs/packages/core/src/lib/models/common.ts
  30. 42
      npm/ng-packs/packages/core/src/lib/services/routes.service.ts
  31. 68
      npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts
  32. 2
      npm/ng-packs/packages/core/src/lib/tests/utils/common.utils.ts
  33. 1
      npm/ng-packs/packages/core/src/lib/tokens/index.ts
  34. 3
      npm/ng-packs/packages/core/src/lib/tokens/others-group.token.ts
  35. 29
      npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts

56
docs/en/UI/Angular/Modifying-the-Menu.md

@ -88,14 +88,65 @@ function configureRoutes(routes: RoutesService) {
}
```
We can also define a group for navigation elements. It's an optional property
- **Note:** It'll also include groups that were defined at the modules
```js
// route.provider.ts
import { RoutesService } from '@abp/ng.core';
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
//etc..
group: 'ModuleName::GroupName'
},
{
path: '/your-path/child',
name: 'Your child navigation',
parentName: 'Your navigation',
requiredPolicy: 'permission key here',
order: 1,
},
]);
};
}
```
To get the route items as grouped we can use the `groupedVisible` (or Observable one `groupedVisible$`) getter methods
- It returns `RouteGroup<T>[]` if there is any group in the route tree, otherwise it returns `undefined`
```js
import { ABP, RoutesService, RouteGroup } from "@abp/ng.core";
import { Component } from "@angular/core";
@Component(/* component metadata */)
export class AppComponent {
visible: RouteGroup<ABP.Route>[] | undefined = this.routes.groupedVisible;
//Or
visible$:Observable<RouteGroup<ABP.Route>[] | undefined> = this.routes.groupedVisible$;
constructor(private routes: RoutesService) {}
}
```
...and then in app.module.ts...
- The `groupedVisible` method will return the `Others` group for ungrouped items, the default key is `AbpUi::OthersGroup`, we can change this `key` via the `OTHERS_GROUP` injection token
```js
import { NgModule } from '@angular/core';
import { OTHERS_GROUP } from '@abp/ng.core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
providers: [APP_ROUTE_PROVIDER],
providers: [
APP_ROUTE_PROVIDER,
{
provide: OTHERS_GROUP,
useValue: 'ModuleName::MyOthersGroupKey',
},
],
// imports, declarations, and bootstrap
})
export class AppModule {}
@ -109,8 +160,9 @@ Here is what every property works as:
- `requiredPolicy` is the permission key to access the page. See the [Permission Management document](./Permission-Management.md)
- `order` is the order of the navigation element. "Administration" has an order of `100`, so keep that in mind when ordering top level menu items.
- `iconClass` is the class of the `i` tag, which is placed to the left of the navigation label.
- `layout` defines in which layout the route will be loaded. (default: `eLayoutType.empty`)
- `layout` defines in which layout the route is loaded. (default: `eLayoutType.empty`)
- `invisible` makes the item invisible in the menu. (default: `false`)
- `group` is an optional property that is used to group together related routes in an application. (type: `string`, default: `AbpUi::OthersGroup`)
### Via `routes` Property in `AppRoutingModule`

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json

@ -48,6 +48,7 @@
"Search": "بحث",
"ItemWillBeDeletedMessageWithFormat": "سيتم حذف {0}!",
"ItemWillBeDeletedMessage": "سوف يتم حذف هذا البند!",
"ManageYourAccount": "إدارة حسابك"
"ManageYourAccount": "إدارة حسابك",
"OthersGroup": "آخرون"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json

@ -48,6 +48,7 @@
"Search": "Vyhledávání",
"ItemWillBeDeletedMessageWithFormat": "{0} bude smazáno!",
"ItemWillBeDeletedMessage": "Tato položka bude smazána!",
"ManageYourAccount": "Spravujte svůj účet"
"ManageYourAccount": "Spravujte svůj účet",
"OthersGroup": "Jiný"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json

@ -48,6 +48,7 @@
"Search": "Suche",
"ItemWillBeDeletedMessageWithFormat": "{0} wird gelöscht!",
"ItemWillBeDeletedMessage": "Dieses Element wird gelöscht!",
"ManageYourAccount": "Verwalten Sie Ihr Benutzerkonto"
"ManageYourAccount": "Verwalten Sie Ihr Benutzerkonto",
"OthersGroup":"Andere"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json

@ -48,6 +48,7 @@
"Search": "Αναζήτηση",
"ItemWillBeDeletedMessageWithFormat": "Το {0} θα διαγραφεί",
"ItemWillBeDeletedMessage": "Αυτό το στοιχείο θα διαγραφεί!",
"ManageYourAccount": "Διαχείριση Λογαριασμού"
"ManageYourAccount": "Διαχείριση Λογαριασμού",
"OthersGroup":"άλλος"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json

@ -48,6 +48,7 @@
"Search": "Search",
"ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!",
"ItemWillBeDeletedMessage": "This item will be deleted!",
"ManageYourAccount": "Manage your account"
"ManageYourAccount": "Manage your account",
"OthersGroup": "Other"
}
}

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json

@ -49,6 +49,7 @@
"ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!",
"ItemWillBeDeletedMessage": "This item will be deleted!",
"ManageYourAccount": "Manage your account",
"OthersGroup": "Other",
"Today": "Today",
"Apply": "Apply"
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json

@ -48,6 +48,7 @@
"Search": "Buscar",
"ItemWillBeDeletedMessageWithFormat": "{0} serán borrados!",
"ItemWillBeDeletedMessage": "Este elemento será borrado",
"ManageYourAccount": "Administrar cuenta"
"ManageYourAccount": "Administrar cuenta",
"OthersGroup": "Otra"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json

@ -48,6 +48,7 @@
"Search": "جستجو",
"ItemWillBeDeletedMessageWithFormat": "{0} حذف خواهد شد!",
"ItemWillBeDeletedMessage": "این مورد حذف خواهد شد!",
"ManageYourAccount": "حساب خود را مدیریت کنید"
"ManageYourAccount": "حساب خود را مدیریت کنید",
"OthersGroup": "دیگر"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json

@ -48,6 +48,7 @@
"Search": "Hae",
"ItemWillBeDeletedMessageWithFormat": "{0} poistetaan!",
"ItemWillBeDeletedMessage": "Tämä kohde poistetaan!",
"ManageYourAccount": "Hallitse tiliäsi"
"ManageYourAccount": "Hallitse tiliäsi",
"OthersGroup": "Muut"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json

@ -48,6 +48,7 @@
"Search": "Recherche",
"ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!",
"ItemWillBeDeletedMessage": "Cet objet va être supprimé!",
"ManageYourAccount": "Gérer votre compte"
"ManageYourAccount": "Gérer votre compte",
"OthersGroup": "Autre"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json

@ -48,6 +48,7 @@
"Search": "खोज",
"ItemWillBeDeletedMessageWithFormat": "{0} हटा दिया जाएगा!",
"ItemWillBeDeletedMessage": "यह आइटम हटा दिया जाएगा!",
"ManageYourAccount": "अपने खाते का प्रबंधन"
"ManageYourAccount": "अपने खाते का प्रबंधन",
"OthersGroup": "अन्य"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json

@ -48,6 +48,7 @@
"Search": "Pretraga",
"ItemWillBeDeletedMessageWithFormat": "{0} zapis će biti obrisan!",
"ItemWillBeDeletedMessage": "Ovaj zapis će biti obrisan!",
"ManageYourAccount": "Upravljaj korisničkim računom"
"ManageYourAccount": "Upravljaj korisničkim računom",
"OthersGroup": "Drugi"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json

@ -48,6 +48,7 @@
"Search": "Keresés",
"ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!",
"ItemWillBeDeletedMessage": "Ez az elem törlődik!",
"ManageYourAccount": "Kezelje fiókját"
"ManageYourAccount": "Kezelje fiókját",
"OthersGroup": "Egyéb"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json

@ -48,6 +48,7 @@
"Search": "Leita",
"ItemWillBeDeletedMessageWithFormat": "{0} verður eytt!",
"ItemWillBeDeletedMessage": "Þessum lið verður eytt!",
"ManageYourAccount": "Stillingar notandaaðgangs"
"ManageYourAccount": "Stillingar notandaaðgangs",
"OthersGroup": "Annað"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json

@ -48,6 +48,7 @@
"Search": "Ricerca",
"ItemWillBeDeletedMessageWithFormat": "{0} sarà eliminato!",
"ItemWillBeDeletedMessage": "Questo elemento sarà eliminato!",
"ManageYourAccount": "Gestisci il tuo account"
"ManageYourAccount": "Gestisci il tuo account",
"OthersGroup": "Altra"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json

@ -48,6 +48,7 @@
"Search": "Zoeken",
"ItemWillBeDeletedMessageWithFormat": "{0} wordt verwijderd!",
"ItemWillBeDeletedMessage": "Dit item wordt verwijderd!",
"ManageYourAccount": "Beheer uw account"
"ManageYourAccount": "Beheer uw account",
"OthersGroup": "Ander"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json

@ -48,6 +48,7 @@
"Search": "Szukaj",
"ItemWillBeDeletedMessageWithFormat": "{0} zostanie usunięty!",
"ItemWillBeDeletedMessage": "Ten element zostanie usunięty!",
"ManageYourAccount": "Zarządzaj kontem"
"ManageYourAccount": "Zarządzaj kontem",
"OthersGroup": "Inny"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json

@ -48,6 +48,7 @@
"Search": "Procurar",
"ItemWillBeDeletedMessageWithFormat": "{0} será excluído!",
"ItemWillBeDeletedMessage": "Este item será excluído!",
"ManageYourAccount": "Gerenciar sua conta"
"ManageYourAccount": "Gerenciar sua conta",
"OthersGroup": "Outra"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json

@ -48,6 +48,7 @@
"Search": "Caută",
"ItemWillBeDeletedMessageWithFormat": "{0} va fi şters!",
"ItemWillBeDeletedMessage": "Acest articol va fi şters!",
"ManageYourAccount": "Administraţi-vă contul"
"ManageYourAccount": "Administraţi-vă contul",
"OthersGroup": "Alte"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json

@ -48,6 +48,7 @@
"Search": "поиск",
"ItemWillBeDeletedMessageWithFormat": "{0} будет удален!",
"ItemWillBeDeletedMessage": "Этот предмет будет удален!",
"ManageYourAccount": "Настройте свой аккаунт"
"ManageYourAccount": "Настройте свой аккаунт",
"OthersGroup": "Другой"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json

@ -48,6 +48,7 @@
"Search": "Hľadať",
"ItemWillBeDeletedMessageWithFormat": "{0} sa vymaže!",
"ItemWillBeDeletedMessage": "Táto položka bude vymazaná!",
"ManageYourAccount": "Spravovať svoje konto"
"ManageYourAccount": "Spravovať svoje konto",
"OthersGroup": "Iné"
}
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json

@ -48,6 +48,7 @@
"Search": "Iskanje",
"ItemWillBeDeletedMessageWithFormat": "{0} bo izbrisan!",
"ItemWillBeDeletedMessage": "Ta element bo izbrisan!",
"ManageYourAccount": "Upravljajte svoj račun"
"ManageYourAccount": "Upravljajte svoj račun",
"OthersGroup": "Ostalo"
}
}
}

1
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/tr.json

@ -49,6 +49,7 @@
"ItemWillBeDeletedMessageWithFormat": "{0} silinecektir!",
"ItemWillBeDeletedMessage": "Bu nesne silinecektir!",
"ManageYourAccount": "Hesap yönetimi",
"OthersGroup": "Diğer",
"Today": "Bugün",
"Apply": "Uygula"
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json

@ -48,6 +48,7 @@
"Search": "Tìm kiếm",
"ItemWillBeDeletedMessageWithFormat": "{0} sẽ bị xóa!",
"ItemWillBeDeletedMessage": "Vật phẩm này sẽ bị xoá!",
"ManageYourAccount": "Quản lý tài khoản của bạn"
"ManageYourAccount": "Quản lý tài khoản của bạn",
"OthersGroup": "Khác"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json

@ -48,6 +48,7 @@
"Search": "搜索",
"ItemWillBeDeletedMessageWithFormat": "{0} 将被删除!",
"ItemWillBeDeletedMessage": "此项将被删除!",
"ManageYourAccount": "管理你的账户"
"ManageYourAccount": "管理你的账户",
"OthersGroup": "其他"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json

@ -48,6 +48,7 @@
"Search": "查詢",
"ItemWillBeDeletedMessageWithFormat": "{0} 將被刪除!",
"ItemWillBeDeletedMessage": "此項目將被刪除!",
"ManageYourAccount": "管理個人帳號"
"ManageYourAccount": "管理個人帳號",
"OthersGroup": "其他"
}
}

5
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -24,6 +24,7 @@ import { ToInjectorPipe } from './pipes/to-injector.pipe';
import { CookieLanguageProvider } from './providers/cookie-language.provider';
import { LocaleProvider } from './providers/locale.provider';
import { LocalizationService } from './services/localization.service';
import { OTHERS_GROUP } from './tokens';
import { localizationContributor, LOCALIZATIONS } from './tokens/localization.token';
import { CORE_OPTIONS, coreOptionsFactory } from './tokens/options.token';
import { TENANT_KEY } from './tokens/tenant-key.token';
@ -179,6 +180,10 @@ export class CoreModule {
provide: QUEUE_MANAGER,
useClass: DefaultQueueManager,
},
{
provide: OTHERS_GROUP,
useValue: options.othersGroup || 'AbpUi::OthersGroup',
},
IncludeLocalizationResourcesProvider,
],
};

2
npm/ng-packs/packages/core/src/lib/models/common.ts

@ -12,6 +12,7 @@ export namespace ABP {
sendNullsAsQueryParam?: boolean;
tenantKey?: string;
localizations?: Localization[];
othersGroup?: string;
}
export interface Child {
@ -70,6 +71,7 @@ export namespace ABP {
path?: string;
layout?: eLayoutType;
iconClass?: string;
group?: string;
}
export interface Tab extends Nav {

42
npm/ng-packs/packages/core/src/lib/services/routes.service.ts

@ -1,13 +1,20 @@
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';
// eslint-disable-next-line @typescript-eslint/ban-types
export abstract class AbstractTreeService<T extends {[key: string | number | symbol]: any}> {
export abstract class AbstractTreeService<T extends { [key: string | number | symbol]: any }> {
abstract id: string;
abstract parentId: string;
abstract hide: (item: T) => boolean;
@ -17,6 +24,8 @@ export abstract class AbstractTreeService<T extends {[key: string | number | sym
private _tree$ = new BehaviorSubject<TreeNode<T>[]>([]);
private _visible$ = new BehaviorSubject<TreeNode<T>[]>([]);
protected othersGroup: string;
get flat(): T[] {
return this._flat$.value;
}
@ -50,6 +59,15 @@ export abstract class AbstractTreeService<T extends {[key: string | number | sym
);
}
protected createGroupedTree(list: TreeNode<T>[]): RouteGroup<T>[] | undefined {
const map = createGroupMap<T>(list, this.othersGroup);
if (!map) {
return undefined;
}
return Array.from(map, ([key, items]) => ({ group: key, items }));
}
private filterWith(setOrMap: Set<string> | Map<string, T>): T[] {
return this._flat$.value.filter(item => !setOrMap.has(item[this.id]));
}
@ -157,6 +175,7 @@ export abstract class AbstractNavTreeService<T extends ABP.Nav>
.createOnUpdateStream(state => state)
.subscribe(() => this.refresh());
this.permissionService = injector.get(PermissionService);
this.othersGroup = injector.get(OTHERS_GROUP);
}
protected isGranted({ requiredPolicy }: T): boolean {
@ -180,4 +199,19 @@ export abstract class AbstractNavTreeService<T extends ABP.Nav>
}
@Injectable({ providedIn: 'root' })
export class RoutesService extends AbstractNavTreeService<ABP.Route> {}
export class RoutesService extends AbstractNavTreeService<ABP.Route> {
private hasPathOrChild(item: TreeNode<ABP.Route>): boolean {
return Boolean(item.path) || this.hasChildren(item.name);
}
get groupedVisible(): RouteGroup<ABP.Route>[] | undefined {
return this.createGroupedTree(this.visible.filter(item => this.hasPathOrChild(item)));
}
get groupedVisible$(): Observable<RouteGroup<ABP.Route>[] | undefined> {
return this.visible$.pipe(
map(items => items.filter(item => this.hasPathOrChild(item))),
map(visible => this.createGroupedTree(visible)),
);
}
}

68
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';
@ -10,6 +10,7 @@ export const mockRoutesService = (injectorPayload = {} as { [key: string]: any }
const injector = new DummyInjector({
PermissionService: mockPermissionService(),
ConfigStateService: { createOnUpdateStream: () => updateStream$ },
OTHERS_GROUP: 'OthersGroup',
...injectorPayload,
});
return new RoutesService(injector);
@ -17,6 +18,11 @@ export const mockRoutesService = (injectorPayload = {} as { [key: string]: any }
describe('Routes Service', () => {
let service: RoutesService;
const fooGroup = 'FooGroup';
const barGroup = 'BarGroup';
const othersGroup = 'OthersGroup';
const routes = [
{ path: '/foo', name: 'foo' },
{ path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 },
@ -25,6 +31,14 @@ describe('Routes Service', () => {
{ path: '/foo/x', name: 'x', parentName: 'foo', order: 1 },
];
const groupedRoutes = [
{ path: '/foo', name: 'foo', group: fooGroup },
{ path: '/foo/y', name: 'y', parentName: 'foo' },
{ path: '/foo/bar', name: 'bar', group: barGroup },
{ path: '/foo/bar/baz', name: 'baz', group: barGroup },
{ path: '/foo/z', name: 'z' },
];
beforeEach(() => {
service = mockRoutesService();
});
@ -33,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');
@ -59,6 +73,52 @@ describe('Routes Service', () => {
});
});
describe('#groupedVisible', () => {
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 = await lastValueFrom(service.groupedVisible$.pipe(take(1)));
expect(tree.length).toBe(3);
expect(tree[0].group).toBe('FooGroup');
expect(tree[0].items[0].name).toBe('foo');
expect(tree[0].items[0].children[0].name).toBe('y');
expect(tree[1].group).toBe('BarGroup');
expect(tree[1].items[0].name).toBe('bar');
expect(tree[1].items[1].name).toBe('baz');
expect(tree[2].group).toBe(othersGroup);
expect(tree[2].items[0].name).toBe('z');
});
});
describe('#find', () => {
it('should return node found based on query', () => {
service.add(routes);

2
npm/ng-packs/packages/core/src/lib/tests/utils/common.utils.ts

@ -14,6 +14,6 @@ export class DummyInjector extends Injector {
): T;
get(token: any, notFoundValue?: any): any;
get(token, notFoundValue?, flags?: InjectFlags): any {
return this.payload[token.name || token];
return this.payload[token.name || token._desc || token];
}
}

1
npm/ng-packs/packages/core/src/lib/tokens/index.ts

@ -12,3 +12,4 @@ export * from './pipe-to-login.token';
export * from './set-token-response-to-storage.token';
export * from './check-authentication-state';
export * from './http-context.token';
export * from './others-group.token'

3
npm/ng-packs/packages/core/src/lib/tokens/others-group.token.ts

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const OTHERS_GROUP = new InjectionToken<string>('OTHERS_GROUP');

29
npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts

@ -1,3 +1,5 @@
import { isArray } from './common-utils';
/* eslint-disable @typescript-eslint/ban-types */
export class BaseTreeNode<T extends object> {
children: TreeNode<T>[] = [];
@ -74,6 +76,28 @@ export function createTreeNodeFilterCreator<T extends object>(
};
}
export function createGroupMap<T extends { group?: string }>(
list: TreeNode<T>[],
othersGroupKey: string,
) {
if (!isArray(list) || !list.some(node => Boolean(node.group))) return undefined;
const mapGroup = new Map<string, TreeNode<T>[]>();
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<T extends object> = {
[K in keyof T]: T[K];
} & {
@ -82,6 +106,11 @@ export type TreeNode<T extends object> = {
parent?: TreeNode<T>;
};
export type RouteGroup<T extends object> = {
readonly group: string;
readonly items: TreeNode<T>[];
};
export type NodeKey = number | string | symbol | undefined | null;
export type NodeValue<T extends object, F extends (...args: any) => any> = F extends undefined

Loading…
Cancel
Save