virtual scroll of organization is implemented

This commit is contained in:
병준 박 2019-11-15 10:13:18 +09:00
parent 241fd65d31
commit 50c8c2a02d
11 changed files with 355 additions and 215 deletions

View File

@ -33,10 +33,10 @@
.mat-tab-labels { .mat-tab-labels {
flex-flow: column; flex-flow: column;
height: 280px; height: 280px;
padding-top:10px; padding-top: 10px;
.mat-tab-label { .mat-tab-label {
height: 80px; height: 80px;
padding:0 16px; padding: 0 16px;
} }
} }
.mat-ink-bar { .mat-ink-bar {
@ -63,7 +63,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform: scale(0.9); transform: scale(0.9);
transition: transform .3s cubic-bezier(.4,0,0,1); transition: transform 0.3s cubic-bezier(0.4, 0, 0, 1);
svg { svg {
width: 24px; width: 24px;
@ -82,7 +82,7 @@
.mat-tab-label-content { .mat-tab-label-content {
.icon-item { .icon-item {
background: #ef4c73; background: #ef4c73;
transform: scale(1.0); transform: scale(1);
/*svg { /*svg {
stroke: #ef4c73; stroke: #ef4c73;
fill: #ef4c73; fill: #ef4c73;

View File

@ -71,7 +71,7 @@ export class GroupComponent implements OnInit, OnDestroy {
UserInfo | UserInfoSS | UserInfoF | UserInfoDN UserInfo | UserInfoSS | UserInfoF | UserInfoDN
>(); >();
@ViewChild('groupExpansionPanel', { static: true }) @ViewChild('groupExpansionPanel', { static: false })
groupExpansionPanel: GroupExpansionPanelComponent; groupExpansionPanel: GroupExpansionPanelComponent;
@ViewChild('profileContextMenuTrigger', { static: true }) @ViewChild('profileContextMenuTrigger', { static: true })

View File

@ -10,7 +10,7 @@
> >
</ucap-organization-tenant-search> </ucap-organization-tenant-search>
</div> </div>
<div class="oraganization-tab" *ngIf="departmentInfoList$ | async"> <div class="oraganization-tab">
<div class="oraganization-tab-tree"> <div class="oraganization-tab-tree">
<ucap-organization-tree <ucap-organization-tree
[oraganizationList]="departmentInfoList$ | async" [oraganizationList]="departmentInfoList$ | async"

View File

@ -6,7 +6,7 @@ import {
ElementRef, ElementRef,
AfterViewInit, AfterViewInit,
Output, Output,
EventEmitter, EventEmitter
} from '@angular/core'; } from '@angular/core';
import { import {
ucapAnimations, ucapAnimations,
@ -20,7 +20,7 @@ import {
AlertDialogData, AlertDialogData,
AlertDialogResult, AlertDialogResult,
FileUploadQueueComponent, FileUploadQueueComponent,
StringUtil, StringUtil
} from '@ucap-webmessenger/ui'; } from '@ucap-webmessenger/ui';
import { Store, select } from '@ngrx/store'; import { Store, select } from '@ngrx/store';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
@ -33,7 +33,7 @@ import {
isRecallable, isRecallable,
InfoResponse, InfoResponse,
EventJson, EventJson,
FileEventJson, FileEventJson
} from '@ucap-webmessenger/protocol-event'; } from '@ucap-webmessenger/protocol-event';
import * as AppStore from '@app/store'; import * as AppStore from '@app/store';
@ -47,36 +47,36 @@ import {
EnvironmentsInfo, EnvironmentsInfo,
KEY_ENVIRONMENTS_INFO, KEY_ENVIRONMENTS_INFO,
UserSelectDialogType, UserSelectDialogType,
RightDrawer, RightDrawer
} from '@app/types'; } from '@app/types';
import { RoomInfo, UserInfo, RoomType } from '@ucap-webmessenger/protocol-room'; import { RoomInfo, UserInfo, RoomType } from '@ucap-webmessenger/protocol-room';
import { tap, take, map, catchError } from 'rxjs/operators'; import { tap, take, map, catchError } from 'rxjs/operators';
import { import {
FileInfo, FileInfo,
FormComponent as UCapUiChatFormComponent, FormComponent as UCapUiChatFormComponent
} from '@ucap-webmessenger/ui-chat'; } from '@ucap-webmessenger/ui-chat';
import { KEY_VER_INFO } from '@app/types/ver-info.type'; import { KEY_VER_INFO } from '@app/types/ver-info.type';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public'; import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { import {
MatMenuTrigger, MatMenuTrigger,
MatSnackBarRef, MatSnackBarRef,
SimpleSnackBar, SimpleSnackBar
} from '@angular/material'; } from '@angular/material';
import { import {
CommonApiService, CommonApiService,
FileUploadItem, FileUploadItem,
FileTalkSaveRequest, FileTalkSaveRequest,
FileTalkSaveResponse, FileTalkSaveResponse
} from '@ucap-webmessenger/api-common'; } from '@ucap-webmessenger/api-common';
import { import {
CreateChatDialogComponent, CreateChatDialogComponent,
CreateChatDialogData, CreateChatDialogData,
CreateChatDialogResult, CreateChatDialogResult
} from '../dialogs/chat/create-chat.dialog.component'; } from '../dialogs/chat/create-chat.dialog.component';
import { import {
FileViewerDialogComponent, FileViewerDialogComponent,
FileViewerDialogData, FileViewerDialogData,
FileViewerDialogResult, FileViewerDialogResult
} from '@app/layouts/common/dialogs/file-viewer.dialog.component'; } from '@app/layouts/common/dialogs/file-viewer.dialog.component';
import { CONST, FileUtil } from '@ucap-webmessenger/core'; import { CONST, FileUtil } from '@ucap-webmessenger/core';
import { PerfectScrollbarComponent } from 'ngx-perfect-scrollbar'; import { PerfectScrollbarComponent } from 'ngx-perfect-scrollbar';
@ -84,12 +84,12 @@ import { StatusCode } from '@ucap-webmessenger/api';
import { import {
EditChatRoomDialogComponent, EditChatRoomDialogComponent,
EditChatRoomDialogResult, EditChatRoomDialogResult,
EditChatRoomDialogData, EditChatRoomDialogData
} from '../dialogs/chat/edit-chat-room.dialog.component'; } from '../dialogs/chat/edit-chat-room.dialog.component';
import { import {
SelectGroupDialogComponent, SelectGroupDialogComponent,
SelectGroupDialogResult, SelectGroupDialogResult,
SelectGroupDialogData, SelectGroupDialogData
} from '../dialogs/group/select-group.dialog.component'; } from '../dialogs/group/select-group.dialog.component';
import { GroupDetailData } from '@ucap-webmessenger/protocol-sync'; import { GroupDetailData } from '@ucap-webmessenger/protocol-sync';
@ -97,7 +97,7 @@ import { GroupDetailData } from '@ucap-webmessenger/protocol-sync';
selector: 'app-layout-messenger-messages', selector: 'app-layout-messenger-messages',
templateUrl: './messages.component.html', templateUrl: './messages.component.html',
styleUrls: ['./messages.component.scss'], styleUrls: ['./messages.component.scss'],
animations: ucapAnimations, animations: ucapAnimations
}) })
export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit { export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() @Output()
@ -379,7 +379,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
// duration: 3000, // duration: 3000,
verticalPosition: 'bottom', verticalPosition: 'bottom',
horizontalPosition: 'center', horizontalPosition: 'center',
panelClass: ['chat-snackbar-class'], panelClass: ['chat-snackbar-class']
}); });
this.snackBarPreviewEvent.onAction().subscribe(() => { this.snackBarPreviewEvent.onAction().subscribe(() => {
this.setEventMoreInit(); this.setEventMoreInit();
@ -420,7 +420,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
EventStore.info({ EventStore.info({
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq,
baseSeq: seq, baseSeq: seq,
requestCount: CONST.EVENT_INFO_READ_COUNT, requestCount: CONST.EVENT_INFO_READ_COUNT
}) })
); );
} }
@ -438,8 +438,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
width: '360px', width: '360px',
data: { data: {
title: 'Alert', title: 'Alert',
message: `대화내용을 입력해주세요.`, message: `대화내용을 입력해주세요.`
}, }
}); });
return; return;
} }
@ -453,8 +453,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq,
eventType: EventType.MassText, eventType: EventType.MassText,
// sentMessage: message.replace(/\n/gi, '\r\n') // sentMessage: message.replace(/\n/gi, '\r\n')
sentMessage: message, sentMessage: message
}, }
}) })
); );
} else { } else {
@ -464,8 +464,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
req: { req: {
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq,
eventType: EventType.Character, eventType: EventType.Character,
sentMessage: message, sentMessage: message
}, }
}) })
); );
} }
@ -479,7 +479,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
onMassDetail(value: number) { onMassDetail(value: number) {
this.store.dispatch( this.store.dispatch(
ChatStore.selectedMassDetail({ ChatStore.selectedMassDetail({
massEventSeq: value, massEventSeq: value
}) })
); );
} }
@ -491,7 +491,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
FileViewerDialogResult FileViewerDialogResult
>(FileViewerDialogComponent, { >(FileViewerDialogComponent, {
position: { position: {
top: '30px', top: '30px'
}, },
maxWidth: '100vw', maxWidth: '100vw',
maxHeight: '100vh', maxHeight: '100vh',
@ -504,8 +504,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
downloadUrl: this.sessionVerInfo.downloadUrl, downloadUrl: this.sessionVerInfo.downloadUrl,
deviceType: this.environmentsInfo.deviceType, deviceType: this.environmentsInfo.deviceType,
token: this.loginRes.tokenString, token: this.loginRes.tokenString,
userSeq: this.loginRes.userSeq, userSeq: this.loginRes.userSeq
}, }
}); });
} }
@ -531,7 +531,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
const info = { const info = {
senderSeq: this.loginRes.userSeq, senderSeq: this.loginRes.userSeq,
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq
}; };
const allObservables: Observable<FileTalkSaveResponse>[] = []; const allObservables: Observable<FileTalkSaveResponse>[] = [];
@ -552,7 +552,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
'rv', 'rv',
'ts', 'ts',
'webm', 'webm',
'wmv', 'wmv'
].indexOf(FileUtil.getExtension(fileUploadItem.file.name)) ].indexOf(FileUtil.getExtension(fileUploadItem.file.name))
) { ) {
thumbnail = await FileUtil.thumbnail(fileUploadItem.file); thumbnail = await FileUtil.thumbnail(fileUploadItem.file);
@ -567,7 +567,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
file: fileUploadItem.file, file: fileUploadItem.file,
fileName: fileUploadItem.file.name, fileName: fileUploadItem.file.name,
thumb: thumbnail, thumb: thumbnail,
fileUploadItem, fileUploadItem
}; };
allObservables.push( allObservables.push(
@ -599,8 +599,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
req: { req: {
roomSeq: info.roomSeq, roomSeq: info.roomSeq,
eventType: EventType.File, eventType: EventType.File,
sentMessage: res.returnJson, sentMessage: res.returnJson
}, }
}) })
); );
} }
@ -626,7 +626,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.messageContextMenuTrigger.menu.focusFirstItem('mouse'); this.messageContextMenuTrigger.menu.focusFirstItem('mouse');
this.messageContextMenuTrigger.menuData = { this.messageContextMenuTrigger.menuData = {
message: params.message, message: params.message,
loginRes: this.loginRes, loginRes: this.loginRes
}; };
this.messageContextMenuTrigger.openMenu(); this.messageContextMenuTrigger.openMenu();
} }
@ -646,7 +646,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.snackBarService.open('클립보드에 복사되었습니다.', '', { this.snackBarService.open('클립보드에 복사되었습니다.', '', {
duration: 3000, duration: 3000,
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'center', horizontalPosition: 'center'
}); });
} }
} }
@ -658,7 +658,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
userSeq: this.loginRes.userSeq, userSeq: this.loginRes.userSeq,
deviceType: this.environmentsInfo.deviceType, deviceType: this.environmentsInfo.deviceType,
token: this.loginRes.tokenString, token: this.loginRes.tokenString,
eventMassSeq: message.seq, eventMassSeq: message.seq
}) })
.pipe(take(1)) .pipe(take(1))
.subscribe(res => { .subscribe(res => {
@ -669,7 +669,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
{ {
duration: 3000, duration: 3000,
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'center', horizontalPosition: 'center'
} }
); );
} }
@ -693,8 +693,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
data: { data: {
type: UserSelectDialogType.MessageForward, type: UserSelectDialogType.MessageForward,
title: 'MessageForward', title: 'MessageForward',
ignoreRoom: [this.roomInfo], ignoreRoom: [this.roomInfo]
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
@ -718,10 +718,10 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
req: { req: {
roomSeq: '-999', roomSeq: '-999',
eventType: message.type, eventType: message.type,
sentMessage: message.sentMessage, sentMessage: message.sentMessage
}, },
trgtUserSeqs: userSeqs, trgtUserSeqs: userSeqs,
trgtRoomSeq: roomSeq, trgtRoomSeq: roomSeq
}) })
); );
} }
@ -737,9 +737,9 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
req: { req: {
roomSeq: '-999', roomSeq: '-999',
eventType: message.type, eventType: message.type,
sentMessage: message.sentMessage, sentMessage: message.sentMessage
}, },
trgtUserSeqs: [this.loginRes.talkWithMeBotSeq], trgtUserSeqs: [this.loginRes.talkWithMeBotSeq]
}) })
); );
} }
@ -755,15 +755,15 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
width: '220px', width: '220px',
data: { data: {
title: 'Delete', title: 'Delete',
html: `선택한 메시지를 삭제하시겠습니까?<br/>삭제된 메시지는 내 대화방에서만 적용되며 상대방의 대화방에서는 삭제되지 않습니다.`, html: `선택한 메시지를 삭제하시겠습니까?<br/>삭제된 메시지는 내 대화방에서만 적용되며 상대방의 대화방에서는 삭제되지 않습니다.`
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
this.store.dispatch( this.store.dispatch(
EventStore.del({ EventStore.del({
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq,
eventSeq: message.seq, eventSeq: message.seq
}) })
); );
} }
@ -779,8 +779,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
width: '220px', width: '220px',
data: { data: {
title: 'ReCall', title: 'ReCall',
html: `해당 대화를 회수하시겠습니까?<br/>상대방 대화창에서도 회수됩니다.`, html: `해당 대화를 회수하시겠습니까?<br/>상대방 대화창에서도 회수됩니다.`
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
@ -788,7 +788,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
EventStore.cancel({ EventStore.cancel({
roomSeq: this.roomInfo.roomSeq, roomSeq: this.roomInfo.roomSeq,
eventSeq: message.seq, eventSeq: message.seq,
deviceType: this.environmentsInfo.deviceType, deviceType: this.environmentsInfo.deviceType
}) })
); );
} }
@ -805,7 +805,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
{ {
this.store.dispatch( this.store.dispatch(
ChatStore.selectedRightDrawer({ ChatStore.selectedRightDrawer({
req: RightDrawer.AlbumBox, req: RightDrawer.AlbumBox
}) })
); );
} }
@ -814,7 +814,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
{ {
this.store.dispatch( this.store.dispatch(
ChatStore.selectedRightDrawer({ ChatStore.selectedRightDrawer({
req: RightDrawer.FileBox, req: RightDrawer.FileBox
}) })
); );
} }
@ -823,7 +823,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
{ {
this.store.dispatch( this.store.dispatch(
ChatStore.selectedRightDrawer({ ChatStore.selectedRightDrawer({
req: RightDrawer.RoomUser, req: RightDrawer.RoomUser
}) })
); );
} }
@ -841,8 +841,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
title: 'Edit Chat Member', title: 'Edit Chat Member',
curRoomUser: this.userInfoList.filter( curRoomUser: this.userInfoList.filter(
user => user.seq !== this.loginRes.userSeq user => user.seq !== this.loginRes.userSeq
), )
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
@ -864,8 +864,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
RoomStore.inviteOrOpen({ RoomStore.inviteOrOpen({
req: { req: {
divCd: 'Invite', divCd: 'Invite',
userSeqs, userSeqs
}, }
}) })
); );
} }
@ -881,8 +881,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
>(SelectGroupDialogComponent, { >(SelectGroupDialogComponent, {
width: '600px', width: '600px',
data: { data: {
title: 'Group Select', title: 'Group Select'
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
@ -899,7 +899,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.store.dispatch( this.store.dispatch(
SyncStore.updateGroupMember({ SyncStore.updateGroupMember({
oldGroup, oldGroup,
trgtUserSeq, trgtUserSeq
}) })
); );
} }
@ -916,8 +916,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
width: '600px', width: '600px',
data: { data: {
title: 'Edit Chat Room', title: 'Edit Chat Room',
roomInfo: this.roomInfo, roomInfo: this.roomInfo
}, }
}); });
if (!!result && !!result.choice && result.choice) { if (!!result && !!result.choice && result.choice) {
@ -934,8 +934,8 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
roomName, roomName,
receiveAlarm: roomInfo.receiveAlarm, receiveAlarm: roomInfo.receiveAlarm,
syncAll: syncAll:
roomNameChangeTarget.toUpperCase() === 'ALL' ? true : false, roomNameChangeTarget.toUpperCase() === 'ALL' ? true : false
}, }
}) })
); );
@ -947,7 +947,7 @@ export class MessagesComponent implements OnInit, OnDestroy, AfterViewInit {
this.store.dispatch( this.store.dispatch(
RoomStore.updateTimeRoomInterval({ RoomStore.updateTimeRoomInterval({
roomSeq: roomInfo.roomSeq, roomSeq: roomInfo.roomSeq,
timerInterval: timeRoomInterval, timerInterval: timeRoomInterval
}) })
); );
} }

View File

@ -1,4 +1,4 @@
<div class="chat-messages"> <div class="chat-messages" #scrollMe>
<!-- <div class="message-row" *ngIf="eventRemain"> <!-- <div class="message-row" *ngIf="eventRemain">
<button mat-button (click)="onClickMore($event)">이전 대화 보기</button> <button mat-button (click)="onClickMore($event)">이전 대화 보기</button>
</div> --> </div> -->

View File

@ -1,41 +1,54 @@
<mat-tree <cdk-virtual-scroll-viewport #cvsvOrganization itemSize="48" fxFlexFill>
#orgranizationTree <ng-container
[dataSource]="dataSource" *cdkVirtualFor="let node of dataSource.expandedData$"
[treeControl]="treeControl" ></ng-container>
class="organization-tree" <mat-tree
> #orgranizationTree
<mat-nested-tree-node *matTreeNodeDef="let node"> [dataSource]="dataSource"
<li> [treeControl]="treeControl"
<div class="mat-tree-node" (click)="onClickNode(node)"> class="organization-tree"
{{ node.title }} >
</div> <!-- This is the tree node template for leaf nodes -->
</li> <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
</mat-nested-tree-node> <li>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChildren" class="tree-node-frame"> <div class="mat-tree-node" (click)="onClickNode(node)">
<li> {{ node.name }}
<div class="mat-tree-node" (click)="onClickNode(node)" class="path">
<span class="horizontal-line"></span>
<button
mat-icon-button
color="accent"
matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.filename"
>
<mat-icon class="mat-icon-rtl-mirror">
<!-- {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}-->
{{ treeControl.isExpanded(node) ? 'remove' : 'add' }}
</mat-icon>
</button>
<span class="dept-name">{{ node.title }}</span>
</div>
<ul
[class.organization-tree-node-invisible]="!treeControl.isExpanded(node)"
>
<div *ngIf="treeControl.isExpanded(node)" class="boxnone">
<div class="vertical-line"></div>
<ng-container matTreeNodeOutlet></ng-container>
</div> </div>
</ul> </li>
</li> </mat-tree-node>
</mat-nested-tree-node> <!-- This is the tree node template for expandable nodes -->
</mat-tree> <mat-tree-node
*matTreeNodeDef="let node; when: hasChild"
matTreeNodePadding
class="tree-node-frame"
>
<li>
<div class="mat-tree-node" (click)="onClickNode(node)" class="path">
<span class="horizontal-line"></span>
<button
mat-icon-button
color="accent"
matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.filename"
>
<mat-icon class="mat-icon-rtl-mirror">
<!-- {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}-->
{{ treeControl.isExpanded(node) ? 'remove' : 'add' }}
</mat-icon>
</button>
<span class="dept-name">{{ node.name }}</span>
</div>
<ul
[class.organization-tree-node-invisible]="
!treeControl.isExpanded(node)
"
>
<div *ngIf="treeControl.isExpanded(node)" class="boxnone">
<div class="vertical-line"></div>
<ng-container matTreeNodeOutlet></ng-container>
</div>
</ul>
</li>
</mat-tree-node>
</mat-tree>
</cdk-virtual-scroll-viewport>

View File

@ -1,6 +1,6 @@
@charset 'utf-8'; @charset 'utf-8';
.organization-tree { .organization-tree {
padding:10px; padding: 10px;
ul, ul,
li { li {
margin-top: 0; margin-top: 0;
@ -13,83 +13,83 @@
} }
} }
.tree-node-frame{ .tree-node-frame {
li{ li {
.path{ .path {
.horizontal-line{ .horizontal-line {
display:none; display: none;
} }
} }
} }
.mat-tree-node { .mat-tree-node {
min-height: 30px; min-height: 30px;
font-size: 13px; font-size: 13px;
padding-left:20px; padding-left: 20px;
margin-top:4px; margin-top: 4px;
&:hover { &:hover {
background-color: #f4f4f4; background-color: #f4f4f4;
border:1px solid #cccccc; border: 1px solid #cccccc;
border-radius:4px; border-radius: 4px;
box-shadow: 0 1px 4px rgba(32, 33, 36, 0.1); box-shadow: 0 1px 4px rgba(32, 33, 36, 0.1);
} }
} }
} }
ul .tree-node-frame li .path > .horizontal-line{ ul .tree-node-frame li .path > .horizontal-line {
display:inline-block; display: inline-block;
} }
.boxnone{ .boxnone {
position:relative; position: relative;
.vertical-line{ .vertical-line {
background: rgba(189,189,189,.4); background: rgba(189, 189, 189, 0.4);
bottom: 6px; bottom: 6px;
display: block; display: block;
position: absolute; position: absolute;
top: 0px; top: 0px;
width: 2px; width: 2px;
} }
.mat-nested-tree-node:last-child{ .mat-tree-node:last-child {
padding-bottom:10px; padding-bottom: 10px;
} }
} }
.path { .path {
padding: 6px 4px; padding: 6px 4px;
+ ul{ + ul {
li:last-chlid{ li:last-chlid {
border-bottom:1px solid #dddddd; border-bottom: 1px solid #dddddd;
} }
} }
.horizontal-line{ .horizontal-line {
width:10px; width: 10px;
height:1px; height: 1px;
background-color: #dddddd; background-color: #dddddd;
display:inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin-left:-10px; margin-left: -10px;
} }
.mat-icon-button{ .mat-icon-button {
padding: 0; padding: 0;
min-width: 0; min-width: 0;
width: 20px; width: 20px;
height: 20px; height: 20px;
flex-shrink: 0; flex-shrink: 0;
line-height: 20px; line-height: 20px;
.mat-icon-rtl-mirror{ .mat-icon-rtl-mirror {
border: 1px solid #dddddd; border: 1px solid #dddddd;
padding: 2px; padding: 2px;
font-size: 14px; font-size: 14px;
min-width: 14px; min-width: 14px;
min-height: 14px; min-height: 14px;
line-height: 14px; line-height: 14px;
width:20px; width: 20px;
height:20px; height: 20px;
box-shadow: 0 2px 1px rgba(48, 48, 48, 0.2); box-shadow: 0 2px 1px rgba(48, 48, 48, 0.2);
border-radius: 50%; border-radius: 50%;
} }
} }
.dept-name{ .dept-name {
padding-left:10px; padding-left: 10px;
} }
} }

View File

@ -8,39 +8,26 @@ import {
EventEmitter, EventEmitter,
AfterViewInit AfterViewInit
} from '@angular/core'; } from '@angular/core';
import { MatTreeNestedDataSource, MatTree } from '@angular/material'; import { MatTreeFlattener, MatTree } from '@angular/material';
import { NestedTreeControl } from '@angular/cdk/tree';
import { BehaviorSubject } from 'rxjs';
import { NGXLogger } from 'ngx-logger'; import { NGXLogger } from 'ngx-logger';
import { DeptInfo } from '@ucap-webmessenger/protocol-query'; import { DeptInfo } from '@ucap-webmessenger/protocol-query';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication'; import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { FlatTreeControl } from '@angular/cdk/tree';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { VirtualScrollTreeFlatDataSource } from '@ucap-webmessenger/ui';
export class OraganizationNode { interface OrganizationNode {
private childNodeBehaviorSubject: BehaviorSubject<OraganizationNode[]>; deptInfo: DeptInfo;
private childNodeList: OraganizationNode[]; name: string;
children?: OrganizationNode[];
}
get title(): string { /** Flat node with expandable and level information */
return this.deptInfo.name; interface FlatNode {
} expandable: boolean;
name: string;
get children(): BehaviorSubject<OraganizationNode[]> { level: number;
if (!this.childNodeBehaviorSubject) {
this.childNodeBehaviorSubject = new BehaviorSubject(
undefined === this.childNodeList ? [] : this.childNodeList
);
}
return this.childNodeBehaviorSubject;
}
constructor(public deptInfo: DeptInfo) {}
addChild(childNode: OraganizationNode) {
if (!this.childNodeList) {
this.childNodeList = [];
}
this.childNodeList.push(childNode);
}
} }
@Component({ @Component({
@ -55,87 +42,96 @@ export class TreeComponent implements OnInit, AfterViewInit {
@Input() @Input()
loginRes: LoginResponse; loginRes: LoginResponse;
@Input() @Input()
set oraganizationList(deptInfo: DeptInfo[]) { set oraganizationList(deptInfoList: DeptInfo[]) {
const nodeMap = new Map<number, OraganizationNode>(); if (!deptInfoList || 0 === deptInfoList.length) {
const rootNodeList: OraganizationNode[] = []; return;
const remainChildNodeList: OraganizationNode[] = []; }
let myNode: OraganizationNode; const nodeMap = new Map<number, OrganizationNode>();
const rootNodeList: OrganizationNode[] = [];
const remainChildNodeList: OrganizationNode[] = [];
let myNode: OrganizationNode;
deptInfo.forEach(value => { deptInfoList.forEach(deptInfo => {
const node = new OraganizationNode(value); const node: OrganizationNode = {
if (nodeMap.has(value.seq)) { deptInfo,
this.logger.warn('duplicate seq', value.seq); name: deptInfo.name,
children: []
};
if (nodeMap.has(deptInfo.seq)) {
this.logger.warn('duplicate seq', deptInfo.seq);
return; return;
} }
nodeMap.set(value.seq, node); nodeMap.set(deptInfo.seq, node);
if (value.seq === this.loginRes.departmentCode) { if (deptInfo.seq === this.loginRes.departmentCode) {
myNode = node; myNode = node;
} }
if (0 === value.parentSeq) { if (0 === deptInfo.parentSeq) {
rootNodeList.push(node); rootNodeList.push(node);
return; return;
} }
if (nodeMap.has(value.parentSeq)) { if (nodeMap.has(deptInfo.parentSeq)) {
nodeMap.get(value.parentSeq).addChild(node); nodeMap.get(deptInfo.parentSeq).children.push(node);
} else { } else {
remainChildNodeList.push(node); remainChildNodeList.push(node);
} }
}); });
remainChildNodeList.forEach(value => { remainChildNodeList.forEach(node => {
if (nodeMap.has(value.deptInfo.parentSeq)) { if (nodeMap.has(node.deptInfo.parentSeq)) {
nodeMap.get(value.deptInfo.parentSeq).addChild(value); nodeMap.get(node.deptInfo.parentSeq).children.push(node);
} }
}); });
this.dataSource.data = rootNodeList; this.dataSource.data = rootNodeList;
// console.log('myNode', myNode);
// console.log('this.dataSource.data', this.dataSource.data[0]);
// this.treeControl.expandDescendants(this.dataSource.data[2]);
// console.log('this.dataSource', this.dataSource);
// console.log('this.dataSource.data', this.dataSource.data);
// console.log('this.treeControl', this.treeControl);
// console.log('this.treeControl.dataNodes', this.treeControl.dataNodes);
// const myNode = this.treeControl.dataNodes.filter(
// node => node.deptInfo.seq === this.loginRes.departmentCode
// );
// if (!!myNode && myNode.length > 0) {
// this.treeControl.expand(myNode[0]);
// }
} }
@ViewChild('orgranizationTree', { static: true }) @ViewChild('cvsvOrganization', { static: false })
orgranizationTree: MatTree<OraganizationNode>; cvsvOrganization: CdkVirtualScrollViewport;
levels = new Map<OraganizationNode, number>(); @ViewChild('orgranizationTree', { static: false })
treeControl: NestedTreeControl<OraganizationNode>; orgranizationTree: MatTree<OrganizationNode>;
dataSource: MatTreeNestedDataSource<OraganizationNode>; treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<OrganizationNode, FlatNode>;
dataSource: VirtualScrollTreeFlatDataSource<OrganizationNode, FlatNode>;
constructor( constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private logger: NGXLogger private logger: NGXLogger
) { ) {
this.treeControl = new NestedTreeControl<OraganizationNode>( this.treeControl = new FlatTreeControl<FlatNode>(
this.getChildren node => node.level,
node => node.expandable
); );
this.dataSource = new MatTreeNestedDataSource(); this.treeFlattener = new MatTreeFlattener<OrganizationNode, FlatNode>(
(node: OrganizationNode, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
name: node.name,
level
};
},
node => node.level,
node => node.expandable,
node => node.children
);
this.dataSource = new VirtualScrollTreeFlatDataSource<
OrganizationNode,
FlatNode
>(this.treeControl, this.treeFlattener);
} }
ngOnInit() {} ngOnInit() {}
ngAfterViewInit(): void {} ngAfterViewInit(): void {
this.dataSource.cdkVirtualScrollViewport = this.cvsvOrganization;
}
getChildren = (node: OraganizationNode) => node.children; hasChild = (_: number, node: FlatNode) => node.expandable;
hasChildren = (index: number, node: OraganizationNode) => onClickNode(node: OrganizationNode) {
0 < node.children.value.length;
onClickNode(node: OraganizationNode) {
this.selected.emit(node.deptInfo); this.selected.emit(node.deptInfo);
} }
} }

View File

@ -2,6 +2,8 @@ import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ScrollingModule } from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -21,6 +23,7 @@ const SERVICES = [];
CommonModule, CommonModule,
ReactiveFormsModule, ReactiveFormsModule,
FlexLayoutModule,
ScrollingModule, ScrollingModule,
MatButtonModule, MatButtonModule,

View File

@ -0,0 +1,126 @@
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { FlatTreeControl } from '@angular/cdk/tree';
import {
BehaviorSubject,
merge,
Observable,
Subject,
Subscription
} from 'rxjs';
import { map, share } from 'rxjs/operators';
import { MatTreeFlattener } from '@angular/material';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
export class VirtualScrollTreeFlatDataSource<T, F> extends DataSource<F> {
private flattenedDataSubject = new BehaviorSubject<F[]>([]);
private expandedDataSubject = new BehaviorSubject<F[]>([]);
expandedData$: Observable<F[]>;
private connectSubject: Subject<F[]>;
private dataChangeSubscription: Subscription;
private rangeChangeSubscription: Subscription;
private rangeSubject: BehaviorSubject<{
start: number;
end: number;
}>;
// tslint:disable-next-line: variable-name
private _cdkVirtualScrollViewport: CdkVirtualScrollViewport;
set cdkVirtualScrollViewport(
cdkVirtualScrollViewport: CdkVirtualScrollViewport
) {
if (!cdkVirtualScrollViewport) {
return;
}
this._cdkVirtualScrollViewport = cdkVirtualScrollViewport;
this.rangeSubject = new BehaviorSubject<{ start: number; end: number }>({
start: 0,
end: 1
});
this.rangeChangeSubscription = cdkVirtualScrollViewport.renderedRangeStream.subscribe(
range => {
this.rangeSubject.next({
start: range.start,
end: range.end
});
if (!!this.connectSubject) {
this.connectSubject.next(
this.expandedDataSubject.value.slice(
this.rangeSubject.value.start,
this.rangeSubject.value.end
)
);
}
}
);
}
// tslint:disable-next-line: variable-name
private _data: BehaviorSubject<T[]>;
get data() {
return this._data.value;
}
set data(value: T[]) {
this._data.next(value);
this.flattenedDataSubject.next(this.treeFlattener.flattenNodes(this.data));
this.treeControl.dataNodes = this.flattenedDataSubject.value;
}
constructor(
private treeControl: FlatTreeControl<F>,
private treeFlattener: MatTreeFlattener<T, F>,
initialData: T[] = []
) {
super();
this._data = new BehaviorSubject<T[]>(initialData);
this.expandedData$ = this.expandedDataSubject.asObservable().pipe(share());
}
connect(collectionViewer: CollectionViewer): Observable<F[]> {
this.connectSubject = new Subject<F[]>();
this.dataChangeSubscription = merge(
collectionViewer.viewChange,
this.treeControl.expansionModel.changed,
this.flattenedDataSubject
)
.pipe(
map(() => {
this.expandedDataSubject.next(
this.treeFlattener.expandFlattenedNodes(
this.flattenedDataSubject.value,
this.treeControl
)
);
return !this.rangeSubject
? this.expandedDataSubject.value
: this.expandedDataSubject.value.slice(
this.rangeSubject.value.start,
this.rangeSubject.value.end
);
})
)
.subscribe(datas => {
this.connectSubject.next(datas);
if (!!this._cdkVirtualScrollViewport) {
this._cdkVirtualScrollViewport.checkViewportSize();
}
});
return this.connectSubject.asObservable();
}
disconnect() {
if (!!this.dataChangeSubscription) {
this.dataChangeSubscription.unsubscribe();
}
if (!!this.rangeChangeSubscription) {
this.rangeChangeSubscription.unsubscribe();
}
}
}

View File

@ -13,6 +13,8 @@ export * from './lib/components/file-upload-queue.component';
export * from './lib/components/file-viewer.component'; export * from './lib/components/file-viewer.component';
export * from './lib/components/float-action-button.component'; export * from './lib/components/float-action-button.component';
export * from './lib/data-source/virtual-scroll-tree-flat.data-source';
export * from './lib/dialogs/alert.dialog.component'; export * from './lib/dialogs/alert.dialog.component';
export * from './lib/dialogs/confirm.dialog.component'; export * from './lib/dialogs/confirm.dialog.component';