mirror of https://github.com/abpframework/abp.git
3 changed files with 258 additions and 0 deletions
@ -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<T extends object> { |
|||
abstract id: string; |
|||
abstract parentId: string; |
|||
abstract hide: (item: T) => boolean; |
|||
abstract sort: (a: T, b: T) => number; |
|||
|
|||
private _flat$ = new BehaviorSubject<T[]>([]); |
|||
private _tree$ = new BehaviorSubject<TreeNode<T>[]>([]); |
|||
private _visible$ = new BehaviorSubject<TreeNode<T>[]>([]); |
|||
|
|||
get flat(): T[] { |
|||
return this._flat$.value; |
|||
} |
|||
|
|||
get flat$(): Observable<T[]> { |
|||
return this._flat$.asObservable(); |
|||
} |
|||
|
|||
get tree(): TreeNode<T>[] { |
|||
return this._tree$.value; |
|||
} |
|||
|
|||
get tree$(): Observable<TreeNode<T>[]> { |
|||
return this._tree$.asObservable(); |
|||
} |
|||
|
|||
get visible(): TreeNode<T>[] { |
|||
return this._visible$.value; |
|||
} |
|||
|
|||
get visible$(): Observable<TreeNode<T>[]> { |
|||
return this._visible$.asObservable(); |
|||
} |
|||
|
|||
protected createTree(items: T[]): TreeNode<T>[] { |
|||
return createTreeFromList<T, TreeNode<T>>( |
|||
items, |
|||
item => item[this.id], |
|||
item => item[this.parentId], |
|||
item => BaseTreeNode.create(item), |
|||
); |
|||
} |
|||
|
|||
private filterWith(setOrMap: Set<string> | Map<string, T>): 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<string, T>(); |
|||
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>): 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<string>(); |
|||
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<T>, tree = this.tree): TreeNode<T> { |
|||
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<ABP.Route> { |
|||
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<ABP.Tab> { |
|||
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)); |
|||
} |
|||
} |
|||
@ -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'); |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue