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