Browse Source

Merge pull request #15938 from abpframework/feat/15935

Grouped menu feature @abp/ng.core
pull/16148/head^2
Masum ULU 4 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... ...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 ```js
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { OTHERS_GROUP } from '@abp/ng.core';
import { APP_ROUTE_PROVIDER } from './route.provider'; import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({ @NgModule({
providers: [APP_ROUTE_PROVIDER], providers: [
APP_ROUTE_PROVIDER,
{
provide: OTHERS_GROUP,
useValue: 'ModuleName::MyOthersGroupKey',
},
],
// imports, declarations, and bootstrap // imports, declarations, and bootstrap
}) })
export class AppModule {} 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) - `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. - `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. - `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`) - `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` ### Via `routes` Property in `AppRoutingModule`

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

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

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

@ -48,6 +48,7 @@
"Search": "Vyhledávání", "Search": "Vyhledávání",
"ItemWillBeDeletedMessageWithFormat": "{0} bude smazáno!", "ItemWillBeDeletedMessageWithFormat": "{0} bude smazáno!",
"ItemWillBeDeletedMessage": "Tato položka bude smazána!", "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", "Search": "Suche",
"ItemWillBeDeletedMessageWithFormat": "{0} wird gelöscht!", "ItemWillBeDeletedMessageWithFormat": "{0} wird gelöscht!",
"ItemWillBeDeletedMessage": "Dieses Element 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": "Αναζήτηση", "Search": "Αναζήτηση",
"ItemWillBeDeletedMessageWithFormat": "Το {0} θα διαγραφεί", "ItemWillBeDeletedMessageWithFormat": "Το {0} θα διαγραφεί",
"ItemWillBeDeletedMessage": "Αυτό το στοιχείο θα διαγραφεί!", "ItemWillBeDeletedMessage": "Αυτό το στοιχείο θα διαγραφεί!",
"ManageYourAccount": "Διαχείριση Λογαριασμού" "ManageYourAccount": "Διαχείριση Λογαριασμού",
"OthersGroup":"άλλος"
} }
} }

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

@ -48,6 +48,7 @@
"Search": "Search", "Search": "Search",
"ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!", "ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!",
"ItemWillBeDeletedMessage": "This item 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!", "ItemWillBeDeletedMessageWithFormat": "{0} will be deleted!",
"ItemWillBeDeletedMessage": "This item will be deleted!", "ItemWillBeDeletedMessage": "This item will be deleted!",
"ManageYourAccount": "Manage your account", "ManageYourAccount": "Manage your account",
"OthersGroup": "Other",
"Today": "Today", "Today": "Today",
"Apply": "Apply" "Apply": "Apply"
} }

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

@ -48,6 +48,7 @@
"Search": "Buscar", "Search": "Buscar",
"ItemWillBeDeletedMessageWithFormat": "{0} serán borrados!", "ItemWillBeDeletedMessageWithFormat": "{0} serán borrados!",
"ItemWillBeDeletedMessage": "Este elemento será borrado", "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": "جستجو", "Search": "جستجو",
"ItemWillBeDeletedMessageWithFormat": "{0} حذف خواهد شد!", "ItemWillBeDeletedMessageWithFormat": "{0} حذف خواهد شد!",
"ItemWillBeDeletedMessage": "این مورد حذف خواهد شد!", "ItemWillBeDeletedMessage": "این مورد حذف خواهد شد!",
"ManageYourAccount": "حساب خود را مدیریت کنید" "ManageYourAccount": "حساب خود را مدیریت کنید",
"OthersGroup": "دیگر"
} }
} }

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

@ -48,6 +48,7 @@
"Search": "Hae", "Search": "Hae",
"ItemWillBeDeletedMessageWithFormat": "{0} poistetaan!", "ItemWillBeDeletedMessageWithFormat": "{0} poistetaan!",
"ItemWillBeDeletedMessage": "Tämä kohde 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", "Search": "Recherche",
"ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!", "ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!",
"ItemWillBeDeletedMessage": "Cet objet va être 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": "खोज", "Search": "खोज",
"ItemWillBeDeletedMessageWithFormat": "{0} हटा दिया जाएगा!", "ItemWillBeDeletedMessageWithFormat": "{0} हटा दिया जाएगा!",
"ItemWillBeDeletedMessage": "यह आइटम हटा दिया जाएगा!", "ItemWillBeDeletedMessage": "यह आइटम हटा दिया जाएगा!",
"ManageYourAccount": "अपने खाते का प्रबंधन" "ManageYourAccount": "अपने खाते का प्रबंधन",
"OthersGroup": "अन्य"
} }
} }

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

@ -48,6 +48,7 @@
"Search": "Pretraga", "Search": "Pretraga",
"ItemWillBeDeletedMessageWithFormat": "{0} zapis će biti obrisan!", "ItemWillBeDeletedMessageWithFormat": "{0} zapis će biti obrisan!",
"ItemWillBeDeletedMessage": "Ovaj 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", "Search": "Keresés",
"ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!", "ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!",
"ItemWillBeDeletedMessage": "Ez az elem törlődik!", "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", "Search": "Leita",
"ItemWillBeDeletedMessageWithFormat": "{0} verður eytt!", "ItemWillBeDeletedMessageWithFormat": "{0} verður eytt!",
"ItemWillBeDeletedMessage": "Þessum lið 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", "Search": "Ricerca",
"ItemWillBeDeletedMessageWithFormat": "{0} sarà eliminato!", "ItemWillBeDeletedMessageWithFormat": "{0} sarà eliminato!",
"ItemWillBeDeletedMessage": "Questo elemento 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", "Search": "Zoeken",
"ItemWillBeDeletedMessageWithFormat": "{0} wordt verwijderd!", "ItemWillBeDeletedMessageWithFormat": "{0} wordt verwijderd!",
"ItemWillBeDeletedMessage": "Dit item 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", "Search": "Szukaj",
"ItemWillBeDeletedMessageWithFormat": "{0} zostanie usunięty!", "ItemWillBeDeletedMessageWithFormat": "{0} zostanie usunięty!",
"ItemWillBeDeletedMessage": "Ten element 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", "Search": "Procurar",
"ItemWillBeDeletedMessageWithFormat": "{0} será excluído!", "ItemWillBeDeletedMessageWithFormat": "{0} será excluído!",
"ItemWillBeDeletedMessage": "Este item 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ă", "Search": "Caută",
"ItemWillBeDeletedMessageWithFormat": "{0} va fi şters!", "ItemWillBeDeletedMessageWithFormat": "{0} va fi şters!",
"ItemWillBeDeletedMessage": "Acest articol 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": "поиск", "Search": "поиск",
"ItemWillBeDeletedMessageWithFormat": "{0} будет удален!", "ItemWillBeDeletedMessageWithFormat": "{0} будет удален!",
"ItemWillBeDeletedMessage": "Этот предмет будет удален!", "ItemWillBeDeletedMessage": "Этот предмет будет удален!",
"ManageYourAccount": "Настройте свой аккаунт" "ManageYourAccount": "Настройте свой аккаунт",
"OthersGroup": "Другой"
} }
} }

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

@ -48,6 +48,7 @@
"Search": "Hľadať", "Search": "Hľadať",
"ItemWillBeDeletedMessageWithFormat": "{0} sa vymaže!", "ItemWillBeDeletedMessageWithFormat": "{0} sa vymaže!",
"ItemWillBeDeletedMessage": "Táto položka bude vymazaná!", "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", "Search": "Iskanje",
"ItemWillBeDeletedMessageWithFormat": "{0} bo izbrisan!", "ItemWillBeDeletedMessageWithFormat": "{0} bo izbrisan!",
"ItemWillBeDeletedMessage": "Ta element 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!", "ItemWillBeDeletedMessageWithFormat": "{0} silinecektir!",
"ItemWillBeDeletedMessage": "Bu nesne silinecektir!", "ItemWillBeDeletedMessage": "Bu nesne silinecektir!",
"ManageYourAccount": "Hesap yönetimi", "ManageYourAccount": "Hesap yönetimi",
"OthersGroup": "Diğer",
"Today": "Bugün", "Today": "Bugün",
"Apply": "Uygula" "Apply": "Uygula"
} }

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

@ -48,6 +48,7 @@
"Search": "Tìm kiếm", "Search": "Tìm kiếm",
"ItemWillBeDeletedMessageWithFormat": "{0} sẽ bị xóa!", "ItemWillBeDeletedMessageWithFormat": "{0} sẽ bị xóa!",
"ItemWillBeDeletedMessage": "Vật phẩm này sẽ bị xoá!", "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": "搜索", "Search": "搜索",
"ItemWillBeDeletedMessageWithFormat": "{0} 将被删除!", "ItemWillBeDeletedMessageWithFormat": "{0} 将被删除!",
"ItemWillBeDeletedMessage": "此项将被删除!", "ItemWillBeDeletedMessage": "此项将被删除!",
"ManageYourAccount": "管理你的账户" "ManageYourAccount": "管理你的账户",
"OthersGroup": "其他"
} }
} }

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

@ -48,6 +48,7 @@
"Search": "查詢", "Search": "查詢",
"ItemWillBeDeletedMessageWithFormat": "{0} 將被刪除!", "ItemWillBeDeletedMessageWithFormat": "{0} 將被刪除!",
"ItemWillBeDeletedMessage": "此項目將被刪除!", "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 { CookieLanguageProvider } from './providers/cookie-language.provider';
import { LocaleProvider } from './providers/locale.provider'; import { LocaleProvider } from './providers/locale.provider';
import { LocalizationService } from './services/localization.service'; import { LocalizationService } from './services/localization.service';
import { OTHERS_GROUP } from './tokens';
import { localizationContributor, LOCALIZATIONS } from './tokens/localization.token'; import { localizationContributor, LOCALIZATIONS } from './tokens/localization.token';
import { CORE_OPTIONS, coreOptionsFactory } from './tokens/options.token'; import { CORE_OPTIONS, coreOptionsFactory } from './tokens/options.token';
import { TENANT_KEY } from './tokens/tenant-key.token'; import { TENANT_KEY } from './tokens/tenant-key.token';
@ -179,6 +180,10 @@ export class CoreModule {
provide: QUEUE_MANAGER, provide: QUEUE_MANAGER,
useClass: DefaultQueueManager, useClass: DefaultQueueManager,
}, },
{
provide: OTHERS_GROUP,
useValue: options.othersGroup || 'AbpUi::OthersGroup',
},
IncludeLocalizationResourcesProvider, IncludeLocalizationResourcesProvider,
], ],
}; };

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

@ -12,6 +12,7 @@ export namespace ABP {
sendNullsAsQueryParam?: boolean; sendNullsAsQueryParam?: boolean;
tenantKey?: string; tenantKey?: string;
localizations?: Localization[]; localizations?: Localization[];
othersGroup?: string;
} }
export interface Child { export interface Child {
@ -70,6 +71,7 @@ export namespace ABP {
path?: string; path?: string;
layout?: eLayoutType; layout?: eLayoutType;
iconClass?: string; iconClass?: string;
group?: string;
} }
export interface Tab extends Nav { 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 { 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 { ABP } from '../models/common';
import { OTHERS_GROUP } from '../tokens';
import { pushValueTo } from '../utils/array-utils'; 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 { ConfigStateService } from './config-state.service';
import { PermissionService } from './permission.service'; import { PermissionService } from './permission.service';
// eslint-disable-next-line @typescript-eslint/ban-types // 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 id: string;
abstract parentId: string; abstract parentId: string;
abstract hide: (item: T) => boolean; 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 _tree$ = new BehaviorSubject<TreeNode<T>[]>([]);
private _visible$ = new BehaviorSubject<TreeNode<T>[]>([]); private _visible$ = new BehaviorSubject<TreeNode<T>[]>([]);
protected othersGroup: string;
get flat(): T[] { get flat(): T[] {
return this._flat$.value; 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[] { private filterWith(setOrMap: Set<string> | Map<string, T>): T[] {
return this._flat$.value.filter(item => !setOrMap.has(item[this.id])); 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) .createOnUpdateStream(state => state)
.subscribe(() => this.refresh()); .subscribe(() => this.refresh());
this.permissionService = injector.get(PermissionService); this.permissionService = injector.get(PermissionService);
this.othersGroup = injector.get(OTHERS_GROUP);
} }
protected isGranted({ requiredPolicy }: T): boolean { protected isGranted({ requiredPolicy }: T): boolean {
@ -180,4 +199,19 @@ export abstract class AbstractNavTreeService<T extends ABP.Nav>
} }
@Injectable({ providedIn: 'root' }) @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 { take } from 'rxjs/operators';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { DummyInjector } from './utils/common.utils'; import { DummyInjector } from './utils/common.utils';
@ -10,6 +10,7 @@ export const mockRoutesService = (injectorPayload = {} as { [key: string]: any }
const injector = new DummyInjector({ const injector = new DummyInjector({
PermissionService: mockPermissionService(), PermissionService: mockPermissionService(),
ConfigStateService: { createOnUpdateStream: () => updateStream$ }, ConfigStateService: { createOnUpdateStream: () => updateStream$ },
OTHERS_GROUP: 'OthersGroup',
...injectorPayload, ...injectorPayload,
}); });
return new RoutesService(injector); return new RoutesService(injector);
@ -17,6 +18,11 @@ export const mockRoutesService = (injectorPayload = {} as { [key: string]: any }
describe('Routes Service', () => { describe('Routes Service', () => {
let service: RoutesService; let service: RoutesService;
const fooGroup = 'FooGroup';
const barGroup = 'BarGroup';
const othersGroup = 'OthersGroup';
const routes = [ const routes = [
{ path: '/foo', name: 'foo' }, { path: '/foo', name: 'foo' },
{ path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 }, { 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 }, { 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(() => { beforeEach(() => {
service = mockRoutesService(); service = mockRoutesService();
}); });
@ -33,9 +47,9 @@ describe('Routes Service', () => {
it('should add given routes as flat$, tree$, and visible$', async () => { it('should add given routes as flat$, tree$, and visible$', async () => {
service.add(routes); service.add(routes);
const flat = await service.flat$.pipe(take(1)).toPromise(); const flat = await lastValueFrom(service.flat$.pipe(take(1)));
const tree = await service.tree$.pipe(take(1)).toPromise(); const tree = await lastValueFrom(service.tree$.pipe(take(1)));
const visible = await service.visible$.pipe(take(1)).toPromise(); const visible = await lastValueFrom(service.visible$.pipe(take(1)));
expect(flat.length).toBe(5); expect(flat.length).toBe(5);
expect(flat[0].name).toBe('baz'); 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', () => { describe('#find', () => {
it('should return node found based on query', () => { it('should return node found based on query', () => {
service.add(routes); 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; ): T;
get(token: any, notFoundValue?: any): any; get(token: any, notFoundValue?: any): any;
get(token, notFoundValue?, flags?: InjectFlags): 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 './set-token-response-to-storage.token';
export * from './check-authentication-state'; export * from './check-authentication-state';
export * from './http-context.token'; 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 */ /* eslint-disable @typescript-eslint/ban-types */
export class BaseTreeNode<T extends object> { export class BaseTreeNode<T extends object> {
children: TreeNode<T>[] = []; 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> = { export type TreeNode<T extends object> = {
[K in keyof T]: T[K]; [K in keyof T]: T[K];
} & { } & {
@ -82,6 +106,11 @@ export type TreeNode<T extends object> = {
parent?: TreeNode<T>; 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 NodeKey = number | string | symbol | undefined | null;
export type NodeValue<T extends object, F extends (...args: any) => any> = F extends undefined export type NodeValue<T extends object, F extends (...args: any) => any> = F extends undefined

Loading…
Cancel
Save