Merge pull request #4404 from abpframework/refactor/nav-items

Created NavItemsService
pull/4493/head
Levent Arman Özak 5 years ago committed by GitHub
commit 9824dfba27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,11 +0,0 @@
import { Layout } from '../models/layout';
export class AddNavigationElement {
static readonly type = '[Layout] Add Navigation Element';
constructor(public payload: Layout.NavigationElement | Layout.NavigationElement[]) {}
}
export class RemoveNavigationElementByName {
static readonly type = '[Layout] Remove Navigation ElementByName';
constructor(public name: string) {}
}

@ -34,12 +34,8 @@
<abp-nav-items <abp-nav-items
*abpReplaceableTemplate="{ *abpReplaceableTemplate="{
componentKey: navItemsComponentKey, componentKey: navItemsComponentKey
inputs: {
smallScreen: { value: smallScreen }
}
}" }"
[smallScreen]="smallScreen"
></abp-nav-items> ></abp-nav-items>
</ng-template> </ng-template>
</div> </div>

@ -2,6 +2,8 @@ export * from './account-layout/account-layout.component';
export * from './application-layout/application-layout.component'; export * from './application-layout/application-layout.component';
export * from './empty-layout/empty-layout.component'; export * from './empty-layout/empty-layout.component';
export * from './logo/logo.component'; export * from './logo/logo.component';
export * from './nav-items/current-user.component';
export * from './nav-items/languages.component';
export * from './nav-items/nav-items.component'; export * from './nav-items/nav-items.component';
export * from './routes/routes.component'; export * from './routes/routes.component';
export * from './validation-error/validation-error.component'; export * from './validation-error/validation-error.component';

@ -0,0 +1,67 @@
import { ApplicationConfiguration, AuthService, ConfigState } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Select } from '@ngxs/store';
import { Observable } from 'rxjs';
@Component({
selector: 'abp-current-user',
// tslint:disable-next-line: component-max-inline-declarations
template: `
<ng-template #loginBtn>
<a role="button" class="nav-link" routerLink="/account/login">{{
'AbpAccount::Login' | abpLocalization
}}</a>
</ng-template>
<div
*ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn"
ngbDropdown
class="dropdown"
#currentUserDropdown="ngbDropdown"
display="static"
>
<a
ngbDropdownToggle
class="nav-link"
href="javascript:void(0)"
role="button"
id="dropdownMenuLink"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ (currentUser$ | async)?.userName }}
</a>
<div
class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && currentUserDropdown.isOpen()"
>
<a class="dropdown-item" routerLink="/account/manage-profile"
><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a
>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a
>
</div>
</div>
`,
})
export class CurrentUserComponent implements OnInit {
@Select(ConfigState.getOne('currentUser'))
currentUser$: Observable<ApplicationConfiguration.CurrentUser>;
get smallScreen(): boolean {
return window.innerWidth < 992;
}
constructor(private authService: AuthService, private router: Router) {}
ngOnInit() {}
logout() {
this.authService.logout().subscribe(() => {
this.router.navigate(['/'], { state: { redirectUrl: this.router.url } });
});
}
}

@ -0,0 +1,88 @@
import { Component, OnInit, Input } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { SetLanguage, ConfigState, ApplicationConfiguration, SessionState } from '@abp/ng.core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import snq from 'snq';
@Component({
selector: 'abp-languages',
// tslint:disable-next-line: component-max-inline-declarations
template: `
<div
*ngIf="(dropdownLanguages$ | async)?.length > 0"
class="dropdown"
ngbDropdown
#languageDropdown="ngbDropdown"
display="static"
>
<a
ngbDropdownToggle
class="nav-link"
href="javascript:void(0)"
role="button"
id="dropdownMenuLink"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ defaultLanguage$ | async }}
</a>
<div
class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && languageDropdown.isOpen()"
>
<a
*ngFor="let lang of dropdownLanguages$ | async"
href="javascript:void(0)"
class="dropdown-item"
(click)="onChangeLang(lang.cultureName)"
>{{ lang?.displayName }}</a
>
</div>
</div>
`,
})
export class LanguagesComponent implements OnInit {
get smallScreen(): boolean {
return window.innerWidth < 992;
}
@Select(ConfigState.getDeep('localization.languages'))
languages$: Observable<ApplicationConfiguration.Language[]>;
get defaultLanguage$(): Observable<string> {
return this.languages$.pipe(
map(
languages =>
snq(
() => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName,
),
'',
),
);
}
get dropdownLanguages$(): Observable<ApplicationConfiguration.Language[]> {
return this.languages$.pipe(
map(
languages =>
snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)),
[],
),
);
}
get selectedLangCulture(): string {
return this.store.selectSnapshot(SessionState.getLanguage);
}
constructor(private store: Store) {}
ngOnInit() {}
onChangeLang(cultureName: string) {
this.store.dispatch(new SetLanguage(cultureName));
}
}

@ -1,81 +1,16 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
<ng-container <li
*ngFor="let element of rightPartElements; trackBy: trackByFn" class="nav-item d-flex align-items-center"
[ngTemplateOutlet]="element" *ngFor="let item of navItems.items$ | async; trackBy: trackByFn"
[ngTemplateOutletContext]="{ smallScreen: smallScreen }" [abpPermission]="item.requiredPolicy"
></ng-container> >
</ul> <ng-container
*ngIf="item.component; else htmlTemplate"
<ng-template #language let-smallScreen="smallScreen"> [ngComponentOutlet]="item.component"
<li *ngIf="(dropdownLanguages$ | async)?.length > 0" class="nav-item"> ></ng-container>
<div class="dropdown" ngbDropdown #languageDropdown="ngbDropdown" display="static">
<a
ngbDropdownToggle
class="nav-link"
href="javascript:void(0)"
role="button"
id="dropdownMenuLink"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ defaultLanguage$ | async }}
</a>
<div
class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && languageDropdown.isOpen()"
>
<a
*ngFor="let lang of dropdownLanguages$ | async"
href="javascript:void(0)"
class="dropdown-item"
(click)="onChangeLang(lang.cultureName)"
>{{ lang?.displayName }}</a
>
</div>
</div>
</li>
</ng-template>
<ng-template #currentUser let-smallScreen="smallScreen"> <ng-template #htmlTemplate>
<li class="nav-item"> <div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div>
<ng-template #loginBtn>
<a role="button" class="nav-link" routerLink="/account/login">{{
'AbpAccount::Login' | abpLocalization
}}</a>
</ng-template> </ng-template>
<div
*ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn"
ngbDropdown
class="dropdown"
#currentUserDropdown="ngbDropdown"
display="static"
>
<a
ngbDropdownToggle
class="nav-link"
href="javascript:void(0)"
role="button"
id="dropdownMenuLink"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ (currentUser$ | async)?.userName }}
</a>
<div
class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && currentUserDropdown.isOpen()"
>
<a class="dropdown-item" routerLink="/account/manage-profile"
><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a
>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a
>
</div>
</div>
</li> </li>
</ng-template> </ul>

@ -1,125 +1,12 @@
import { import { NavItem, NavItemsService } from '@abp/ng.theme.shared';
ABP, import { Component, Input, TrackByFunction } from '@angular/core';
ApplicationConfiguration,
AuthService,
ConfigState,
SessionState,
SetLanguage,
takeUntilDestroy,
} from '@abp/ng.core';
import {
AfterViewInit,
Component,
Input,
OnDestroy,
TemplateRef,
TrackByFunction,
ViewChild,
} from '@angular/core';
import { Navigate, RouterState } from '@ngxs/router-plugin';
import { Select, Store } from '@ngxs/store';
import compare from 'just-compare';
import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import snq from 'snq';
import { AddNavigationElement } from '../../actions/layout.actions';
import { eNavigationElementNames } from '../../enums/navigation-element-names';
import { Layout } from '../../models/layout';
import { LayoutState } from '../../states/layout.state';
@Component({ @Component({
selector: 'abp-nav-items', selector: 'abp-nav-items',
templateUrl: 'nav-items.component.html', templateUrl: 'nav-items.component.html',
}) })
export class NavItemsComponent implements AfterViewInit, OnDestroy { export class NavItemsComponent {
@Select(LayoutState.getNavigationElements) trackByFn: TrackByFunction<NavItem> = (_, element) => element.id;
navElements$: Observable<Layout.NavigationElement[]>;
@Select(ConfigState.getOne('currentUser')) constructor(public readonly navItems: NavItemsService) {}
currentUser$: Observable<ApplicationConfiguration.CurrentUser>;
@Select(ConfigState.getDeep('localization.languages'))
languages$: Observable<ApplicationConfiguration.Language[]>;
@ViewChild('currentUser', { static: false, read: TemplateRef })
currentUserRef: TemplateRef<any>;
@ViewChild('language', { static: false, read: TemplateRef })
languageRef: TemplateRef<any>;
@Input()
smallScreen: boolean;
rightPartElements: TemplateRef<any>[] = [];
trackByFn: TrackByFunction<ABP.Route> = (_, element) => element.name;
get defaultLanguage$(): Observable<string> {
return this.languages$.pipe(
map(
languages =>
snq(
() => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName,
),
'',
),
);
}
get dropdownLanguages$(): Observable<ApplicationConfiguration.Language[]> {
return this.languages$.pipe(
map(
languages =>
snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)),
[],
),
);
}
get selectedLangCulture(): string {
return this.store.selectSnapshot(SessionState.getLanguage);
}
constructor(private store: Store, private authService: AuthService) {}
ngAfterViewInit() {
const navigations = this.store
.selectSnapshot(LayoutState.getNavigationElements)
.map(({ name }) => name);
if (navigations.indexOf(eNavigationElementNames.Language) < 0) {
this.store.dispatch(
new AddNavigationElement([
{ element: this.languageRef, order: 4, name: eNavigationElementNames.Language },
{ element: this.currentUserRef, order: 5, name: eNavigationElementNames.User },
]),
);
}
this.navElements$
.pipe(
map(elements => elements.map(({ element }) => element)),
filter(elements => !compare(elements, this.rightPartElements)),
takeUntilDestroy(this),
)
.subscribe(elements => {
setTimeout(() => (this.rightPartElements = elements), 0);
});
}
ngOnDestroy() {}
onChangeLang(cultureName: string) {
this.store.dispatch(new SetLanguage(cultureName));
}
logout() {
this.authService.logout().subscribe(() => {
this.store.dispatch(
new Navigate(['/'], null, {
state: { redirectUrl: this.store.selectSnapshot(RouterState).state.url },
}),
);
});
}
} }

@ -5,4 +5,6 @@ export const enum eThemeBasicComponents {
Logo = 'Theme.LogoComponent', Logo = 'Theme.LogoComponent',
Routes = 'Theme.RoutesComponent', Routes = 'Theme.RoutesComponent',
NavItems = 'Theme.NavItemsComponent', NavItems = 'Theme.NavItemsComponent',
CurrentUser = 'Theme.CurrentUserComponent',
Languages = 'Theme.LanguagesComponent',
} }

@ -1,2 +1 @@
export * from './components'; export * from './components';
export * from './navigation-element-names';

@ -1,4 +0,0 @@
export const enum eNavigationElementNames {
Language = 'LanguageRef',
User = 'CurrentUserRef',
}

@ -1 +1,2 @@
export * from './nav-item.provider';
export * from './styles.provider'; export * from './styles.provider';

@ -0,0 +1,31 @@
import { NavItemsService } from '@abp/ng.theme.shared';
import { APP_INITIALIZER } from '@angular/core';
import { CurrentUserComponent } from '../components/nav-items/current-user.component';
import { LanguagesComponent } from '../components/nav-items/languages.component';
import { eThemeBasicComponents } from '../enums/components';
export const BASIC_THEME_NAV_ITEM_PROVIDERS = [
{
provide: APP_INITIALIZER,
useFactory: configureNavItems,
deps: [NavItemsService],
multi: true,
},
];
export function configureNavItems(navItems: NavItemsService) {
return () => {
navItems.addItems([
{
id: eThemeBasicComponents.Languages,
order: 100,
component: LanguagesComponent,
},
{
id: eThemeBasicComponents.CurrentUser,
order: 101,
component: CurrentUserComponent,
},
]);
};
}

@ -1 +0,0 @@
export * from './layout-state.service';

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import { LayoutState } from '../states/layout.state';
import { AddNavigationElement, RemoveNavigationElementByName } from '../actions';
import { Layout } from '../models/layout';
@Injectable({ providedIn: 'root' })
export class LayoutStateService {
constructor(private store: Store) {}
getNavigationElements() {
return this.store.selectSnapshot(LayoutState.getNavigationElements);
}
dispatchAddNavigationElement(...args: ConstructorParameters<typeof AddNavigationElement>) {
return this.store.dispatch(new AddNavigationElement(...args));
}
dispatchRemoveNavigationElementByName(
...args: ConstructorParameters<typeof RemoveNavigationElementByName>
) {
return this.store.dispatch(new RemoveNavigationElementByName(...args));
}
}

@ -1,67 +0,0 @@
import { Injectable } from '@angular/core';
import { Action, Selector, State, StateContext } from '@ngxs/store';
import snq from 'snq';
import { AddNavigationElement, RemoveNavigationElementByName } from '../actions/layout.actions';
import { Layout } from '../models/layout';
@State<Layout.State>({
name: 'LayoutState',
defaults: { navigationElements: [] } as Layout.State,
})
@Injectable()
export class LayoutState {
@Selector()
static getNavigationElements({ navigationElements }: Layout.State): Layout.NavigationElement[] {
return navigationElements;
}
@Action(AddNavigationElement)
layoutAddAction(
{ getState, patchState }: StateContext<Layout.State>,
{ payload = [] }: AddNavigationElement,
) {
let { navigationElements } = getState();
if (!Array.isArray(payload)) {
payload = [payload];
}
if (navigationElements.length) {
payload = snq(
() =>
(payload as Layout.NavigationElement[]).filter(
({ name }) => navigationElements.findIndex(nav => nav.name === name) < 0,
),
[],
);
}
if (!payload.length) return;
navigationElements = [...navigationElements, ...payload]
.map(element => ({ ...element, order: element.order || 99 }))
.sort((a, b) => a.order - b.order);
return patchState({
navigationElements,
});
}
@Action(RemoveNavigationElementByName)
layoutRemoveAction(
{ getState, patchState }: StateContext<Layout.State>,
{ name }: RemoveNavigationElementByName,
) {
let { navigationElements } = getState();
const index = navigationElements.findIndex(element => element.name === name);
if (index > -1) {
navigationElements = navigationElements.splice(index, 1);
}
return patchState({
navigationElements,
});
}
}

@ -1,57 +0,0 @@
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store';
import * as LayoutActions from '../actions';
import { LayoutStateService } from '../services/layout-state.service';
import { LayoutState } from '../states/layout.state';
describe('LayoutStateService', () => {
let service: LayoutStateService;
let spectator: SpectatorService<LayoutStateService>;
let store: SpyObject<Store>;
const createService = createServiceFactory({ service: LayoutStateService, mocks: [Store] });
beforeEach(() => {
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
});
test('should have the all LayoutState static methods', () => {
const reg = /(?<=static )(.*)(?=\()/gm;
LayoutState.toString()
.match(reg)
.forEach(fnName => {
expect(service[fnName]).toBeTruthy();
const spy = jest.spyOn(store, 'selectSnapshot');
spy.mockClear();
const isDynamicSelector = LayoutState[fnName].name !== 'memoized';
if (isDynamicSelector) {
LayoutState[fnName] = jest.fn((...args) => args);
service[fnName]('test', 0, {});
expect(LayoutState[fnName]).toHaveBeenCalledWith('test', 0, {});
} else {
service[fnName]();
expect(spy).toHaveBeenCalledWith(LayoutState[fnName]);
}
});
});
test('should have a dispatch method for every LayoutState action', () => {
const reg = /(?<=dispatch)(\w+)(?=\()/gm;
LayoutStateService.toString()
.match(reg)
.forEach(fnName => {
expect(LayoutActions[fnName]).toBeTruthy();
const spy = jest.spyOn(store, 'dispatch');
spy.mockClear();
const params = Array.from(new Array(LayoutActions[fnName].length));
service[`dispatch${fnName}`](...params);
expect(spy).toHaveBeenCalledWith(new LayoutActions[fnName](...params));
});
});
});

@ -3,7 +3,6 @@ import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
import { NgxsModule } from '@ngxs/store';
import { AccountLayoutComponent } from './components/account-layout/account-layout.component'; import { AccountLayoutComponent } from './components/account-layout/account-layout.component';
import { ApplicationLayoutComponent } from './components/application-layout/application-layout.component'; import { ApplicationLayoutComponent } from './components/application-layout/application-layout.component';
import { EmptyLayoutComponent } from './components/empty-layout/empty-layout.component'; import { EmptyLayoutComponent } from './components/empty-layout/empty-layout.component';
@ -11,8 +10,10 @@ import { LogoComponent } from './components/logo/logo.component';
import { NavItemsComponent } from './components/nav-items/nav-items.component'; import { NavItemsComponent } from './components/nav-items/nav-items.component';
import { RoutesComponent } from './components/routes/routes.component'; import { RoutesComponent } from './components/routes/routes.component';
import { ValidationErrorComponent } from './components/validation-error/validation-error.component'; import { ValidationErrorComponent } from './components/validation-error/validation-error.component';
import { CurrentUserComponent } from './components/nav-items/current-user.component';
import { LanguagesComponent } from './components/nav-items/languages.component';
import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider'; import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider';
import { LayoutState } from './states/layout.state'; import { BASIC_THEME_NAV_ITEM_PROVIDERS } from './providers/nav-item.provider';
export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent]; export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent];
@ -23,6 +24,17 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
LogoComponent, LogoComponent,
NavItemsComponent, NavItemsComponent,
RoutesComponent, RoutesComponent,
CurrentUserComponent,
LanguagesComponent,
],
exports: [
...LAYOUTS,
ValidationErrorComponent,
LogoComponent,
NavItemsComponent,
RoutesComponent,
CurrentUserComponent,
LanguagesComponent,
], ],
imports: [ imports: [
CoreModule, CoreModule,
@ -30,7 +42,6 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
NgbCollapseModule, NgbCollapseModule,
NgbDropdownModule, NgbDropdownModule,
NgxValidateCoreModule, NgxValidateCoreModule,
NgxsModule.forFeature([LayoutState]),
NgxValidateCoreModule.forRoot({ NgxValidateCoreModule.forRoot({
targetSelector: '.form-group', targetSelector: '.form-group',
blueprints: { blueprints: {
@ -52,20 +63,13 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
errorTemplate: ValidationErrorComponent, errorTemplate: ValidationErrorComponent,
}), }),
], ],
exports: [ entryComponents: [...LAYOUTS, ValidationErrorComponent, CurrentUserComponent, LanguagesComponent],
...LAYOUTS,
ValidationErrorComponent,
LogoComponent,
NavItemsComponent,
RoutesComponent,
],
entryComponents: [...LAYOUTS, ValidationErrorComponent],
}) })
export class ThemeBasicModule { export class ThemeBasicModule {
static forRoot(): ModuleWithProviders<ThemeBasicModule> { static forRoot(): ModuleWithProviders<ThemeBasicModule> {
return { return {
ngModule: ThemeBasicModule, ngModule: ThemeBasicModule,
providers: [BASIC_THEME_STYLES_PROVIDERS], providers: [BASIC_THEME_NAV_ITEM_PROVIDERS, BASIC_THEME_STYLES_PROVIDERS],
}; };
} }
} }

@ -2,11 +2,8 @@
* Public API Surface of theme-basic * Public API Surface of theme-basic
*/ */
export * from './lib/actions';
export * from './lib/components'; export * from './lib/components';
export * from './lib/enums'; export * from './lib/enums';
export * from './lib/models'; export * from './lib/models';
export * from './lib/providers'; export * from './lib/providers';
export * from './lib/services';
export * from './lib/states';
export * from './lib/theme-basic.module'; export * from './lib/theme-basic.module';

@ -2,3 +2,4 @@ export * from './common';
export * from './confirmation'; export * from './confirmation';
export * from './statistics'; export * from './statistics';
export * from './toaster'; export * from './toaster';
export * from './nav-item';

@ -0,0 +1,10 @@
import { Type } from '@angular/core';
export interface NavItem {
id: string | number;
component?: Type<any>;
html?: string;
action?: () => void;
order?: number;
requiredPolicy?: string;
}

@ -1,3 +1,4 @@
export * from './confirmation.service'; export * from './confirmation.service';
export * from './modal.service'; export * from './modal.service';
export * from './toaster.service'; export * from './toaster.service';
export * from './nav-items.service';

@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { NavItem } from '../models/nav-item';
@Injectable({ providedIn: 'root' })
export class NavItemsService {
private _items$ = new BehaviorSubject<NavItem[]>([]);
get items(): NavItem[] {
return this._items$.value;
}
get items$(): Observable<NavItem[]> {
return this._items$.asObservable();
}
addItems(items: NavItem[]) {
this._items$.next([...this.items, ...items].sort(sortItems));
}
removeItem(id: string | number) {
const index = this.items.findIndex(item => item.id === id);
if (index > -1) {
this._items$.next([...this.items.slice(0, index), ...this.items.slice(index + 1)]);
}
}
patchItem(id: string | number, item: Partial<Omit<NavItem, 'id'>>) {
const index = this.items.findIndex(i => i.id === id);
if (index > -1) {
this._items$.next(
[
...this.items.slice(0, index),
{ ...this.items[index], ...item },
...this.items.slice(index + 1),
].sort(sortItems),
);
}
}
}
function sortItems(a: NavItem, b: NavItem) {
if (!a.order) return 1;
if (!b.order) return -1;
return a.order < b.order ? -1 : 1;
}

@ -1,4 +1,3 @@
export * from './date-parser-formatter'; export * from './date-parser-formatter';
export * from './nav-items';
export * from './validation-utils'; export * from './validation-utils';
export * from './widget-utils'; export * from './widget-utils';

@ -1,22 +0,0 @@
import { Type } from '@angular/core';
import { ReplaySubject } from 'rxjs';
export interface NavItem {
component?: Type<any>;
html?: string;
action?: () => void;
order?: number;
permission?: string;
}
const navItems: NavItem[] = [];
const navItems$ = new ReplaySubject<NavItem[]>(1);
export function addNavItem(item: NavItem) {
navItems.push(item);
navItems$.next(navItems.sort((a, b) => (a.order ? a.order - b.order : 1)));
}
export function getNavItems() {
return navItems$.asObservable();
}
Loading…
Cancel
Save