diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index f01dc876de..064dcbd35e 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -9,5 +9,6 @@ export * from './localization.service'; export * from './profile-state.service'; export * from './profile.service'; export * from './rest.service'; +export * from './routes.service'; export * from './session-state.service'; export * from './track-by.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts new file mode 100644 index 0000000000..4b9f3e7899 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts @@ -0,0 +1,144 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { ABP } from '../models/common'; +import { ConfigState } from '../states/config.state'; +import { pushValueTo } from '../utils/array-utils'; +import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; + +export abstract class AbstractTreeService { + abstract id: string; + abstract parentId: string; + abstract hide: (item: T) => boolean; + abstract sort: (a: T, b: T) => number; + + private _flat$ = new BehaviorSubject([]); + private _tree$ = new BehaviorSubject[]>([]); + private _visible$ = new BehaviorSubject[]>([]); + + get flat(): T[] { + return this._flat$.value; + } + + get flat$(): Observable { + return this._flat$.asObservable(); + } + + get tree(): TreeNode[] { + return this._tree$.value; + } + + get tree$(): Observable[]> { + return this._tree$.asObservable(); + } + + get visible(): TreeNode[] { + return this._visible$.value; + } + + get visible$(): Observable[]> { + return this._visible$.asObservable(); + } + + protected createTree(items: T[]): TreeNode[] { + return createTreeFromList>( + items, + item => item[this.id], + item => item[this.parentId], + item => BaseTreeNode.create(item), + ); + } + + private filterWith(setOrMap: Set | Map): T[] { + return this._flat$.value.filter( + item => !setOrMap.has(item[this.id]) && !setOrMap.has(item[this.parentId]), + ); + } + + private publish(flatItems: T[], visibleItems: T[]): T[] { + this._flat$.next(flatItems); + this._tree$.next(this.createTree(flatItems)); + this._visible$.next(this.createTree(visibleItems)); + return flatItems; + } + + add(items: T[]): T[] { + const map = new Map(); + items.forEach(item => map.set(item[this.id], item)); + + const flatItems = this.filterWith(map); + map.forEach(pushValueTo(flatItems)); + + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + patch(identifier: string, props: Partial): T[] | false { + const flatItems = this._flat$.value; + const index = flatItems.findIndex(item => item[this.id] === identifier); + if (index < 0) return false; + + flatItems[index] = { ...flatItems[index], ...props }; + + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + remove(identifiers: string[]): T[] { + const set = new Set(); + identifiers.forEach(id => set.add(id)); + + const flatItems = this.filterWith(set); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + search(params: Partial, tree = this.tree): TreeNode { + const searchKeys = Object.keys(params); + + return tree.reduce( + (acc, node) => + acc + ? acc + : searchKeys.every(key => node[key] === params[key]) + ? node + : node.children + ? this.search(params, node.children) + : acc, + null, + ); + } +} + +@Injectable({ + providedIn: 'root', +}) +export class RoutesService extends AbstractTreeService { + readonly id = 'name'; + readonly parentId = 'parentName'; + readonly hide = (item: ABP.Route) => item.invisible; + readonly sort = (a: ABP.Route, b: ABP.Route) => a.order - b.order; +} + +@Injectable({ + providedIn: 'root', +}) +export class SettingTabsService extends AbstractTreeService { + readonly id = 'name'; + readonly parentId = 'parentName'; + readonly hide = (setting: ABP.Tab) => setting.invisible || !this.isGranted(setting); + readonly sort = (a: ABP.Tab, b: ABP.Tab) => a.order - b.order; + + constructor(private store: Store) { + super(); + } + + private isGranted(setting: ABP.Tab): boolean { + return this.store.selectSnapshot(ConfigState.getGrantedPolicy(setting.requiredPolicy)); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts new file mode 100644 index 0000000000..2392a86183 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts @@ -0,0 +1,113 @@ +import { take } from 'rxjs/operators'; +import { RoutesService } from '../services'; + +const routes = [ + { path: '/foo', name: 'foo' }, + { path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 }, + { path: '/foo/bar/baz', name: 'baz', parentName: 'bar', order: 1 }, + { path: '/foo/x', name: 'x', parentName: 'foo', order: 1 }, +]; + +describe('Routes Service', () => { + describe('#add', () => { + it('should add given routes as flat$, tree$, and visible$', async () => { + const service = new RoutesService(); + service.add(routes); + + const flat = await service.flat$.pipe(take(1)).toPromise(); + const tree = await service.tree$.pipe(take(1)).toPromise(); + const visible = await service.visible$.pipe(take(1)).toPromise(); + + expect(flat.length).toBe(4); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('baz'); + expect(flat[2].name).toBe('x'); + expect(flat[3].name).toBe('bar'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(2); + expect(tree[0].children[0].name).toBe('x'); + expect(tree[0].children[1].name).toBe('bar'); + expect(tree[0].children[1].children[0].name).toBe('baz'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(1); + expect(visible[0].children[0].name).toBe('x'); + }); + }); + + describe('#remove', () => { + it('should remove routes based on given routeNames', () => { + const service = new RoutesService(); + service.add(routes); + service.remove(['bar']); + + const flat = service.flat; + const tree = service.tree; + const visible = service.visible; + + expect(flat.length).toBe(2); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('x'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(1); + expect(tree[0].children[0].name).toBe('x'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(1); + expect(visible[0].children[0].name).toBe('x'); + }); + }); + + describe('#patch', () => { + it('should patch propeties of routes based on given routeNames', () => { + const service = new RoutesService(); + service.add(routes); + service.patch('x', { invisible: true }); + + const flat = service.flat; + const tree = service.tree; + const visible = service.visible; + + expect(flat.length).toBe(4); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('baz'); + expect(flat[2].name).toBe('x'); + expect(flat[3].name).toBe('bar'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(2); + expect(tree[0].children[0].name).toBe('x'); + expect(tree[0].children[1].name).toBe('bar'); + expect(tree[0].children[1].children[0].name).toBe('baz'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(0); + }); + + it('should return false when route name is not found', () => { + const service = new RoutesService(); + service.add(routes); + const result = service.patch('A man has no name.', { invisible: true }); + expect(result).toBe(false); + }); + }); + + describe('#search', () => { + it('should return node found when route name is not found', () => { + const service = new RoutesService(); + service.add(routes); + const result = service.search({ invisible: true }); + expect(result.name).toBe('bar'); + expect(result.children.length).toBe(1); + expect(result.children[0].name).toBe('baz'); + }); + }); +});