From e5634fcfa592b796c3b28c9157567173848df3e4 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Mon, 15 Jun 2020 12:38:06 +0300 Subject: [PATCH 01/60] feat: add LazyModuleFactory --- .../core/src/lib/tests/factory-utils.spec.ts | 38 +++++++++++++++++++ .../core/src/lib/utils/factory-utils.ts | 31 +++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + 3 files changed, 70 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts new file mode 100644 index 0000000000..06e8a8d044 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts @@ -0,0 +1,38 @@ +import { Injector, ModuleWithProviders, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { LazyModuleFactory } from '../utils/factory-utils'; + +@NgModule() +class Module { + static forChild(): ModuleWithProviders { + return { + ngModule: Module, + providers: [{ provide: 'foo', useValue: 'bar' }], + }; + } +} + +describe('LazyModuleFactory', () => { + const factory = new LazyModuleFactory(Module.forChild()); + + describe('#moduleType', () => { + it('should return the ngModule property of given ModuleWithProviders', () => { + expect(factory.moduleType).toBe(Module); + }); + }); + + describe('#create', () => { + it('should return an instance of NgModuleRef_', () => { + TestBed.configureTestingModule({}); + + const injector = TestBed.inject(Injector); + const moduleRef = factory.create(injector); + + expect('componentFactoryResolver' in moduleRef).toBe(true); + expect('destroy' in moduleRef).toBe(true); + expect('injector' in moduleRef).toBe(true); + expect('instance' in moduleRef).toBe(true); + expect('onDestroy' in moduleRef).toBe(true); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts new file mode 100644 index 0000000000..bc6273fa82 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts @@ -0,0 +1,31 @@ +import { + Compiler, + Injector, + ModuleWithProviders, + NgModuleFactory, + NgModuleRef, + StaticProvider, + Type, +} from '@angular/core'; + +export class LazyModuleFactory extends NgModuleFactory { + get moduleType(): Type { + return this.moduleWithProviders.ngModule; + } + + constructor(private moduleWithProviders: ModuleWithProviders) { + super(); + } + + create(parentInjector: Injector | null): NgModuleRef { + const injector = Injector.create({ + parent: parentInjector, + providers: this.moduleWithProviders.providers as StaticProvider[], + }); + + const compiler = injector.get(Compiler); + const factory = compiler.compileModuleSync(this.moduleType); + + return factory.create(injector); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 4dde8134ef..b75fef8efe 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -1,4 +1,5 @@ export * from './common-utils'; +export * from './factory-utils'; export * from './form-utils'; export * from './generator-utils'; export * from './initial-utils'; From 117b47bba633c205f616516e8366ad37cfc0b7ac Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 10:35:51 +0300 Subject: [PATCH 02/60] feat: add pushValueTo array util to core --- .../core/src/lib/tests/array-utils.spec.ts | 17 +++++++++++++++++ .../packages/core/src/lib/utils/array-utils.ts | 6 ++++++ .../packages/core/src/lib/utils/index.ts | 1 + 3 files changed, 24 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/array-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts new file mode 100644 index 0000000000..3ccc91461c --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts @@ -0,0 +1,17 @@ +import { pushValueTo } from '../utils/array-utils'; + +describe('Array Utils', () => { + describe('#pushValueTo', () => { + test.each` + source | target | expected + ${[]} | ${[0, 1, 2, 3]} | ${[0, 1, 2, 3]} + ${[3]} | ${[0, 1, 2]} | ${[0, 1, 2, 3]} + ${[2, 3]} | ${[0, 1]} | ${[0, 1, 2, 3]} + ${[1, 2, 3]} | ${[0]} | ${[0, 1, 2, 3]} + ${[0, 1, 2, 3]} | ${[]} | ${[0, 1, 2, 3]} + `('should push $source to $target when called in forEach', ({ source, target, expected }) => { + source.forEach(pushValueTo(target)); + expect(target).toEqual(expected); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts new file mode 100644 index 0000000000..d2d74105b3 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts @@ -0,0 +1,6 @@ +export function pushValueTo(array: T[]) { + return (element: T) => { + array.push(element); + return array; + }; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index b75fef8efe..ce89b4dddb 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -1,3 +1,4 @@ +export * from './array-utils'; export * from './common-utils'; export * from './factory-utils'; export * from './form-utils'; From ed41a884bc38c5eb0a8f3531f0c899d7b6082ae3 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 11:25:09 +0300 Subject: [PATCH 03/60] feat: add tree utils to core --- .../core/src/lib/tests/tree-utils.spec.ts | 68 +++++++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + .../packages/core/src/lib/utils/tree-utils.ts | 51 ++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts new file mode 100644 index 0000000000..454c1c6891 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts @@ -0,0 +1,68 @@ +import { createTreeFromList, TreeNode } from '../utils/tree-utils'; + +const LIST_1 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, +]; +const LIST_2 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, + { id: 3, pid: 1 }, +]; +const LIST_3 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, + { id: 3, pid: 2 }, +]; +const TREE_1 = [ + { id: 1, pid: null, isLeaf: false, children: [{ id: 2, pid: 1, isLeaf: true, children: [] }] }, +]; +const TREE_2 = [ + { + id: 1, + pid: null, + isLeaf: false, + children: [ + { id: 2, pid: 1, isLeaf: true, children: [] }, + { id: 3, pid: 1, isLeaf: true, children: [] }, + ], + }, +]; +const TREE_3 = [ + { + id: 1, + pid: null, + isLeaf: false, + children: [ + { id: 2, pid: 1, isLeaf: false, children: [{ id: 3, pid: 2, isLeaf: true, children: [] }] }, + ], + }, +]; +describe('Tree Utils', () => { + describe('createTreeFromList', () => { + test.each` + list | expected + ${LIST_1} | ${TREE_1} + ${LIST_2} | ${TREE_2} + ${LIST_3} | ${TREE_3} + `('should return $expected when given $list', ({ list, expected }: TestCreateTreeFromList) => { + expect( + createTreeFromList( + list, + x => x.id, + x => x.pid, + ), + ).toEqual(expected); + }); + }); +}); + +interface TestCreateTreeFromList { + list: ModelA[]; + expected: TreeNode[]; +} + +interface ModelA { + id: 1; + pid: null; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index ce89b4dddb..6f6136b195 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -9,3 +9,4 @@ export * from './localization-utils'; export * from './number-utils'; export * from './route-utils'; export * from './rxjs-utils'; +export * from './tree-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts new file mode 100644 index 0000000000..86844a4375 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -0,0 +1,51 @@ +export class TreeNode { + children: TreeNode[] = []; + isLeaf = true; + + constructor(props: T) { + Object.assign(this, props); + } +} + +export function createTreeFromList>( + list: T[], + keySelector: (item: T) => NodeKey, + parentKeySelector: (item: T) => NodeKey, + valueMapper = (item: T) => new TreeNode(item) as R, +) { + const map = createMapFromList(list, keySelector, valueMapper); + const tree: ReturnType[] = []; + + list.forEach(row => { + const id = keySelector(row); + const parentId = parentKeySelector(row); + const node = map.get(id); + + if (parentId) { + const parent = map.get(parentId); + parent.children.push(node); + parent.isLeaf = false; + } else { + tree.push(node); + } + }); + + return tree; +} + +export function createMapFromList>( + list: T[], + keySelector: (item: T) => NodeKey, + valueMapper = (item: T) => new TreeNode(item) as R, +) { + const map = new Map, ReturnType>(); + list.forEach(row => map.set(keySelector(row), valueMapper(row))); + return map; +} + +type NodeKey = number | string | Symbol; + +interface BranchOrLeaf { + children: BranchOrLeaf[]; + isLeaf: boolean; +} From 66111b9afa8546103d4cbf0b597c77c889d0a7cd Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 14:21:28 +0300 Subject: [PATCH 04/60] feat: improve tree util types --- .../packages/core/src/lib/utils/tree-utils.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts index 86844a4375..607a30ec52 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -1,20 +1,24 @@ -export class TreeNode { +export class TreeNodeFactory { children: TreeNode[] = []; isLeaf = true; constructor(props: T) { Object.assign(this, props); } + + static create(props: T) { + return new TreeNodeFactory(props) as TreeNode; + } } -export function createTreeFromList>( +export function createTreeFromList( list: T[], - keySelector: (item: T) => NodeKey, - parentKeySelector: (item: T) => NodeKey, - valueMapper = (item: T) => new TreeNode(item) as R, + keySelector: (item: T) => number | string | Symbol, + parentKeySelector: typeof keySelector, + valueMapper: (item: T) => R, ) { const map = createMapFromList(list, keySelector, valueMapper); - const tree: ReturnType[] = []; + const tree: NodeValue[] = []; list.forEach(row => { const id = keySelector(row); @@ -23,8 +27,8 @@ export function createTreeFromList>( if (parentId) { const parent = map.get(parentId); - parent.children.push(node); - parent.isLeaf = false; + (parent as any).children.push(node); + (parent as any).isLeaf = false; } else { tree.push(node); } @@ -33,19 +37,25 @@ export function createTreeFromList>( return tree; } -export function createMapFromList>( +export function createMapFromList( list: T[], - keySelector: (item: T) => NodeKey, - valueMapper = (item: T) => new TreeNode(item) as R, + keySelector: (item: T) => number | string | Symbol, + valueMapper: (item: T) => R, ) { - const map = new Map, ReturnType>(); + type Key = ReturnType; + type Value = NodeValue; + const map = new Map(); list.forEach(row => map.set(keySelector(row), valueMapper(row))); return map; } -type NodeKey = number | string | Symbol; - -interface BranchOrLeaf { - children: BranchOrLeaf[]; +export type TreeNode = { + [K in keyof T]: T[K]; +} & { + children: TreeNode[]; isLeaf: boolean; -} +}; + +type NodeValue any> = F extends undefined + ? TreeNode + : ReturnType; From c72bcf48f44f2a1198b1284e0eb8623b62504f54 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:40:34 +0300 Subject: [PATCH 05/60] feat: add circular reference to parents of nodes --- .../core/src/lib/tests/tree-utils.spec.ts | 31 ++++++++++++------- .../packages/core/src/lib/utils/tree-utils.ts | 12 ++++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts index 454c1c6891..6115114872 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts @@ -1,4 +1,4 @@ -import { createTreeFromList, TreeNode } from '../utils/tree-utils'; +import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; const LIST_1 = [ { id: 1, pid: null }, @@ -46,23 +46,32 @@ describe('Tree Utils', () => { ${LIST_2} | ${TREE_2} ${LIST_3} | ${TREE_3} `('should return $expected when given $list', ({ list, expected }: TestCreateTreeFromList) => { - expect( - createTreeFromList( - list, - x => x.id, - x => x.pid, - ), - ).toEqual(expected); + const tree = createTreeFromList( + list, + x => x.id, + x => x.pid, + x => BaseTreeNode.create(x), + ); + + expect(removeParents(tree)).toEqual(expected); }); }); }); +function removeParents(tree: TreeNode[]) { + return tree.map(v => { + const { parent, ...node } = v; + node.children = removeParents(node.children); + return node; + }); +} + interface TestCreateTreeFromList { - list: ModelA[]; - expected: TreeNode[]; + list: Model[]; + expected: TreeNode[]; } -interface ModelA { +interface Model { id: 1; pid: null; } diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts index 607a30ec52..d4c0ba0ab3 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -1,19 +1,20 @@ -export class TreeNodeFactory { +export class BaseTreeNode { children: TreeNode[] = []; isLeaf = true; + parent: TreeNode; constructor(props: T) { Object.assign(this, props); } static create(props: T) { - return new TreeNodeFactory(props) as TreeNode; + return new BaseTreeNode(props) as TreeNode; } } export function createTreeFromList( list: T[], - keySelector: (item: T) => number | string | Symbol, + keySelector: (item: T) => number | string | symbol, parentKeySelector: typeof keySelector, valueMapper: (item: T) => R, ) { @@ -27,8 +28,10 @@ export function createTreeFromList( if (parentId) { const parent = map.get(parentId); + if (!parent) return; (parent as any).children.push(node); (parent as any).isLeaf = false; + (node as any).parent = parent; } else { tree.push(node); } @@ -39,7 +42,7 @@ export function createTreeFromList( export function createMapFromList( list: T[], - keySelector: (item: T) => number | string | Symbol, + keySelector: (item: T) => number | string | symbol, valueMapper: (item: T) => R, ) { type Key = ReturnType; @@ -54,6 +57,7 @@ export type TreeNode = { } & { children: TreeNode[]; isLeaf: boolean; + parent?: TreeNode; }; type NodeValue any> = F extends undefined From 6478f57317473b381f8909834314e2f5c29aa2be Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:42:15 +0300 Subject: [PATCH 06/60] feat: add node, nav, and tab types --- .../packages/core/src/lib/models/common.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 773233857f..ee160fef6d 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from '@angular/core'; +import { EventEmitter, Type } from '@angular/core'; import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { eLayoutType } from '../enums/common'; @@ -35,21 +35,25 @@ export namespace ABP { maxResultCount?: number; } - export interface Route { - children?: Route[]; - invisible?: boolean; - layout?: eLayoutType; + export interface Node { name: string; - order?: number; parentName?: string; + order?: number; + invisible?: boolean; + } + + export interface Nav extends Node { path: string; requiredPolicy?: string; + } + + export interface Route extends Nav { + layout?: eLayoutType; iconClass?: string; } - export interface FullRoute extends Route { - url?: string; - wrapper?: boolean; + export interface Tab extends Nav { + component: Type; } export interface BasicItem { From 41f1f2740e074ff840679431d7f58653a97d4aee Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:43:30 +0300 Subject: [PATCH 07/60] feat: add utility function for getting a route path --- .../core/src/lib/tests/route-utils.spec.ts | 60 +++++++++++++ .../core/src/lib/utils/route-utils.ts | 87 ++----------------- 2 files changed, 65 insertions(+), 82 deletions(-) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts new file mode 100644 index 0000000000..9c5f81ac2a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { RouterOutletComponent } from '../components'; +import { getRoutePath } from '../utils/route-utils'; + +// tslint:disable-next-line +@Component({ template: '' }) +class DummyComponent {} + +describe('Route Utils', () => { + describe('#getRoutePath', () => { + let spectator: SpectatorRouting; + const createRouting = createRoutingFactory({ + component: RouterOutletComponent, + stubsEnabled: false, + declarations: [DummyComponent], + imports: [RouterModule], + routes: [ + { + path: '', + children: [ + { + path: 'foo', + children: [ + { + path: 'bar', + children: [ + { + path: 'baz', + component: DummyComponent, + }, + ], + }, + ], + }, + ], + }, + ], + }); + + beforeEach(async () => { + spectator = createRouting(); + }); + + test.each` + url | expected + ${''} | ${'/'} + ${'/'} | ${'/'} + ${'/foo'} | ${'/foo'} + ${'/foo/bar'} | ${'/foo/bar'} + ${'/foo/bar/baz'} | ${'/foo/bar/baz'} + ${'/foo?bar=baz'} | ${'/foo'} + ${'/foo#bar'} | ${'/foo'} + `('should return $expected when url is $url', async ({ url, expected }) => { + await spectator.router.navigateByUrl(url); + expect(getRoutePath(spectator.router)).toBe(expected); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts index 51abe4218b..7ef062986a 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts @@ -1,85 +1,8 @@ -import { ABP } from '../models/common'; +import { PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; -export function organizeRoutes( - routes: ABP.FullRoute[], - wrappers: ABP.FullRoute[] = [], - parentNameArr = [] as ABP.FullRoute[], - parentName: string = null, -): ABP.FullRoute[] { - const filter = route => { - if (route.children && route.children.length) { - route.children = organizeRoutes(route.children, wrappers, parentNameArr, route.name); - } +export function getRoutePath(router: Router) { + const emptyGroup = { segments: [] } as UrlSegmentGroup; + const primaryGroup = router.parseUrl(router.url).root.children[PRIMARY_OUTLET]; - if (route.parentName && route.parentName !== parentName) { - parentNameArr.push(route); - return false; - } - - return true; - }; - - if (parentName) { - // recursive block - return routes.filter(filter); - } - - const filteredRoutes = routes.filter(filter); - - if (parentNameArr.length) { - return sortRoutes(setChildRoute([...filteredRoutes, ...wrappers], parentNameArr)); - } - - return filteredRoutes; -} - -export function setChildRoute( - routes: ABP.FullRoute[], - parentNameArr: ABP.FullRoute[], -): ABP.FullRoute[] { - return routes.map(route => { - if (route.children && route.children.length) { - route.children = setChildRoute(route.children, parentNameArr); - } - - const foundedChildren = parentNameArr.filter(parent => parent.parentName === route.name); - if (foundedChildren && foundedChildren.length) { - route.children = [...(route.children || []), ...foundedChildren]; - } - - return route; - }); -} - -export function sortRoutes(routes: ABP.FullRoute[] = []): ABP.FullRoute[] { - if (!routes.length) return []; - return routes - .map((route, index) => { - return { - ...route, - order: typeof route.order === 'undefined' ? index + 1 : route.order, - }; - }) - .sort((a, b) => a.order - b.order) - .map(route => { - if (route.children && route.children.length) { - route.children = sortRoutes(route.children); - } - - return route; - }); -} - -const ABP_ROUTES = [] as ABP.FullRoute[]; - -export function addAbpRoutes(routes: ABP.FullRoute | ABP.FullRoute[]): void { - if (!Array.isArray(routes)) { - routes = [routes]; - } - - ABP_ROUTES.push(...routes); -} - -export function getAbpRoutes(): ABP.FullRoute[] { - return ABP_ROUTES; + return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/'); } From 19a405f3f7f721667750dc907039b6914405968c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:44:43 +0300 Subject: [PATCH 08/60] feat: add a service for routes and setting tabs --- .../packages/core/src/lib/services/index.ts | 1 + .../core/src/lib/services/routes.service.ts | 144 ++++++++++++++++++ .../core/src/lib/tests/routes.service.spec.ts | 113 ++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/routes.service.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts 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'); + }); + }); +}); From 82188a0d2916f86fba427bea5b2b70666c827b2b Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:53:37 +0300 Subject: [PATCH 09/60] feat: use RoutesService in DynamicLayoutComponent --- .../components/dynamic-layout.component.ts | 73 ++++++++----------- .../tests/dynamic-layout.component.spec.ts | 66 +++++++++-------- 2 files changed, 68 insertions(+), 71 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index c07f11580f..5455187086 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -1,13 +1,14 @@ -import { Component, OnDestroy, Type } from '@angular/core'; -import { ActivatedRoute, NavigationEnd, Router, UrlSegment } from '@angular/router'; +import { Component, Injector, OnDestroy, Type } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngxs/store'; -import snq from 'snq'; import { eLayoutType } from '../enums/common'; -import { ABP } from '../models/common'; +import { ABP } from '../models'; import { ReplaceableComponents } from '../models/replaceable-components'; -import { ConfigState } from '../states/config.state'; +import { RoutesService } from '../services/routes.service'; import { ReplaceableComponentsState } from '../states/replaceable-components.state'; +import { getRoutePath } from '../utils/route-utils'; import { takeUntilDestroy } from '../utils/rxjs-utils'; +import { TreeNode } from '../utils/tree-utils'; @Component({ selector: 'abp-dynamic-layout', @@ -22,23 +23,37 @@ import { takeUntilDestroy } from '../utils/rxjs-utils'; export class DynamicLayoutComponent implements OnDestroy { layout: Type; - constructor(private router: Router, private route: ActivatedRoute, private store: Store) { - const { routes } = this.store.selectSnapshot(ConfigState.getAll); + constructor( + injector: Injector, + private route: ActivatedRoute, + private routes: RoutesService, + private store: Store, + ) { + const router = injector.get(Router); + const layouts = { + application: this.getComponent('Theme.ApplicationLayoutComponent'), + account: this.getComponent('Theme.AccountLayoutComponent'), + empty: this.getComponent('Theme.EmptyLayoutComponent'), + }; router.events.pipe(takeUntilDestroy(this)).subscribe(event => { if (event instanceof NavigationEnd) { - const segments = snq(() => router.parseUrl(event.url).root.children.primary.segments, [ - { path: router.url.replace('/', '') }, - ] as any); + let expectedLayout = (this.route.snapshot.data || {}).layout; + const path = getRoutePath(router); - const layouts = { - application: this.getComponent('Theme.ApplicationLayoutComponent'), - account: this.getComponent('Theme.AccountLayoutComponent'), - empty: this.getComponent('Theme.EmptyLayoutComponent'), - }; + if (!expectedLayout) { + let node = { parent: this.routes.search({ path }) } as TreeNode; + while (node.parent) { + node = node.parent; - const expectedLayout = - (this.route.snapshot.data || {}).layout || findLayout(segments, routes); + if (node.layout) { + expectedLayout = node.layout; + break; + } + } + } + + if (!expectedLayout) expectedLayout = eLayoutType.empty; this.layout = layouts[expectedLayout].component; } @@ -51,27 +66,3 @@ export class DynamicLayoutComponent implements OnDestroy { ngOnDestroy() {} } - -function findLayout(segments: UrlSegment[], routes: ABP.FullRoute[]): eLayoutType { - let layout = eLayoutType.empty; - - const route = routes - .reduce((acc, val) => (val.wrapper ? [...acc, ...val.children] : [...acc, val]), []) - .find(r => r.path === segments[0].path); - - if (route) { - if (route.layout) { - layout = route.layout; - } - - if (route.children && route.children.length && segments.length > 1) { - const child = route.children.find(c => c.path === segments[1].path); - - if (child && child.layout) { - layout = child.layout; - } - } - } - - return layout; -} diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index e1a678a19f..89a6a683b9 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -1,13 +1,13 @@ +import { HttpClient } from '@angular/common/http'; import { Component, NgModule } from '@angular/core'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; import { NgxsModule, Store } from '@ngxs/store'; import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; -import { eLayoutType } from '../enums'; +import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; -import { ConfigState, ReplaceableComponentsState } from '../states'; -import { ApplicationConfigurationService } from '../services'; -import { HttpClient } from '@angular/common/http'; +import { ApplicationConfigurationService, RoutesService } from '../services'; +import { ReplaceableComponentsState } from '../states'; @Component({ selector: 'abp-layout-application', @@ -48,28 +48,36 @@ class DummyComponent { constructor(public route: ActivatedRoute) {} } -const storeData = { - ConfigState: { - routes: [ - { - path: '', - wrapper: true, - children: [ - { - path: 'parentWithLayout', - layout: eLayoutType.application, - children: [ - { path: 'childWithoutLayout' }, - { path: 'childWithLayout', layout: eLayoutType.account }, - ], - }, - ], - }, - { path: 'withData', layout: eLayoutType.application }, - , - ] as ABP.FullRoute[], - environment: { application: {} }, +const routes: ABP.Route[] = [ + { + path: '', + name: 'Root', + }, + { + path: '/parentWithLayout', + name: 'ParentWithLayout', + parentName: 'Root', + layout: eLayoutType.application, + }, + { + path: '/parentWithLayout/childWithoutLayout', + name: 'ChildWithoutLayout', + parentName: 'ParentWithLayout', + }, + { + path: '/parentWithLayout/childWithLayout', + name: 'ChildWithLayout', + parentName: 'ParentWithLayout', + layout: eLayoutType.account, + }, + { + path: '/withData', + name: 'WithData', + layout: eLayoutType.application, }, +]; + +const storeData = { ReplaceableComponentsState: { replaceableComponents: [ { @@ -94,11 +102,7 @@ describe('DynamicLayoutComponent', () => { stubsEnabled: false, declarations: [DummyComponent, DynamicLayoutComponent], mocks: [ApplicationConfigurationService, HttpClient], - imports: [ - RouterModule, - DummyLayoutModule, - NgxsModule.forRoot([ConfigState, ReplaceableComponentsState]), - ], + imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])], routes: [ { path: '', component: RouterOutletComponent }, { @@ -150,6 +154,8 @@ describe('DynamicLayoutComponent', () => { beforeEach(async () => { spectator = createComponent(); store = spectator.get(Store); + const routesService = spectator.get(RoutesService); + routesService.add(routes); store.reset(storeData); }); From 9c0fe4c8e145ef1519fa8dadf54adedc999325af Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:55:22 +0300 Subject: [PATCH 10/60] refactor: use ABP.Route type instead of ABP.FullRoute --- .../nav-items/nav-items.component.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts index 7bae923989..6819807cd7 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts @@ -1,31 +1,31 @@ -import { - Component, - AfterViewInit, - TrackByFunction, - TemplateRef, - ViewChild, - OnDestroy, - Input, -} from '@angular/core'; import { ABP, - takeUntilDestroy, - SetLanguage, + ApplicationConfiguration, AuthService, ConfigState, - ApplicationConfiguration, SessionState, + SetLanguage, + takeUntilDestroy, } from '@abp/ng.core'; -import { LayoutState } from '../../states/layout.state'; -import { Store, Select } from '@ngxs/store'; -import { eNavigationElementNames } from '../../enums/navigation-element-names'; -import { AddNavigationElement } from '../../actions/layout.actions'; -import { map, filter } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { Layout } from '../../models/layout'; +import { + AfterViewInit, + Component, + Input, + OnDestroy, + TemplateRef, + TrackByFunction, + ViewChild, +} from '@angular/core'; import { Navigate, RouterState } from '@ngxs/router-plugin'; -import snq from 'snq'; +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({ selector: 'abp-nav-items', @@ -52,7 +52,7 @@ export class NavItemsComponent implements AfterViewInit, OnDestroy { rightPartElements: TemplateRef[] = []; - trackByFn: TrackByFunction = (_, element) => element; + trackByFn: TrackByFunction = (_, element) => element.name; get defaultLanguage$(): Observable { return this.languages$.pipe( From 962e636640f497348860e63429a218bc2076ea87 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:56:28 +0300 Subject: [PATCH 11/60] feat: use RoutesService in RoutesComponent --- .../components/routes/routes.component.html | 33 +++++++------------ .../lib/components/routes/routes.component.ts | 33 +++---------------- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html index 6437233cb1..4d22ddc8d2 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html @@ -1,6 +1,6 @@