mirror of https://github.com/abpframework/abp.git
committed by
GitHub
26 changed files with 286 additions and 405 deletions
@ -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