0528 sync

This commit is contained in:
Park Byung Eun 2020-05-28 21:52:25 +09:00
parent 02c2645e01
commit 176eb2bbd6
45 changed files with 1230 additions and 360 deletions

37
package-lock.json generated
View File

@ -3314,9 +3314,9 @@
"dev": true
},
"@ucap/api-common": {
"version": "0.0.3",
"resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/api-common/-/api-common-0.0.3.tgz",
"integrity": "sha512-rifcCToIXdWZb9R3UXu2bXDqj/KBX3xfxHPFgx+Wp1TBUh6d+xszNYN7+mZbDKKSOYUgpVeuaF8leKkTwXUh9g==",
"version": "0.0.5",
"resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/api-common/-/api-common-0.0.5.tgz",
"integrity": "sha512-7V/Ea4FbLIKE+/ti414Spm6PQS3ZDcsHTZStLLYkMQrU7x+GbrGnHGOcvzzAQtE2SmpoNd3q6JzoYY+CK1eCkA==",
"dev": true
},
"@ucap/api-external": {
@ -3393,8 +3393,8 @@
"dev": true
},
"@ucap/ng-core": {
"version": "file:pack/ucap-ng-core-0.0.1.tgz",
"integrity": "sha512-okuTcVh/9VkF9dA1d/nsPuLL8ft2+4E77mgqfuPI2mI6IA7udQpC+hdMtyqtAN4LmT39iF1VGFv7UJYRK7fN7w==",
"version": "file:pack/ucap-ng-core-0.0.7.tgz",
"integrity": "sha512-ZC6LE3A0bg+REGbzDI/i1ad7mGpKsw6X0UtZ+Q8TUthHNv0DfWEieHFCgfYTRY1u022XyQ4ViOsrq9KunU1vfw==",
"dev": true
},
"@ucap/ng-i18n": {
@ -3508,8 +3508,8 @@
"dev": true
},
"@ucap/ng-store-chat": {
"version": "file:pack/ucap-ng-store-chat-0.0.13.tgz",
"integrity": "sha512-o+BCCSMxneUenRHEW47sSY22+Zt3lyr202Lg4bub9OVRbW5CVohHez8H+JwK+w+Lf8KbqG32V1ZjKLGclTpboA==",
"version": "file:pack/ucap-ng-store-chat-0.0.16.tgz",
"integrity": "sha512-Rwupo2Gqa+4+0ANBZLtup+mdlTU42Tj5gkGnJJ86Vylo4xQcdR+PUdedWZ/lG7DyzrfBc8ON80aUPRikpWUCow==",
"dev": true
},
"@ucap/ng-store-group": {
@ -3528,13 +3528,8 @@
"dev": true
},
"@ucap/ng-ui-authentication": {
"version": "file:pack/ucap-ng-ui-authentication-0.0.24.tgz",
"integrity": "sha512-6QMJ8dieTnbPANsBzg2Ll3HH5q6Bzl2iSM19yHq8Ct7XOmElrYqrEZmxbDyYO+aCXIAwd2t7vu+rTsHfz3XOQg==",
"dev": true
},
"@ucap/ng-ui-chat": {
"version": "file:pack/ucap-ng-ui-chat-0.0.9.tgz",
"integrity": "sha512-6qvzcTuylkxVjsqajsLW15laOyOskxVMy238/Ju1yYvwCRyHygwS1i67APoG5tv+SWu+l38f9uWIqzfy7WYHkQ==",
"version": "file:pack/ucap-ng-ui-authentication-0.0.25.tgz",
"integrity": "sha512-/KgMR8JHcnOm+XwMQVhHGud61Hivi/fX0NucxUglsOguYiWL6Ms61FdRezB3wuKs5h/5+zI4QIWLdWdT8GT/Dg==",
"dev": true
},
"@ucap/ng-ui-group": {
@ -3544,13 +3539,7 @@
},
"@ucap/ng-ui-material": {
"version": "file:pack/ucap-ng-ui-material-0.0.4.tgz",
"integrity": "sha512-ySPULAbP+nQ65hBG2VWZ2H5Hr7muuTGGNXs6A+S3lsxLaW452wM3GNyUBhvUopr8LaSsoOPpp4nK1JeC0fG6pA==",
"dev": true
},
"@ucap/ng-ui-organization": {
"version": "file:pack/ucap-ng-ui-organization-0.0.55.tgz",
"integrity": "sha512-vfpKd3fbd+I0Od8aB2nIFfjuI7wj3Ziu/uiTEmZxKwZy7uZrNYm59BPbctKW3AQsQ4UtnLofhlBbAA7e9pT80Q==",
"dev": true
"integrity": "sha512-ySPULAbP+nQ65hBG2VWZ2H5Hr7muuTGGNXs6A+S3lsxLaW452wM3GNyUBhvUopr8LaSsoOPpp4nK1JeC0fG6pA=="
},
"@ucap/ng-ui-skin-default": {
"version": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz",
@ -3640,9 +3629,9 @@
"dev": true
},
"@ucap/protocol-room": {
"version": "0.0.5",
"resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-room/-/protocol-room-0.0.5.tgz",
"integrity": "sha512-RSzLtnz5JVeDz9Y8gP17+LO6OG2NtLIPpW+JuHnqhfJyB93v2QmqqK5T7NbGYok72jRp4m7cJZEp1tLNYCKcmA==",
"version": "0.0.6",
"resolved": "https://nexus.loafle.net/repository/npm-all/@ucap/protocol-room/-/protocol-room-0.0.6.tgz",
"integrity": "sha512-PaEztUnZgmsH/Vo4JijJxpu9DBSA4SuIBW7y+L8BcBdPtzl49jBolm08+S9vgIr8cf9MtONdU37Al+uOTVki2g==",
"dev": true
},
"@ucap/protocol-service": {

View File

@ -148,7 +148,7 @@
"@types/moment-timezone": "^0.5.12",
"@types/node": "^12.12.30",
"@ucap/api": "~0.0.1",
"@ucap/api-common": "~0.0.1",
"@ucap/api-common": "~0.0.5",
"@ucap/api-external": "~0.0.2",
"@ucap/api-message": "~0.0.1",
"@ucap/api-prompt": "~0.0.1",
@ -162,7 +162,7 @@
"@ucap/ng-api-message": "file:pack/ucap-ng-api-message-0.0.1.tgz",
"@ucap/ng-api-prompt": "file:pack/ucap-ng-api-prompt-0.0.1.tgz",
"@ucap/ng-api-public": "file:pack/ucap-ng-api-public-0.0.1.tgz",
"@ucap/ng-core": "file:pack/ucap-ng-core-0.0.1.tgz",
"@ucap/ng-core": "file:pack/ucap-ng-core-0.0.7.tgz",
"@ucap/ng-i18n": "file:pack/ucap-ng-i18n-0.0.6.tgz",
"@ucap/ng-logger": "file:pack/ucap-ng-logger-0.0.2.tgz",
"@ucap/ng-native": "file:pack/ucap-ng-native-0.0.1.tgz",
@ -185,15 +185,15 @@
"@ucap/ng-protocol-sync": "file:pack/ucap-ng-protocol-sync-0.0.3.tgz",
"@ucap/ng-protocol-umg": "file:pack/ucap-ng-protocol-umg-0.0.3.tgz",
"@ucap/ng-store-authentication": "file:pack/ucap-ng-store-authentication-0.0.11.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.13.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.16.tgz",
"@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.14.tgz",
"@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.8.tgz",
"@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.19.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.24.tgz",
"@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.9.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.25.tgz",
"@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.12.tgz",
"@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.33.tgz",
"@ucap/ng-ui-material": "file:pack/ucap-ng-ui-material-0.0.4.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.55.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.83.tgz",
"@ucap/ng-ui-skin-default": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz",
"@ucap/ng-web-socket": "file:pack/ucap-ng-web-socket-0.0.2.tgz",
"@ucap/ng-web-storage": "file:pack/ucap-ng-web-storage-0.0.3.tgz",
@ -209,7 +209,7 @@
"@ucap/protocol-option": "~0.0.7",
"@ucap/protocol-ping": "~0.0.4",
"@ucap/protocol-query": "~0.0.5",
"@ucap/protocol-room": "~0.0.5",
"@ucap/protocol-room": "~0.0.6",
"@ucap/protocol-service": "~0.0.4",
"@ucap/protocol-status": "~0.0.5",
"@ucap/protocol-sync": "~0.0.4",

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-core",
"version": "0.0.1",
"version": "0.0.7",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -0,0 +1,24 @@
import { Params } from '@angular/router';
export class ParamsUtil {
static getParameter<T>(params: Params, key: string, defaultValue: T): T {
const v = params[key];
if (undefined === v) {
if (undefined !== defaultValue) {
return defaultValue;
}
return undefined;
}
switch (typeof defaultValue) {
case 'boolean':
return ((v as string).toLowerCase() === 'true' ? true : false) as any;
case 'number':
return Number(v) as any;
default:
break;
}
return v;
}
}

View File

@ -10,6 +10,7 @@ export interface StickerFilesInfo {
index: string;
path: string;
}
export const KEY_STICKER_HISTORY = 'ucap::Sticker_History';
export const StickerMap: StickerInfo[] = [
{
index: '00',
@ -138,15 +139,18 @@ export const StickerMap: StickerInfo[] = [
}
];
const ActiveAndOrdering: string[] = ['00', '01', '02', '03', '04', '05'];
export const ActiveAndOrdering: string[] = ['00', '01', '02', '03', '04', '05'];
export class StickerUtil {
static getStickerInfoList(): StickerInfo[] {
static getStickerInfoList(
stickerMap: StickerInfo[] = StickerMap,
activeAndOrdering: string[] = ActiveAndOrdering
): StickerInfo[] {
const rtnStickerList: StickerInfo[] = [];
ActiveAndOrdering.forEach(idx => {
const stickerInfos: StickerInfo[] = StickerMap.filter(
sticker => sticker.index === idx && sticker.useYn
activeAndOrdering.forEach((idx) => {
const stickerInfos: StickerInfo[] = stickerMap.filter(
(sticker) => sticker.index === idx && sticker.useYn
);
if (!!stickerInfos && stickerInfos.length > 0) {
@ -156,17 +160,4 @@ export class StickerUtil {
return rtnStickerList;
}
static getStickerInfoSub(index: string): StickerFilesInfo[] {
const stickerFilesList: StickerFilesInfo[] = [];
const stickerInfos: StickerInfo[] = StickerMap.filter(
sticker => sticker.index === index && sticker.useYn
);
if (!!stickerInfos && stickerInfos.length > 0) {
stickerFilesList.concat(stickerInfos[0].fileInfos);
}
return stickerFilesList;
}
}

View File

@ -2,6 +2,7 @@
* Public API Surface of core
*/
export * from './lib/utils/params.util';
export * from './lib/utils/sticker.util';
export * from './lib/utils/string.util';
export * from './lib/utils/window.util';

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-store-chat",
"version": "0.0.13",
"version": "0.0.16",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -83,7 +83,22 @@ export const fileInfosFailure = createAction(
*/
export const addEvent = createAction(
'[ucap::chat::chatting] addEvent',
props<{ req: FileInfoRequest }>()
props<{
roomId: string;
info: Info<EventJson>;
SVC_TYPE?: number;
SSVC_TYPE?: number;
}>()
);
/**
* add new event Success.
*/
export const addEventSuccess = createAction(
'[ucap::chat::chatting] addEvent Success',
props<{
roomId: string;
info: Info<EventJson>;
}>()
);
export const read = createAction(
@ -105,3 +120,26 @@ export const readFailure = createAction(
'[ucap::chat::chatting] read Failure',
props<{ error: any }>()
);
export const send = createAction(
'[ucap::chat::chatting] Send',
props<{ senderSeq: string; req: SendEventRequest }>()
);
export const sendSuccess = createAction(
'[ucap::chat::chatting] Send Success',
props<{
senderSeq: string;
res: SendEventResponse;
}>()
);
export const sendFailure = createAction(
'[ucap::chat::chatting] Send Failure',
props<{ error: any }>()
);
export const sendNotification = createAction(
'[ucap::chat::chatting] Send Notification',
props<{ noti: SendNotification }>()
);

View File

@ -5,13 +5,16 @@ import {
map,
catchError,
exhaustMap,
concatMap,
withLatestFrom,
debounceTime
} from 'rxjs/operators';
import { Injectable, Inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Store, select } from '@ngrx/store';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Dictionary } from '@ngrx/entity';
import { EventProtocolService } from '@ucap/ng-protocol-event';
import { RoomProtocolService } from '@ucap/ng-protocol-room';
@ -27,13 +30,24 @@ import {
readSuccess,
fileInfos,
fileInfosFailure,
fileInfosSuccess
fileInfosSuccess,
send,
sendSuccess,
sendFailure,
addEvent,
addEventSuccess
} from './actions';
import { InfoRequest, ReadResponse, FileType } from '@ucap/protocol-event';
import {
InfoRequest,
ReadResponse,
FileType,
EventType
} from '@ucap/protocol-event';
import { ModuleConfig } from '../../config/module-config';
import { _MODULE_CONFIG } from '../../config/token';
import { Chatting } from './state';
@Injectable()
export class Effects {
@ -158,6 +172,89 @@ export class Effects {
{ dispatch: false }
);
addEvent$ = createEffect(
() => {
return this.actions$.pipe(
ofType(addEvent),
withLatestFrom(
this.store.pipe(
select(
(state: any) =>
state.chat.chatting.chattings.entities as Dictionary<Chatting>
)
),
this.store.pipe(
select((state: any) => state.chat.chatting.activeRoomId)
)
),
tap(([action, chattings, activeRoomId]) => {
// Room in chattings state >> event add
if (!!chattings && !!chattings[action.roomId]) {
if (action.info.type === EventType.File) {
// Retrieve event of FileType in rooms
this.store.dispatch(
fileInfos({
req: {
roomId: action.roomId,
type: FileType.All
}
})
);
}
}
// room > rooms :: finalEvent-Infos refresh
this.store.dispatch(
addEventSuccess({
roomId: action.roomId,
info: action.info
})
);
})
);
},
{ dispatch: false }
);
send$ = createEffect(() =>
this.actions$.pipe(
ofType(send),
concatMap((action) =>
this.eventProtocolService.send(action.req).pipe(
map((res) => {
return sendSuccess({
senderSeq: action.senderSeq,
res
});
}),
catchError((error) => of(sendFailure({ error })))
)
)
)
);
sendSuccess$ = createEffect(
() => {
return this.actions$.pipe(
ofType(sendSuccess),
tap((action) => {
const res = action.res;
console.log('sendSuccess', action);
this.store.dispatch(
addEvent({
roomId: res.roomId,
info: res.info,
SVC_TYPE: res.SVC_TYPE,
SSVC_TYPE: res.SSVC_TYPE
})
);
})
);
},
{ dispatch: false }
);
constructor(
private actions$: Actions,
private store: Store<any>,

View File

@ -15,7 +15,8 @@ import {
eventsFailure,
readSuccess,
fileInfosSuccess,
fileInfosFailure
fileInfosFailure,
addEvent
} from './actions';
export const reducer = createReducer(
@ -163,5 +164,29 @@ export const reducer = createReducer(
...state.chattings
})
};
}),
on(addEvent, (state, action) => {
const roomId = action.roomId;
const eventInfo = action.info;
const chatting = state.chattings.entities[roomId];
if (!!chatting) {
return {
...state,
chattings: adapterChatting.upsertOne(
{
...chatting,
eventList: adapterEventList.upsertOne(eventInfo, {
...chatting.eventList
})
},
{ ...state.chattings }
)
};
} else {
return state;
}
})
);

View File

@ -119,6 +119,20 @@ export const roomFailure = createAction(
'[ucap::chat::room] room Failure',
props<{ error: any }>()
);
/**
* Success of rooms2 request
* RoomUserData is detail, short type both.
*/
export const room2Success = createAction(
'[ucap::chat::room] room2 Success',
props<{
roomInfo: RoomInfo;
roomUserInfo: {
userInfoShortList: UserInfoShort[];
userInfoList: RoomUserInfo[];
};
}>()
);
/**
* create room

View File

@ -28,10 +28,10 @@ import {
} from '@ucap/protocol-room';
import { RoomProtocolService } from '@ucap/ng-protocol-room';
import { SyncProtocolService } from '@ucap/ng-protocol-sync';
import { LoginActions } from '@ucap/ng-store-authentication';
import { addEventSuccess } from '../Chatting/actions';
import { RoomSelector } from '../state';
@ -77,8 +77,11 @@ import {
delMulti,
delMultiSuccess,
delMultiFailure,
selectedRoom
selectedRoom,
room2Success
} from './actions';
import { Router } from '@angular/router';
import { LocaleCode } from '@ucap/core';
@Injectable()
export class Effects {
@ -100,8 +103,6 @@ export class Effects {
isDetail: false
};
console.log(req);
// retrieve room info
this.store.dispatch(room({ req }));
@ -151,13 +152,25 @@ export class Effects {
room$ = createEffect(() => {
return this.actions$.pipe(
ofType(room),
map((action) => action.req),
switchMap((req) => {
return this.roomProtocolService.info(req).pipe(
switchMap((action) => {
const req = action.req;
// // CASE :: RoomUser Data 중 Detail data 만 수집.
// return this.roomProtocolService.info(req).pipe(
// map((res) =>
// roomSuccess({
// roomInfo: res.roomInfo,
// userInfoList: res.userInfoList
// })
// ),
// catchError((error) => of(roomFailure({ error })))
// );
// CASE :: RoomUser Data 중 Detail data, Short data 수집.
return this.roomProtocolService.info2(req).pipe(
map((res) =>
roomSuccess({
room2Success({
roomInfo: res.roomInfo,
userInfoList: res.userInfoList
roomUserInfo: res.roomUserInfo
})
),
catchError((error) => of(roomFailure({ error })))
@ -166,19 +179,40 @@ export class Effects {
);
});
create$ = createEffect(() =>
this.actions$.pipe(
create$ = createEffect(
() => {
return this.actions$.pipe(
ofType(create),
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.open(req).pipe(
map((res: CreateResponse) => {
return createSuccess({ res });
console.log('CreateResponse', res);
this.store.dispatch(createSuccess({ res }));
this.router.navigate(
[
'chat',
{
outlets: { content: 'chatroom' }
}
],
{
queryParams: { roomId: res.roomId }
}
);
// if (!res.newRoom) {
// } else {
// }
}),
catchError((error) => of(createFailure({ error })))
);
})
)
);
},
{ dispatch: false }
);
createTimer$ = createEffect(() =>
@ -390,9 +424,30 @@ export class Effects {
);
});
/*******************************************************************
* [Chatting Action watching.]
*******************************************************************/
addEventSuccess$ = createEffect(
() => {
return this.actions$.pipe(
ofType(addEventSuccess),
withLatestFrom(this.store.pipe(select(RoomSelector.rooms))),
map(([action, roomList]) => {
const roomId = action.roomId;
if (!roomList.find((roomInfo) => roomInfo.roomId === roomId)) {
this.store.dispatch(rooms({ localeCode: LocaleCode.Korean }));
}
})
);
},
{ dispatch: false }
);
constructor(
private actions$: Actions,
private store: Store<any>,
private router: Router,
private syncProtocolService: SyncProtocolService,
private roomProtocolService: RoomProtocolService
) {}

View File

@ -24,9 +24,12 @@ import {
delSuccess,
rooms2Success,
delMultiSuccess,
updateSuccess
updateSuccess,
room2Success,
createSuccess
} from './actions';
import { userInfo } from 'os';
import { Info, EventJson } from '@ucap/protocol-event';
import { ChatUtil } from '../../utils/chat.util';
export const reducer = createReducer(
initialState,
@ -136,6 +139,49 @@ export const reducer = createReducer(
};
}),
on(room2Success, (state, action) => {
const roomUserMapList: RoomUserMap[] = [];
const roomUserShortMapList: RoomUserShortMap[] = [];
if (
!!action.roomUserInfo.userInfoList &&
0 < action.roomUserInfo.userInfoList.length
) {
roomUserMapList.push({
roomId: action.roomInfo.roomId,
userInfos: action.roomUserInfo.userInfoList
});
}
if (
!!action.roomUserInfo.userInfoShortList &&
0 < action.roomUserInfo.userInfoShortList.length
) {
roomUserShortMapList.push({
roomId: action.roomInfo.roomId,
userInfos: action.roomUserInfo.userInfoShortList
});
}
return {
...state,
rooms: adapterRoom.upsertOne(action.roomInfo, {
...state.rooms
}),
roomUsers:
0 < roomUserMapList.length
? adapterRoomUser.upsertMany(roomUserMapList, {
...state.roomUsers
})
: state.roomUsers,
roomUsersShort:
0 < roomUserShortMapList.length
? adapterRoomUserShort.upsertMany(roomUserShortMapList, {
...state.roomUsersShort
})
: state.roomUsersShort
};
}),
on(excludeUserSuccess, (state, action) => {
const roomId = action.roomId;
const roomUserMap = state.roomUsers.entities[roomId];
@ -191,6 +237,41 @@ export const reducer = createReducer(
};
}),
on(createSuccess, (state, action) => {
const standby = state.standbyRooms;
const curRoomId = action.res.roomId;
if (standby.findIndex((roomId) => roomId === curRoomId) > -1) {
// Exist.
return state;
} else {
// Not Exist.
return {
...state,
standbyRooms: [...state.standbyRooms, curRoomId]
};
}
}),
on(delSuccess, (state, action) => {
const roomId = action.res.roomId;
const room = state.rooms.entities[roomId];
if (!room) {
return state;
}
return {
...state,
rooms: adapterRoom.removeOne(roomId, { ...state.rooms }),
roomUsers: adapterRoomUser.removeOne(roomId, {
...state.roomUsers
}),
roomUsersShort: adapterRoomUserShort.removeOne(roomId, {
...state.roomUsersShort
})
};
}),
on(updateSuccess, (state, action) => {
const roomInfo = {
...state.rooms.entities[action.res.roomId],
@ -243,9 +324,9 @@ export const reducer = createReducer(
};
}),
/**
/*******************************************************************
* [Chatting Action watching.]
*/
*******************************************************************/
on(chattingActions.readSuccess, (state, action) => {
const roomId = action.roomId;
const trgtUserSeq = action.SENDER_SEQ;
@ -279,7 +360,10 @@ export const reducer = createReducer(
userInfos: state.roomUsersShort.entities[roomId].userInfos.map(
(roomUserInfo) => {
if (roomUserInfo.seq === Number(trgtUserSeq)) {
return { ...roomUserInfo, lastReadEventSeq: action.lastReadSeq };
return {
...roomUserInfo,
lastReadEventSeq: action.lastReadSeq
};
} else {
return roomUserInfo;
}
@ -301,5 +385,44 @@ export const reducer = createReducer(
})
: state.roomUsersShort
};
}),
on(chattingActions.addEventSuccess, (state, action) => {
const roomId = action.roomId;
const info: Info<EventJson> = action.info;
if (!roomId || !info) {
return state;
}
const currentRoomInfo = state.rooms.entities[roomId];
const fixedStandByRooms = state.standbyRooms.filter(
(standbyRoomId) => standbyRoomId !== roomId
);
if (!!currentRoomInfo) {
const finalEventMessage:
| string
| null = ChatUtil.convertFinalEventMessage(
action.info.type,
action.info.sentMessageJson || action.info.sentMessage
);
const roomInfo = {
...currentRoomInfo,
finalEventType: info.type,
finalEventDate: info.sendDate,
finalEventMessage,
finalEventSeq: info.seq
} as RoomInfo;
return {
...state,
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms }),
standbyRooms: fixedStandByRooms
};
} else {
return state;
}
})
);

View File

@ -47,6 +47,7 @@ export interface State {
rooms: RoomState;
roomUsers: RoomUserState;
roomUsersShort: RoomUserShortState;
standbyRooms: string[];
}
const roomInitialState: RoomState = adapterRoom.getInitialState({
@ -60,7 +61,8 @@ const roomUserShortInitialState: RoomUserShortState = adapterRoomUserShort.getIn
export const initialState: State = {
rooms: roomInitialState,
roomUsers: roomUserInitialState,
roomUsersShort: roomUserShortInitialState
roomUsersShort: roomUserShortInitialState,
standbyRooms: []
};
const {
@ -128,6 +130,10 @@ export function selectors<S>(selector: Selector<any, State>) {
);
}
),
standbyRooms: createSelector(
selector,
(state: State) => state.standbyRooms
),
unreadTotal: createSelector(
selectRooms,
selectAllForRoom,

View File

@ -0,0 +1,138 @@
import {
EventJson,
EventType,
FileEventJson,
FileType,
VideoConferenceEventJson,
VideoConferenceContentsType,
MassTextEventJson,
TranslationEventJson,
MassTranslationEventJson,
PlanEventJson,
PlanContentType
} from '@ucap/protocol-event';
export class ChatUtil {
public static convertFinalEventMessage(
eventType: EventType,
finalEventMessage: EventJson
): string | null {
let eventMessage: string = null;
switch (eventType) {
case EventType.Join:
case EventType.Exit:
case EventType.ForcedExit:
case EventType.RenameRoom:
case EventType.NotificationForTimerRoom:
case EventType.GuideForRoomTimerChanged:
{
/**
* .
* @description Edit with ui-chat > messages.component.ts
*/
}
break;
case EventType.Sticker:
{
eventMessage = '스티커';
}
break;
case EventType.File:
{
const m = finalEventMessage as FileEventJson;
if (FileType.Image === m.fileType) {
eventMessage = '이미지';
} else {
eventMessage = '첨부파일';
}
}
break;
case EventType.VideoConference:
{
const m = finalEventMessage as VideoConferenceEventJson;
switch (m.contents) {
case VideoConferenceContentsType.Now:
eventMessage = '화상회의가 개설되었습니다.';
break;
case VideoConferenceContentsType.New:
eventMessage = '화상회의가 등록되었습니다.';
break;
case VideoConferenceContentsType.Update:
eventMessage = '화상회의가 수정되었습니다.';
break;
case VideoConferenceContentsType.Delete:
eventMessage = '화상회의가 취소되었습니다.';
break;
default:
eventMessage = '화상회의';
break;
}
}
break;
case EventType.MassText:
{
const m = finalEventMessage as MassTextEventJson;
eventMessage = m.content;
}
break;
case EventType.Translation:
{
const m = finalEventMessage as TranslationEventJson;
eventMessage = m.original;
}
break;
case EventType.MassTranslation:
{
const m = finalEventMessage as MassTranslationEventJson;
eventMessage = m.original;
}
break;
case EventType.Plan:
{
const m = finalEventMessage as PlanEventJson;
switch (m.contents) {
case PlanContentType.New:
eventMessage = '새로운 일정이 등록되었습니다.';
break;
case PlanContentType.Update:
eventMessage = '일정이 수정되었습니다.';
break;
case PlanContentType.Delete:
eventMessage = '일정이 취소되었습니다.';
break;
default:
eventMessage = '일정이 업데이트 되었습니다.';
break;
}
}
break;
case EventType.NotificationForiOSCapture:
{
const m = finalEventMessage as string;
eventMessage = `${m}님이 대화내용을 캡쳐하였습니다.`;
}
break;
case EventType.AllimElephant: // [daesang] 알림
{
eventMessage = '코끼리 알림';
}
break;
case EventType.AllimTms: // [daesang] 알림
{
eventMessage = 'TMS 알림';
}
break;
default:
{
const m = finalEventMessage as string;
eventMessage = m;
}
break;
}
return eventMessage;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-authentication",
"version": "0.0.24",
"version": "0.0.25",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -6,7 +6,7 @@
class="ucap-authentication-login-input-field"
[style.display]="!!fixedCompanyCode ? 'none' : 'block'"
>
<mat-form-field>
<mat-form-field color="accent">
<mat-label>{{ 'login.fields.company' | ucapI18n }}</mat-label>
<mat-select
[formControl]="companyCodeFormControl"
@ -26,16 +26,27 @@
<div class="ucap-authentication-login-input-field">
<!--<span class="input-icon"><i class="mid mdi-account-tie-outline"></i></span>-->
<span class="ucap-authentication-login-input-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20.82 22">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0H24V24H0z" style="fill: none;" />
<g transform="translate(.118 -1)">
<path
d="M14.39,11.88a5.7,5.7,0,1,0-4.78,0A10.41,10.41,0,0,0,1.59,22v1H22.41V22A10.41,10.41,0,0,0,14.39,11.88Z"
transform="translate(-1.59 -1)"
style="fill: #fff;"
/>
<path
d="M14.39,11.88a5.7,5.7,0,1,0-4.78,0A10.41,10.41,0,0,0,1.59,22v1H22.41V22A10.41,10.41,0,0,0,14.39,11.88ZM8.3,6.7A3.71,3.71,0,1,1,12,10.41,3.7,3.7,0,0,1,8.3,6.7ZM3.65,21a8.42,8.42,0,0,1,7.58-7.37L10,16.58l2,2,2-2-1.23-2.95A8.42,8.42,0,0,1,20.35,21Z"
transform="translate(-1.59 -1)"
d="M58.571 48.146a8.3 8.3 0 1 0-16.571 0"
transform="translate(-38.403 -25.592)"
style="
stroke-miterlimit: 10;
stroke: #4c4c4c;
stroke-width: 2px;
fill: none;
"
/>
<g
transform="translate(7.037 3.808)"
style="stroke: #4c4c4c; stroke-width: 2px; fill: none;"
>
<circle cx="4.5" cy="4.5" r="4.5" style="stroke: none;" />
<circle cx="4.5" cy="4.5" r="3.5" style="fill: none;" />
</g>
</g>
</svg>
</span>
<mat-form-field>
@ -47,30 +58,41 @@
<div class="ucap-authentication-login-input-field">
<!--<span class="input-icon"><i class="mid mdi-lock-outline"></i></span>-->
<span class="ucap-authentication-login-input-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 22.07">
<title>login-lock</title>
<path
d="M19,10H18V7A6,6,0,0,0,6,7v3H5a3,3,0,0,0-3,3v7a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V13A3,3,0,0,0,19,10Z"
transform="translate(-2 -0.93)"
style="fill: #fff;"
/>
<rect x="9.08" y="12.94" width="1.84" height="3.72" />
<path
d="M19,22.93H5a3,3,0,0,1-3-3v-7a3,3,0,0,1,3-3H19a3,3,0,0,1,3,3v7A3,3,0,0,1,19,22.93Zm-14-11a1,1,0,0,0-1,1v7a1,1,0,0,0,1,1H19a1,1,0,0,0,1-1v-7a1,1,0,0,0-1-1Z"
transform="translate(-2 -0.93)"
/>
<path
d="M17,11.93a1,1,0,0,1-1-1v-4a4,4,0,0,0-8,0v4a1,1,0,0,1-2,0v-4a6,6,0,0,1,12,0v4A1,1,0,0,1,17,11.93Z"
transform="translate(-2 -0.93)"
/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0H24V24H0z" style="fill: none;" />
<g
transform="translate(2 9)"
style="stroke-width: 2px; stroke: #4c4c4c; fill: #fff;"
>
<rect width="20" height="14" style="stroke: none;" rx="2" />
<rect
x="9.58"
y="13.36"
width="0.84"
height="2.72"
style="fill: #fff;"
width="18"
height="12"
x="1"
y="1"
style="fill: none;"
rx="1"
/>
<rect x="9.08" y="12.86" width="1.84" height="3.72" />
</g>
<g style="fill: #fff;">
<path
d="M11 8H1V6c0-2.757 2.243-5 5-5s5 2.243 5 5v2z"
style="stroke: none;"
transform="translate(6 2)"
/>
<path
d="M10 7V6c0-2.205-1.794-4-4-4-2.205 0-4 1.795-4 4v1h8m1.917 2H.083C.028 8.67 0 8.332 0 8V6c0-3.308 2.692-6 6-6s6 2.692 6 6v2c0 .335-.028.67-.083.998V9z"
style="stroke: none; fill: #4c4c4c;"
transform="translate(6 2)"
/>
</g>
<g
transform="translate(10 14)"
style="fill: #4c4c4c; stroke: #4c4c4c;"
>
<circle cx="2" cy="2" r="2" style="stroke: none;" />
<circle cx="2" cy="2" r="1.5" style="fill: none;" />
</g>
</svg>
</span>
<mat-form-field>

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-chat",
"version": "0.0.9",
"version": "0.0.12",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -107,7 +107,7 @@ export class ChatUiRootModule {}
providers: [
{
provide: UCAP_I18N_NAMESPACE,
useValue: ['chat']
useValue: ['chat', 'common']
}
]
})

View File

@ -5,6 +5,7 @@
>
<ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.File"
[fileInfo]="fileInfo"
[expired]="getExpiredFile()"
(openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)"
@ -13,6 +14,7 @@
<ucap-chat-message-box-attach-file
*ngSwitchCase="FileType.Sound"
[fileInfo]="fileInfo"
[expired]="getExpiredFile()"
(openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)"
@ -21,6 +23,7 @@
<ucap-chat-message-box-image
*ngSwitchCase="FileType.Image"
[fileInfo]="fileInfo"
[expired]="getExpiredFile()"
(openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)"
@ -28,14 +31,14 @@
<ucap-chat-message-box-video
*ngSwitchCase="FileType.Video"
[fileInfo]="fileInfo"
[expired]="getExpiredFile()"
(openViewer)="onClickFileViewer(fileInfo)"
(save)="onSave($event)"
></ucap-chat-message-box-video>
<!--
<ucap-chat-message-box-text
*ngSwitchDefault
[message]="message"
></ucap-chat-message-box-text> -->
></ucap-chat-message-box-text>
</ng-container>

View File

@ -1,88 +1,53 @@
<div class="ucap-chat-message-box-information">
<ng-container [ngSwitch]="message.type">
<ng-container *ngSwitchCase="EventType.Join">
{{ getI18nForSir(getOwnerForJoinEventJson()) }}이
{{ getI18nForSir(getInviterForJoinEventJson()) }}을 초대했습니다.
<!-- {{
'chat.event.inviteToRoomWith'
| translate
{{
'event.inviteToRoomWith'
| ucapI18n
: {
owner: getI18nForSir(message.sentMessageJson.owner),
inviter: getI18nForSir(message.sentMessageJson.inviter)
owner: getOwnerForJoinEventJson(),
inviter: getInviterForJoinEventJson()
}
}} -->
}}
</ng-container>
<ng-container *ngSwitchCase="EventType.Exit">
{{ message.sentMessage }}님이 퇴장하셨습니다.
<!-- {{
'chat.event.exitFromRoomWith'
| translate: { exitor: message.sentMessage }
}} -->
{{ 'event.exitFromRoomWith' | ucapI18n: { exitor: message.sentMessage } }}
</ng-container>
<ng-container *ngSwitchCase="EventType.ForcedExit">
{{ message.exitForcingRequestUserName }}님이 {{ message.sentMessage }}님을
퇴장 시키셨습니다.
<!-- {{
'chat.event.ejectedFromRoomWith'
| translate
{{
'event.ejectedFromRoomWith'
| ucapI18n
: {
requester: message.exitForcingRequestUserName,
ejected: message.sentMessage
}
}} -->
}}
</ng-container>
<ng-container *ngSwitchCase="EventType.RenameRoom">
{{ getRequesterForRenameRoom() }}님이 대화방명을 '{{
getRoomNameForRenameRoom()
}}'으로 변경하셨습니다.
<!-- {{
'chat.event.renamedRoomWith'
| translate
{{
'event.renamedRoomWith'
| ucapI18n
: {
requester: message.sentMessageJson.requester,
roomName: message.sentMessageJson.roomName
requester: getRequesterForRenameRoom(),
roomName: getRoomNameForRenameRoom()
}
}} -->
}}
</ng-container>
<ng-container *ngSwitchCase="EventType.NotificationForTimerRoom">
{{ message.sentMessage }}
</ng-container>
<ng-container *ngSwitchCase="EventType.GuideForRoomTimerChanged">
{{ senderName }}님이 타이머를 설정하였습니다. ({{
getCalcTimerForGuideForRoomTimerChanged()
}})
<!-- {{
'chat.event.setTimerWith'
| translate
{{
'event.setTimerWith'
| ucapI18n
: {
requester: senderName,
timer: getCalcTimer(message.sentMessageJson.time * 1000)
timer: getCalcTimerForGuideForRoomTimerChanged()
}
}} -->
}}
</ng-container>
<ng-container *ngSwitchCase="EventType.NotificationForiOSCapture">
{{ message.sentMessage }}님이 대화내용을 캡쳐하였습니다.
<!-- {{
'chat.event.iosCapture'
| translate
: {
requester: message.sentMessage
}
}} -->
{{ 'event.iosCapture' | ucapI18n: { requester: message.sentMessage } }}
</ng-container>
</ng-container>
</div>
<!-- <div class="ucap-chat-info-event">
<div class="info bg-primary-chat">
<strong class="text-warn-chat"
>김나영, 김준혁,강은정,나정미,박미영,박정은, 박훈, 문영준, 이지은, 이진현,
이현준, 임영찬, 임찬우, 정민우,정현욱, 최남진, 최영은, 최진우, 한고은,
한정후, 한지민</strong
>님이 초대되었습니다.
</div>
<div class="chat-event others bg-accent-chat">
자동 삭제 타이머가 10분으로 변경되었습니다.
</div>
</div> -->

View File

@ -7,7 +7,7 @@ import {
GuideForRoomTimerChangedEventJson,
JoinEventJson
} from '@ucap/protocol-event';
import { TranslateService } from '@ucap/ng-ui-organization';
import { I18nService } from '@ucap/ng-i18n';
@Component({
selector: 'ucap-chat-message-box-information',
@ -23,105 +23,64 @@ export class InformationComponent implements OnInit {
EventType = EventType;
constructor(private translateService: TranslateService) {}
constructor(private i18nService: I18nService) {}
ngOnInit() {}
getI18nForSir(names: string | string[]) {
protected getI18nForSir(names: string | string[]) {
if (Array.isArray(names)) {
const inviter: string[] = [];
// names.forEach((userName, idx) => {
// inviter.push(
// this.translateService.instant('common.messages.sirWith', {
// sir: userName
// })
// );
// });
names.forEach((userName, idx) => {
inviter.push(userName);
});
return inviter.join(',');
return names.join(',');
} else {
// return this.translateService.instant('common.messages.sirWith', {
// sir: names
// });
return names;
}
}
getOwnerForJoinEventJson() {
return (this.message.sentMessageJson as JoinEventJson).owner;
return this.getI18nForSir(
(this.message.sentMessageJson as JoinEventJson).owner
);
}
getInviterForJoinEventJson() {
return (this.message.sentMessageJson as JoinEventJson).inviter;
return this.getI18nForSir(
(this.message.sentMessageJson as JoinEventJson).inviter
);
}
getCalcTimerForGuideForRoomTimerChanged() {
const millisec =
Number(
(this.message.sentMessageJson as GuideForRoomTimerChangedEventJson).time
) * 1000;
// const langs = this.translateService.instant([
// 'common.units.hourFrom',
// 'common.units.minute',
// 'common.units.second'
// ]);
// switch (millisec) {
// case 5000:
// return `5 ${langs['common.units.second']}`;
// case 10000:
// return `10 ${langs['common.units.second']}`;
// case 30000:
// return `30 ${langs['common.units.second']}`;
// case 60000:
// return `1 ${langs['common.units.minute']}`;
// case 300000:
// return `5 ${langs['common.units.minute']}`;
// case 600000:
// return `10 ${langs['common.units.minute']}`;
// case 1800000:
// return `30 ${langs['common.units.minute']}`;
// case 3600000:
// return `1 ${langs['common.units.hourFrom']}`;
// case 21600000:
// return `6 ${langs['common.units.hourFrom']}`;
// case 43200000:
// return `12 ${langs['common.units.hourFrom']}`;
// case 86400000:
// return `24 ${langs['common.units.hourFrom']}`;
// default:
// return `0 ${langs['common.units.second']}`;
// }
const langs = ['초', '분', '시간'];
const langs = this.i18nService.t([
'common.units.hourFrom',
'common.units.minute',
'common.units.second'
]);
switch (millisec) {
case 5000:
return `5 ${langs[0]}`;
return `5 ${langs['common.units.second']}`;
case 10000:
return `10 ${langs[0]}`;
return `10 ${langs['common.units.second']}`;
case 30000:
return `30 ${langs[0]}`;
return `30 ${langs['common.units.second']}`;
case 60000:
return `1 ${langs[1]}`;
return `1 ${langs['common.units.minute']}`;
case 300000:
return `5 ${langs[1]}`;
return `5 ${langs['common.units.minute']}`;
case 600000:
return `10 ${langs[1]}`;
return `10 ${langs['common.units.minute']}`;
case 1800000:
return `30 ${langs[1]}`;
return `30 ${langs['common.units.minute']}`;
case 3600000:
return `1 ${langs[2]}`;
return `1 ${langs['common.units.hourFrom']}`;
case 21600000:
return `6 ${langs[2]}`;
return `6 ${langs['common.units.hourFrom']}`;
case 43200000:
return `12 ${langs[2]}`;
return `12 ${langs['common.units.hourFrom']}`;
case 86400000:
return `24 ${langs[2]}`;
return `24 ${langs['common.units.hourFrom']}`;
default:
return `0 ${langs[0]}`;
return `0 ${langs['common.units.second']}`;
}
}

View File

@ -1,7 +1,10 @@
<div class="ucap-chat-message-box-sticker">
<ul>
<li *ngIf="stickerUrl">
<img [src]="stickerUrl" />
<li>
<ng-content
select="[ucapUiChatStickerComponent='stickerImage']"
></ng-content>
<!-- <img [src]="stickerUrl" /> -->
</li>
<li
*ngIf="contents"

View File

@ -18,19 +18,23 @@ export class StickerComponent implements OnInit, AfterViewInit {
@Input()
message: Info<StickerEventJson>;
@Input()
stickerUrl?: string;
// @Input()
// stickerImageRoot: string;
@Input()
stickerDefaultImagePath: string;
// @Input()
// stickerDefaultImage: string;
@Output()
openLink = new EventEmitter<string>();
// stickerUrl?: string;
contents: string;
constructor(private elementRef: ElementRef) {}
ngOnInit() {
// if (!!this.message.sentMessageJson.file) {
// this.stickerUrl = `assets/sticker/sticker_s_${this.message.sentMessageJson.file}.png`;
// }
if (!!this.message.sentMessageJson?.chat) {
this.contents = this.message.sentMessageJson.chat;
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-organization",
"version": "0.0.55",
"version": "0.0.83",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},

View File

@ -1,5 +1,8 @@
@import '~@ucap/ng-ui-material/material';
/// var
/// --ucap-organization-profile-list-item-01-size: 70px
.ucap-organization-profile-list-item-01-container {
width: 100%;
height: 100%;
@ -23,6 +26,11 @@
flex-grow: 1.07;
padding-left: 17px;
.profile-image {
width: var(--ucap-organization-profile-list-item-01-size);
height: var(--ucap-organization-profile-list-item-01-size);
}
.user-info {
display: flex;
flex-direction: column;
@ -37,7 +45,7 @@
height: 22px;
.user-name {
// @include ellipsis-column(1);
@include ellipsis-column(1);
height: 22px;
font: {
size: 14px;
@ -49,7 +57,7 @@
}
.user-grade {
// @include ellipsis(1);
@include ellipsis(1);
align-self: stretch;
font: {
size: 13px;
@ -62,7 +70,7 @@
}
.dept-name {
// @include ellipsis(1);
@include ellipsis(1);
font: {
size: 12px;
}

View File

@ -36,7 +36,7 @@ export class ProfileListItem01Component implements OnInit, OnDestroy {
@Output()
changeCheck = new EventEmitter<{
isChecked: boolean;
checked: boolean;
userInfo: UserInfoSS;
}>();
@ -62,9 +62,9 @@ export class ProfileListItem01Component implements OnInit, OnDestroy {
}
}
onChangeCheck(isChecked: boolean): void {
onChangeCheck(checked: boolean): void {
this.changeCheck.emit({
isChecked,
checked,
userInfo: this.userInfo
});
}

View File

@ -0,0 +1,41 @@
<div class="ucap-organization-profile-list-item-01-container">
<div class="user-profile-info">
<div class="profile-image">
<ng-content
select="[ucapOrganizationProfileListItem01='profileImage']"
></ng-content>
</div>
<div class="user-info">
<div class="user-n-g">
<div class="user-name">
{{ userInfo | ucapOrganizationTranslate: 'name' }}
</div>
<div class="user-grade">
{{ userInfo | ucapOrganizationTranslate: 'grade' }}
</div>
</div>
<div class="dept-name">
{{ userInfo | ucapOrganizationTranslate: 'deptName' }}
</div>
</div>
</div>
<div class="user-info">
<ng-content
class="user-info-content"
select="[appProfileListItemUserInfo='info']"
></ng-content>
</div>
<div class="user-action">
<ng-content
class="user-action-content"
select="[appProfileListItemUserAction='action']"
></ng-content>
</div>
<div *ngIf="checkable">
<mat-checkbox
#checkbox
[checked]="checked"
(change)="onChangeCheck(checkbox.checked)"
></mat-checkbox>
</div>
</div>

View File

@ -0,0 +1,113 @@
@import '~@ucap/ng-ui-material/material';
/// var
/// --ucap-organization-profile-list-item-01-size: 70px
.ucap-organization-profile-list-item-01-container {
width: 100%;
height: 100%;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
padding: 0;
height: 70px;
align-items: center;
&.line-top {
border-top: 1px solid #cccccc;
}
&.line-bottom {
border-bottom: 1px solid #cccccc;
}
.user-profile-info {
display: inline-flex;
flex-direction: row;
flex-grow: 1.07;
padding-left: 17px;
.profile-image {
width: var(--ucap-organization-profile-list-item-01-size);
height: var(--ucap-organization-profile-list-item-01-size);
}
.user-info {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding-left: 16px;
.user-n-g {
display: flex;
flex-flow: row-reverse nowrap;
align-items: flex-end;
height: 22px;
.user-name {
@include ellipsis-column(1);
height: 22px;
font: {
size: 14px;
weight: 600;
}
color: #212121;
order: 1;
-ms-flex-order: 1;
}
.user-grade {
@include ellipsis(1);
align-self: stretch;
font: {
size: 13px;
}
color: #707070;
margin-left: 4px;
order: 0;
-ms-flex-order: 0;
}
}
.dept-name {
@include ellipsis(1);
font: {
size: 12px;
}
color: #666666;
line-height: 16px;
}
}
}
.company-info {
flex-grow: 1.4;
padding-left: 16px;
position: relative;
font: {
size: 14px;
}
color: #333333;
&:before {
content: '';
width: 1px;
height: 36px;
position: absolute;
left: 0;
top: calc(50% - 18px);
border-left: 1px solid #cccccc;
}
}
.contact {
flex-grow: 1;
font: {
size: 14px;
}
color: #333333;
}
.chk-box {
flex: 0 0 56px;
display: flex;
justify-content: center;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ProfileListItem01Component } from './profile-list-item-01.component';
describe('ucap::ui-organization::ProfileListItem01Component', () => {
let component: ProfileListItem01Component;
let fixture: ComponentFixture<ProfileListItem01Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProfileListItem01Component]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProfileListItem01Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,68 @@
import { moduleMetadata } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UserInfoF } from '@ucap/protocol-query';
import { OrganizationUiModule } from '../organization-ui.module';
import { ProfileListItem02Component } from './profile-list-item-02.component';
import { RoleCode } from '@ucap/protocol-authentication';
import { CallMode, EmployeeType } from '@ucap/core';
const userInfo: UserInfoF = {
seq: 1234,
name: '사용자1',
profileImageFile: '사진파일',
grade: '직급',
intro: '업무소개',
companyCode: '기관코드',
hpNumber: '핸드폰번호',
lineNumber: '내선번호',
email: '이메일',
isMobile: true,
isBuddy: true,
isFavorit: true,
/** */
deptName: '부서명',
/** */
order: '조회순서',
isActive: true,
roleCd: RoleCode.Self,
employeeNum: 'A34567',
madn: 'MADN',
hardSadn: 'HARDPHONE_SADN',
fmcSadn: 'FMC_SADN',
nickName: 'nickName',
nameEn: '사용자명(영어)',
nameCn: '사용자명(중국어)',
gradeEn: '직급(영어)',
gradeCn: '직급(중국어)',
deptNameEn: '부서명(영어)',
deptNameCn: '부서명(중국어)',
isPrivacyAgree: true,
isValidLogin: true,
employeeType: EmployeeType.Regular
};
export default {
title: 'ui-organization::ProfileListItem02Component',
component: ProfileListItem02Component,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, OrganizationUiModule],
providers: []
})
]
};
export const Default = () => ({
component: ProfileListItem02Component,
props: {
userInfo
}
});

View File

@ -0,0 +1,24 @@
@import '~@ucap/ng-ui-material/material';
@mixin ucap-organization-profile-list-item-01-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.ucap-organization-profile-list-item-01-container {
border-color: mat-color($foreground, secondary-text);
}
}
@mixin ucap-organization-profile-list-item-01-typography($config) {
.ucap-organization-profile-list-item-01-container {
.searchword-container {
.form-field-input-searchword {
font-family: mat-font-family($config);
}
}
}
}

View File

@ -0,0 +1,62 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Output,
EventEmitter
} from '@angular/core';
import { UserInfoF } from '@ucap/protocol-query';
@Component({
selector: 'ucap-organization-profile-list-item-02',
templateUrl: './profile-list-item-02.component.html',
styleUrls: ['./profile-list-item-02.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileListItem02Component implements OnInit, OnDestroy {
@Input()
userInfo: UserInfoF;
@Input()
authCall = false;
@Input()
checkable = true;
@Input()
checked = false;
@Output()
changeCheck = new EventEmitter<{
checked: boolean;
userInfo: UserInfoF;
}>();
private ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject();
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.next();
this.ngOnDestroySubject.complete();
}
}
onChangeCheck(checked: boolean): void {
this.changeCheck.emit({
checked,
userInfo: this.userInfo
});
}
}

View File

@ -1,4 +1,4 @@
<div class="ucap-organization-profile-list-container">
<div class="ucap-organization-profile-list-container" fxFlexFill>
<cdk-virtual-scroll-viewport #cvsvList perfectScrollbar fxFlexFill>
<ng-container *cdkVirtualFor="let userInfo of userInfos">
<ng-container

View File

@ -1,27 +1,31 @@
<div class="ucap-organization-profile-selection-01-container">
<div class="ucap-organization-profile-selection-01-body">
<div class="ucap-organization-profile-selection-01-container" fxLayout="column">
<div
*ngIf="!!headerTemplate"
class="ucap-organization-profile-selection-01-header"
fxFlex="0 0 48px"
>
<ng-container [ngTemplateOutlet]="headerTemplate"></ng-container>
</div>
<div class="ucap-organization-profile-selection-01-body" fxFlex="1 1 auto">
<mat-chip-list aria-label="User selection" perfectScrollbar>
<mat-chip
*ngFor="let profileSelection of _profileSelectionList"
[color]="profileSelection.color"
[selectable]="profileSelection.selectable"
[selected]="profileSelection.selected"
[removable]="profileSelection.removable"
(selectionChange)="
_onSelectionChange($event, profileSelection.userInfo)
"
(removed)="_onRemoved($event, profileSelection.userInfo)"
*ngFor="let userInfo of _userInfoList"
[color]="_colorFor(userInfo)"
[removable]="_removableFor(userInfo)"
(removed)="_onRemoved($event, userInfo)"
>
{{ profileSelection.userInfo.name }}
<mat-icon matChipRemove *ngIf="profileSelection.removable">
{{ userInfo.name }}
<mat-icon matChipRemove *ngIf="removableFor(userInfo)">
clear
</mat-icon>
</mat-chip>
</mat-chip-list>
</div>
<div class="ucap-organization-profile-selection-01-actions">
<ng-content
select="[ucapOrganizationSelectedProfileList01='action']"
></ng-content>
<div
*ngIf="!!actionsTemplate"
class="ucap-organization-profile-selection-01-actions"
fxFlex="0 0 60px"
>
<ng-container [ngTemplateOutlet]="actionsTemplate"></ng-container>
</div>
</div>

View File

@ -3,4 +3,9 @@
.ucap-organization-profile-selection-01-container {
width: 100%;
height: 100%;
.ucap-organization-profile-selection-01-body {
width: 100%;
height: 100%;
}
}

View File

@ -7,11 +7,15 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Output,
EventEmitter
EventEmitter,
Input,
Directive,
ContentChild,
TemplateRef
} from '@angular/core';
import { UserInfo } from '@ucap/protocol-sync';
import { MatChipSelectionChange, MatChipEvent } from '@angular/material/chips';
import { MatChipEvent } from '@angular/material/chips';
export interface ProfileSelection {
userInfo: UserInfo;
@ -21,6 +25,16 @@ export interface ProfileSelection {
removable?: boolean;
}
@Directive({
selector: '[ucapOrganizationProfileSelection01Header]'
})
export class ProfileSelection01HeaderDirective {}
@Directive({
selector: '[ucapOrganizationProfileSelection01Actions]'
})
export class ProfileSelection01ActionsDirective {}
@Component({
selector: 'ucap-organization-profile-selection-01',
templateUrl: './profile-selection-01.component.html',
@ -28,17 +42,34 @@ export interface ProfileSelection {
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileSelection01Component implements OnInit, OnDestroy {
@Output()
selectionChange: EventEmitter<{
selected: boolean;
userInfo: UserInfo;
}> = new EventEmitter();
@Input()
set userInfoList(value: UserInfo[]) {
console.log('userInfoList', value);
this._userInfoList = value;
}
// tslint:disable-next-line: variable-name
_userInfoList: UserInfo[];
@Input()
removableFor: (userInfo: UserInfo) => boolean;
@Input()
colorFor: (userInfo: UserInfo) => string;
@Output()
removed: EventEmitter<UserInfo> = new EventEmitter();
// tslint:disable-next-line: variable-name
_profileSelectionList: ProfileSelection[] = [];
@ContentChild(ProfileSelection01HeaderDirective, {
read: TemplateRef,
static: false
})
headerTemplate: TemplateRef<ProfileSelection01HeaderDirective>;
@ContentChild(ProfileSelection01ActionsDirective, {
read: TemplateRef,
static: false
})
actionsTemplate: TemplateRef<ProfileSelection01ActionsDirective>;
private ngOnDestroySubject: Subject<void>;
@ -55,54 +86,21 @@ export class ProfileSelection01Component implements OnInit, OnDestroy {
}
}
addProfileSelection(profileSelectionList: ProfileSelection[]): void {
if (!profileSelectionList || 0 === profileSelectionList.length) {
return;
_colorFor(userInfo: UserInfo): string {
if (!this.colorFor) {
return undefined;
}
return this.colorFor(userInfo);
}
profileSelectionList.forEach((v) => {
const i = this.indexOfProfileSelection(v);
if (-1 === i) {
this._profileSelectionList.push(v);
_removableFor(userInfo: UserInfo): boolean {
if (!this.removableFor) {
return undefined;
}
});
}
removeProfileSelection(profileSelectionList: ProfileSelection[]): void {
if (!profileSelectionList || 0 === profileSelectionList.length) {
return;
}
profileSelectionList.forEach((v) => {
const i = this.indexOfProfileSelection(v);
if (-1 < i) {
this._profileSelectionList.splice(i, 1);
}
});
}
_onSelectionChange(event: MatChipSelectionChange, userInfo: UserInfo) {
this.selectionChange.emit({
selected: event.selected,
userInfo
});
return this.removableFor(userInfo);
}
_onRemoved(event: MatChipEvent, userInfo: UserInfo) {
this.removed.emit(userInfo);
}
private indexOfProfileSelection(profileSelection: ProfileSelection): number {
if (
!this._profileSelectionList ||
0 === this._profileSelectionList.length ||
!profileSelection
) {
return -1;
}
return this._profileSelectionList.findIndex(
(v) => v.userInfo.seq === profileSelection.userInfo.seq
);
}
}

View File

@ -28,6 +28,7 @@
matInput
#inputSearchWord
[placeholder]="placeholder"
[value]="!!defaultSearchWord ? defaultSearchWord : ''"
(keyup.enter)="onKeyupEnter($event)"
/>
<button

View File

@ -14,8 +14,8 @@
display: flex;
border: {
width: var(--ucap-organization-search-for-tenant-border-width, 1px);
color: var(--ucap-organization-search-for-tenant-border-color, #e42f66);
width: var(--ucap-organization-search-for-tenant-border-width);
color: var(--ucap-organization-search-for-tenant-border-color);
style: solid;
}

View File

@ -23,7 +23,6 @@ export default {
export const Default = () => ({
component: SearchForTenantComponent,
props: {
text: 'Hello SearchForTenantComponent',
companyList: [
{
companyCode: 'CD1',

View File

@ -29,6 +29,9 @@ export class SearchForTenantComponent implements OnInit, OnDestroy {
@Input()
defaultCompany: string;
@Input()
defaultSearchWord: string;
@Input()
placeholder: string;

View File

@ -4,21 +4,25 @@
*cdkVirtualFor="let node of dataSource.expandedData$"
></ng-container>
<mat-tree #treeList [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree
#treeList
[dataSource]="dataSource"
[treeControl]="treeControl"
[trackBy]="trackBy"
>
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodePadding
matTreeNodePaddingIndent="20"
class="tree-no-child"
(click)="onClickNode($event, node)"
>
<li
(click)="onClickNode(node)"
matRipple
[ngClass]="
currentDeptSeq === node?.data?.deptInfo?.seq ? 'current' : ''
"
>
<div class="tree-node-body">
<div matRipple class="tree-node-body">
{{ node?.data?.deptInfo | ucapOrganizationTranslate: 'name' }}
</div>
</li>
@ -29,9 +33,11 @@
matTreeNodePadding
matTreeNodePaddingIndent="20"
class="tree-has-child"
(click)="onClickNode($event, node)"
>
<li (click)="onClickNode(node)" matRipple>
<li>
<div
matRipple
class="tree-node-body"
[ngClass]="
currentDeptSeq === node?.data?.deptInfo?.seq ? 'current' : ''

View File

@ -30,6 +30,7 @@ import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
export interface OrganizationNode {
deptInfo: DeptInfo;
ancestorSeq?: number;
children?: OrganizationNode[];
}
@ -94,7 +95,9 @@ export class TreeComponent implements OnInit, OnDestroy {
}
if (nodeMap.has(deptInfo.parentSeq)) {
nodeMap.get(deptInfo.parentSeq).children.push(node);
const ancestor = nodeMap.get(deptInfo.parentSeq);
node.ancestorSeq = ancestor.deptInfo.seq;
ancestor.children.push(node);
} else {
remainChildNodeList.push(node);
}
@ -110,28 +113,32 @@ export class TreeComponent implements OnInit, OnDestroy {
rootNodeList = [...rootNodeList[0].children];
}
this._deptInfoList = deptInfoList;
remainChildNodeList.forEach((node) => {
if (nodeMap.has(node.deptInfo.parentSeq)) {
nodeMap.get(node.deptInfo.parentSeq).children.push(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) {
data.expanded.forEach((s) => {
this.expand(s);
});
this.expand(...data.expanded);
}
this.changeDetectorRef.detectChanges();
}
currentDeptSeq: number;
@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
_deptInfoList: DeptInfo[];
_currentDeptSeq: number;
@Output()
clickNode = new EventEmitter<DeptInfo>();
@ -201,19 +208,32 @@ export class TreeComponent implements OnInit, OnDestroy {
hasChild = (_: number, node: FlatNode) => node.expandable;
onClickNode(node: FlatNode) {
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) {
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 === deptSeq
(n) => n.data.deptInfo.seq === s
);
if (!!node) {
this.treeControl.expand(node);
flatNodes.push(node);
this.selectHierarchy(flatNodes, node);
}
});
if (0 < flatNodes.length) {
this.treeControl.expansionModel.select(...flatNodes);
}
}
@ -236,4 +256,30 @@ export class TreeComponent implements OnInit, OnDestroy {
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);
}
}
}
}

View File

@ -25,12 +25,17 @@ import { _MODULE_CONFIG } from './config/token';
import { Profile01Component } from './components/profile-01.component';
import { ProfileListItem01Component } from './components/profile-list-item-01.component';
import { ProfileListItem02Component } from './components/profile-list-item-02.component';
import { ProfileImage01Component } from './components/profile-image-01.component';
import {
ProfileListComponent,
ProfileListNodeDirective
} from './components/profile-list.component';
import { ProfileSelection01Component } from './components/profile-selection-01.component';
import {
ProfileSelection01Component,
ProfileSelection01HeaderDirective,
ProfileSelection01ActionsDirective
} from './components/profile-selection-01.component';
import { SearchForTenantComponent } from './components/search-for-tenant.component';
import { TreeComponent } from './components/tree.component';
@ -42,6 +47,7 @@ import { UCAP_I18N_NAMESPACE, I18nModule } from '@ucap/ng-i18n';
const COMPONENTS = [
Profile01Component,
ProfileListItem01Component,
ProfileListItem02Component,
ProfileListComponent,
ProfileImage01Component,
ProfileSelection01Component,
@ -50,7 +56,11 @@ const COMPONENTS = [
];
const DIALOGS = [];
const PIPES = [TranslatePipe];
const DIRECTIVES = [ProfileListNodeDirective];
const DIRECTIVES = [
ProfileListNodeDirective,
ProfileSelection01HeaderDirective,
ProfileSelection01ActionsDirective
];
const SERVICES = [TranslateService];
@NgModule({

View File

@ -163,7 +163,7 @@ export class PresenceUtil {
return statusBulkInfo.phoneStatus;
case PresenceType.CONFERENCE:
return statusBulkInfo.conferenceStatus;
case PresenceType.IMESSENER:
case PresenceType.IMESSENGER:
return statusBulkInfo.imessengerStatus;
default:
return undefined;