feat: add a service for routes and setting tabs

pull/4377/head
Arman Ozak 5 years ago
parent 41f1f2740e
commit 19a405f3f7

@ -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';

@ -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…
Cancel
Save