통합검색에 UX 적용.

This commit is contained in:
leejinho 2020-03-06 16:42:14 +09:00
parent 3cfa9a39ac
commit 05a148d30a
9 changed files with 448 additions and 131 deletions

View File

@ -152,7 +152,7 @@ export class OrganizationTreeComponent implements OnInit, OnDestroy {
}
/** 검색 취소 */
onClickCancel() {
this.store.dispatch(QueryStore.cancelSearchDeptUser({}));
this.store.dispatch(QueryStore.clearSearchDeptUser({}));
}
/** 조직도 부서 선택 */

View File

@ -8,7 +8,7 @@
></div>
<div class="organization-info">
<h3 class="organization-name">
<ng-container *ngIf="!(isSearch | async)">
<ng-container *ngIf="!isSearch">
<ng-container
*ngIf="
!!(selectedDepartmentProcessing$ | async);
@ -21,7 +21,7 @@
{{ selectedDepartment$ | async | ucapTranslate: 'name' }}
</ng-template>
</ng-container>
<ng-container *ngIf="!!(isSearch | async)">
<ng-container *ngIf="!!isSearch">
{{ 'common.searchResult' | translate
}}<span class="text-accent-color"
>({{ departmentUserInfoList.length }}

View File

@ -259,7 +259,6 @@ export class OrganizationComponent implements OnInit, OnDestroy {
}
/** Handling chipset for selectedUserList */
/** 선택된 사용자 취소 */
onClickDeleteUser(userInfo: UserInfoSS) {
this.selectedUserList = this.selectedUserList.filter(
item => item.seq !== userInfo.seq

View File

@ -12,20 +12,127 @@
</button>
</mat-card-header>
<mat-card-content>
<ucap-integrated-search
#integratedSearch
[sessionVerinfo]="sessionVerinfo"
[presence]="presence$ | async"
[searchWord]="!!data.keyword ? data.keyword : ''"
[searchingProcessing]="searchingProcessing"
[searchUserInfos]="searchUserInfos"
[totalCount]="totalCount"
[pageCurrent]="pageCurrent"
[pageListCount]="pageListCount"
(search)="onReSearch($event)"
(changePage)="onChangePage($event)"
(openProfile)="onClickOpenProfile($event)"
<div fxLayout="column" fxFlex="1 1 auto" class="rightDrawer-notice">
<div class="search-area">
<ucap-integrated-search-form
[searchWord]="!!currentSearchWord ? currentSearchWord : ''"
(search)="onSearch($event)"
>
</ucap-integrated-search>
</ucap-integrated-search-form>
</div>
<div style="position: relative;">
<div
*ngIf="searchingProcessing$ | async"
style="position: absolute; width: 100%; z-index: 101;"
>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
</div>
<div class="contents-table">
<div fxFlex="0 0 auto" class="table-box">
<ucap-organization-detail-table
[loginRes]="loginRes"
[presence$]="presenceSubject.asObservable()"
[departmentUserInfoList]="departmentUserInfoList"
[profileImageRoot]="profileImageRoot"
[selectedUserList]="selectedUserList"
(openProfile)="onClickOpenProfile($event)"
(toggleAllUser)="onToggleAllUser($event)"
(toggleUser)="onToggleUser($event)"
class="detail-table"
></ucap-organization-detail-table>
</div>
<div class="footer-fix search-result-footer">
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'organization.selectedUser' | translate }}
<span *ngIf="selectedUserList.length > 0">
({{ selectedUserList.length }})
{{ 'common.units.persons' | translate }}
</span>
</mat-panel-title>
<mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header>
<div class="list-chip">
<mat-chip-list aria-label="User selection">
<mat-chip
*ngFor="let userInfo of selectedUserList"
(removed)="onClickDeleteUser(userInfo)"
>
{{ userInfo.name }}
<mat-icon matChipRemove>clear</mat-icon>
</mat-chip>
</mat-chip-list>
</div>
<div class="btn-box">
<ul>
<li>
<button
mat-flat-button
[disabled]="
selectedUserList.length > 0 ? 'false' : 'true'
"
(click)="onClickAddGroup()"
class="mat-primary"
>
{{ 'organization.addToGroup' | translate }}
</button>
</li>
<li>
<button
mat-flat-button
[disabled]="
selectedUserList.length > 0 ? 'false' : 'true'
"
(click)="onClickChatOpen()"
class="mat-primary"
>
{{ 'organization.startChat' | translate }}
</button>
</li>
<li *ngIf="!!authInfo && authInfo.canVideoConference">
<button
mat-flat-button
[disabled]="
selectedUserList.length > 0 ? 'false' : 'true'
"
(click)="onClickConference()"
class="mat-primary"
>
{{ 'organization.startVideoConference' | translate }}
</button>
</li>
</ul>
</div>
</mat-expansion-panel>
</mat-accordion>
</div>
</div>
<div class="footer-fix search-result-footer">
<ul>
<li>
<button
mat-flat-button
(click)="onClickAddGroup()"
class="mat-primary"
>
검색 초기화 & 닫기
</button>
</li>
<li>
<button
mat-flat-button
(click)="onClickChatOpen()"
class="mat-primary"
>
접어두기
</button>
</li>
</ul>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@ -1,23 +1,25 @@
import { Component, OnInit, Inject, OnDestroy, ViewChild } from '@angular/core';
import {
Component,
OnInit,
Inject,
OnDestroy,
ChangeDetectorRef
} from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { KEY_VER_INFO } from '@app/types';
import { KEY_VER_INFO, MainMenu, KEY_AUTH_INFO } from '@app/types';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { Store, select } from '@ngrx/store';
import * as AppStore from '@app/store';
import * as StatusStore from '@app/store/messenger/status';
import {
UserInfoSS,
QueryProtocolService,
DeptSearchType,
SSVC_TYPE_QUERY_DEPT_USER_DATA,
DeptUserData,
SSVC_TYPE_QUERY_DEPT_USER_RES,
DeptUserResponse
} from '@ucap-webmessenger/protocol-query';
import * as QueryStore from '@app/store/messenger/query';
import * as ChatStore from '@app/store/messenger/chat';
import * as SettingsStore from '@app/store/messenger/settings';
import * as SyncStore from '@app/store/messenger/sync';
import { UserInfoSS, AuthResponse } from '@ucap-webmessenger/protocol-query';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { map, catchError, take, tap } from 'rxjs/operators';
import { Subscription, of, Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators';
import { Subscription, Observable, BehaviorSubject } from 'rxjs';
import { EnvironmentsInfo, KEY_ENVIRONMENTS_INFO } from '@app/types';
import { NGXLogger } from 'ngx-logger';
import { NativeService, UCAP_NATIVE_SERVICE } from '@ucap-webmessenger/native';
@ -25,16 +27,20 @@ import { environment } from '../../../../../environments/environment';
import { StatusBulkInfo } from '@ucap-webmessenger/protocol-status';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { DaesangProtocolService } from '@ucap-webmessenger/daesang';
import {
DialogService,
IntegratedSearchComponent
} from '@ucap-webmessenger/ui';
import { DialogService } from '@ucap-webmessenger/ui';
import {
ProfileDialogComponent,
ProfileDialogData,
ProfileDialogResult
} from '../profile/profile.dialog.component';
import { PageEvent } from '@angular/material/paginator';
import {
SelectGroupDialogComponent,
SelectGroupDialogData,
SelectGroupDialogResult
} from '../group/select-group.dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { GroupDetailData } from '@ucap-webmessenger/protocol-sync';
import { ConferenceService } from '@ucap-webmessenger/api-prompt';
export interface IntegratedSearchDialogData {
keyword: string;
@ -52,19 +58,21 @@ export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
loginResSubscription: Subscription;
sessionVerinfo: VersionInfo2Response;
environmentsInfo: EnvironmentsInfo;
authInfo: AuthResponse;
presence$: Observable<StatusBulkInfo[]>;
searchSubscription: Subscription;
searchUserInfos: UserInfoSS[] = [];
searchingProcessing = false;
searchDepartmentUserInfoListSubscription: Subscription;
searchingProcessing$: Observable<boolean>;
departmentUserInfoList: UserInfoSS[] = [];
originDepartmentUserInfoList: UserInfoSS[] = [];
selectedUserList: UserInfoSS[] = []; // selected user in departmentUserList detail
profileImageRoot: string;
presenceSubscription: Subscription;
presenceSubject = new BehaviorSubject<StatusBulkInfo[]>(undefined);
currentSearchWord: string;
totalCount = 0;
pageCurrent = 1;
pageListCount = 20;
@ViewChild('integratedSearch', { static: false })
integratedSearchComponent: IntegratedSearchComponent;
constructor(
public dialogRef: MatDialogRef<
@ -73,22 +81,30 @@ export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
>,
@Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService,
@Inject(MAT_DIALOG_DATA) public data: IntegratedSearchDialogData,
private queryProtocolService: QueryProtocolService,
private sessionStorageService: SessionStorageService,
private daesangProtocolService: DaesangProtocolService,
private dialogService: DialogService,
private translateService: TranslateService,
private conferenceService: ConferenceService,
private store: Store<any>,
private changeDetectorRef: ChangeDetectorRef,
private logger: NGXLogger
) {
this.environmentsInfo = this.sessionStorageService.get<EnvironmentsInfo>(
KEY_ENVIRONMENTS_INFO
);
this.authInfo = this.sessionStorageService.get<AuthResponse>(KEY_AUTH_INFO);
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
KEY_VER_INFO
);
}
ngOnInit() {
this.profileImageRoot = this.sessionVerinfo.profileRoot;
this.loginResSubscription = this.store
.pipe(
select(AppStore.AccountSelector.AuthenticationSelector.loginRes),
@ -98,26 +114,48 @@ export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
)
.subscribe();
this.onSearch(this.data.keyword);
this.searchDepartmentUserInfoListSubscription = this.store
.pipe(
select(
AppStore.MessengerSelector.QuerySelector
.integrateSearchDepartmentUserInfoList
)
)
.subscribe(list => {
this.departmentUserInfoList = list;
this.originDepartmentUserInfoList = list;
});
this.presence$ = this.store.pipe(
select(AppStore.MessengerSelector.StatusSelector.selectAllStatusBulkInfo)
this.searchingProcessing$ = this.store.pipe(
select(
AppStore.MessengerSelector.QuerySelector
.integrateSearchDepartmentProcessing
)
);
this.presenceSubscription = this.store
.pipe(
select(
AppStore.MessengerSelector.StatusSelector.selectAllStatusBulkInfo
)
)
.subscribe(presence => {
this.presenceSubject.next(presence);
});
this.onSearch(this.data.keyword);
}
ngOnDestroy(): void {
if (!!this.searchSubscription) {
this.searchSubscription.unsubscribe();
}
if (!!this.loginResSubscription) {
this.loginResSubscription.unsubscribe();
}
if (!!this.searchDepartmentUserInfoListSubscription) {
this.searchDepartmentUserInfoListSubscription.unsubscribe();
}
if (!!this.presenceSubscription) {
this.presenceSubscription.unsubscribe();
}
onReSearch(searchWord: string) {
this.pageCurrent = 1;
this.integratedSearchComponent.paginator.pageIndex = 0;
this.onSearch(searchWord);
}
onSearch(searchWord: string) {
@ -126,78 +164,122 @@ export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
}
if (searchWord.trim().length > 0) {
this.searchingProcessing = true;
const searchUserInfos: UserInfoSS[] = [];
this.searchSubscription = this.queryProtocolService
.deptUser({
divCd: 'INT_SRCH',
companyCode: this.loginRes.companyCode,
searchRange: DeptSearchType.All,
search: searchWord.trim(),
senderCompanyCode: this.loginRes.companyCode,
senderEmployeeType: this.loginRes.userInfo.employeeType,
pageCurrent: this.pageCurrent,
pageListCount: this.pageListCount
})
.pipe(
map(res => {
switch (res.SSVC_TYPE) {
case SSVC_TYPE_QUERY_DEPT_USER_DATA:
const userInfos = (res as DeptUserData).userInfos;
searchUserInfos.push(...userInfos);
break;
case SSVC_TYPE_QUERY_DEPT_USER_RES:
{
const response = res as DeptUserResponse;
// 검색 결과 처리.
this.searchUserInfos = searchUserInfos.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
);
this.totalCount = response.pageTotalCount;
this.pageCurrent = response.pageCurrent;
this.pageListCount = response.pageListCount;
this.searchingProcessing = false;
// 검색 결과에 따른 프레즌스 조회.
if (searchUserInfos.length > 0) {
this.store.dispatch(
StatusStore.bulkInfo({
divCd: 'INT_SRCH',
userSeqs: this.searchUserInfos.map(user => user.seq)
QueryStore.integrateSearchDeptUser({
companyCode: this.loginRes.companyCode,
search: searchWord.trim()
})
);
}
}
break;
}
}),
catchError(error => {
this.searchingProcessing = false;
return of(this.logger.error(error));
})
)
.subscribe();
} else {
// clear list.
this.searchingProcessing = false;
this.searchUserInfos = [];
this.store.dispatch(QueryStore.integrateClearSearchDeptUser({}));
}
}
onCancel(): void {
this.dialogRef.close({});
/** Selected User Handling */
onToggleAllUser(params: { isChecked: boolean; userInfos: UserInfoSS[] }) {
params.userInfos.forEach(userInfo => {
if (params.isChecked) {
if (
this.selectedUserList.filter(user => user.seq === userInfo.seq)
.length === 0
) {
this.selectedUserList = [...this.selectedUserList, userInfo];
}
} else {
this.selectedUserList = this.selectedUserList.filter(
user => user.seq !== userInfo.seq
);
}
});
}
onToggleUser(userInfo: UserInfoSS) {
if (userInfo.seq === this.loginRes.userSeq) {
return;
}
onChangePage(event: PageEvent) {
this.pageCurrent = event.pageIndex + 1;
this.pageListCount = event.pageSize;
this.onSearch(this.currentSearchWord);
if (
this.selectedUserList.filter(user => user.seq === userInfo.seq).length ===
0
) {
this.selectedUserList = [...this.selectedUserList, userInfo];
} else {
this.selectedUserList = this.selectedUserList.filter(
item => item.seq !== userInfo.seq
);
}
this.changeDetectorRef.detectChanges();
}
/** Handling chipset for selectedUserList */
onClickDeleteUser(userInfo: UserInfoSS) {
this.selectedUserList = this.selectedUserList.filter(
item => item.seq !== userInfo.seq
);
this.changeDetectorRef.detectChanges();
}
/** Handling Button */
async onClickAddGroup() {
this.logger.debug('onClickAddGroup', this.selectedUserList);
const result = await this.dialogService.open<
SelectGroupDialogComponent,
SelectGroupDialogData,
SelectGroupDialogResult
>(SelectGroupDialogComponent, {
width: '600px',
data: {
title: this.translateService.instant('group.selectTargetGroup')
}
});
if (!!result && !!result.choice && result.choice) {
if (!!result.group) {
const oldGroup: GroupDetailData = result.group;
const trgtUserSeq: number[] = [];
result.group.userSeqs.map(seq => trgtUserSeq.push(seq));
this.selectedUserList
.filter(v => result.group.userSeqs.indexOf(v.seq) < 0)
.forEach(user => {
trgtUserSeq.push(user.seq);
});
this.store.dispatch(
SyncStore.updateGroupMember({
oldGroup,
trgtUserSeq
})
);
}
}
}
onClickChatOpen() {
if (!!this.selectedUserList && this.selectedUserList.length > 0) {
// Open Room.
const seq: number[] = [];
this.selectedUserList.map(user => seq.push(user.seq));
this.store.dispatch(ChatStore.openRoom({ userSeqList: seq }));
// GNB Change to Chat
this.store.dispatch(
SettingsStore.selectedGnbMenuIndex({
menuIndex: MainMenu.Chat
})
);
}
}
onClickConference() {
const targetUserSeqs = this.selectedUserList.map(userInfo => userInfo.seq);
if (!!targetUserSeqs && targetUserSeqs.length > 0) {
this.conferenceService.conferenceCreate({
userSeq: this.loginRes.userSeq,
deviceType: this.environmentsInfo.deviceType,
tokenKey: this.loginRes.tokenString,
targetUserSeqs
});
}
}
onClickOpenProfile(userSeq: number) {
if (!userSeq || userSeq < 0) {
return;
@ -234,4 +316,7 @@ export class IntegratedSearchDialogComponent implements OnInit, OnDestroy {
)
.subscribe();
}
onCancel(): void {
this.dialogRef.close({});
}
}

View File

@ -100,10 +100,34 @@ export const searchDeptUserSuccess = createAction(
export const searchDeptUserFailure = createAction(
'[Messenger::Query] Search Dept User Failure',
props<{ error: any }>()
);
export const clearSearchDeptUser = createAction(
'[Messenger::Query] Clear Search Dept User Success',
props()
);
export const cancelSearchDeptUser = createAction(
'[Messenger::Query] Cancel Search Dept User Success',
/** 통합검색 > 조회 */
export const integrateSearchDeptUser = createAction(
'[Messenger::Query] Integration Search Dept User',
props<{
companyCode: string;
search: string;
}>()
);
export const integrateSearchDeptUserSuccess = createAction(
'[Messenger::Query] Integration Search Dept User Success.',
props<{ userInfos: UserInfoSS[] }>()
);
export const integrateSearchDeptUserFailure = createAction(
'[Messenger::Query] Integration Search Dept User Failure.',
props<{ error: any }>()
);
export const integrateClearSearchDeptUser = createAction(
'[Messenger::Query] Integration Clear Search Dept User Success',
props()
);

View File

@ -28,7 +28,11 @@ import {
selectedDeptSuccess,
searchDeptUser,
searchDeptUserSuccess,
cancelSearchDeptUser
clearSearchDeptUser,
integrateSearchDeptUser,
integrateSearchDeptUserSuccess,
integrateSearchDeptUserFailure,
searchDeptUserFailure
} from './actions';
import {
@ -146,7 +150,58 @@ export class Effects {
this.store.dispatch(selectedDeptSuccess({}));
}),
catchError(error => of(deptUserFailure({ error })))
catchError(error => of(searchDeptUserFailure({ error })))
);
})
);
},
{ dispatch: false }
);
integrateSearchDeptUser$ = createEffect(
() => {
return this.actions$.pipe(
ofType(integrateSearchDeptUser),
withLatestFrom(
this.store.pipe(
select(
(state: any) =>
state.account.authentication.loginRes as LoginResponse
)
)
),
switchMap(([req, loginResInfo]) => {
return this.organizationService
.getDeptUser({
divCd: 'INT_SRCH',
companyCode: req.companyCode,
search: req.search,
searchRange: DeptSearchType.All,
senderCompanyCode: loginResInfo.companyCode,
senderEmployeeType: loginResInfo.userInfo.employeeType
})
.pipe(
map(datas => {
const userInfos: UserInfoSS[] = datas.userInfos;
this.store.dispatch(
integrateSearchDeptUserSuccess({
userInfos
})
);
// 검색 결과에 따른 프레즌스 조회.
const userSeqList: number[] = [];
userInfos.map(user => userSeqList.push(user.seq));
if (userSeqList.length > 0) {
this.store.dispatch(
StatusStore.bulkInfo({
divCd: 'inttrSrch',
userSeqs: userSeqList
})
);
}
}),
catchError(error => of(integrateSearchDeptUserFailure({ error })))
);
})
);
@ -179,7 +234,6 @@ export class Effects {
.pipe(
map(datas => {
const userInfos: UserInfoSS[] = datas.userInfos;
this.store.dispatch(
searchDeptUserSuccess({
userInfos

View File

@ -11,8 +11,12 @@ import {
searchDeptUserSuccess,
searchDeptUserFailure,
deptUserFailure,
cancelSearchDeptUser,
myDeptUserFailure
clearSearchDeptUser,
myDeptUserFailure,
integrateSearchDeptUser,
integrateSearchDeptUserSuccess,
integrateSearchDeptUserFailure,
integrateClearSearchDeptUser
} from './actions';
import * as AuthenticationStore from '@app/store/account/authentication';
@ -96,7 +100,7 @@ export const reducer = createReducer(
};
}),
on(cancelSearchDeptUser, (state, action) => {
on(clearSearchDeptUser, (state, action) => {
return {
...state,
isSearch: false,
@ -104,6 +108,35 @@ export const reducer = createReducer(
};
}),
on(integrateSearchDeptUser, (state, action) => {
return {
...state,
integrateSearchDepartmentProcessing: true
};
}),
on(integrateSearchDeptUserSuccess, (state, action) => {
return {
...state,
integrateSearchDepartmentUserInfoList: action.userInfos,
integrateSearchDepartmentProcessing: false
};
}),
on(integrateSearchDeptUserFailure, (state, action) => {
return {
...state,
integrateSearchDepartmentProcessing: false
};
}),
on(integrateClearSearchDeptUser, (state, action) => {
return {
...state,
integrateSearchDepartmentUserInfoList: null
};
}),
on(AuthenticationStore.logoutInitialize, (state, action) => {
return {
...initialState

View File

@ -18,6 +18,9 @@ export interface State {
searchDepartmentUserInfoList: UserInfoSS[] | null;
departmentUserInfoList: UserInfoSS[] | null;
myDepartmentUserInfoList: UserInfoSS[] | null;
integrateSearchDepartmentProcessing: boolean;
integrateSearchDepartmentUserInfoList: UserInfoSS[] | null;
}
export const initialState: State = {
@ -30,7 +33,10 @@ export const initialState: State = {
searchDepartmentUserInfoList: null,
departmentUserInfoList: null,
myDepartmentUserInfoList: null
myDepartmentUserInfoList: null,
integrateSearchDepartmentProcessing: false,
integrateSearchDepartmentUserInfoList: null
};
export function selectors<S>(selector: Selector<any, State>) {
@ -62,6 +68,15 @@ export function selectors<S>(selector: Selector<any, State>) {
myDepartmentUserInfoList: createSelector(
selector,
(state: State) => state.myDepartmentUserInfoList
),
integrateSearchDepartmentProcessing: createSelector(
selector,
(state: State) => state.integrateSearchDepartmentProcessing
),
integrateSearchDepartmentUserInfoList: createSelector(
selector,
(state: State) => state.integrateSearchDepartmentUserInfoList
)
};
}