0517 sync

This commit is contained in:
Park Byung Eun 2020-05-17 11:24:49 +09:00
parent aee13b8a4d
commit b4ef37624c
87 changed files with 3106 additions and 2102 deletions

View File

@ -1,3 +1,5 @@
const path = require('path');
module.exports = {
stories: ['../projects/**/*.stories.ts'],
addons: [
@ -5,5 +7,19 @@ module.exports = {
'@storybook/addon-links',
'@storybook/addon-notes',
'@storybook/addon-knobs'
]
],
webpackFinal: async (config) => {
for (const rule of config.module.rules) {
if (-1 < String(rule.test).indexOf('scss')) {
rule.use.push({
loader: 'sass-resources-loader',
options: {
resources: [path.join(__dirname, 'styles.scss')]
}
});
}
}
return config;
}
};

10
.storybook/styles.scss Normal file
View File

@ -0,0 +1,10 @@
:root {
--ucap-screen-max-xs: 575;
--ucap-screen-min-sm: 576;
--ucap-screen-max-sm: 767;
--ucap-screen-min-md: 768;
--ucap-screen-max-md: 991;
--ucap-screen-min-lg: 992;
--ucap-screen-max-lg: 1199;
--ucap-screen-min-xl: 1200;
}

View File

@ -1436,6 +1436,49 @@
}
}
},
"ui-chat": {
"projectType": "library",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "projects/ui-chat",
"sourceRoot": "projects/ui-chat/src",
"prefix": "ucap-chat",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/ui-chat/tsconfig.lib.json",
"project": "projects/ui-chat/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/ui-chat/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/ui-chat/src/test.ts",
"tsConfig": "projects/ui-chat/tsconfig.spec.json",
"karmaConfig": "projects/ui-chat/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/ui-chat/tsconfig.lib.json",
"projects/ui-chat/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"]
}
}
}
},
"ui-skin-default": {
"projectType": "library",
"schematics": {

1925
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -45,11 +45,12 @@
"build:store-chat": "node ./scripts/build.js store-chat",
"build:store-group": "node ./scripts/build.js store-group",
"build:store-organization": "node ./scripts/build.js store-organization",
"build:ui:all": "npm-run-all -s build:ui build:ui-organization build:ui-authentication build:ui-group",
"build:ui:all": "npm-run-all -s build:ui build:ui-organization build:ui-authentication build:ui-group build:ui-chat",
"build:ui": "node ./scripts/build.js ui",
"build:ui-organization": "node ./scripts/build.js ui-organization",
"build:ui-authentication": "node ./scripts/build.js ui-authentication",
"build:ui-group": "node ./scripts/build.js ui-group",
"build:ui-chat": "node ./scripts/build.js ui-chat",
"build:ui-skin:all": "npm-run-all -s build:ui-skin-default",
"build:ui-skin-default": "node ./scripts/build.js ui-skin-default useScssBundle",
"publish:all": "npm-run-all -s publish:logger publish:core publish:util:all publish:api:all publish:protocol:all publish:native:all publish:store:all publish:ui:all publish:ui-skin:all",
@ -91,11 +92,12 @@
"publish:store-chat": "cd ./dist/store-chat && npm publish",
"publish:store-group": "cd ./dist/store-group && npm publish",
"publish:store-organization": "cd ./dist/store-organization && npm publish",
"publish:ui:all": "npm-run-all -s publish:ui publish:ui-organization publish:ui-authentication publish:ui-group",
"publish:ui:all": "npm-run-all -s publish:ui publish:ui-organization publish:ui-authentication publish:ui-group publish:ui-chat",
"publish:ui": "cd ./dist/ui && npm publish",
"publish:ui-organization": "cd ./dist/ui-organization && npm publish",
"publish:ui-authentication": "cd ./dist/ui-authentication && npm publish",
"publish:ui-group": "cd ./dist/ui-group && npm publish",
"publish:ui-chat": "cd ./dist/ui-chat && npm publish",
"publish:ui-skin:all": "npm-run-all -s publish:ui-skin-default",
"publish:ui-skin-default": "cd ./dist/ui-skin-default && npm publish",
"test": "ng test",
@ -179,13 +181,14 @@
"@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.10.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.5.tgz",
"@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.6.tgz",
"@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.4.tgz",
"@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.5.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.19.tgz",
"@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.27.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.12.tgz",
"@ucap/ng-store-chat": "file:pack/ucap-ng-store-chat-0.0.8.tgz",
"@ucap/ng-store-group": "file:pack/ucap-ng-store-group-0.0.9.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.11.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.20.tgz",
"@ucap/ng-ui-chat": "file:pack/ucap-ng-ui-chat-0.0.3.tgz",
"@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.30.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.27.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",
@ -206,6 +209,7 @@
"@ucap/protocol-status": "~0.0.5",
"@ucap/protocol-sync": "~0.0.4",
"@ucap/protocol-umg": "~0.0.5",
"@ucap/ui-scss": "~0.0.3",
"@ucap/web-socket": "~0.0.5",
"@ucap/web-storage": "~0.0.5",
"autolinker": "^3.13.0",
@ -236,6 +240,7 @@
"queueing-subject": "^0.3.4",
"rimraf": "^3.0.2",
"rxjs": "~6.5.4",
"sass-resources-loader": "^2.0.3",
"scss-bundle": "^3.1.1",
"ts-node": "~8.3.0",
"tslib": "^1.11.1",

View File

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

View File

@ -13,6 +13,8 @@ import {
Open3Response as CreateTimerResponse,
ExitRequest as DeleteRequest,
ExitResponse as DeleteResponse,
ExitAllRequest as DeleteMultiRequest,
ExitAllResponse as DeleteMultiResponse,
UpdateRequest,
UpdateResponse,
InviteRequest,
@ -20,7 +22,8 @@ import {
ExitForcingRequest,
ExitForcingResponse,
UpdateTimerSetRequest,
UpdateTimerSetResponse
UpdateTimerSetResponse,
UserInfoShort
} from '@ucap/protocol-room';
/**
@ -32,6 +35,7 @@ export const rooms = createAction(
);
/**
* Success of rooms request
* RoomUserData is detail type.
*/
export const roomsSuccess = createAction(
'[ucap::chat::room] rooms Success',
@ -51,6 +55,31 @@ export const roomsFailure = createAction(
props<{ error: any }>()
);
/**
* Success of rooms2 request
* RoomUserData is detail, short type both.
*/
export const rooms2Success = createAction(
'[ucap::chat::room] rooms2 Success',
props<{
roomList: RoomInfo[];
roomUserInfoMap: {
[param: string]: {
userInfoShortList: UserInfoShort[];
userInfoList: RoomUserInfo[];
};
};
syncDate: string;
}>()
);
/**
* Failure of rooms2 request
*/
export const rooms2Failure = createAction(
'[ucap::chat::room] rooms2 Failure',
props<{ error: any }>()
);
/**
* retrieve room information and list of user information
*/
@ -142,6 +171,28 @@ export const delFailure = createAction(
props<{ error: any }>()
);
/**
* exit from multi room
*/
export const delMulti = createAction(
'[ucap::chat::room] delMulti',
props<{ req: DeleteMultiRequest }>()
);
/**
* Success of delMulti request
*/
export const delMultiSuccess = createAction(
'[ucap::chat::room] delMulti Success',
props<{ res: DeleteMultiResponse }>()
);
/**
* Failure of delMulti request
*/
export const delMultiFailure = createAction(
'[ucap::chat::room] delMulti Failure',
props<{ error: any }>()
);
/**
* update room information
*/

View File

@ -17,6 +17,7 @@ import {
OpenResponse as CreateResponse,
Open3Response as CreateTimerResponse,
ExitResponse as DeleteResponse,
ExitAllResponse as DeleteMultiResponse,
UpdateResponse,
InviteResponse,
ExitForcingResponse,
@ -67,7 +68,12 @@ import {
expelFailure,
updateTimeRoomInterval,
updateTimeRoomIntervalSuccess,
updateTimeRoomIntervalFailure
updateTimeRoomIntervalFailure,
rooms2Success,
rooms2Failure,
delMulti,
delMultiSuccess,
delMultiFailure
} from './actions';
@Injectable()
@ -75,7 +81,7 @@ export class Effects {
sessionCreatedForRooms$ = createEffect(() => {
return this.actions$.pipe(
ofType(LoginActions.sessionCreated),
map(action => rooms({ localeCode: action.loginSession.localeCode }))
map((action) => rooms({ localeCode: action.loginSession.localeCode }))
);
});
@ -84,17 +90,32 @@ export class Effects {
ofType(rooms),
withLatestFrom(this.store.pipe(select(RoomSelector.roomsSyncDate))),
switchMap(([action, syncDate]) => {
// // CASE :: RoomUser Data 중 Detail data 만 수집.
// return this.syncProtocolService
// .room({ syncDate, localeCode: action.localeCode })
// .pipe(
// map((res) => {
// return roomsSuccess({
// roomList: res.roomList,
// roomUserInfoMap: res.roomUserInfoMap,
// syncDate: res.res.syncDate
// });
// }),
// catchError((error) => of(roomsFailure({ error })))
// );
// CASE :: RoomUser Data 중 Detail data, Short data 수집.
return this.syncProtocolService
.room({ syncDate, localeCode: action.localeCode })
.room2({ syncDate, localeCode: action.localeCode })
.pipe(
map(res =>
roomsSuccess({
map((res) => {
return rooms2Success({
roomList: res.roomList,
roomUserInfoMap: res.roomUserInfoMap,
syncDate: res.res.syncDate
})
),
catchError(error => of(roomsFailure({ error })))
});
}),
catchError((error) => of(rooms2Failure({ error })))
);
})
);
@ -103,16 +124,16 @@ export class Effects {
room$ = createEffect(() => {
return this.actions$.pipe(
ofType(room),
map(action => action.req),
switchMap(req => {
map((action) => action.req),
switchMap((req) => {
return this.roomProtocolService.info(req).pipe(
map(res =>
map((res) =>
roomSuccess({
roomInfo: res.roomInfo,
userInfoList: res.userInfoList
})
),
catchError(error => of(roomFailure({ error })))
catchError((error) => of(roomFailure({ error })))
);
})
);
@ -121,13 +142,13 @@ export class Effects {
create$ = createEffect(() =>
this.actions$.pipe(
ofType(create),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.open(req).pipe(
map((res: CreateResponse) => {
return createSuccess({ res });
}),
catchError(error => of(createFailure({ error })))
catchError((error) => of(createFailure({ error })))
);
})
)
@ -136,13 +157,13 @@ export class Effects {
createTimer$ = createEffect(() =>
this.actions$.pipe(
ofType(createTimer),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.open3(req).pipe(
map((res: CreateTimerResponse) => {
return createTimerSuccess({ res });
}),
catchError(error => of(createTimerFailure({ error })))
catchError((error) => of(createTimerFailure({ error })))
);
})
)
@ -151,14 +172,30 @@ export class Effects {
del$ = createEffect(() =>
this.actions$.pipe(
ofType(del),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.exit(req).pipe(
switchMap((res: DeleteResponse) => [
close({ roomIds: [res.roomId] }),
delSuccess({ res })
]),
catchError(error => of(delFailure({ error })))
catchError((error) => of(delFailure({ error })))
);
})
)
);
delMulti$ = createEffect(() =>
this.actions$.pipe(
ofType(delMulti),
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.exitAll(req).pipe(
switchMap((res: DeleteMultiResponse) => [
close({ roomIds: res.roomIds }),
delMultiSuccess({ res })
]),
catchError((error) => of(delMultiFailure({ error })))
);
})
)
@ -167,13 +204,13 @@ export class Effects {
update$ = createEffect(() =>
this.actions$.pipe(
ofType(update),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.update(req).pipe(
map((res: UpdateResponse) => {
return updateSuccess({ res });
}),
catchError(error => of(updateFailure({ error })))
catchError((error) => of(updateFailure({ error })))
);
})
)
@ -182,7 +219,7 @@ export class Effects {
excludeUser$ = createEffect(() => {
return this.actions$.pipe(
ofType(excludeUser),
map(action =>
map((action) =>
excludeUserSuccess({
roomId: action.roomId,
userSeqs: action.userSeqs
@ -194,21 +231,21 @@ export class Effects {
open$ = createEffect(() => {
return this.actions$.pipe(
ofType(open),
map(action => openSuccess({ roomIds: [...action.roomIds] }))
map((action) => openSuccess({ roomIds: [...action.roomIds] }))
);
});
close$ = createEffect(() => {
return this.actions$.pipe(
ofType(close),
map(action => closeSuccess({ roomIds: [...action.roomIds] }))
map((action) => closeSuccess({ roomIds: [...action.roomIds] }))
);
});
inviteOrCreate$ = createEffect(() =>
this.actions$.pipe(
ofType(inviteOrCreate),
map(action => {
map((action) => {
const roomInfo = action.roomInfo;
const localeCode = action.localeCode;
@ -235,7 +272,7 @@ export class Effects {
invite$ = createEffect(() =>
this.actions$.pipe(
ofType(invite),
exhaustMap(action => {
exhaustMap((action) => {
const req = action.req;
const localeCode = action.localeCode;
@ -252,7 +289,7 @@ export class Effects {
})
];
}),
catchError(error => of(inviteFailure({ error })))
catchError((error) => of(inviteFailure({ error })))
);
})
)
@ -261,13 +298,13 @@ export class Effects {
expel$ = createEffect(() =>
this.actions$.pipe(
ofType(expel),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.exitForcing(req).pipe(
map((res: ExitForcingResponse) => {
return expelSuccess({ res });
}),
catchError(error => of(expelFailure({ error })))
catchError((error) => of(expelFailure({ error })))
);
})
)
@ -276,13 +313,13 @@ export class Effects {
updateTimeRoomInterval$ = createEffect(() =>
this.actions$.pipe(
ofType(updateTimeRoomInterval),
map(action => action.req),
exhaustMap(req => {
map((action) => action.req),
exhaustMap((req) => {
return this.roomProtocolService.updateTimerSet(req).pipe(
map((res: UpdateTimerSetResponse) => {
return updateTimeRoomIntervalSuccess({ res });
}),
catchError(error => of(updateTimeRoomIntervalFailure({ error })))
catchError((error) => of(updateTimeRoomIntervalFailure({ error })))
);
})
)
@ -291,7 +328,7 @@ export class Effects {
inviteNotification$ = createEffect(() => {
return this.actions$.pipe(
ofType(inviteNotification),
map(action =>
map((action) =>
room({
req: {
roomId: action.noti.roomId,
@ -306,7 +343,7 @@ export class Effects {
exitNotification$ = createEffect(() => {
return this.actions$.pipe(
ofType(exitNotification),
switchMap(action => {
switchMap((action) => {
if (action.userSeq === action.senderSeq) {
return [
close({ roomIds: [action.roomId] }),

View File

@ -1,6 +1,10 @@
import { createReducer, on } from '@ngrx/store';
import { UserInfo as RoomUserInfo } from '@ucap/protocol-room';
import {
UserInfo as RoomUserInfo,
UserInfoShort as RoomUserInfoShort,
RoomInfo
} from '@ucap/protocol-room';
import * as chattingActions from '../chatting/actions';
@ -8,14 +12,19 @@ import {
initialState,
adapterRoom,
adapterRoomUser,
RoomUserMap
RoomUserMap,
RoomUserShortMap,
adapterRoomUserShort
} from './state';
import {
roomsSuccess,
roomSuccess,
excludeUser,
excludeUserSuccess,
delSuccess
delSuccess,
rooms2Success,
delMultiSuccess,
updateSuccess
} from './actions';
export const reducer = createReducer(
@ -40,7 +49,7 @@ export const reducer = createReducer(
const value = state.rooms.entities[key];
unreadTotal += isNaN(value.noReadCnt) ? 0 : value.noReadCnt;
}
action.roomList.map(item => (unreadTotal += item.noReadCnt));
action.roomList.map((item) => (unreadTotal += item.noReadCnt));
return {
...state,
@ -55,6 +64,53 @@ export const reducer = createReducer(
};
}),
on(rooms2Success, (state, action) => {
const roomUserMapList: RoomUserMap[] = [];
const roomUserShortMapList: RoomUserShortMap[] = [];
for (const key in action.roomUserInfoMap) {
if (action.roomUserInfoMap.hasOwnProperty(key)) {
const userInfosMap = action.roomUserInfoMap[key];
if (!!userInfosMap && 0 < userInfosMap.userInfoList.length) {
roomUserMapList.push({
roomId: key,
userInfos: userInfosMap.userInfoList
});
}
if (!!userInfosMap && 0 < userInfosMap.userInfoShortList.length) {
roomUserShortMapList.push({
roomId: key,
userInfos: userInfosMap.userInfoShortList
});
}
}
}
let unreadTotal = 0;
// tslint:disable-next-line: forin
for (const key in state.rooms.entities) {
const value = state.rooms.entities[key];
unreadTotal += isNaN(value.noReadCnt) ? 0 : value.noReadCnt;
}
action.roomList.map((item) => (unreadTotal += item.noReadCnt));
return {
...state,
rooms: adapterRoom.upsertMany(action.roomList, {
...state.rooms,
syncDate: action.syncDate
}),
roomUsers: adapterRoomUser.upsertMany(roomUserMapList, {
...state.roomUsers
}),
roomUsersShort: adapterRoomUserShort.upsertMany(roomUserShortMapList, {
...state.roomUsersShort
}),
unreadTotal
};
}),
on(roomSuccess, (state, action) => {
const roomUserMapList: RoomUserMap[] = [];
@ -82,17 +138,27 @@ export const reducer = createReducer(
on(excludeUserSuccess, (state, action) => {
const roomId = action.roomId;
const roomUserMap = state.roomUsers.entities[roomId];
const roomUserMapShort = state.roomUsersShort.entities[roomId];
const userInfos: RoomUserInfo[] = [...roomUserMap.userInfos];
const userInfosShort: RoomUserInfoShort[] = [...roomUserMapShort.userInfos];
action.userSeqs.forEach(userSeq => {
action.userSeqs.forEach((userSeq) => {
const userInfo: RoomUserInfo = userInfos.find(
u => userSeq === String(u.seq)
(u) => userSeq === String(u.seq)
);
if (!!userInfo && !!userInfo.seq) {
userInfo.isJoinRoom = false;
}
const userInfoShort: RoomUserInfoShort = userInfosShort.find(
(u) => userSeq === String(u.seq)
);
if (!!userInfoShort && !!userInfoShort.seq) {
userInfoShort.isJoinRoom = false;
}
});
return {
@ -108,10 +174,35 @@ export const reducer = createReducer(
{
...state.roomUsers
}
),
roomUsersShort: adapterRoomUserShort.updateOne(
{
id: roomId,
changes: {
roomId,
userInfos: userInfosShort
}
},
{
...state.roomUsersShort
}
)
};
}),
on(updateSuccess, (state, action) => {
const roomInfo = {
...state.rooms.entities[action.res.roomId],
roomName: action.res.roomName,
receiveAlarm: action.res.receiveAlarm
} as RoomInfo;
return {
...state,
rooms: adapterRoom.upsertOne(roomInfo, { ...state.rooms })
};
}),
on(delSuccess, (state, action) => {
const roomId = action.res.roomId;
const room = state.rooms.entities[roomId];
@ -123,7 +214,27 @@ export const reducer = createReducer(
return {
...state,
rooms: adapterRoom.removeOne(roomId, { ...state.rooms }),
roomUsers: adapterRoomUser.removeOne(roomId, { ...state.roomUsers })
roomUsers: adapterRoomUser.removeOne(roomId, { ...state.roomUsers }),
roomUsersShort: adapterRoomUserShort.removeOne(roomId, {
...state.roomUsersShort
})
};
}),
on(delMultiSuccess, (state, action) => {
const roomIds: string[] = action.res.roomIds;
if (!roomIds || roomIds.length === 0) {
return state;
}
return {
...state,
rooms: adapterRoom.removeMany(roomIds, { ...state.rooms }),
roomUsers: adapterRoomUser.removeMany(roomIds, { ...state.roomUsers }),
roomUsersShort: adapterRoomUserShort.removeMany(roomIds, {
...state.roomUsersShort
})
};
})
);

View File

@ -3,22 +3,22 @@ import moment from 'moment';
import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter, Dictionary } from '@ngrx/entity';
import { RoomUserDetailData } from '@ucap/protocol-sync';
import { RoomInfo, UserInfo as RoomUserInfo } from '@ucap/protocol-room';
import { RoomUserDetailData, RoomUserData } from '@ucap/protocol-sync';
import {
RoomInfo,
UserInfo as RoomUserInfo,
UserInfoShort as RoomUserInfoShort
} from '@ucap/protocol-room';
export interface RoomState extends EntityState<RoomInfo> {
syncDate: string;
}
export const adapterRoom = createEntityAdapter<RoomInfo>({
selectId: roomInfo => roomInfo.roomId,
selectId: (roomInfo) => roomInfo.roomId,
sortComparer: (a, b) => {
return (
moment(b.finalEventDate)
.toDate()
.getTime() -
moment(a.finalEventDate)
.toDate()
.getTime()
moment(b.finalEventDate).toDate().getTime() -
moment(a.finalEventDate).toDate().getTime()
);
}
});
@ -28,24 +28,39 @@ export interface RoomUserMap {
userInfos: RoomUserInfo[];
}
export interface RoomUserShortMap {
roomId: string;
userInfos: RoomUserInfoShort[];
}
export interface RoomUserState extends EntityState<RoomUserMap> {}
export const adapterRoomUser = createEntityAdapter<RoomUserMap>({
selectId: roomUserDetailData => roomUserDetailData.roomId
selectId: (roomUserDetailData) => roomUserDetailData.roomId
});
export interface RoomUserShortState extends EntityState<RoomUserShortMap> {}
export const adapterRoomUserShort = createEntityAdapter<RoomUserShortMap>({
selectId: (roomUserShortData) => roomUserShortData.roomId
});
export interface State {
rooms: RoomState;
roomUsers: RoomUserState;
roomUsersShort: RoomUserShortState;
}
const roomInitialState: RoomState = adapterRoom.getInitialState({
syncDate: ''
});
const roomUserInitialState: RoomUserState = adapterRoomUser.getInitialState({});
const roomUserShortInitialState: RoomUserShortState = adapterRoomUserShort.getInitialState(
{}
);
export const initialState: State = {
rooms: roomInitialState,
roomUsers: roomUserInitialState
roomUsers: roomUserInitialState,
roomUsersShort: roomUserShortInitialState
};
const {
@ -62,6 +77,13 @@ const {
selectTotal: selectTotalForRoomUser
} = adapterRoomUser.getSelectors();
const {
selectAll: selectAllForRoomUserShort,
selectEntities: selectEntitiesForRoomUserShort,
selectIds: selectIdsForRoomUserShort,
selectTotal: selectTotalForRoomUserShort
} = adapterRoomUserShort.getSelectors();
export function selectors<S>(selector: Selector<any, State>) {
const selectRooms = createSelector(selector, (state: State) => state.rooms);
@ -70,6 +92,11 @@ export function selectors<S>(selector: Selector<any, State>) {
(state: State) => state.roomUsers
);
const selectRoomUsersShort = createSelector(
selector,
(state: State) => state.roomUsersShort
);
return {
rooms: createSelector(selectRooms, selectAllForRoom),
room: createSelector(
@ -78,7 +105,10 @@ export function selectors<S>(selector: Selector<any, State>) {
(roomState: RoomState, entities: Dictionary<RoomInfo>, roomId: string) =>
entities && entities[roomId]
),
roomsSyncDate: createSelector(selectRooms, roomState => roomState.syncDate),
roomsSyncDate: createSelector(
selectRooms,
(roomState) => roomState.syncDate
),
roomUsers: createSelector(selectRoomUsers, selectAllForRoomUser),
roomUser: createSelector(
selectRoomUsers,
@ -89,6 +119,19 @@ export function selectors<S>(selector: Selector<any, State>) {
roomId: string
) => entities && entities[roomId]
),
roomUsersShort: createSelector(
selectRoomUsersShort,
selectAllForRoomUserShort
),
roomUserShort: createSelector(
selectRoomUsersShort,
selectEntitiesForRoomUserShort,
(
roomUserShortState: RoomUserShortState,
entities: Dictionary<RoomUserData>,
roomId: string
) => entities && entities[roomId]
),
unreadTotal: createSelector(
selectRooms,
selectAllForRoom,

View File

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

View File

@ -23,7 +23,10 @@ import { SyncProtocolService } from '@ucap/ng-protocol-sync';
import { BuddyProtocolService } from '@ucap/ng-protocol-buddy';
import { DepartmentSelector } from '@ucap/ng-store-organization';
import {
DepartmentSelector,
PresenceActions
} from '@ucap/ng-store-organization';
import { LoginActions } from '@ucap/ng-store-authentication';
import * as groupActions from '../group/actions';
@ -53,7 +56,7 @@ export class Effects {
sessionCreatedForBuddy2$ = createEffect(() => {
return this.actions$.pipe(
ofType(LoginActions.sessionCreated),
map(action => buddy2())
map((action) => buddy2())
);
});
@ -63,13 +66,21 @@ export class Effects {
withLatestFrom(this.store.pipe(select(BuddySelector.buddySyncDate))),
switchMap(([action, syncDate]) => {
return this.syncProtocolService.buddy2({ syncDate }).pipe(
map(res =>
buddy2Success({
map((res) => {
// Buddy Presence
this.store.dispatch(
PresenceActions.bulkInfo({
divCd: 'buddyBulk',
userSeqs: res.buddyInfos.map((userInfo) => userInfo.seq + '')
})
);
return buddy2Success({
buddyList: res.buddyInfos,
syncDate: res.res.syncDate
})
),
catchError(error => of(buddy2Failure({ error })))
});
}),
catchError((error) => of(buddy2Failure({ error })))
);
})
);
@ -78,13 +89,13 @@ export class Effects {
add$ = createEffect(() =>
this.actions$.pipe(
ofType(add),
map(action => action.req),
exhaustMap(req =>
map((action) => action.req),
exhaustMap((req) =>
this.buddyProtocolService.add(req).pipe(
map((res: BuddyAddResponse) => {
return buddy2();
}),
catchError(error => of(addFailure({ error })))
catchError((error) => of(addFailure({ error })))
)
)
)
@ -93,13 +104,13 @@ export class Effects {
del$ = createEffect(() =>
this.actions$.pipe(
ofType(del),
map(action => action.req),
exhaustMap(req =>
map((action) => action.req),
exhaustMap((req) =>
this.buddyProtocolService.del(req).pipe(
map((res: BuddyDelResponse) => {
return delSuccess({ res });
}),
catchError(error => of(delFailure({ error })))
catchError((error) => of(delFailure({ error })))
)
)
)
@ -122,7 +133,7 @@ export class Effects {
this.moduleConfig.useMyDeptGroup &&
!!this.moduleConfig.fixedGroupSeqs &&
this.moduleConfig.fixedGroupSeqs.filter(
fixedGroupSeq => fixedGroupSeq === group.seq
(fixedGroupSeq) => fixedGroupSeq === group.seq
).length > 0
) {
// skip;;
@ -134,7 +145,7 @@ export class Effects {
groupSeq: group.seq,
groupName: group.name,
userSeqs: group.userSeqs.filter(
userSeq => userSeq !== String(req.seq)
(userSeq) => userSeq !== String(req.seq)
)
}
})
@ -147,8 +158,9 @@ export class Effects {
if (
!!this.moduleConfig.useMyDeptGroup &&
this.moduleConfig.useMyDeptGroup &&
myDeptUserList.filter(deptUser => deptUser.seq === String(req.seq))
.length > 0
myDeptUserList.filter(
(deptUser) => deptUser.seq === String(req.seq)
).length > 0
) {
// skip;;
} else {
@ -174,13 +186,13 @@ export class Effects {
update$ = createEffect(() =>
this.actions$.pipe(
ofType(update),
map(action => action.req),
exhaustMap(req =>
map((action) => action.req),
exhaustMap((req) =>
this.buddyProtocolService.update(req).pipe(
map((res: BuddyUpdateResponse) => {
return updateSuccess({ res });
}),
catchError(error => of(updateFailure({ error })))
catchError((error) => of(updateFailure({ error })))
)
)
)
@ -196,13 +208,15 @@ export class Effects {
if (!!targetUserSeqs && 0 < targetUserSeqs.length) {
const addBuddyList: string[] = [];
targetUserSeqs.forEach(userSeq => {
targetUserSeqs.forEach((userSeq) => {
if (!buddyList) {
addBuddyList.push(userSeq);
return;
}
const index = buddyList.findIndex(b => String(b.seq) === userSeq);
const index = buddyList.findIndex(
(b) => String(b.seq) === userSeq
);
if (-1 < index) {
addBuddyList.push(userSeq);
return;
@ -230,13 +244,15 @@ export class Effects {
if (!!targetUserSeqs && 0 < targetUserSeqs.length) {
const addBuddyList: string[] = [];
targetUserSeqs.forEach(userSeq => {
targetUserSeqs.forEach((userSeq) => {
if (!buddyList) {
addBuddyList.push(userSeq);
return;
}
const index = buddyList.findIndex(b => String(b.seq) === userSeq);
const index = buddyList.findIndex(
(b) => String(b.seq) === userSeq
);
if (-1 < index) {
addBuddyList.push(userSeq);
return;
@ -249,7 +265,7 @@ export class Effects {
}
if (!!userSeqsForDelete && 0 < userSeqsForDelete.length) {
userSeqsForDelete.forEach(userSeq => {
userSeqsForDelete.forEach((userSeq) => {
this.store.dispatch(
update({ req: { seq: Number(userSeq), isFavorit: false } })
);

View File

@ -31,7 +31,7 @@ import { _MODULE_CONFIG } from '../../config/token';
import * as buddyActions from '../buddy/actions';
import { GroupSelector } from '../state';
import { GroupSelector, BuddySelector } from '../state';
import {
groups,
@ -57,7 +57,7 @@ export class Effects {
sessionCreatedForGroups$ = createEffect(() => {
return this.actions$.pipe(
ofType(LoginActions.sessionCreated),
map(action => groups())
map((action) => groups())
);
});
@ -67,13 +67,13 @@ export class Effects {
withLatestFrom(this.store.pipe(select(GroupSelector.groupSyncDate))),
switchMap(([action, syncDate]) => {
return this.syncProtocolService.group2({ syncDate }).pipe(
map(res =>
map((res) =>
groupsSuccess({
groupList: res.groupInfos,
syncDate: res.res.syncDate
})
),
catchError(error => of(groupsFailure({ error })))
catchError((error) => of(groupsFailure({ error })))
);
})
);
@ -82,7 +82,8 @@ export class Effects {
create$ = createEffect(() =>
this.actions$.pipe(
ofType(create),
exhaustMap(action => {
withLatestFrom(this.store.pipe(select(BuddySelector.buddies))),
exhaustMap(([action, buddies]) => {
return this.groupProtocolService
.add({
groupName: action.groupName
@ -92,6 +93,19 @@ export class Effects {
const actions: any[] = [];
if (!!action.targetUserSeqs && 0 < action.targetUserSeqs.length) {
const addBuddies: number[] = [];
action.targetUserSeqs.forEach((item) => {
if (!buddies[item]) {
addBuddies.push(Number(item));
}
});
if (addBuddies.length > 0) {
actions.push(
buddyActions.add({
req: { userSeqs: action.targetUserSeqs }
})
);
}
actions.push(
update({
req: {
@ -108,7 +122,7 @@ export class Effects {
return actions;
}),
catchError(error => of(createFailure({ error })))
catchError((error) => of(createFailure({ error })))
);
})
)
@ -128,7 +142,7 @@ export class Effects {
// Del Buddy
let userSeqsForDelete: string[] = targetGroup.userSeqs.filter(
v => targetUserSeqs.indexOf(v) < 0
(v) => targetUserSeqs.indexOf(v) < 0
);
// 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다.
@ -137,18 +151,18 @@ export class Effects {
this.moduleConfig.useMyDeptGroup
) {
userSeqsForDelete = userSeqsForDelete.filter(
delbuddy =>
myDeptUserList.filter(deptUser => deptUser.seq === delbuddy)
(delbuddy) =>
myDeptUserList.filter((deptUser) => deptUser.seq === delbuddy)
.length === 0
);
}
userSeqsForDelete = userSeqsForDelete.filter(delbuddy => {
userSeqsForDelete = userSeqsForDelete.filter((delbuddy) => {
let exist = false;
for (const group of groupList) {
if (
group.seq !== targetGroup.seq &&
group.userSeqs.filter(v => v === delbuddy).length > 0
group.userSeqs.filter((v) => v === delbuddy).length > 0
) {
exist = true;
break;
@ -184,10 +198,10 @@ export class Effects {
withLatestFrom(this.store.pipe(select(GroupSelector.groups))),
exhaustMap(([action, groupList]) => {
// copy to
const toGroup = groupList.find(g => g.seq === action.toGroup.seq);
const toGroup = groupList.find((g) => g.seq === action.toGroup.seq);
let toTrgtUserSeqs = toGroup.userSeqs;
action.targetUserSeq.forEach(trgtSeq => {
action.targetUserSeq.forEach((trgtSeq) => {
if (toTrgtUserSeqs.indexOf(trgtSeq) > -1) {
// ignore
} else {
@ -205,7 +219,7 @@ export class Effects {
exhaustMap((resTo: GroupUpdateResponse) => {
// del from
const fromGroup = groupList.find(
g => g.seq === action.fromGroup.seq
(g) => g.seq === action.fromGroup.seq
);
const fromTrgtUserSeqs = fromGroup.userSeqs;
@ -215,17 +229,17 @@ export class Effects {
groupSeq: action.fromGroup.seq,
groupName: action.fromGroup.name,
userSeqs: fromTrgtUserSeqs.filter(
trgtSeq => action.targetUserSeq.indexOf(trgtSeq) < 0
(trgtSeq) => action.targetUserSeq.indexOf(trgtSeq) < 0
)
})
.pipe(
map((resFrom: GroupUpdateResponse) => {
return groups();
}),
catchError(error => of(moveFromFailure({ error })))
catchError((error) => of(moveFromFailure({ error })))
);
}),
catchError(error => of(moveToFailure({ error })))
catchError((error) => of(moveToFailure({ error })))
);
})
)
@ -246,21 +260,23 @@ export class Effects {
// 소속부서(내부서) 고정그룹 사용시 소속부서원을 삭제하지 않는다.
if (
!!this.moduleConfig.useMyDeptGroup &&
this.moduleConfig.useMyDeptGroup
this.moduleConfig.useMyDeptGroup &&
!!myDeptUserList &&
myDeptUserList.length > 0
) {
userSeqsForDelete = targetUserSeqs.filter(
delbuddy =>
myDeptUserList.filter(deptUser => deptUser.seq === delbuddy)
(delbuddy) =>
myDeptUserList.filter((deptUser) => deptUser.seq === delbuddy)
.length === 0
);
}
userSeqsForDelete = targetUserSeqs.filter(delbuddy => {
userSeqsForDelete = targetUserSeqs.filter((delbuddy) => {
let exist = false;
for (const group of groupList) {
if (
group.seq !== action.group.seq &&
group.userSeqs.filter(v => v === delbuddy).length > 0
group.userSeqs.filter((v) => v === delbuddy).length > 0
) {
exist = true;
break;
@ -271,7 +287,7 @@ export class Effects {
if (userSeqsForDelete.length > 0) {
// 즐겨찾기 해제.
userSeqsForDelete.forEach(buddySeq => {
userSeqsForDelete.forEach((buddySeq) => {
this.store.dispatch(
buddyActions.update({
req: {
@ -296,7 +312,7 @@ export class Effects {
map((res: GroupDelResponse) => {
return delSuccess({ res });
}),
catchError(error => of(delFailure({ error })))
catchError((error) => of(delFailure({ error })))
);
})
)
@ -305,13 +321,13 @@ export class Effects {
update$ = createEffect(() =>
this.actions$.pipe(
ofType(update),
map(action => action.req),
concatMap(req =>
map((action) => action.req),
concatMap((req) =>
this.groupProtocolService.update2(req).pipe(
map((res: GroupUpdateResponse) => {
return groups();
}),
catchError(error => of(updateFailure({ error })))
catchError((error) => of(updateFailure({ error })))
)
)
)

View File

@ -6,9 +6,13 @@
"umdModuleIds": {
"@ngrx/store": "@ngrx/store",
"@ngrx/effects": "@ngrx/effects",
"@ngrx/entity": "@ngrx/entity",
"@ucap/protocol-query": "@ucap/protocol-query",
"@ucap/protocol-status": "@ucap/protocol-status",
"@ucap/ng-api-external": "@ucap/ng-api-external",
"@ucap/ng-protocol-query": "@ucap/ng-protocol-query",
"@ucap/ng-protocol-status": "@ucap/ng-protocol-status",
"@ucap/ng-store-authentication": "@ucap/ng-store-authentication"
}
}

View File

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

View File

@ -3,9 +3,11 @@ import { Type } from '@angular/core';
import { Effects as CommonEffects } from './common/effects';
import { Effects as CompanyEffects } from './company/effects';
import { Effects as DepartmentEffects } from './department/effects';
import { Effects as PresenceEffects } from './presence/effects';
export const effects: Type<any>[] = [
CommonEffects,
CompanyEffects,
DepartmentEffects
DepartmentEffects,
PresenceEffects
];

View File

@ -0,0 +1,58 @@
import { createAction, props } from '@ngrx/store';
import {
BulkInfoRequest,
StatusBulkInfo,
StatusNotification,
StatusRequest,
StatusResponse,
MessageUpdateRequest
} from '@ucap/protocol-status';
export const bulkInfo = createAction(
'[ucap::organization::presence] Bulk Info',
props<BulkInfoRequest>()
);
export const bulkInfoSuccess = createAction(
'[ucap::organization::presence] Bulk Info Success',
props<{ statusBulkInfoList: StatusBulkInfo[] }>()
);
export const bulkInfoFailure = createAction(
'[ucap::organization::presence] Bulk Info Failure',
props<{ error: any }>()
);
export const statusNotification = createAction(
'[ucap::organization::presence] Status Notification',
props<{ noti: StatusNotification }>()
);
export const status = createAction(
'[ucap::organization::presence] Status',
props<{ req: StatusRequest }>()
);
export const statusSuccess = createAction(
'[ucap::organization::presence] Status Success',
props<{
res: StatusResponse;
}>()
);
export const statusFailure = createAction(
'[ucap::organization::presence] Status Failure',
props<{ error: any }>()
);
export const changeMyIdleCheckTime = createAction(
'[ucap::organization::presence] Change MyIdleCheckTime',
props<{ checkTime: number }>()
);
export const messageUpdate = createAction(
'[ucap::organization::presence] status message update of Others',
props<{ req: MessageUpdateRequest }>()
);
export const messageUpdateFailure = createAction(
'[ucap::organization::presence] status message update of Others Failure',
props<{ error: any }>()
);

View File

@ -0,0 +1,63 @@
import { of } from 'rxjs';
import { map, exhaustMap, catchError, tap, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import {
status,
statusSuccess,
statusFailure,
bulkInfo,
bulkInfoSuccess,
bulkInfoFailure
} from './actions';
import { StatusProtocolService } from '@ucap/ng-protocol-status';
import { StatusBulkInfo } from '@ucap/protocol-status';
@Injectable()
export class Effects {
bulkInfo$ = createEffect(
() => {
return this.actions$.pipe(
ofType(bulkInfo),
switchMap((req) => {
return this.statusProtocolService.bulkInfo(req).pipe(
map((res) => {
this.store.dispatch(
bulkInfoSuccess({
statusBulkInfoList: res.statusBulkInfoList
})
);
}),
catchError((error) => of(bulkInfoFailure({ error })))
);
})
);
},
{ dispatch: false }
);
status$ = createEffect(() =>
this.actions$.pipe(
ofType(status),
map((action) => action.req),
exhaustMap((req) => {
return this.statusProtocolService.status(req).pipe(
map((res) => {
return statusSuccess({ res });
}),
catchError((error) => of(statusFailure({ error })))
);
})
)
);
constructor(
private actions$: Actions,
private store: Store<any>,
private statusProtocolService: StatusProtocolService
) {}
}

View File

@ -0,0 +1,79 @@
import { createReducer, on } from '@ngrx/store';
import { initialState, State, adapterStatusBulkInfo } from './state';
import {
bulkInfoSuccess,
statusNotification,
statusSuccess,
bulkInfo
} from './actions';
import {
StatusBulkInfo,
TerminalStatusInfo,
TerminalStatusNumber
} from '@ucap/protocol-status';
export const reducer = createReducer(
initialState,
on(bulkInfo, (state, action) => {
return {
...state,
statusBulkInfoProcessing: true
} as State;
}),
on(bulkInfoSuccess, (state, action) => {
return {
...state,
statusBulkInfo: adapterStatusBulkInfo.upsertMany(
action.statusBulkInfoList,
{
...state.statusBulkInfo
}
),
statusBulkInfoProcessing: false
} as State;
}),
on(statusNotification, (state, action) => {
const noti = action.noti;
const statusBulkInfoState: StatusBulkInfo = {
userSeq: noti.userSeq,
conferenceStatus: noti.conferenceStatus,
imessengerStatus: noti.imessengerStatus,
mobileConferenceStatus: noti.mobileConferenceStatus,
mobileStatus: noti.mobileStatus,
pcStatus: noti.pcStatus,
phoneStatus: noti.phoneStatus,
statusMessage: noti.statusMessage,
terminalStatus: TerminalStatusInfo.Unknown,
terminalStatusNumber: TerminalStatusNumber.Unknown,
workstatus: noti.workstatus
};
return {
...state,
statusBulkInfo: adapterStatusBulkInfo.upsertOne(statusBulkInfoState, {
...state.statusBulkInfo
}),
statusBulkInfoProcessing: false
};
}),
on(statusSuccess, (state, action) => {
const statusBulkInfoState: StatusBulkInfo = {
...state.statusBulkInfo.entities[action.res.SENDER_SEQ],
pcStatus: action.res.statusType,
statusMessage: action.res.statusMessage
};
return {
...state,
statusBulkInfo: adapterStatusBulkInfo.updateOne(
{ id: action.res.SENDER_SEQ, changes: statusBulkInfoState },
{ ...state.statusBulkInfo }
),
statusBulkInfoProcessing: false
} as State;
})
);

View File

@ -0,0 +1,56 @@
import { Selector, createSelector } from '@ngrx/store';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { StatusBulkInfo } from '@ucap/protocol-status';
export interface StatusBulkInfoState extends EntityState<StatusBulkInfo> {}
export interface State {
statusBulkInfo: StatusBulkInfoState;
statusBulkInfoProcessing: boolean;
}
export const adapterStatusBulkInfo = createEntityAdapter<StatusBulkInfo>({
selectId: (statusBulkInfo) => statusBulkInfo.userSeq
});
const statusBulkInfoInitialState: StatusBulkInfoState = adapterStatusBulkInfo.getInitialState(
{}
);
export const initialState: State = {
statusBulkInfo: statusBulkInfoInitialState,
statusBulkInfoProcessing: false
};
const {
selectAll: ngeSelectAllStatusBulkInfo,
selectEntities: ngeSelectEntitiesStatusBulkInfo
} = adapterStatusBulkInfo.getSelectors();
export function selectors<S>(selector: Selector<any, State>) {
const selectStatusBulkInfo = createSelector(
selector,
(state: State) => state.statusBulkInfo
);
return {
selectAllStatusBulkInfo: createSelector(
selectStatusBulkInfo,
ngeSelectAllStatusBulkInfo
),
selectEntitiesStatusBulkInfo: createSelector(
selectStatusBulkInfo,
ngeSelectEntitiesStatusBulkInfo
),
selectStatusBulkInfo: (userSeq: number) =>
createSelector(
selectStatusBulkInfo,
ngeSelectEntitiesStatusBulkInfo,
(_, entities) => (!!entities ? entities[userSeq] : undefined)
),
statusBulkInfoProcessing: createSelector(
selector,
(state: State) => state.statusBulkInfoProcessing
)
};
}

View File

@ -3,11 +3,13 @@ import { combineReducers, Action } from '@ngrx/store';
import { reducer as CommonReducer } from './common/reducers';
import { reducer as CompanyReducer } from './company/reducers';
import { reducer as DepartmentReducer } from './department/reducers';
import { reducer as PresenceReducer } from './presence/reducers';
export function reducers(state: any | undefined, action: Action) {
return combineReducers({
common: CommonReducer,
company: CompanyReducer,
department: DepartmentReducer
department: DepartmentReducer,
presence: PresenceReducer
})(state, action);
}

View File

@ -3,6 +3,7 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as CommonState from './common/state';
import * as CompanyState from './company/state';
import * as DepartmentState from './department/state';
import * as PresenceState from './presence/state';
export const KEY_FEATURE = 'organization';
@ -10,6 +11,7 @@ export interface State {
common: CommonState.State;
company: CompanyState.State;
department: DepartmentState.State;
presence: PresenceState.State;
}
export const Selector = createFeatureSelector<State>(KEY_FEATURE);
@ -25,3 +27,7 @@ export const CompanySelector = CompanyState.selectors(
export const DepartmentSelector = DepartmentState.selectors(
createSelector(Selector, (state: State) => state.department)
);
export const PresenceSelector = PresenceState.selectors(
createSelector(Selector, (state: State) => state.presence)
);

View File

@ -5,10 +5,11 @@
import * as CommonActions from './lib/store/common/actions';
import * as CompanyActions from './lib/store/company/actions';
import * as DepartmentActions from './lib/store/department/actions';
import * as PresenceActions from './lib/store/presence/actions';
export * from './lib/config/module-config';
export { CommonActions, CompanyActions, DepartmentActions };
export { CommonActions, CompanyActions, DepartmentActions, PresenceActions };
export * from './lib/store/state';

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-authentication",
"version": "0.0.19",
"version": "0.0.20",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -10,6 +10,7 @@
"@angular/core": "^9.0.2",
"@angular/material": "^9.0.0",
"@ucap/core": "~0.0.1",
"@ucap/uc-scss": "~0.0.1",
"@ucap/ng-i18n": "~0.0.1",
"@ucap/ng-ui": "~0.0.1",
"tslib": "^1.10.0"

View File

@ -1,11 +1,12 @@
<div class="ucap-login-container">
<div class="ucap-authentication-login-container">
<ng-content select="[ucapAuthenticationLogin='header']"></ng-content>
<form name="loginForm" [formGroup]="loginForm" novalidate>
<mat-form-field
<div
class="ucap-authentication-login-input-field"
[style.display]="!!fixedCompanyCode ? 'none' : 'block'"
class="login-company"
>
<mat-form-field>
<mat-label>{{ 'login.fields.company' | ucapI18n }}</mat-label>
<mat-select
[formControl]="companyCodeFormControl"
@ -20,10 +21,11 @@
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="input-lineless">
<!--<span class="icon-img"><i class="mid mdi-account-tie-outline"></i></span>-->
<span class="icon-img">
<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">
<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"
@ -36,15 +38,15 @@
/>
</svg>
</span>
<mat-form-field class="login-id">
<mat-form-field>
<mat-label>{{ 'login.fields.loginId' | ucapI18n }}</mat-label>
<input matInput [formControl]="loginIdFormControl" />
</mat-form-field>
</div>
<div class="input-lineless">
<!--<span class="icon-img"><i class="mid mdi-lock-outline"></i></span>-->
<span class="icon-img">
<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
@ -71,7 +73,7 @@
<rect x="9.08" y="12.86" width="1.84" height="3.72" />
</svg>
</span>
<mat-form-field class="login-pw">
<mat-form-field>
<mat-label>{{ 'login.fields.loginPw' | ucapI18n }}</mat-label>
<input
matInput
@ -82,13 +84,14 @@
</mat-form-field>
</div>
<div class="error-container">
<div class="ucap-authentication-login-error-container">
<mat-error
*ngIf="
companyCodeFormControl.dirty &&
companyCodeFormControl.invalid &&
companyCodeFormControl.hasError('required')
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireCompany' | ucapI18n }}
</mat-error>
@ -98,6 +101,7 @@
loginIdFormControl.invalid &&
loginIdFormControl.hasError('required')
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireLoginId' | ucapI18n }}
</mat-error>
@ -107,17 +111,18 @@
loginPwFormControl.invalid &&
loginPwFormControl.hasError('required')
"
class="ucap-authentication-login-error"
>
{{ 'login.errors.requireLoginPw' | ucapI18n }}
</mat-error>
<mat-error *ngIf="loginFailed">
<mat-error *ngIf="loginFailed" class="ucap-authentication-login-error">
{{ 'login.errors.failed' | ucapI18n }}
</mat-error>
</div>
<button
mat-raised-button
class="submit-button bg-accent-dark"
class="ucap-authentication-login-button-submit"
aria-label="LOG IN"
[disabled]="
loginForm.invalid ||

View File

@ -1,67 +1,30 @@
$desktop-l-width: 1440px;
$tablet-l-width: 1024px;
$tablet-s-width: 768px;
$mob-l-width: 640px;
$login-max-height: 800px;
@import '~@ucap/ui-scss/ucap';
@mixin desktop-m {
@media screen and (max-width: #{$desktop-l-width}) {
@content;
}
}
// 태블릿
@mixin tab {
@media screen and (max-width: #{$tablet-l-width}) {
@content;
}
}
// 모바일 large
@mixin mob-l {
@media screen and (max-width: #{$mob-l-width}) {
@content;
}
}
.ucap-login-container {
position: relative;
transform: scale(1);
width: 100%;
min-width: 100%;
.ucap-authentication-login-container {
top: 0;
right: 0;
text-align: center;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 1);
border-radius: 0px;
font-size: 14px;
.mat-title {
margin: 10px 0 10px 0;
text-indent: -10000000px;
width: 120px;
height: 120px;
background-repeat: no-repeat;
display: inline-flex;
background-size: 100% auto;
flex: 1 1 auto;
background-position-x: center;
}
text-align: center;
form {
width: 100%;
text-align: left;
.input-lineless {
.ucap-authentication-login-input-field {
background-color: #e0e0e0;
padding: 14px 20px;
margin-top: 6px;
display: flex;
flex-flow: row;
@media screen and (max-width: #{$tablet-s-width}) {
padding: 8px 20px;
}
.icon-img {
.ucap-authentication-login-input-icon {
width: 20px;
height: 20px;
margin-right: 10px;
@ -74,33 +37,15 @@ $login-max-height: 800px;
mat-form-field {
width: 100%;
color: #333333;
&.login-pw {
//margin-top: 10px;
@media screen and (max-width: #{$tablet-s-width}) {
margin-top: 0;
}
}
}
}
mat-checkbox {
margin: 0;
}
.remember-forgot-password {
font-size: 13px;
.remember-me {
}
.auto-login {
font-size: 0.9em;
font-weight: 600;
margin-bottom: 2vh;
.ucap-authentication-login-error-container {
.ucap-authentication-login-error {
}
}
.submit-button {
.ucap-authentication-login-button-submit {
width: 100%;
margin: 0 auto;
margin: 20px 0 10px;
@ -108,115 +53,6 @@ $login-max-height: 800px;
border-radius: 0;
line-height: 50px;
font-size: 1.1em;
@media screen and (max-width: #{$tablet-s-width}) {
line-height: 38px;
}
}
}
.register {
margin: 32px auto 24px auto;
font-weight: 600;
.text {
margin-right: 8px;
}
}
.separator {
font-size: 15px;
font-weight: 600;
margin: 24px auto;
position: relative;
overflow: hidden;
width: 100px;
.text {
display: inline-flex;
position: relative;
padding: 0 8px;
z-index: 9999;
&:before,
&:after {
content: '';
display: block;
width: 30px;
position: absolute;
top: 10px;
border-top: 1px solid;
}
&:before {
right: 100%;
}
&:after {
left: 100%;
}
}
}
.policy {
cursor: pointer;
position: absolute;
bottom: 2vh;
display: block;
background-color: #58b0b1;
width: 100%;
color: #ffffff;
font-size: 0.86em;
display: flex;
flex-flow: row;
align-items: center;
&:hover {
opacity: 1;
}
.icon-img {
padding: 12px 10px;
width: 50px;
color: #ffffff;
padding-right: 10px;
border-right: 1px solid rgba(255, 255, 255, 0.7);
flex: 0 0 auto;
@media screen and (max-width: #{$tablet-s-width}) {
padding: 9px 10px;
}
}
a {
margin-left: 10px;
}
}
button {
&.google,
&.facebook {
width: 192px;
text-transform: none;
color: #ffffff;
font-size: 13px;
}
}
}
@include tab {
.ucap-login-container {
font-size: 0.9em;
.mat-title {
background-size: 90% auto;
}
.mat-form-field .mat-form-field-infix input {
font-size: 0.9em;
}
.policy {
position: absolute;
bottom: 0;
display: flex;
flex-flow: row;
padding: 0;
border-radius: 0;
width: 100%;
font-size: 0.9em;
padding: 0px;
line-height: 3em;
}
}
}

View File

@ -2,47 +2,23 @@ import { moduleMetadata } from '@storybook/angular';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { AuthenticationUiModule } from '../authentication-ui.module';
import { LoginComponent } from './login.component';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ChangeDetectorRef } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { I18nService, UCAP_I18N_NAMESPACE, I18nModule } from '@ucap/ng-i18n';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { LogService } from '@ucap/logger';
import { Company } from '@ucap/api-external';
import { I18nService, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { AuthenticationUiModule } from '../authentication-ui.module';
import { LoginComponent } from './login.component';
export default {
title: 'LoginComponent',
decorators: [
moduleMetadata({
imports: [
BrowserModule,
BrowserAnimationsModule,
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatSelectModule,
I18nModule
],
imports: [BrowserModule, BrowserAnimationsModule, AuthenticationUiModule],
providers: [
AuthenticationUiModule,
{ provide: I18nService, useValue: new I18nService(new LogService({})) },
{
provide: UCAP_I18N_NAMESPACE,

View File

@ -0,0 +1,25 @@
# UiChat
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2.
## Code scaffolding
Run `ng generate component component-name --project ui-chat` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ui-chat`.
> Note: Don't forget to add `--project ui-chat` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build ui-chat` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build ui-chat`, go to the dist folder `cd dist/ui-chat` and run `npm publish`.
## Running unit tests
Run `ng test ui-chat` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/ui-organization'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

View File

@ -0,0 +1,14 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ui-chat",
"lib": {
"entryFile": "src/public-api.ts",
"umdModuleIds": {
"ngx-perfect-scrollbar": "ngx-perfect-scrollbar",
"@ucap/core": "@ucap/core",
"@ucap/protocol-room": "@ucap/protocol-room",
"@ucap/ng-logger": "@ucap/ng-logger",
"@ucap/ng-ui": "@ucap/ng-ui"
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "@ucap/ng-ui-chat",
"version": "0.0.3",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
"peerDependencies": {
"@angular/cdk": "^9.0.0",
"@angular/common": "^9.0.2",
"@angular/core": "^9.0.2",
"@angular/material": "^9.0.0",
"@ucap/core": "~0.0.1",
"@ucap/protocol-room": "~0.0.1",
"@ucap/uc-scss": "~0.0.1",
"@ucap/ng-ui": "~0.0.1",
"tslib": "^1.10.0"
}
}

View File

@ -0,0 +1,83 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatRippleModule } from '@angular/material/core';
import { MatTreeModule } from '@angular/material/tree';
import { MatMenuModule } from '@angular/material/menu';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { UiModule } from '@ucap/ng-ui';
import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { ModuleConfig } from './config/module-config';
import { _MODULE_CONFIG } from './config/token';
import {
RoomExpansionComponent,
RoomExpansionHeaderDirective,
RoomExpansionNodeDirective
} from './components/room-expansion.component';
import { RoomListItem01Component } from './components/room-list-item-01.component';
const COMPONENTS = [RoomExpansionComponent, RoomListItem01Component];
const DIALOGS = [];
const PIPES = [];
const DIRECTIVES = [RoomExpansionHeaderDirective, RoomExpansionNodeDirective];
const SERVICES = [];
@NgModule({
declarations: [],
imports: [],
exports: []
})
export class ChatUiRootModule {}
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
ScrollingModule,
MatBadgeModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatRippleModule,
MatTreeModule,
MatMenuModule,
PerfectScrollbarModule,
I18nModule,
UiModule
],
exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
entryComponents: [...DIALOGS],
providers: [
{
provide: UCAP_I18N_NAMESPACE,
useValue: ['chat']
}
]
})
export class ChatUiModule {
public static forRoot(
config: ModuleConfig
): ModuleWithProviders<ChatUiRootModule> {
return {
ngModule: ChatUiRootModule,
providers: [{ provide: _MODULE_CONFIG, useValue: config }, ...SERVICES]
};
}
}

View File

@ -0,0 +1,63 @@
<div class="ucap-chat-room-expansion-container" fxFlexFill>
<cdk-virtual-scroll-viewport #cvsvList perfectScrollbar fxFlexFill>
<ng-container
*cdkVirtualFor="
let node of dataSource.expandedData$;
templateCacheSize: 0
"
></ng-container>
<mat-tree #treeList [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node
*matTreeNodeDef="let node"
[attr.node-type]="node?.nodeType"
matRipple
>
<li>
<div>
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: node?.node }"
></ng-container>
</div>
</li>
</mat-tree-node>
<mat-tree-node
*matTreeNodeDef="let node; when: isHeader"
class="tree-node-frame ucap-clickable"
[attr.node-type]="node?.nodeType"
matRipple
>
<li class="tree-node-header" matTreeNodeToggle>
<div class="path">
<button
mat-icon-button
[attr.aria-label]="'toggle '"
class="btn-toggle"
>
<mat-icon class="mat-icon-rtl-mirror">
{{
treeControl.isExpanded(node) ? 'expand_less' : 'expand_more'
}}
</mat-icon>
</button>
<div class="group-info">
<ng-container
[ngTemplateOutlet]="headerTemplate"
[ngTemplateOutletContext]="{ $implicit: node?.node }"
>
</ng-container>
</div>
</div>
<ul [class.group-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>
</div>

View File

@ -0,0 +1,2 @@
.ucap-chat-room-expansion-container {
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { RoomExpansionComponent } from './room-expansion.component';
describe('ucap::chat::RoomExpansionComponent', () => {
let component: RoomExpansionComponent;
let fixture: ComponentFixture<RoomExpansionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [RoomExpansionComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RoomExpansionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,181 @@
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
Component,
OnInit,
OnDestroy,
Input,
ViewChild,
ContentChild,
TemplateRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
Directive
} from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { FlatTreeControl } from '@angular/cdk/tree';
import { MatTreeFlattener, MatTree } from '@angular/material/tree';
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
import { RoomInfo } from '@ucap/protocol-room';
import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
export interface RoomNode {
nodeType: string;
roomInfo?: RoomInfo;
children?: RoomNode[];
}
export interface FlatNode {
expandable: boolean;
level: number;
node: RoomNode;
}
@Directive({
selector: '[ucapChatRoomExpansionNode]'
})
export class RoomExpansionNodeDirective {}
@Directive({
selector: '[ucapChatRoomExpansionHeader]'
})
export class RoomExpansionHeaderDirective {}
@Component({
selector: 'ucap-chat-room-expansion',
templateUrl: './room-expansion.component.html',
styleUrls: ['./room-expansion.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RoomExpansionComponent implements OnInit, OnDestroy {
@Input()
set roomGroup(list: { division: string; roomList: RoomInfo[] }[]) {
if (!list || 0 === list.length) {
} else {
list.sort((a, b) =>
a.division < b.division ? 1 : a.division > b.division ? -1 : 0
);
for (const item of list) {
const nodeType = item.division;
const node: RoomNode = {
nodeType,
children: []
};
item.roomList.sort((a, b) =>
a.finalEventDate < b.finalEventDate
? 1
: a.finalEventDate > b.finalEventDate
? -1
: 0
);
item.roomList.forEach((roomInfo) => {
node.children.push({
nodeType,
roomInfo
});
});
if (!!this.nodeMap.get(item.division)) {
this.nodeMap.get(item.division)[0].children = node.children;
} else {
this.nodeMap.set(item.division, [node]);
}
}
}
this.refreshNodes();
}
@ViewChild('treeList', { static: false })
treeList: MatTree<FlatNode>;
@ViewChild('cvsvList', { static: false })
cvsvList: CdkVirtualScrollViewport;
@ViewChild(PerfectScrollbarDirective, { static: false })
psDirectiveRef?: PerfectScrollbarDirective;
@ContentChild(RoomExpansionNodeDirective, {
read: TemplateRef,
static: false
})
nodeTemplate: TemplateRef<RoomExpansionNodeDirective>;
@ContentChild(RoomExpansionHeaderDirective, {
read: TemplateRef,
static: false
})
headerTemplate: TemplateRef<RoomExpansionHeaderDirective>;
treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<RoomNode, FlatNode>;
dataSource: VirtualScrollTreeFlatDataSource<RoomNode, FlatNode>;
private nodeMap: Map<string, RoomNode[]> = new Map();
// tslint:disable-next-line: variable-name
private _ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {
this.treeControl = new FlatTreeControl<FlatNode>(
(node) => node.level,
(node) => node.expandable
);
this.treeFlattener = new MatTreeFlattener<RoomNode, FlatNode>(
(node: RoomNode, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
level,
nodeType: node.nodeType,
node
};
},
(node) => node.level,
(node) => node.expandable,
(node) => node.children
);
this.dataSource = new VirtualScrollTreeFlatDataSource<RoomNode, FlatNode>(
this.treeControl,
this.treeFlattener
);
}
ngOnInit(): void {
this._ngOnDestroySubject = new Subject();
this.dataSource.cdkVirtualScrollViewport = this.cvsvList;
this.treeControl.expansionModel.changed
.pipe(takeUntil(this._ngOnDestroySubject))
.subscribe(() => {
this.cvsvList.checkViewportSize();
this.psDirectiveRef.update();
});
}
ngOnDestroy(): void {
if (!!this._ngOnDestroySubject) {
this._ngOnDestroySubject.next();
this._ngOnDestroySubject.complete();
}
}
isHeader = (_: number, node: FlatNode) => 0 === node.level;
private refreshNodes() {
const rootNode: RoomNode[] = [];
this.nodeMap.forEach((node) => rootNode.push(...node));
this.dataSource.data = rootNode;
this.treeControl.expandAll();
this.changeDetectorRef.detectChanges();
}
}

View File

@ -0,0 +1,74 @@
<div class="ucap-chat-room-list-item1">
<div class="ucap-chat-room-list-item1-profile-image">
<img
ucapImage
[base]="profileImageRoot"
[path]="profileImage"
[default]="defaultProfileImage"
/>
</div>
<div class="ucap-chat-room-list-item1-info">
<div class="roomName">
{{ roomName }}
<strong *ngIf="roomInfo.roomType === RoomType.Multi"
>({{ roomInfo.joinUserCount }})</strong
>
/ {{ roomInfo.receiveAlarm }}
</div>
<div class="lastMessage">{{ roomInfo.finalEventMessage }}</div>
</div>
<div class="date">{{ roomInfo.finalEventDate | ucapDate: 'LT' }}</div>
<span
class="noti-sum"
*ngIf="!!roomInfo.noReadCnt && roomInfo.noReadCnt > 0"
[matBadgeHidden]="roomInfo.noReadCnt === 0"
[matBadge]="roomInfo.noReadCnt"
matBadgeOverlap="true"
matBadgeColor="accent"
matBadgePosition="below after"
></span>
<button
mat-icon-button
aria-label="room-menu"
*ngIf="!checkable"
[matMenuTriggerFor]="roomMenu"
(click)="$event.stopPropagation()"
>
<mat-icon>more_vert</mat-icon>
</button>
<mat-checkbox
*ngIf="checkable"
#checkbox
[checked]="checked"
(change)="onToggleItem(checkbox.checked)"
(click)="$event.stopPropagation()"
class="group-check"
>
</mat-checkbox>
</div>
<mat-menu #roomMenu="matMenu">
<span class="manu-title">{{ roomName }}</span>
<button mat-menu-item (click)="onOpenChatRoom()">
<mat-icon>dialpad</mat-icon>
<span>{{ 'room.openRoom' | ucapI18n }}</span>
</button>
<button mat-menu-item (click)="onToggleAlarm()">
<mat-icon>dialpad</mat-icon>
<span>
{{
(roomInfo.receiveAlarm
? 'room.turnOffRoomAlert'
: 'room.turnOnRoomAlert'
) | ucapI18n
}}</span
>
</button>
<button mat-menu-item (click)="onDelRoom()">
<mat-icon>dialpad</mat-icon>
<span>{{ 'room.exitFromRoom' | ucapI18n }}</span>
</button>
</mat-menu>

View File

@ -0,0 +1,7 @@
.ucap-chat-room-list-item1 {
width: 100%;
height: 100%;
.ucap-chat-room-list-item1-profile-image {
}
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RoomListItem01Component } from './room-list-item-01.component';
describe('ucap::chat::RoomListItem01Component', () => {
let component: RoomListItem01Component;
let fixture: ComponentFixture<RoomListItem01Component>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [RoomListItem01Component]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(RoomListItem01Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input,
OnDestroy,
EventEmitter,
Output,
ChangeDetectorRef
} from '@angular/core';
import { RoomInfo, RoomType } from '@ucap/protocol-room';
@Component({
selector: 'ucap-chat-room-list-item-01',
templateUrl: './room-list-item-01.component.html',
styleUrls: ['./room-list-item-01.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RoomListItem01Component implements OnInit, OnDestroy {
@Input()
roomInfo: RoomInfo;
@Input()
profileImageRoot: string;
@Input()
defaultProfileImage: string;
@Input()
profileImage: string;
@Input()
roomName: string;
@Input()
checkable = false;
@Input()
checked = false;
@Output()
toggleItem = new EventEmitter<{
checked: boolean;
roomInfo: RoomInfo;
}>();
@Output()
openChatRoom = new EventEmitter<RoomInfo>();
@Output()
toggleAlarm = new EventEmitter<RoomInfo>();
@Output()
delRoom = new EventEmitter<RoomInfo>();
RoomType = RoomType;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {}
ngOnDestroy(): void {}
onToggleItem(value: boolean): void {
this.toggleItem.emit({
checked: value,
roomInfo: this.roomInfo
});
this.changeDetectorRef.detectChanges();
}
onOpenChatRoom(): void {
this.openChatRoom.emit(this.roomInfo);
}
onToggleAlarm(): void {
this.toggleAlarm.emit(this.roomInfo);
}
onDelRoom(): void {
this.delRoom.emit(this.roomInfo);
}
}

View File

@ -0,0 +1,4 @@
import { ModuleConfig as CoreModuleConfig } from '@ucap/core';
// tslint:disable-next-line: no-empty-interface
export interface ModuleConfig extends CoreModuleConfig {}

View File

@ -0,0 +1,5 @@
import { InjectionToken } from '@angular/core';
export const _MODULE_CONFIG = new InjectionToken(
'@ucap/ui-chat config of module'
);

View File

@ -0,0 +1,10 @@
/*
* Public API Surface of ui-chat
*/
export * from './lib/config/module-config';
export * from './lib/components/room-expansion.component';
export * from './lib/components/room-list-item-01.component';
export * from './lib/chat-ui.module';

View File

@ -0,0 +1,26 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.lib.json",
"angularCompilerOptions": {
"enableIvy": false
}
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tslint.json",
"rules": {
"directive-selector": [true, "attribute", "ucapChat", "camelCase"],
"component-selector": [true, "element", "ucap-chat", "kebab-case"]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-group",
"version": "0.0.27",
"version": "0.0.30",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -10,6 +10,7 @@
"@angular/core": "^9.0.2",
"@angular/material": "^9.0.0",
"@ucap/core": "~0.0.1",
"@ucap/uc-scss": "~0.0.1",
"@ucap/ng-ui": "~0.0.1",
"tslib": "^1.10.0"
}

View File

@ -1,7 +1,10 @@
<div class="ucap-group-expansion-container" fxFlexFill>
<cdk-virtual-scroll-viewport #cvsvList perfectScrollbar fxFlexFill>
<ng-container
*cdkVirtualFor="let node of dataSource.expandedData$"
*cdkVirtualFor="
let node of dataSource.expandedData$;
templateCacheSize: 0
"
></ng-container>
<mat-tree #treeList [dataSource]="dataSource" [treeControl]="treeControl">

View File

@ -0,0 +1,6 @@
.ucap-group-expansion-container {
background-position: var(
--ucap-group-expansion-container-backgroupd-position,
absolute
);
}

View File

@ -22,6 +22,9 @@ import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
export enum NodeType {
None = 'None',
@ -129,6 +132,8 @@ export class ExpansionComponent implements OnInit, OnDestroy {
this.nodeMap.set(NodeType.Default, []);
this.nodeMap.set(NodeType.Buddy, []);
this.groupList = list;
for (const item of list) {
let nodeType = NodeType.Buddy;
if (0 === item.group.seq) {
@ -186,12 +191,27 @@ export class ExpansionComponent implements OnInit, OnDestroy {
| UserInfoDN
)[] = [];
@Output()
clickMoreMenu = new EventEmitter<{
event: MouseEvent;
node: FlatNode;
}>();
@Output()
checkGroup = new EventEmitter<{
isChecked: boolean;
groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] };
}>();
@ViewChild('treeList', { static: false })
treeList: MatTree<FlatNode>;
@ViewChild('cvsvList', { static: false })
cvsvList: CdkVirtualScrollViewport;
@ViewChild(PerfectScrollbarDirective, { static: false })
psDirectiveRef?: PerfectScrollbarDirective;
@ContentChild(ExpansionNodeDirective, {
read: TemplateRef,
static: false
@ -221,8 +241,11 @@ export class ExpansionComponent implements OnInit, OnDestroy {
dataSource: VirtualScrollTreeFlatDataSource<GroupNode, FlatNode>;
NodeType = NodeType;
groupList: { group: GroupDetailData; buddyList: UserInfo[] }[];
private nodeMap: Map<NodeType, GroupNode[]> = new Map();
// tslint:disable-next-line: variable-name
private _ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {
this.treeControl = new FlatTreeControl<FlatNode>(
@ -251,12 +274,27 @@ export class ExpansionComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this._ngOnDestroySubject = new Subject();
this.dataSource.cdkVirtualScrollViewport = this.cvsvList;
this.treeControl.expansionModel.changed
.pipe(takeUntil(this._ngOnDestroySubject))
.subscribe(() => {
this.cvsvList.checkViewportSize();
this.psDirectiveRef.update();
});
}
ngOnDestroy(): void {}
ngOnDestroy(): void {
if (!!this._ngOnDestroySubject) {
this._ngOnDestroySubject.next();
this._ngOnDestroySubject.complete();
}
}
onClickHeaderMenu(event: MouseEvent, node: FlatNode) {}
onClickHeaderMenu(event: MouseEvent, node: FlatNode) {
this.clickMoreMenu.emit({ event, node });
}
isCheckedGroup(node: FlatNode): boolean {
const groupDetail = node.node.groupDetail;
@ -306,7 +344,20 @@ export class ExpansionComponent implements OnInit, OnDestroy {
return true;
}
onChangeCheckGroup(value: boolean, node: FlatNode) {}
onChangeCheckGroup(value: boolean, node: FlatNode) {
const trgGroup = node.node.groupDetail;
const groupInfos = this.groupList.filter(
(groupInfo) => groupInfo.group.seq === trgGroup.seq
);
if (groupInfos.length > 0) {
this.checkGroup.emit({
isChecked: value,
groupBuddyList: groupInfos[0]
});
}
}
isHeader = (_: number, node: FlatNode) =>
NodeType.Profile !== node.node.nodeType && 0 === node.level;
@ -324,4 +375,18 @@ export class ExpansionComponent implements OnInit, OnDestroy {
this.dataSource.data = rootNode;
this.changeDetectorRef.detectChanges();
}
expandMore() {
this.treeList.treeControl.expandAll();
if (!!this.psDirectiveRef) {
this.psDirectiveRef.scrollToTop();
}
}
expandLess() {
this.treeList.treeControl.collapseAll();
if (!!this.psDirectiveRef) {
this.psDirectiveRef.scrollToTop();
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui-organization",
"version": "0.0.12",
"version": "0.0.27",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -10,6 +10,7 @@
"@angular/core": "^9.0.2",
"@angular/material": "^9.0.0",
"@ucap/core": "~0.0.1",
"@ucap/uc-scss": "~0.0.1",
"@ucap/ng-ui": "~0.0.1",
"tslib": "^1.10.0"
}

View File

@ -0,0 +1,140 @@
<div class="ucap-organization-profile-01-container">
<!--Profile -->
<div class="profile-card-box">
<div class="user-profile-info">
<ng-container
*ngIf="isMe; then mineProfile; else otherProfile"
></ng-container>
<ng-template #otherProfile>
<!--[[ 대화상대 프로필-->
<!-- 모바일이 온라인일 경우 + mobile-ing -->
<div class="user-profile-thumb mobile-ing">
<span class="presence">온라인</span>
<div class="profileImage">
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 122px; height: 122px;"
/>
</div>
</div>
<!--]]-->
</ng-template>
<ng-template #mineProfile>
<!--[[ 내프로필-->
<div class="user-profile-thumb">
<span class="presence">온라인</span>
<div class="profileImage">
<img
src="../../../assets/images/ico/img_nophoto.svg"
style="width: 122px; height: 122px;"
/>
</div>
<div class="btn-profile-ctrl">
<button mat-mini-fab color="primary" class="mat-mini36-fab">
<mat-icon class="material-icons-outlined">camera_alt</mat-icon>
</button>
</div>
</div>
<!--]]-->
</ng-template>
<div class="userInfo">
<div class="user-n-g">
<div class="name">홍길동</div>
<div class="grade">대리</div>
</div>
<div class="deptName">
(Hong Gi Dong)
</div>
<!-- + 대화상대 프로필 추가 -->
<!--
<div class="nickName">
<div class="nickName-info">닉네임 미설정</div>
<button
mat-icon-button
aria-label="icon create"
class="color-white"
>
<mat-icon>create</mat-icon>
</button>
</div>
<div class="address-txt">
마곡 사이언스 파크 E14동 9층
</div>
-->
</div>
<div class="btn-profile-add">
<button
mat-icon-button
class="btn-star-add"
aria-label="Example icon-button with a heart icon"
>
<img src="../../../assets/images/ico/btn_favorite_w24_s.svg" alt="" />
</button>
<button
mat-icon-button
class="btn-star-add"
aria-label="Example icon-button with a heart icon"
>
<img src="../../../assets/images/ico/btn_group_add_w24.svg" alt="" />
</button>
</div>
</div>
<!-- + 대회상대 프로필-->
<!--
<div class="btn-partner-set">
<button mat-icon-button aria-label="chat">
<img
src="../../../assets/images/ico/btn_lise_chat_a24.svg"
alt=""
/>
</button>
<button mat-icon-button aria-label="message">
<img
src="../../../assets/images/ico/btn_list_message_a24.svg"
alt=""
/>
</button>
<button mat-icon-button aria-label="mobile">
<img
src="../../../assets/images/ico/btn_list_mobile_a24.svg"
alt=""
/>
</button>
<button mat-icon-button aria-label="call">
<img
src="../../../assets/images/ico/btn_list_call_a24.svg"
alt=""
/>
</button>
<button mat-icon-button aria-label="vc">
<img
src="../../../assets/images/ico/btn_list_vc-a24.svg"
alt=""
/>
</button>
</div>
-->
<!-- + 내프로필 -->
<div class="my-input inputtype">
<mat-form-field class="example-full-width my-in-input" appearance="none">
<mat-label></mat-label>
<input matInput placeholder="" value="마곡 사이언스 파크 E14동 9층" />
</mat-form-field>
<button mat-icon-button aria-label="icon create" class="color-white">
<mat-icon>create</mat-icon>
</button>
</div>
<div class="user-profile-info-list">
<ul>
<li><span>회사</span> LGCNS</li>
<li><span>부서</span> 아키텍쳐 솔루션</li>
<li><span>이메일</span> gildonghong@cns.com</li>
<li><span>사무실</span> 02-9876-5432</li>
<li><span>모바일</span> 010-1234-5678</li>
</ul>
</div>
</div>
<!-- //Profile-->
</div>

View File

@ -0,0 +1,178 @@
@import '~@ucap/ui-scss/ucap';
.ucap-organization-profile-01-container {
width: 100%;
height: 100%;
min-width: 450px;
@include screen(mid) {
height: auto;
}
.profile-card-box {
display: flex;
flex-direction: column;
padding: 60px 8.7%;
width: 100%;
position: relative;
.user-profile-info {
display: inline-flex;
flex-direction: row;
align-items: center;
// Profile thumb//////////////////
.user-profile-thumb {
}
//////////////////Profile thumb //
.userInfo {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding-left: 38px;
.user-n-g {
display: flex;
flex-flow: row-reverse nowrap;
align-items: flex-end;
height: 44px;
.name {
font: {
size: 26px;
weight: 600;
}
order: 1;
-ms-flex-order: 1;
}
.grade {
font: {
size: 18px;
}
color: #f1f1f1;
margin-left: 6px;
order: 0;
-ms-flex-order: 0;
}
& + .deptName {
margin-top: 9px;
}
}
.deptName {
font-size: 22px;
line-height: 25px;
font-weight: 600;
}
.nickName {
display: flex;
flex-direction: row;
margin-top: 18px;
align-items: center;
.nickName-info {
padding: 0 16px;
height: 30px;
line-height: 30px;
border-radius: 15px;
border: solid 1px #fc5182;
background-color: rgba(255, 255, 255, 0.95);
font-size: 14px;
}
button {
}
}
.address-txt {
font-size: 16px;
line-height: 21px;
margin-top: 20px;
}
}
.btn-profile-add {
position: absolute;
z-index: 5;
top: 40px;
right: 40px;
button {
margin: 0 2px;
&.btn-star-add {
line-height: 24px !important;
}
}
}
}
.btn-partner-set {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 25px;
border-top: 1px solid rgba(255, 255, 255, 0.8);
border-bottom: 1px solid rgba(255, 255, 255, 0.8);
height: 70px;
margin-top: 30px;
img {
vertical-align: top;
}
}
.my-input {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
margin: 0;
width: 100%;
margin-top: 78px;
.my-in-input {
font-size: 16px;
flex-grow: 1;
height: 24px;
line-height: 24px;
margin-top: 8px;
}
button {
margin-bottom: 5px;
}
}
.user-profile-info-list {
margin-top: 60px;
ul {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 250px;
li {
font-size: 16px;
font-weight: 600;
span {
width: 100px;
height: 34px;
border-radius: 18px;
border: solid 1px #f8f9fd;
background-color: #aaa0a5;
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 40px;
}
}
}
}
}
///////////////////////////////////////////////////////////////////////////////// 제거
.profile-card {
//mat-card
width: 100%;
background-color: transparent;
box-shadow: none;
.profileImage {
width: 126px;
height: 126px;
border-radius: 50%;
box-sizing: border-box;
overflow: hidden;
display: flex;
img {
display: flex;
align-items: center;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////
}

View File

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

View File

@ -0,0 +1,28 @@
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 { OrganizationUiModule } from '../organization-ui.module';
import { Profile01Component } from './profile-01.component';
export default {
title: 'Profile01Component',
component: Profile01Component,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, OrganizationUiModule],
providers: []
})
]
};
export const Text = () => ({
component: Profile01Component,
props: {
text: 'Hello Profile01Component'
}
});

View File

@ -0,0 +1,108 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Output,
EventEmitter,
ViewChild,
ElementRef
} from '@angular/core';
import { UserInfoSS, AuthResponse } from '@ucap/protocol-query';
import { OpenProfileOptions } from '@ucap/protocol-buddy';
import { FileUploadItem } from '@ucap/api';
import { Subject } from 'rxjs';
@Component({
selector: 'ucap-organization-profile-01',
templateUrl: './profile-01.component.html',
styleUrls: ['./profile-01.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Profile01Component implements OnInit, OnDestroy {
@Input()
authRes: AuthResponse;
@Input()
userInfo: UserInfoSS;
@Input()
profileImageRoot: string;
@Input()
isMe: boolean;
@Input()
isBuddy: boolean;
@Input()
isFavorite: boolean;
@Input()
myMadn: string;
@Input()
canVideoConfernece: boolean;
@Input()
canBuddyToggleButton: boolean;
@Input()
openProfileOptions: OpenProfileOptions;
@Output()
toggleFavorite: EventEmitter<{
userInfo: UserInfoSS;
isFavorite: boolean;
}> = new EventEmitter();
@Output()
toggleBuddy: EventEmitter<{
userInfo: UserInfoSS;
isBuddy: boolean;
}> = new EventEmitter();
@Output()
openChat: EventEmitter<UserInfoSS> = new EventEmitter();
@Output()
sendCall: EventEmitter<string> = new EventEmitter();
@Output()
sendSms: EventEmitter<string> = new EventEmitter();
@Output()
createConference: EventEmitter<number> = new EventEmitter();
@Output()
sendMessage: EventEmitter<UserInfoSS> = new EventEmitter();
@Output()
profileImageView: EventEmitter<void> = new EventEmitter();
@Output()
uploadProfileImage: EventEmitter<FileUploadItem> = new EventEmitter();
@Output()
changeIntro: EventEmitter<string> = new EventEmitter();
@ViewChild('profileImageFileInput', { static: false })
profileImageFileInput: ElementRef<HTMLInputElement>;
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();
}
}
}

View File

@ -0,0 +1,50 @@
<div class="ucap-organization-profile-list-item-01-container">
<div class="user-profile-info">
<div class="user-profile-thumb mobile-ing">
<!-- 모바일이 온라인일 경우 + mobile-ing -->
<span class="presence other-business">다른용무</span>
<!-- 기본값:온라인, +offline:오프라인, +absence:부재중, +other-business:다른용무 -->
<!-- <span class="ucap-organization-profile-list-item-presence">bullet</span> -->
<div class="profile-image" (click)="onClickOpenProfile($event)">
<img
ucapImage
[base]="profileImageRoot"
[path]="userInfo.profileImageFile"
[default]="defaultProfileImage"
/>
</div>
</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="company-info">
<div class="companyName">{{ userInfo.companyName }}</div>
<div class="email">{{ userInfo.email }}</div>
</div>
<div class="contact">
<div class="mobileNumber" (click)="onSendCall($event, CallType.Mobile)">
{{ userInfo.hpNumber | ucapPhoneNumber }}
</div>
<div class="officNumber" (click)="onSendCall($event, CallType.Office)">
{{ userInfo.lineNumber | ucapPhoneNumber }}
</div>
</div>
<div *ngIf="checkable">
<mat-checkbox
#checkbox
[checked]="checked"
(change)="onChangeCheck(checkbox.checked)"
></mat-checkbox>
</div>
</div>

View File

@ -0,0 +1,6 @@
@import '~@ucap/ui-scss/ucap';
.ucap-organization-profile-list-item-01-container {
width: 100%;
height: 100%;
}

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,16 @@
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { ProfileListItem01Component } from './profile-list-item-01.component';
export default {
title: 'ProfileListItem01Component',
component: ProfileListItem01Component
};
export const Text = () => ({
component: ProfileListItem01Component,
props: {
text: 'Hello ProfileListItem01Component'
}
});

View File

@ -0,0 +1,97 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Output,
EventEmitter
} from '@angular/core';
import { UserInfoSS } from '@ucap/protocol-query';
import { StatusBulkInfo } from '@ucap/protocol-status';
import { CallType } from '../types/call.type';
import { Subject } from 'rxjs';
@Component({
selector: 'ucap-organization-profile-list-item-01',
templateUrl: './profile-list-item-01.component.html',
styleUrls: ['./profile-list-item-01.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileListItem01Component implements OnInit, OnDestroy {
@Input()
userInfo: UserInfoSS;
@Input()
presenceInfo?: StatusBulkInfo;
@Input()
profileImageRoot: string;
@Input()
defaultProfileImage: string;
@Input()
authCall = false;
@Input()
checkable = true;
@Input()
checked = false;
@Output()
openProfile = new EventEmitter<UserInfoSS>();
@Output()
changeCheck = new EventEmitter<{
isChecked: boolean;
userInfo: UserInfoSS;
}>();
@Output()
sendCall = new EventEmitter<{
type: CallType;
}>();
CallType = CallType;
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();
}
}
onClickOpenProfile(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
this.openProfile.emit(this.userInfo);
}
onChangeCheck(isChecked: boolean): void {
this.changeCheck.emit({
isChecked,
userInfo: this.userInfo
});
}
onSendCall(event: MouseEvent, type: CallType): void {
event.preventDefault();
event.stopPropagation();
this.sendCall.emit({
type
});
}
}

View File

@ -0,0 +1,10 @@
<div class="ucap-organization-profile-list-container">
<cdk-virtual-scroll-viewport #cvsvList perfectScrollbar fxFlexFill>
<ng-container *cdkVirtualFor="let userInfo of userInfos">
<ng-container
[ngTemplateOutlet]="nodeTemplate"
[ngTemplateOutletContext]="{ $implicit: userInfo }"
></ng-container>
</ng-container>
</cdk-virtual-scroll-viewport>
</div>

View File

@ -0,0 +1,6 @@
@import '~@ucap/ui-scss/ucap';
.ucap-organization-profile-list-container {
width: 100%;
height: 100%;
}

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 { ProfileListComponent } from './profile-list.component';
describe('ucap::organization::ProfileListComponent', () => {
let component: ProfileListComponent;
let fixture: ComponentFixture<ProfileListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProfileListComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProfileListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
import { ProfileListComponent } from './profile-list.component';
export default {
title: 'ProfileListComponent',
component: ProfileListComponent
};
export const Text = () => ({
component: ProfileListComponent,
props: {
text: 'Hello ProfileListComponent'
}
});

View File

@ -0,0 +1,55 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Directive,
ContentChild,
TemplateRef
} from '@angular/core';
import { UserInfo } from '@ucap/protocol-sync';
import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query';
export type UserInfoTypes = UserInfo | UserInfoSS | UserInfoF | UserInfoDN;
@Directive({
selector: '[ucapOrganizationProfileListNode]'
})
export class ProfileListNodeDirective {}
@Component({
selector: 'ucap-organization-profile-list',
templateUrl: './profile-list.component.html',
styleUrls: ['./profile-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileListComponent implements OnInit, OnDestroy {
@Input()
userInfos: UserInfo[];
@ContentChild(ProfileListNodeDirective, {
read: TemplateRef,
static: false
})
nodeTemplate: TemplateRef<ProfileListNodeDirective>;
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();
}
}
}

View File

@ -0,0 +1,55 @@
<div class="ucap-organization-search-for-tenant-container">
<div
class="ucap-organization-search-for-tenant-inner ucap-mat-input-container"
fxLayout="row"
>
<div fxFlex="0 0 auto">
<mat-form-field appearance="none" class="ucap-mat-form-field-for-select">
<mat-select
placeholder=""
class="ucap-mat-select"
[value]="defaultCompany"
disableOptionCentering
(selectionChange)="onChangeTenant($event)"
>
<mat-option
*ngFor="let company of companyList"
[value]="company.companyCode"
>{{ company.companyName }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex="1 1 auto">
<mat-form-field
fxFlexFill
class="ucap-mat-form-field-for-input"
appearance="none"
enabled="true"
matAutosize="true"
>
<input
matInput
#searchWordInput
[placeholder]="placeholder"
(keydown.enter)="onKeyDownEnter(searchWordInput.value)"
/>
<button
mat-button
matSuffix
mat-icon-button
aria-label="Clear"
(click)="searchWordInput.value = ''; onClickCancel()"
class="btn-close"
>
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<div fxFlex="0 0 40px">
<button mat-flat-button color="primary" class="btn-ico-search">
<mat-icon>search</mat-icon>
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
@import '~@ucap/ui-scss/ucap';
.ucap-organization-search-for-tenant-container {
width: 100%;
height: 100%;
.ucap-organization-search-for-tenant-inner {
}
}

View File

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

View File

@ -0,0 +1,28 @@
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 { OrganizationUiModule } from '../organization-ui.module';
import { SearchForTenantComponent } from './search-for-tenant.component';
export default {
title: 'SearchForTenantComponent',
component: SearchForTenantComponent,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, OrganizationUiModule],
providers: []
})
]
};
export const Text = () => ({
component: SearchForTenantComponent,
props: {
text: 'Hello SearchForTenantComponent'
}
});

View File

@ -0,0 +1,69 @@
import { Subject } from 'rxjs';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from '@angular/core';
import { Company } from '@ucap/api-external';
import { MatSelectChange } from '@angular/material/select';
@Component({
selector: 'ucap-organization-search-for-tenant',
templateUrl: './search-for-tenant.component.html',
styleUrls: ['./search-for-tenant.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchForTenantComponent implements OnInit, OnDestroy {
@Input()
companyList: Company[];
@Input()
defaultCompany: string;
@Input()
placeholder: string;
@Output()
changed: EventEmitter<{
companyCode: string;
searchWord: string;
}> = new EventEmitter();
@Output()
canceled: EventEmitter<void> = new EventEmitter();
companyCode: string;
private ngOnDestroySubject: Subject<void>;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject();
this.companyCode = this.defaultCompany;
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onChangeTenant(event: MatSelectChange) {
this.companyCode = event.value;
}
onKeyDownEnter(searchWord: string) {
this.changed.emit({ companyCode: this.companyCode, searchWord });
}
onClickCancel() {
this.canceled.emit();
}
}

View File

@ -34,7 +34,7 @@
<div
class="tree-node-body"
[ngClass]="
currentDeptSeq === node?.data?.deptInfo.seq ? 'current' : ''
currentDeptSeq === node?.data?.deptInfo?.seq ? 'current' : ''
"
>
<span class="horizontal-line"></span>

View File

@ -0,0 +1,33 @@
@import '~@ucap/ui-scss/ucap';
.ucap-organization-tree-container {
padding-left: 17px;
height: 100%;
.mat-tree {
background-color: transparent;
padding-right: 17px;
&:before {
content: '';
width: 21px;
height: 100%;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
position: absolute;
z-index: -1;
left: 10px;
bottom: 0;
}
&:after {
content: '';
width: 21px;
height: 100%;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
position: absolute;
z-index: -1;
left: 50px;
bottom: 0;
}
}
}

View File

@ -3,14 +3,11 @@ import {
OnInit,
OnDestroy,
Input,
Output,
EventEmitter,
ViewChild,
ContentChild,
TemplateRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
Directive
EventEmitter,
Output
} from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@ -24,6 +21,9 @@ import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
import { LogService } from '@ucap/ng-logger';
import { LoginResponse } from '@ucap/protocol-authentication';
import { trigger, transition, style, animate } from '@angular/animations';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar';
export interface OrganizationNode {
deptInfo: DeptInfo;
@ -119,16 +119,25 @@ export class TreeComponent implements OnInit, OnDestroy {
// tslint:disable-next-line: variable-name
_deptInfoList: DeptInfo[];
@Output()
clickNode = new EventEmitter<DeptInfo>();
@ViewChild('treeList', { static: false })
treeList: MatTree<FlatNode>;
@ViewChild('cvsvList', { static: false })
cvsvList: CdkVirtualScrollViewport;
@ViewChild(PerfectScrollbarDirective, { static: false })
psDirectiveRef?: PerfectScrollbarDirective;
treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<OrganizationNode, FlatNode>;
dataSource: VirtualScrollTreeFlatDataSource<OrganizationNode, FlatNode>;
// tslint:disable-next-line: variable-name
private _ngOnDestroySubject: Subject<void>;
constructor(
private changeDetectorRef: ChangeDetectorRef,
private logService: LogService
@ -158,12 +167,27 @@ export class TreeComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this._ngOnDestroySubject = new Subject();
this.dataSource.cdkVirtualScrollViewport = this.cvsvList;
this.treeControl.expansionModel.changed
.pipe(takeUntil(this._ngOnDestroySubject))
.subscribe(() => {
this.cvsvList.checkViewportSize();
this.psDirectiveRef.update();
});
}
ngOnDestroy(): void {}
ngOnDestroy(): void {
if (!!this._ngOnDestroySubject) {
this._ngOnDestroySubject.next();
this._ngOnDestroySubject.complete();
}
}
hasChild = (_: number, node: FlatNode) => node.expandable;
onClickNode(node: FlatNode) {}
onClickNode(node: FlatNode) {
this.clickNode.emit(node.data.deptInfo);
}
}

View File

@ -7,8 +7,11 @@ import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatRippleModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { MatTreeModule } from '@angular/material/tree';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
@ -18,17 +21,29 @@ import { UiModule } from '@ucap/ng-ui';
import { ModuleConfig } from './config/module-config';
import { _MODULE_CONFIG } from './config/token';
import { ProfileListItemComponent } from './components/profile-list-item.component';
import { Profile01Component } from './components/profile-01.component';
import { ProfileListItem01Component } from './components/profile-list-item-01.component';
import {
ProfileListComponent,
ProfileListNodeDirective
} from './components/profile-list.component';
import { SearchForTenantComponent } from './components/search-for-tenant.component';
import { TreeComponent } from './components/tree.component';
import { TranslatePipe } from './pipes/translate.pipe';
import { TranslateService } from './services/translate.service';
const COMPONENTS = [TreeComponent, ProfileListItemComponent];
const COMPONENTS = [
TreeComponent,
Profile01Component,
ProfileListItem01Component,
ProfileListComponent,
SearchForTenantComponent
];
const DIALOGS = [];
const PIPES = [TranslatePipe];
const DIRECTIVES = [];
const DIRECTIVES = [ProfileListNodeDirective];
const SERVICES = [TranslateService];
@NgModule({
@ -46,8 +61,11 @@ export class OrganizationUiRootModule {}
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatRippleModule,
MatSelectModule,
MatTreeModule,
PerfectScrollbarModule,

View File

@ -113,7 +113,7 @@ export class TranslateService {
if (target instanceof Array) {
const result: string[] = [];
target.forEach(t => {
target.forEach((t) => {
result.push(this.get(t, key, separator));
});
return result.join(!!separator ? separator : '');
@ -127,7 +127,12 @@ export class TranslateService {
const langKey = `${key}${lang}`;
if (-1 !== keys.indexOf(langKey)) {
return target[langKey];
const rtnValue = target[langKey];
if (!!rtnValue && rtnValue.trim().length > 0) {
return rtnValue;
} else {
return target[key];
}
}
} catch (error) {}

View File

@ -0,0 +1,4 @@
export enum CallType {
Office = 'OFFICE',
Mobile = 'MOBILE'
}

View File

@ -4,11 +4,14 @@
export * from './lib/config/module-config';
export * from './lib/components/profile-list-item.component';
export * from './lib/components/profile-01.component';
export * from './lib/components/profile-list-item-01.component';
export * from './lib/components/tree.component';
export * from './lib/pipes/translate.pipe';
export * from './lib/services/translate.service';
export * from './lib/types/call.type';
export * from './lib/organization-ui.module';

View File

@ -4,5 +4,7 @@
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
"peerDependencies": {}
"peerDependencies": {
"@ucap/uc-scss": "~0.0.1"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@ucap/ng-ui",
"version": "0.0.5",
"version": "0.0.11",
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-ucap/"
},
@ -12,6 +12,7 @@
"@ucap/core": "~0.0.1",
"@ucap/api": "~0.0.1",
"@ucap/protocol-event": "~0.0.1",
"@ucap/uc-scss": "~0.0.1",
"@ucap/ng-core": "~0.0.1",
"@ucap/ng-api-common": "~0.0.1",
"autolinker": "^3.13.0",

View File

@ -1,39 +1,29 @@
import { storiesOf, moduleMetadata } from '@storybook/angular';
import { withKnobs, text, object } from '@storybook/addon-knobs';
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 { UiModule } from '../ui.module';
import { FloatActionButtonComponent } from './float-action-button.component';
import { ucapAnimations } from '../animations';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSliderModule } from '@angular/material/slider';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CommonModule } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { I18nModule } from '@ucap/ng-i18n';
storiesOf('ui/FloatActionButtonComponent', module)
.addDecorator(withKnobs)
.addDecorator(
export default {
title: 'FloatActionButtonComponent',
component: FloatActionButtonComponent,
decorators: [
moduleMetadata({
imports: [BrowserModule, BrowserAnimationsModule, UiModule],
providers: []
})
)
.add(
'Default',
() => {
const props: { [K in keyof FloatActionButtonComponent]?: any } = {
]
};
export const Text = () => ({
props: {
buttons: object('Buttons', [
{
icon: 'chat',
@ -50,25 +40,5 @@ storiesOf('ui/FloatActionButtonComponent', module)
console.log(e);
action('buttonClick')(e);
}
};
return {
component: FloatActionButtonComponent,
props
};
},
{
note: 'Default component.'
}
);
// export default {
// title: 'FloatActionButtonComponent',
// component: FloatActionButtonComponent
// };
// export const Text = () => ({
// component: FloatActionButtonComponent,
// props: {
// text: 'Hello FloatActionButtonComponent'
// }
// });
});

View File

@ -6,9 +6,13 @@ import { PhoneNumberUtil } from '../utils/phone-number.util';
export class PhoneNumberPipe implements PipeTransform {
public transform(
value: string,
country: CountryCode,
country?: CountryCode,
mask: boolean = false
): string {
return PhoneNumberUtil.format(country, value, mask);
if (!!value && value.trim().length > 0) {
return PhoneNumberUtil.format(value, country, mask);
} else {
return value;
}
}
}

View File

@ -265,4 +265,41 @@ export class DateService {
return m.format('MM.DD');
}
}
/**
* Checking today
* @returns boolean true is today.
*/
public isToday(date: any, options?: DateOptions): boolean {
if (!date || 0 === date.length) {
return false;
}
let d = moment.tz(date, 'Asia/Seoul').utc();
d = d.tz(this.currentTimezone);
if (!!options) {
if (!!options.manipulate) {
if (!!options.manipulate.amount && !!options.manipulate.unit) {
d = d.add(options.manipulate.amount, options.manipulate.unit);
}
if (!!options.manipulate.startOf) {
d = d.startOf(options.manipulate.startOf);
}
if (!!options.manipulate.endOf) {
d = d.startOf(options.manipulate.endOf);
}
}
}
const today = moment();
if (d.isSame(today, 'day')) {
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,52 @@
import { TemplateRef, Injectable } from '@angular/core';
import { ComponentType } from '@angular/cdk/portal';
import {
MatDialog,
MatDialogConfig,
MatDialogRef
} from '@angular/material/dialog';
import { of } from 'rxjs';
import { take, map, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DialogService {
constructor(private matDialog: MatDialog) {}
public open<T, D = any, R = any>(
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
config?: MatDialogConfig<D>
): Promise<R> {
return new Promise<R>((resolve, reject) => {
config = { ...config, autoFocus: false };
const dialogRef = this.matDialog.open<T, D, R>(
componentOrTemplateRef,
config
);
dialogRef
.afterClosed()
.pipe(
take(1),
map(result => {
return resolve(result);
}),
catchError(err => {
return of(reject(err));
})
)
.subscribe();
});
}
getDialogById(id: string): MatDialogRef<any, any> {
return this.matDialog.getDialogById(id);
}
closeAll() {
this.matDialog.closeAll();
}
}

View File

@ -1,10 +1,19 @@
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js';
export class PhoneNumberUtil {
static format(country: CountryCode, value: string, mask: boolean = false) {
static format(
value: string,
country: CountryCode = 'KR',
mask: boolean = false
) {
const phoneNumber = parsePhoneNumberFromString(value, country);
const s = phoneNumber.formatNational();
let s = value;
if (!!phoneNumber) {
if (!!phoneNumber.isValid()) {
s = phoneNumber.formatNational();
}
}
if (!mask) {
return s;