mirror of https://github.com/abpframework/abp
Merge pull request #4404 from abpframework/refactor/nav-items
Created NavItemsServicepull/4493/head
commit
9824dfba27
@ -1 +0,0 @@
|
|||||||
export * from './layout.actions';
|
|
||||||
@ -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) {}
|
|
||||||
}
|
|
||||||
@ -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 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 +0,0 @@
|
|||||||
export * from './layout.state';
|
|
||||||
@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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…
Reference in new issue