286 lines
7.1 KiB
TypeScript
286 lines
7.1 KiB
TypeScript
import { Subject } from 'rxjs';
|
|
import { takeUntil } from 'rxjs/operators';
|
|
|
|
import {
|
|
Component,
|
|
OnInit,
|
|
OnDestroy,
|
|
Input,
|
|
ViewChild,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
EventEmitter,
|
|
Output
|
|
} from '@angular/core';
|
|
|
|
import { trigger, transition, style, animate } from '@angular/animations';
|
|
|
|
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
|
|
|
import { MatTreeFlattener, MatTree } from '@angular/material/tree';
|
|
|
|
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
|
|
|
|
import { LoginResponse } from '@ucap/protocol-authentication';
|
|
import { DeptInfo } from '@ucap/protocol-query';
|
|
|
|
import { LogService } from '@ucap/ng-logger';
|
|
import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
|
|
|
|
export interface OrganizationNode {
|
|
deptInfo: DeptInfo;
|
|
ancestorSeq?: number;
|
|
children?: OrganizationNode[];
|
|
}
|
|
|
|
export interface FlatNode {
|
|
expandable: boolean;
|
|
level: number;
|
|
data: OrganizationNode;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'ucap-organization-tree',
|
|
templateUrl: './tree.component.html',
|
|
styleUrls: ['./tree.component.scss'],
|
|
animations: [
|
|
trigger('removeAdd', [
|
|
transition('remove <=> add', [
|
|
style({
|
|
transform: `rotate(45deg)`,
|
|
opacity: 0
|
|
}),
|
|
animate('.2s 0s ease-out')
|
|
])
|
|
])
|
|
],
|
|
changeDetection: ChangeDetectionStrategy.OnPush
|
|
})
|
|
export class TreeComponent implements OnInit, OnDestroy {
|
|
@Input()
|
|
loginRes: LoginResponse;
|
|
|
|
@Input()
|
|
set deptInfoList(data: {
|
|
deptInfoList: DeptInfo[];
|
|
expanded?: number[];
|
|
displayRoot?: boolean;
|
|
}) {
|
|
const deptInfoList = data.deptInfoList;
|
|
const displayRoot =
|
|
undefined === data.displayRoot ? true : data.displayRoot;
|
|
|
|
if (!deptInfoList || 0 === deptInfoList.length) {
|
|
return;
|
|
}
|
|
const nodeMap = new Map<number, OrganizationNode>();
|
|
let rootNodeList: OrganizationNode[] = [];
|
|
const remainChildNodeList: OrganizationNode[] = [];
|
|
|
|
deptInfoList.forEach((deptInfo) => {
|
|
const node: OrganizationNode = {
|
|
deptInfo,
|
|
children: []
|
|
};
|
|
if (nodeMap.has(deptInfo.seq)) {
|
|
this.logService.warn('duplicate seq', deptInfo.seq);
|
|
return;
|
|
}
|
|
nodeMap.set(deptInfo.seq, node);
|
|
|
|
if (0 === deptInfo.parentSeq) {
|
|
rootNodeList.push(node);
|
|
return;
|
|
}
|
|
|
|
if (nodeMap.has(deptInfo.parentSeq)) {
|
|
const ancestor = nodeMap.get(deptInfo.parentSeq);
|
|
node.ancestorSeq = ancestor.deptInfo.seq;
|
|
ancestor.children.push(node);
|
|
} else {
|
|
remainChildNodeList.push(node);
|
|
}
|
|
});
|
|
|
|
if (
|
|
!displayRoot &&
|
|
0 < rootNodeList.length &&
|
|
0 === rootNodeList[0].deptInfo.parentSeq &&
|
|
!!rootNodeList[0].children &&
|
|
0 < rootNodeList[0].children.length
|
|
) {
|
|
rootNodeList = [...rootNodeList[0].children];
|
|
}
|
|
|
|
remainChildNodeList.forEach((node) => {
|
|
const ancestor = nodeMap.get(node.deptInfo.parentSeq);
|
|
if (!!ancestor) {
|
|
node.ancestorSeq = ancestor.deptInfo.seq;
|
|
ancestor.children.push(node);
|
|
}
|
|
});
|
|
|
|
this.dataSource.data = rootNodeList;
|
|
|
|
if (!!data.expanded) {
|
|
this.expand(...data.expanded);
|
|
}
|
|
|
|
this.changeDetectorRef.detectChanges();
|
|
}
|
|
|
|
@Output() currentDeptSeqChange: EventEmitter<number> = new EventEmitter();
|
|
@Input() set currentDeptSeq(deptInfoSeq: number) {
|
|
this._currentDeptSeq = deptInfoSeq;
|
|
}
|
|
get currentDeptSeq() {
|
|
return this._currentDeptSeq;
|
|
}
|
|
// tslint:disable-next-line: variable-name
|
|
_currentDeptSeq: number;
|
|
|
|
@Output()
|
|
clickNode = new EventEmitter<DeptInfo>();
|
|
|
|
@ViewChild('treeList', { static: false })
|
|
treeList: MatTree<FlatNode>;
|
|
|
|
@ViewChild('cvsvList', { static: false })
|
|
cvsvList: CdkVirtualScrollViewport;
|
|
|
|
@ViewChild(PerfectScrollbarDirective, { static: false })
|
|
psDirectiveRef?: PerfectScrollbarDirective;
|
|
|
|
treeControl: FlatTreeControl<FlatNode>;
|
|
treeFlattener: MatTreeFlattener<OrganizationNode, FlatNode>;
|
|
dataSource: VirtualScrollTreeFlatDataSource<OrganizationNode, FlatNode>;
|
|
|
|
// tslint:disable-next-line: variable-name
|
|
private _ngOnDestroySubject: Subject<void>;
|
|
|
|
constructor(
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
private logService: LogService
|
|
) {
|
|
this.treeControl = new FlatTreeControl<FlatNode>(
|
|
(node) => node.level,
|
|
(node) => node.expandable
|
|
);
|
|
|
|
this.treeFlattener = new MatTreeFlattener<OrganizationNode, FlatNode>(
|
|
(node: OrganizationNode, level: number) => {
|
|
return {
|
|
expandable: !!node.children && node.children.length > 0,
|
|
level,
|
|
data: node
|
|
};
|
|
},
|
|
(node) => node.level,
|
|
(node) => node.expandable,
|
|
(node) => node.children
|
|
);
|
|
|
|
this.dataSource = new VirtualScrollTreeFlatDataSource<
|
|
OrganizationNode,
|
|
FlatNode
|
|
>(this.treeControl, this.treeFlattener);
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this._ngOnDestroySubject = new Subject();
|
|
|
|
this.dataSource.cdkVirtualScrollViewport = this.cvsvList;
|
|
this.treeControl.expansionModel.changed
|
|
.pipe(takeUntil(this._ngOnDestroySubject))
|
|
.subscribe(() => {
|
|
this.cvsvList.checkViewportSize();
|
|
this.psDirectiveRef.update();
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (!!this._ngOnDestroySubject) {
|
|
this._ngOnDestroySubject.next();
|
|
this._ngOnDestroySubject.complete();
|
|
}
|
|
}
|
|
|
|
hasChild = (_: number, node: FlatNode) => node.expandable;
|
|
|
|
trackBy = (_: number, node: FlatNode) => node.data.deptInfo.seq;
|
|
|
|
onClickNode(event: Event, node: FlatNode) {
|
|
event.stopPropagation();
|
|
|
|
this.clickNode.emit(node.data.deptInfo);
|
|
}
|
|
|
|
expand(...deptSeq: number[]) {
|
|
if (!this.treeControl.dataNodes || !deptSeq || 0 === deptSeq.length) {
|
|
return;
|
|
}
|
|
const flatNodes: FlatNode[] = [];
|
|
|
|
deptSeq.forEach((s) => {
|
|
const node = this.treeControl.dataNodes.find(
|
|
(n) => n.data.deptInfo.seq === s
|
|
);
|
|
if (!!node) {
|
|
flatNodes.push(node);
|
|
this.selectHierarchy(flatNodes, node);
|
|
}
|
|
});
|
|
|
|
if (0 < flatNodes.length) {
|
|
this.treeControl.expansionModel.select(...flatNodes);
|
|
}
|
|
}
|
|
|
|
expandAll() {
|
|
this.treeControl.expandAll();
|
|
}
|
|
|
|
collapse(deptSeq: number) {
|
|
if (!this.treeControl.dataNodes) {
|
|
return;
|
|
}
|
|
const node = this.treeControl.dataNodes.find(
|
|
(n) => n.data.deptInfo.seq === deptSeq
|
|
);
|
|
if (!!node) {
|
|
this.treeControl.collapse(node);
|
|
}
|
|
}
|
|
|
|
collapseAll() {
|
|
this.treeControl.collapseAll();
|
|
}
|
|
|
|
private selectHierarchy(flatNodes: FlatNode[], flatNode: FlatNode): void {
|
|
// tslint:disable-next-line: variable-name
|
|
let _flatNode = flatNode;
|
|
while (true) {
|
|
if (!_flatNode) {
|
|
return;
|
|
}
|
|
const ancestorSeq = _flatNode.data.ancestorSeq;
|
|
if (!ancestorSeq) {
|
|
return;
|
|
}
|
|
_flatNode = this.treeControl.dataNodes.find(
|
|
(n) => n.data.deptInfo.seq === ancestorSeq
|
|
);
|
|
if (!_flatNode) {
|
|
return;
|
|
}
|
|
const i = flatNodes.findIndex(
|
|
(n) => n.data.deptInfo.seq === _flatNode.data.deptInfo.seq
|
|
);
|
|
if (-1 === i) {
|
|
flatNodes.push(_flatNode);
|
|
}
|
|
}
|
|
}
|
|
}
|