This commit is contained in:
richard-loafle 2020-05-08 15:22:50 +09:00
parent 0fc503db87
commit 550fff3849
197 changed files with 7904 additions and 3318 deletions

4346
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -69,15 +69,15 @@
"@ucap/ng-protocol-sync": "~0.0.3",
"@ucap/ng-protocol-umg": "~0.0.3",
"@ucap/ng-store-authentication": "~0.0.10",
"@ucap/ng-store-chat": "~0.0.5",
"@ucap/ng-store-group": "~0.0.6",
"@ucap/ng-store-chat": "~0.0.6",
"@ucap/ng-store-group": "~0.0.7",
"@ucap/ng-store-organization": "~0.0.4",
"@ucap/ng-web-socket": "~0.0.2",
"@ucap/ng-web-storage": "~0.0.3",
"@ucap/ng-ui": "~0.0.4",
"@ucap/ng-ui-organization": "~0.0.2",
"@ucap/ng-ui-authentication": "~0.0.16",
"@ucap/ng-ui-group": "~0.0.3",
"@ucap/ng-ui": "~0.0.7",
"@ucap/ng-ui-organization": "~0.0.15",
"@ucap/ng-ui-authentication": "~0.0.19",
"@ucap/ng-ui-group": "~0.0.28",
"@ucap/ng-ui-skin-default": "~0.0.1",
"@ucap/pi": "~0.0.5",
"@ucap/protocol": "~0.0.17",
@ -97,7 +97,7 @@
"@ucap/protocol-sync": "~0.0.4",
"@ucap/protocol-umg": "~0.0.5",
"@ucap/web-socket": "~0.0.10",
"@ucap/web-storage": "~0.0.5",
"@ucap/web-storage": "~0.0.9",
"autolinker": "^3.13.0",
"axios": "^0.19.2",
"classlist.js": "^1.1.20150312",

View File

@ -14,10 +14,16 @@ import { AppSessionResolver } from './resolvers/app-session.resolver';
import { AppAuthenticationService } from './services/app-authentication.service';
import { AppNativeService } from './services/app-native.service';
import { AppService } from './services/app.service';
import { AppChatService } from './services/app-chat.service';
const GUARDS = [AppAuthenticationGuard];
const RESOLVERS = [AppSessionResolver];
const SERVICES = [AppService, AppAuthenticationService, AppNativeService];
const SERVICES = [
AppService,
AppAuthenticationService,
AppNativeService,
AppChatService
];
const axiosFactory = () => {
const i = axios.create();

View File

@ -1,2 +1,3 @@
<app-layouts-top-bar></app-layouts-top-bar>
<router-outlet></router-outlet>
<div class="app-container">
<router-outlet></router-outlet>
</div>

View File

@ -1,4 +1,9 @@
:host {
width: 100%;
height: auto !important;
height: 100%;
.app-container {
width: 100%;
height: 100%;
}
}

View File

@ -12,6 +12,9 @@ import { debounce } from 'rxjs/operators';
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
showTopbar = true;
showFooter = false;
private resizeWindowSubscription: Subscription;
constructor(private store: Store<any>) {

View File

@ -1,7 +1,9 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
@ -65,6 +67,8 @@ import { environment } from '@environments';
BrowserModule,
BrowserAnimationsModule,
FlexLayoutModule,
LoggerModule.forRoot({}),
CommonApiModule.forRoot(environment.commonApiModuleConfig),

View File

@ -16,164 +16,32 @@ $typography: mat-typography-config(
// Setup the typography
@include angular-material-typography($typography);
@mixin components-theme($theme) {
}
// -----------------------------------------------------------------------------------------------------
// @ Define the default theme
// @ Define theme --LG RED
// -----------------------------------------------------------------------------------------------------
// Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/
$lgRed-app-primary: mat-palette($lg-red);
$lgRed-app-accent: mat-palette($lg-red, A200, A100, A400);
// Define the primary, accent and warn palettes
$default-primary-palette: mat-palette($mat-indigo);
$default-accent-palette: mat-palette($mat-light-blue, 600, 400, 700);
$default-warn-palette: mat-palette($mat-red);
// The warn palette is optional (defaults to red).
$lgRed-app-warn: mat-palette($lg-red);
// Create the Material theme object
$theme: mat-light-theme(
$default-primary-palette,
$default-accent-palette,
$default-warn-palette
// Create the theme object (a Sass map containing all of the palettes).
$lgRed-app-theme: mat-light-theme(
$lgRed-app-primary,
$lgRed-app-accent,
$lgRed-app-warn
);
// Add ".theme-default" class to the body to activate this theme.
// Class name must start with "theme-" !!!
/*body.theme-default {
// Create an Angular Material theme from the $theme map
@include angular-material-theme($theme);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
// that you are using.
@include angular-material-theme($lgRed-app-theme);
// Apply the theme to the user components
@include components-theme($theme);
@include ucap-material-theme($theme);
}*/
// -----------------------------------------------------------------------------------------------------
// @ Define a blue-gray dark theme
// -----------------------------------------------------------------------------------------------------
// Define the primary, accent and warn palettes
$blue-gray-dark-theme-primary-palette: mat-palette($mat-blue);
$blue-gray-dark-theme-accent-palette: mat-palette($mat-blue-gray);
$blue-gray-dark-theme-warn-palette: mat-palette($mat-red);
// Create the Material theme object
$blue-gray-dark-theme: mat-dark-theme(
$blue-gray-dark-theme-primary-palette,
$blue-gray-dark-theme-accent-palette,
$blue-gray-dark-theme-warn-palette
);
// Add ".theme-blue-gray-dark" class to the body to activate this theme.
// Class name must start with "theme-" !!!
body.theme-blue-gray-dark {
// Generate the Angular Material theme
@include angular-material-theme($blue-gray-dark-theme);
// Apply the theme to the user components
@include components-theme($blue-gray-dark-theme);
}
// -----------------------------------------------------------------------------------------------------
// @ Define a pink dark theme
// -----------------------------------------------------------------------------------------------------
// Define the primary, accent and warn palettes
$pink-dark-theme-primary-palette: mat-palette($mat-pink);
$pink-dark-theme-accent-palette: mat-palette($mat-pink);
$pink-dark-theme-warn-palette: mat-palette($mat-red);
// Create the Material theme object
$pink-dark-theme: mat-dark-theme(
$pink-dark-theme-primary-palette,
$pink-dark-theme-accent-palette,
$pink-dark-theme-warn-palette
);
// Add ".theme-pink-dark" class to the body to activate this theme.
// Class name must start with "theme-" !!!
body.theme-pink-dark {
// Generate the Angular Material theme
@include angular-material-theme($pink-dark-theme);
// Apply the theme to the user components
@include components-theme($pink-dark-theme);
}
// -----------------------------------------------------------------------------------------------------
// @ Define a pink light theme --LG RED 변경 예정(샘플링)
// -----------------------------------------------------------------------------------------------------
// Define the primary, accent and warn palettes
$lgRed-light-theme-primary-palette: mat-palette($mat-grey, 800);
$lgRed-light-theme-accent-palette: mat-palette($lg-red, 400);
$lgRed-light-theme-warn-palette: mat-palette($mat-cyan);
// Create the Material theme object
$lgRed-light-theme: mat-light-theme(
$lgRed-light-theme-primary-palette,
$lgRed-light-theme-accent-palette,
$lgRed-light-theme-warn-palette
);
// Add ".theme-pink-dark" class to the body to activate this theme.
// Class name must start with "theme-" !!!
body.theme-lgRed {
// Generate the Angular Material theme
@include angular-material-theme($lgRed-light-theme);
// Apply the theme to the user components
@include components-theme($lgRed-light-theme);
@include ucap-material-theme($lgRed-light-theme);
}
// -----------------------------------------------------------------------------------------------------
//aqua-blue-daesang
// -----------------------------------------------------------------------------------------------------
$aquaBlue-light-theme-primary-palette: mat-palette($daesang-grey, 900);
$aquaBlue-theme-accent-palette: mat-palette($aquaBlue-daesang);
$aquaBlue-theme-warn-palette: mat-palette($mat-orange);
// Create the Material theme object
$aquaBlue-light-theme: mat-light-theme(
$aquaBlue-light-theme-primary-palette,
$aquaBlue-theme-accent-palette,
$aquaBlue-theme-warn-palette
);
// Add ".theme-pink-dark" class to the body to activate this theme.
// Class name must start with "theme-" !!!
body.theme-default {
// Generate the Angular Material theme
@include angular-material-theme($aquaBlue-light-theme);
// Apply the theme to the user components
@include components-theme($aquaBlue-light-theme);
@include ucap-material-theme($aquaBlue-light-theme);
}
// -----------------------------------------------------------------------------------------------------
// @ Define a red theme --LF 변경 예정(샘플링)
// -----------------------------------------------------------------------------------------------------
// Define the primary, accent and warn palettes
$lfRed-light-theme-primary-palette: mat-palette($lf-blue-grey, 800);
$lfRed-light-theme-accent-palette: mat-palette($lf-red, 400);
$lfRed-light-theme-warn-palette: mat-palette($lf-amber);
// Create the Material theme object
$lfRed-light-theme: mat-light-theme(
$lfRed-light-theme-primary-palette,
$lfRed-light-theme-accent-palette,
$lfRed-light-theme-warn-palette
);
// Add ".theme-pink-dark" class to the body to activate this theme.
// Class name must start with "theme-" !!!
body.theme-lfRed {
// Generate the Angular Material theme
@include angular-material-theme($lfRed-light-theme);
// Apply the theme to the user components
@include components-theme($lfRed-light-theme);
@include ucap-material-theme($lfRed-light-theme);
}
// Apply the theme to the user components
/*
@include components-theme($lgRed-app-theme);
*/
@include ucap-material-theme($lgRed-app-theme);

View File

@ -12,6 +12,7 @@ import {
import { Store } from '@ngrx/store';
import { LogService } from '@ucap/ng-logger';
import { PiService } from '@ucap/ng-pi';
import { LoginActions } from '@ucap/ng-store-authentication';
@ -26,7 +27,8 @@ export class AppAuthenticationGuard implements CanActivate {
private piService: PiService,
private appAuthenticationService: AppAuthenticationService,
private store: Store<any>,
private router: Router
private router: Router,
private logService: LogService
) {}
canActivate(
@ -38,59 +40,61 @@ export class AppAuthenticationGuard implements CanActivate {
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree> {
return new Promise<boolean | UrlTree>((resolve, reject) => {
if (this.appAuthenticationService.loggedIn()) {
const loggedIn = this.appAuthenticationService.loggedIn();
if (loggedIn) {
resolve(true);
} else {
const userStore = this.appAuthenticationService.useAutoLogin();
if (!!userStore) {
const loginSession = this.appAuthenticationService.getLoginSession();
const onWebLoginFailure = (error: any) => {
userStore.settings.general.autoLogin = false;
this.appAuthenticationService.setUserStore(userStore);
this.router.navigateByUrl('/account/login');
resolve(false);
};
this.piService
.login2({
companyCode: userStore.companyCode,
loginId: userStore.loginId,
loginPw: userStore.loginPw,
deviceType: loginSession.deviceType
})
.pipe(take(1))
.subscribe(
(res) => {
if ('success' !== res.status.toLowerCase()) {
onWebLoginFailure(res.status);
return;
} else {
this.store.dispatch(
LoginActions.webLoginSuccess({
companyCode: userStore.companyCode,
loginId: userStore.loginId,
loginPw: userStore.loginPw,
autoLogin: true,
rememberMe: userStore.rememberMe,
login2Response: res
})
);
resolve(true);
return;
}
},
(error) => {
onWebLoginFailure(error);
},
() => {}
);
} else {
this.router.navigateByUrl('/account/login');
resolve(false);
}
return;
}
const userStore = this.appAuthenticationService.useAutoLogin();
if (!userStore) {
this.router.navigateByUrl('/account/login');
resolve(false);
return;
}
const loginSession = this.appAuthenticationService.getLoginSession();
const onWebLoginFailure = (error: any) => {
userStore.settings.general.autoLogin = false;
this.appAuthenticationService.setUserStore(userStore);
this.router.navigateByUrl('/account/login');
resolve(false);
};
this.piService
.login2({
companyCode: userStore.companyCode,
loginId: userStore.loginId,
loginPw: userStore.loginPw,
deviceType: loginSession.deviceType
})
.pipe(take(1))
.subscribe(
(res) => {
if ('success' !== res.status.toLowerCase()) {
onWebLoginFailure(res.status);
return;
} else {
this.store.dispatch(
LoginActions.webLoginSuccess({
companyCode: userStore.companyCode,
loginId: userStore.loginId,
loginPw: userStore.loginPw,
autoLogin: true,
rememberMe: userStore.rememberMe,
login2Response: res
})
);
resolve(true);
return;
}
},
(error) => {
onWebLoginFailure(error);
}
);
});
}
}

View File

@ -1,5 +1,5 @@
<div fxFlexFill class="layout-container">
<div class="navi-container">
<div class="layout-container" fxLayout="row">
<div class="navitab-page" fxFlex="60px">
<mat-tab-group
#navTabGroup
vertical
@ -93,15 +93,45 @@
</ng-template>
</mat-tab>
</mat-tab-group>
<ucap-float-action-button
*ngIf="fabButtonShow"
[buttons]="fabButtons"
(buttonClick)="onClickFab($event)"
>
</ucap-float-action-button>
</div>
<div class="content-container" fxFlexFill>
<mat-sidenav-container autosize="true" fxFlexFill>
<div class="content-page" fxFlex="1 1 auto">
<mat-sidenav-container autosize="true">
<mat-sidenav #leftSidenav class="left-sidenav" mode="side" opened="true">
<router-outlet></router-outlet>
<div class="left-sidenav-container" fxLayout="column">
<div class="top-bar" fxFlex="40px">
M-Messenger
</div>
<div fxFlex="1 1 auto">
<router-outlet></router-outlet>
</div>
</div>
</mat-sidenav>
<div fxFlex="1 1 auto">
<router-outlet name="content"></router-outlet>
</div>
<mat-sidenav
#rightSidenav
class="right-sidenav"
mode="side"
opened="true"
position="end"
>
Right
</mat-sidenav>
<mat-sidenav-content>
<div class="content-sidenav-container" fxLayout="column">
<div class="content-sidenav-top-bar" fxFlex="40px">
<app-layouts-top-bar></app-layouts-top-bar>
</div>
<div class="content-sidenav-body" fxFlex="1 1 auto">
<router-outlet name="content"></router-outlet>
</div>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</div>
</div>

View File

@ -1,22 +1,47 @@
.layout-container {
display: flex;
:host {
width: 100%;
height: 100%;
}
.navi-container {
width: 70px;
.layout-container {
width: 100%;
height: 100%;
.navitab-page {
}
.content-container {
.left-sidenav {
display: flex;
flex-direction: column;
width: 370px;
height: 100%;
max-width: 90%;
overflow: hidden;
}
.content-page {
width: 100%;
height: 100%;
.content-drawer {
flex: 0 0 auto;
mat-sidenav-container {
width: 100%;
height: 100%;
.left-sidenav {
width: 370px;
max-width: 90%;
.left-sidenav-container {
width: 100%;
height: 100%;
}
}
.right-sidenav {
width: 370px;
}
mat-sidenav-content {
.content-sidenav-container {
width: 100%;
height: 100%;
.content-sidenav-body {
overflow: auto;
}
}
}
}
}
}

View File

@ -26,6 +26,12 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
@ViewChild('leftSidenav', { static: true })
leftSidenav: MatSidenav;
showFooter = false;
/** FAB */
fabButtonShow = true;
fabButtons: { icon: string; tooltip?: string; divisionType?: string }[];
private windowSizeSubscription: Subscription;
constructor(
@ -50,6 +56,8 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
});
this.setTabGroup(this.router.url);
this.setFabInitial(NAVS[0]);
}
ngOnDestroy(): void {
@ -70,6 +78,7 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
NAVS[event.index],
{ outlets: { content: 'index' } }
]);
this.setFabInitial(NAVS[event.index]);
}
onClickToggleLeftSidenav() {
@ -85,4 +94,101 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy {
url.startsWith(`/${v}`)
);
}
setFabInitial(type: string) {
switch (type) {
case 'group':
{
this.fabButtonShow = true;
this.fabButtons = [
{
icon: 'add',
tooltip: '그룹 추가',
divisionType: 'GROUP_NEW_ADD'
}
];
}
break;
case 'chat':
{
this.fabButtonShow = true;
this.fabButtons = [
{
icon: 'chat',
tooltip: '대화 추가',
divisionType: 'CAHT_NEW_ADD'
}
];
// if (environment.productConfig.CommonSetting.useTimerRoom) {
// this.fabButtons.push({
// icon: 'timer',
// tooltip: this.translateService.instant('chat.newTimerChat'),
// divisionType: 'CHAT_NEW_TIMER_ADD'
// });
// }
}
break;
case 'organization':
{
this.fabButtonShow = false;
}
break;
case 'message':
{
this.fabButtonShow = true;
this.fabButtons = [
{
icon: 'add',
tooltip: '쪽지 추가',
divisionType: 'MESSAGE_NEW'
}
];
}
break;
// case MainMenu.Call:
// {
// this.fabButtonShow = false;
// }
// break;
default: {
this.fabButtonShow = false;
}
}
}
onClickFab(params: { btn: any }) {
const btn = params.btn as {
icon: string;
tooltip?: string;
divisionType?: string;
};
switch (btn.divisionType) {
case 'GROUP_NEW_ADD':
{
this.logService.debug('GROUP_NEW_ADD');
}
break;
case 'CAHT_NEW_ADD':
{
this.logService.debug('CAHT_NEW_ADD');
}
break;
case 'CHAT_NEW_TIMER_ADD':
{
// if (environment.productConfig.CommonSetting.useTimerRoom) {
// this.onClickNewChat('TIMER');
// }
}
break;
case 'MESSAGE_NEW':
{
this.logService.debug('MESSAGE_NEW');
}
break;
}
}
}

View File

@ -1 +1,8 @@
<router-outlet></router-outlet>
<div class="layout-container" fxFlexFill fxLayout="column">
<div *ngIf="showTopbar" class="top-bar" fxFlex="50px">
<app-layouts-top-bar></app-layouts-top-bar>
</div>
<div class="layout-content" fxFlex="1 1 auto">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -0,0 +1,2 @@
.layout-container {
}

View File

@ -6,5 +6,7 @@ import { Component } from '@angular/core';
styleUrls: ['./no-navi.layout.component.scss']
})
export class NoNaviLayoutComponent {
showTopbar = true;
constructor() {}
}

View File

@ -1,4 +1,4 @@
<mat-toolbar class=".title-bar">
<div class="title-bar">
<ucap-title-bar
[platform]="platform"
[native]="native"
@ -7,4 +7,4 @@
(minimized)="onMinimizedTitleBar()"
>
</ucap-title-bar>
</mat-toolbar>
</div>

View File

@ -1,12 +1,6 @@
.title-bar {
width: 100%;
height: 50px;
-webkit-user-select: none;
-webkit-app-region: drag;
position: fixed;
right: 0;
top: 0;
display: flex;
height: 100%;
padding: 0;
cursor: pointer;
background-color: #ffffff;
}

View File

@ -1,12 +1,9 @@
<div class="login-page-container" fxLayout="row">
<div fxFlex="1 1 auto">Login</div>
<div class="login-section-container">
<app-sections-account-login
[companyGroupCode]="companyGroupCode"
[fixedCompanyCode]="fixedCompanyCode"
[userStore]="userStore"
[useRememberMe]="useRememberMe"
[useAutoLogin]="useAutoLogin"
></app-sections-account-login>
</div>
<div class="login-container">
<app-sections-account-login
[companyGroupCode]="companyGroupCode"
[fixedCompanyCode]="fixedCompanyCode"
[userStore]="userStore"
[useRememberMe]="useRememberMe"
[useAutoLogin]="useAutoLogin"
></app-sections-account-login>
</div>

View File

@ -1,6 +1,62 @@
.login-page-container {
.login-section-container {
width: 400px;
margin: 40px;
}
@import '../../../../assets/scss/components';
.login-container {
width: 100%;
overflow: auto;
min-height: 100vh;
background-color: $bg-gray;
background-image: url(../../../../assets/images/bg/bg_login_circle_square01.svg),
url(../../../../assets/images/bg/bg_login_circle_stroke01.svg),
url(../../../../assets/images/bg/bg_login_circle01.svg),
url(../../../../assets/images/bg/bg_login_circle03.svg),
url(../../../../assets/images/bg/bg_login_circle_diagonal01.svg),
url(../../../../assets/images/bg/bg_login_circle_square02.svg),
url(../../../../assets/images/bg/bg_login_circle04.svg),
url(../../../../assets/images/bg/bg_login_circle05.svg),
url(../../../../assets/images/bg/bg_login_circle06.svg),
url(../../../../assets/images/bg/bg_login_circle07.svg),
url(../../../../assets/images/bg/bg_login_circle08.svg),
url(../../../../assets/images/bg/bg_login_circle_diagonal02.svg),
url(../../../../assets/images/bg/bg_login_circle_diagonal03.svg),
url(../../../../assets/images/bg/bg_login_circle_stroke02.svg),
url(../../../../assets/images/bg/bg_login_circle_stroke03.svg),
url(../../../../assets/images/bg/bg_login_circle_stroke04.svg),
url(../../../../assets/images/bg/bg_login_circle_stroke05.svg),
url(../../../../assets/images/bg/bg_login_polygon01.svg),
url(../../../../assets/images/bg/bg_login_polygon02.svg);
background-repeat: no-repeat;
background-position: unquote($login-bg-w * 323 + '%')
unquote($login-bg-h * 179 + '%'),
unquote($login-bg-w * -147 + '%') unquote($login-bg-h * 18 + '%'),
unquote($login-bg-w * 285 + '%') unquote($login-bg-h * 226 + '%'),
unquote($login-bg-w * 1235 + '%') unquote($login-bg-h * -101 + '%'),
unquote($login-bg-w * 1397 + '%') unquote($login-bg-h * 163 + '%'),
unquote($login-bg-w * 1569 + '%') unquote($login-bg-h * 580 + '%'),
unquote($login-bg-w * 426 + '%') unquote($login-bg-h * 293 + '%'),
unquote($login-bg-w * 1531 + '%') unquote($login-bg-h * 250 + '%'),
unquote($login-bg-w * 1774 + '%') unquote($login-bg-h * 166 + '%'),
unquote($login-bg-w * 1362 + '%') unquote($login-bg-h * 673 + '%'),
unquote($login-bg-w * 152 + '%') unquote($login-bg-h * 730 + '%'),
unquote($login-bg-w * 286 + '%') unquote($login-bg-h * 719 + '%'),
unquote($login-bg-w * 683 + '%') unquote($login-bg-h * 593 + '%'),
unquote($login-bg-w * 498 + '%') unquote($login-bg-h * 453 + '%'),
unquote($login-bg-w * 1709 + '%') unquote($login-bg-h * 599 + '%'),
unquote($login-bg-w * 1395 + '%') unquote($login-bg-h * 989 + '%'),
unquote(100 + $login-bg-w * 89 + '%') unquote(100 + $login-bg-h * 137 + '%'),
unquote($login-bg-w * 90 + '%') unquote($login-bg-h * 463 + '%'),
unquote($login-bg-w * 549 + '%') unquote($login-bg-h * 874 + '%');
background-size: unquote($login-bg-w * 79 + '%'),
unquote($login-bg-w * 333 + '%'), unquote($login-bg-w * 84 + '%'),
unquote($login-bg-w * 172 + '%'), unquote($login-bg-w * 210 + '%'),
unquote($login-bg-w * 94 + '%'), unquote($login-bg-w * 44 + '%'),
unquote($login-bg-w * 118 + '%'), unquote($login-bg-w * 52 + '%'),
unquote($login-bg-w * 70 + '%'), unquote($login-bg-w * 172 + '%'),
unquote($login-bg-w * 82 + '%'), unquote($login-bg-w * 135 + '%'),
unquote($login-bg-w * 102 + '%'), unquote($login-bg-w * 130 + '%'),
unquote($login-bg-w * 184 + '%'), unquote($login-bg-w * 370 + '%'),
unquote($login-bg-w * 122 + '%'), unquote($login-bg-w * 75 + '%');
padding-top: 5%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}

View File

@ -6,6 +6,8 @@ import { environment } from '@environments';
import { UserStore } from '@app/models/user-store';
import { AppKey } from '@app/types/app-key.type';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-pages-account-login',
@ -23,14 +25,25 @@ export class LoginPageComponent implements OnInit, OnDestroy {
readonly fixedCompanyCode = environment.companyConfig.fixedCompanyCode;
private ngOnDestroySubject = new Subject<boolean>();
constructor(private localStorageService: LocalStorageService) {}
ngOnInit(): void {
this.userStore = this.localStorageService.encGet<UserStore>(
AppKey.UserStore,
environment.productConfig.localEncriptionKey
);
this.ngOnDestroySubject = new Subject<boolean>();
this.localStorageService
.encGet$<UserStore>(
AppKey.UserStore,
environment.productConfig.localEncriptionKey
)
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe((userStore) => (this.userStore = userStore));
}
ngOnDestroy(): void {}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -3,17 +3,32 @@ import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { AppChatSectionModule } from '@app/sections/chat/chat.section.module';
import { AppChatRoutingPageModule } from './chat-routing.page.module';
import { IndexPageComponent } from './components/index.page.component';
import { SidenavPageComponent } from './components/sidenav.page.component';
import { UiModule } from '@ucap/ng-ui';
export const COMPONENTS = [IndexPageComponent, SidenavPageComponent];
export { IndexPageComponent, SidenavPageComponent };
@NgModule({
imports: [CommonModule, FlexLayoutModule, AppChatRoutingPageModule],
imports: [
CommonModule,
FlexLayoutModule,
MatIconModule,
MatMenuModule,
AppChatSectionModule,
AppChatRoutingPageModule,
UiModule
],
declarations: [...COMPONENTS],
entryComponents: []
})

View File

@ -1,3 +1,27 @@
<div fxFlexFill>
sidenav page of chat is works!
<div fxFlexFill class="sidenav-container">
<div class="group-header">
<h3>대화</h3>
<div class="group-menu-btn">
<button
mat-icon-button
[matMenuTriggerFor]="groupMenu"
aria-label="group menu"
>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</div>
<app-sections-chat-search
(keyDownEnter)="onKeyDownSearch($event)"
(searchCancel)="onClickCancel()"
></app-sections-chat-search>
<app-sections-chat-list
fxFlexFill
[searchObj]="searchObj"
></app-sections-chat-list>
</div>
<mat-menu #groupMenu="matMenu">
<button mat-menu-item>Item 1</button>
<button mat-menu-item>Item 2</button>
</mat-menu>

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, ChangeDetectorRef } from '@angular/core';
import { Router } from '@angular/router';
import { LogService } from '@ucap/ng-logger';
@ -9,5 +9,29 @@ import { LogService } from '@ucap/ng-logger';
styleUrls: ['./sidenav.page.component.scss']
})
export class SidenavPageComponent {
constructor(private logService: LogService) {}
searchObj: any = {
isShowSearch: false,
searchWord: ''
};
constructor(
private logService: LogService,
private changeDetectorRef: ChangeDetectorRef
) {}
onKeyDownSearch(params: { companyCode: string; searchWord: string }) {
this.searchObj = {
isShowSearch: true,
searchWord: params.searchWord
};
this.changeDetectorRef.detectChanges();
}
onClickCancel() {
this.searchObj = {
isShowSearch: false,
searchWord: ''
};
this.changeDetectorRef.detectChanges();
}
}

View File

@ -1,3 +1,12 @@
<div>
index of group
<div fxLayout="column">
<div class="subtitle" fxFlex="30px">Welcome to M-Messenger</div>
<div class="content-container" fxFlex="1 1 auto" fxLayout="row">
<div class="profile-container" fxFlex="44%">
<app-sections-group-profile [userSeq]="userSeq">
</app-sections-group-profile>
</div>
<div class="group-info-container" fxFlex="1 1 auto">
<app-sections-group-info [userSeq]="userSeq"></app-sections-group-info>
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
.profile-container {
height: 100%;
overflow: auto;
}
.group-info-container {
height: 100%;
overflow: auto;
}

View File

@ -21,10 +21,13 @@ export class IndexPageComponent implements OnInit, OnDestroy {
private logService: LogService
) {}
userSeq: string;
ngOnInit(): void {
this.paramsSubscription = this.activatedRoute.queryParams.subscribe(
(params: Params) => {
console.log('IndexPageComponent', params[QueryParams.ID]);
const seqParam = params[QueryParams.ID];
this.userSeq = !!seqParam ? seqParam : undefined;
}
);
}

View File

@ -1,4 +1,50 @@
<div fxFlexFill class="sidenav-container">
<app-sections-group-search></app-sections-group-search>
<app-sections-group-list fxFlexFill></app-sections-group-list>
<div class="sidenav-container" fxFlexFill fxLayout="column">
<div class="sub-header" fxFlex="50px" fxLayout="row">
<h3 fxFlex="1 1 auto">그룹</h3>
<div class="menu-btn" fxFlex="70px">
<button
mat-icon-button
[matMenuTriggerFor]="groupViewMenu"
aria-label="group view menu"
>
<mat-icon>refresh</mat-icon>
</button>
<button
mat-icon-button
[matMenuTriggerFor]="groupMenu"
aria-label="group menu"
>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</div>
<div class="extra-box" fxFlex="50px">
<app-sections-group-search
(keyDownEnter)="onKeyDownSearch($event)"
(searchCancel)="onClickCancel()"
>
</app-sections-group-search>
</div>
<div fxFlex="1 1 auto">
<app-sections-group-list
fxFlexFill
[searchObj]="searchObj"
></app-sections-group-list>
</div>
</div>
<mat-menu #groupMenu="matMenu">
<button mat-menu-item (click)="onClickGroupMenu('GROUP_NEW')">
새그룹 추가
</button>
<button mat-menu-item>그룹전체 열기</button>
<button mat-menu-item>그룹전체 닫기</button>
<button mat-menu-item>그룹순서 바꾸기</button>
</mat-menu>
<mat-menu #groupViewMenu="matMenu">
<button mat-menu-item>전체보기</button>
<button mat-menu-item>접속한 동료만 보기</button>
<button mat-menu-item>온/오프라인 보기</button>
</mat-menu>

View File

@ -1,11 +1,14 @@
import { Subscription } from 'rxjs';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Router, Params } from '@angular/router';
import { LogService } from '@ucap/ng-logger';
import { QueryParams } from '../types/params.type';
import { MatDialog } from '@angular/material/dialog';
import { CreateChatDialogComponent } from '@app/sections/group/components/component-ui/dialogs/create-chat.dialog.component';
import { SelectUserDialogType } from '@app/types';
@Component({
selector: 'app-pages-group-sidenav',
@ -13,25 +16,57 @@ import { QueryParams } from '../types/params.type';
styleUrls: ['./sidenav.page.component.scss']
})
export class SidenavPageComponent implements OnInit, OnDestroy {
private queryParamsSubscription: Subscription;
searchObj: any = {
isShowSearch: false,
companyCode: '',
searchWord: ''
};
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private logService: LogService
private logService: LogService,
private changeDetectorRef: ChangeDetectorRef,
public dialog: MatDialog
) {}
ngOnInit(): void {
this.queryParamsSubscription = this.activatedRoute.queryParams.subscribe(
(params: Params) => {
console.log('SidenavPageComponent', params[QueryParams.ID]);
}
);
ngOnInit(): void {}
ngOnDestroy(): void {}
onClickFab(event: MouseEvent) {}
onKeyDownSearch(params: { companyCode: string; searchWord: string }) {
this.searchObj = {
isShowSearch: true,
companyCode: params.companyCode,
searchWord: params.searchWord
};
this.changeDetectorRef.detectChanges();
}
ngOnDestroy(): void {
if (!!this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
onClickCancel() {
this.searchObj = {
isShowSearch: false,
companyCode: '',
searchWord: ''
};
this.changeDetectorRef.detectChanges();
}
onClickGroupMenu(menuType: string) {
switch (menuType) {
case 'GROUP_NEW':
{
this.dialog.open(CreateChatDialogComponent, {
width: '850px',
height: '600px',
data: {
type: SelectUserDialogType.NewGroup,
title: '새 그룹 추가'
}
});
}
break;
}
}
}

View File

@ -3,6 +3,9 @@ import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { AppGroupSectionModule } from '@app/sections/group/group.section.module';
import { AppGroupRoutingPageModule } from './group-routing.page.module';
@ -10,6 +13,8 @@ import { AppGroupRoutingPageModule } from './group-routing.page.module';
import { IndexPageComponent } from './components/index.page.component';
import { SidenavPageComponent } from './components/sidenav.page.component';
import { UiModule } from '@ucap/ng-ui';
export const COMPONENTS = [IndexPageComponent, SidenavPageComponent];
export { IndexPageComponent, SidenavPageComponent };
@ -18,8 +23,11 @@ export { IndexPageComponent, SidenavPageComponent };
imports: [
CommonModule,
FlexLayoutModule,
MatIconModule,
MatMenuModule,
AppGroupSectionModule,
AppGroupRoutingPageModule
AppGroupRoutingPageModule,
UiModule
],
declarations: [...COMPONENTS],
entryComponents: []

View File

@ -1,3 +1,25 @@
<div fxFlexFill>
sidenav page of ogranization is works!
<div class="sidenav-container" fxFlexFill fxLayout="column">
<div class="sub-header" fxFlex="50px" fxLayout="row">
<h3 fxFlex="1 1 auto">조직도</h3>
<div class="menu-btn" fxFlex="70px">
<button
mat-icon-button
[matMenuTriggerFor]="organizationMenu"
aria-label="organization menu"
>
<mat-icon>more_vert</mat-icon>
</button>
</div>
</div>
<div class="extra-box" fxFlex="50px">
LG CNS
</div>
<div fxFlex="1 1 auto">
<app-sections-organization-tree></app-sections-organization-tree>
</div>
</div>
<mat-menu #organizationMenu="matMenu">
<button mat-menu-item>Item 1</button>
<button mat-menu-item>Item 2</button>
</mat-menu>

View File

@ -0,0 +1,10 @@
.sidenav-container {
padding-bottom: 5px;
overflow: hidden;
.organization-header {
}
.organization-tree {
}
}

View File

@ -3,12 +3,24 @@ import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { AppOrganizationSectionModule } from '@app/sections/organization/organization.section.module';
import { AppOrganizationRoutingPageModule } from './organization-routing.page.module';
import { COMPONENTS } from './components';
@NgModule({
imports: [CommonModule, FlexLayoutModule, AppOrganizationRoutingPageModule],
imports: [
CommonModule,
FlexLayoutModule,
MatIconModule,
MatMenuModule,
AppOrganizationSectionModule,
AppOrganizationRoutingPageModule
],
declarations: [...COMPONENTS],
entryComponents: []
})

View File

@ -1,5 +1,5 @@
import { Observable, forkJoin, Subject } from 'rxjs';
import { take, filter, map, takeUntil } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import {
@ -13,7 +13,6 @@ import { Store } from '@ngrx/store';
import { StatusCode } from '@ucap/api';
import { LogService } from '@ucap/ng-logger';
import { SessionStorageService } from '@ucap/ng-web-storage';
import { PublicApiService } from '@ucap/ng-api-public';
import { ExternalApiService } from '@ucap/ng-api-external';
import { ProtocolService } from '@ucap/ng-protocol';
@ -22,7 +21,6 @@ import { CompanyActions } from '@ucap/ng-store-organization';
import { ConfigurationActions } from '@ucap/ng-store-authentication';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
@Injectable()
@ -32,7 +30,6 @@ export class AppSessionResolver implements Resolve<void> {
private externalApiService: ExternalApiService,
private protocolService: ProtocolService,
private store: Store<any>,
private sessionStorageService: SessionStorageService,
private appAuthenticationService: AppAuthenticationService,
private logService: LogService
) {}
@ -44,7 +41,6 @@ export class AppSessionResolver implements Resolve<void> {
return new Promise<void>(async (resolve, reject) => {
try {
const loginSession = this.appAuthenticationService.getLoginSession();
if (loginSession.alive) {
resolve();
return;
@ -115,18 +111,15 @@ export class AppSessionResolver implements Resolve<void> {
})
);
const destroy$ = new Subject<boolean>();
this.sessionStorageService.changed$
.pipe(
takeUntil(destroy$),
filter((param) => AppKey.LoginSession === param.key),
map((param) => param.value)
)
const destroySubject = new Subject<boolean>();
this.appAuthenticationService
.getLoginSession$()
.pipe(takeUntil(destroySubject))
.subscribe(
(v) => {
if ((v as LoginSession).alive) {
destroy$.next(true);
destroy$.unsubscribe();
destroySubject.next(true);
destroySubject.complete();
resolve();
}
},

View File

@ -10,6 +10,24 @@ import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { AuthenticationUiModule } from '@ucap/ng-ui-authentication';
import { COMPONENTS } from './components';
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 { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSliderModule } from '@angular/material/slider';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatFormFieldModule } from '@angular/material/form-field';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
@ -17,7 +35,28 @@ import { COMPONENTS } from './components';
FlexLayoutModule,
MatCheckboxModule,
I18nModule,
AuthenticationUiModule
AuthenticationUiModule,
ReactiveFormsModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDatepickerModule,
MatDialogModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatTabsModule,
MatTooltipModule,
MatToolbarModule,
MatFormFieldModule,
MatSelectModule
],
exports: [...COMPONENTS],
declarations: [...COMPONENTS],

View File

@ -0,0 +1,127 @@
<div class="login-box">
<ng-content select="[ucapAuthenticationLogin='header']"></ng-content>
<div class="login-content">
<form name="loginForm" [formGroup]="loginForm" novalidate>
<mat-form-field
[style.display]="!!fixedCompanyCode ? 'none' : 'block'"
class="login-company"
appearance="none"
>
<mat-select
[formControl]="companyCodeFormControl"
[value]="companyCode || fixedCompanyCode"
placeholder="{{ 'login.labels.selectCompany' | ucapI18n }}"
class="login-input-area login-select-form"
>
<mat-option
*ngFor="let company of companyList"
[value]="company.companyCode"
>{{ company.companyName }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="login-input-area idpass-type">
<mat-form-field
class="login-idpass-txt"
appearance="none"
floatLabel=""
>
<!-- <mat-label>{{ 'login.fields.loginId' | ucapI18n }}</mat-label> -->
<input
matInput
[formControl]="loginIdFormControl"
placeholder="{{ 'login.fields.loginId' | ucapI18n }}"
/>
</mat-form-field>
</div>
<div class="login-input-area idpass-type pass-type">
<mat-form-field class="login-idpass-txt" appearance="none">
<!-- <mat-label>{{ 'login.fields.loginPw' | ucapI18n }}</mat-label> -->
<input
matInput
type="password"
[formControl]="loginPwFormControl"
placeholder="{{ 'login.fields.loginPw' | ucapI18n }}"
#loginPw
/>
</mat-form-field>
</div>
<div class="error-container">
<mat-error
*ngIf="
companyCodeFormControl.dirty &&
companyCodeFormControl.invalid &&
companyCodeFormControl.hasError('required')
"
>
{{ 'login.errors.requireCompany' | ucapI18n }}
</mat-error>
<mat-error
*ngIf="
loginIdFormControl.dirty &&
loginIdFormControl.invalid &&
loginIdFormControl.hasError('required')
"
>
{{ 'login.errors.requireLoginId' | ucapI18n }}
</mat-error>
<mat-error
*ngIf="
loginPwFormControl.dirty &&
loginPwFormControl.invalid &&
loginPwFormControl.hasError('required')
"
>
{{ 'login.errors.requireLoginPw' | ucapI18n }}
</mat-error>
<mat-error *ngIf="loginFailed">
{{ 'login.errors.failed' | ucapI18n }}
</mat-error>
</div>
<button
mat-raised-button
class="login-input-submit"
aria-label="LOG IN"
[disabled]="
loginForm.invalid ||
disable ||
(!!loginTry && !!loginTry.remainTimeForNextTry)
"
(click)="onClickLogin()"
>
<ng-container
*ngIf="!processing && (!loginTry || !loginTry.remainTimeForNextTry)"
>
{{ 'login.labels.doLogin' | ucapI18n }}
</ng-container>
<ng-container *ngIf="!!loginTry && !!loginTry.remainTimeForNextTry">
{{ 'login.errors.attemptsExceeded' | ucapI18n }}
(
{{
moment
.utc(
moment
.duration(loginTry.remainTimeForNextTry, 'seconds')
.asMilliseconds()
)
.format('mm:ss')
}}
)
</ng-container>
<mat-spinner
*ngIf="processing && (!loginTry || !loginTry.remainTimeForNextTry)"
>
</mat-spinner>
</button>
</form>
<ng-content select="[ucapAuthenticationLogin='footer']"></ng-content>
</div>
</div>

View File

@ -0,0 +1,160 @@
@import '../../../../../assets/scss/components';
.login-box {
@extend %clearfix;
padding: 0 0 45px;
width: 420px;
margin: auto;
text-align: center;
flex-basis: auto;
align-items: center;
.logo-img {
display: block;
text-align: center;
img {
margin-bottom: 7px;
vertical-align: top;
@include screen(mid) {
width: 120px;
}
@include screen(xs) {
width: 100px;
margin-bottom: 6px;
}
}
}
@extend %guideline;
.login-content {
@extend %guideline2; //Guide Line2
margin: 30px auto 0;
.login-input-area {
border: 1px solid #cccccc;
border-radius: 2px;
width: 100%;
max-width: 420px;
min-width: 150px;
height: 60px;
background-color: $white;
margin-top: 10px;
&.login-select-form {
height: 60px;
line-height: 60px;
padding: 0 16px;
@include screen(mid) {
height: 50px;
line-height: 50px;
}
@include screen(xs) {
height: 42px;
line-height: 42px;
}
}
&:first-of-type {
margin-top: 0px;
}
&.idpass-type {
padding-left: 50px;
position: relative;
&::before {
font-family: 'material Icons';
font-size: 24px;
text-align: center;
line-height: 60px;
content: 'perm_identity';
display: block;
position: absolute;
top: 0;
left: 16px;
@include screen(mid) {
line-height: 50px;
}
@include screen(xs) {
line-height: 42px;
}
}
&.pass-type {
&::before {
content: 'https';
}
}
.login-idpass-txt {
width: 368px;
height: 60px;
line-height: 60px;
font-size: 14px;
@include screen(mid) {
width: 358 - 60 + px;
height: 50px;
line-height: 50px;
font-size: 14px;
}
@include screen(xs) {
width: 308 - 60 + px;
font-size: 14px;
height: 42px;
line-height: 42px;
}
input {
font-size: 18px;
line-height: 58px;
margin-top: 0;
vertical-align: top;
background-color: $white;
padding: 0 10px 0 5px;
@include screen(mid) {
font-size: 16px;
line-height: 48px;
}
@include screen(xs) {
font-size: 14px;
line-height: 40px;
}
}
}
}
@include screen(mid) {
margin-top: 8px;
}
}
.login-input-submit {
width: 100%;
height: 60px;
background-color: $black;
border-radius: 2px;
color: $white;
font-size: 20px;
@include font-family($font-semibold);
border: 0;
margin-top: 12px;
font-weight: 600;
cursor: pointer;
@include screen(mid) {
margin-top: 8px;
font-size: 16px;
height: 50px;
}
@include screen(xs) {
font-size: 14px;
height: 42px;
}
}
@include screen(mid) {
margin-top: 23px;
width: 350px;
.login-input-area {
height: 50px;
}
}
@include screen(xs) {
margin-top: 23px;
width: 300px;
.login-input-area {
height: 42px;
}
}
}
}
.login-company {
width: 100%;
}

View File

@ -0,0 +1,92 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef } from '@angular/core';
import { I18nService, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { AuthenticationUiModule } from '../authentication-ui.module';
import { MatSelectModule } from '@angular/material/select';
import { Company } from '@ucap/api-external';
import { LogService } from '@ucap/ng-logger';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
describe('ui::authentication::LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatSelectModule
],
providers: [
AuthenticationUiModule,
// { provide: FormBuilder, useValue: new FormBuilder() },
// { provide: ChangeDetectorRef, useValue: ChangeDetectorRef },
{ provide: I18nService, useValue: new I18nService(new LogService({})) },
{
provide: UCAP_I18N_NAMESPACE,
useValue: 'authentication'
}
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
component.companyList = [
{ companyName: 'LG CNS', companyCode: 'GUC100' },
{ companyName: 'LG UCAP', companyCode: 'GUC101' }
] as Company[];
component.loginId = 'test';
component.companyCode = 'GUC100';
fixture.detectChanges();
component.ngOnInit();
expect(component).toBeTruthy();
});
it('login', (done) => {
component.companyList = [
{ companyName: 'LG CNS', companyCode: 'GUC100' },
{ companyName: 'LG UCAP', companyCode: 'GUC101' }
] as Company[];
component.loginId = 'test';
component.companyCode = 'GUC100';
component.ngOnInit();
component.login.subscribe((value) => {
console.log(value);
done();
});
component.onClickLogin();
});
});

View File

@ -0,0 +1,110 @@
import moment from 'moment';
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
ViewChild,
ElementRef,
ChangeDetectorRef
} from '@angular/core';
import {
FormGroup,
FormBuilder,
Validators,
FormControl,
ValidatorFn
} from '@angular/forms';
import { Company } from '@ucap/api-external';
import { LoginTry } from '@ucap/pi';
@Component({
selector: 'ucap-authentication-login-local',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
@Input()
companyList: Company[];
@Input()
fixedCompanyCode: string;
@Input()
companyCode: string;
@Input()
loginId: string;
@Input()
disable = false;
@Input()
processing = false;
@Input()
loginTry: LoginTry;
@Output()
login = new EventEmitter<{
companyCode: string;
loginId: string;
loginPw: string;
notValid: () => void;
}>();
@ViewChild('loginPw', { static: true }) loginPwElementRef: ElementRef;
loginForm: FormGroup;
companyCodeFormControl = new FormControl('');
loginIdFormControl = new FormControl('');
loginPwFormControl = new FormControl('');
loginFailed = false;
moment = moment;
constructor(
private formBuilder: FormBuilder,
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
const companyCodeValidators: ValidatorFn[] = [Validators.required];
this.companyCodeFormControl.setValidators(companyCodeValidators);
if (!!this.fixedCompanyCode) {
this.companyCodeFormControl.setValue(this.fixedCompanyCode);
}
if (!!this.companyCode) {
this.companyCodeFormControl.setValue(this.companyCode);
}
const loginIdValidators: ValidatorFn[] = [Validators.required];
this.loginIdFormControl.setValidators(loginIdValidators);
if (!!this.loginId) {
this.loginIdFormControl.setValue(this.loginId);
}
const loginPwValidators: ValidatorFn[] = [Validators.required];
this.loginPwFormControl.setValidators(loginPwValidators);
this.loginForm = this.formBuilder.group({
companyCodeFormControl: this.companyCodeFormControl,
loginIdFormControl: this.loginIdFormControl,
loginPwFormControl: this.loginPwFormControl
});
this.changeDetectorRef.detectChanges();
}
onClickLogin() {
this.login.emit({
companyCode: this.loginForm.get('companyCodeFormControl').value,
loginId: this.loginForm.get('loginIdFormControl').value,
loginPw: this.loginForm.get('loginPwFormControl').value,
notValid: () => {
this.loginFailed = true;
this.loginPwElementRef.nativeElement.focus();
}
});
}
}

View File

@ -1,3 +1,4 @@
import { LoginSectionComponent } from './login.section.component';
import { LoginComponent } from './component-ui/login.component';
export const COMPONENTS = [LoginSectionComponent];
export const COMPONENTS = [LoginSectionComponent, LoginComponent];

View File

@ -1,68 +1,67 @@
<ucap-authentication-login
[companyList]="companyList"
[fixedCompanyCode]="fixedCompanyCode"
[companyCode]="userStore?.companyCode"
[loginId]="userStore?.loginId"
[disable]="disableLoginForm"
[processing]="loginProcessing"
[loginTry]="loginTry"
(login)="onLogin($event)"
>
<div
ucapAuthenticationLogin="header"
style="background-image: url(./assets/images/logo/bg_logo_login.png);"
<div class="login-section-container">
<ucap-authentication-login-local
[companyList]="companyList"
[fixedCompanyCode]="fixedCompanyCode"
[companyCode]="userStore?.companyCode"
[loginId]="userStore?.loginId"
[disable]="disableLoginForm"
[processing]="loginProcessing"
[loginTry]="loginTry"
(login)="onLogin($event)"
>
{{ 'login.labels.instructionsOfLogin' | ucapI18n }}
</div>
<div ucapAuthenticationLogin="footer">
<div
class="remember-forgot-password"
fxLayout="row"
fxLayout.xs="column"
fxLayoutAlign="space-between center"
>
<mat-checkbox
#chkUseRememberMe
*ngIf="useRememberMe"
class="remember-me"
aria-label="Remember Me"
[checked]="!!userStore && userStore.rememberMe"
>
{{ 'login.labels.rememberMe' | ucapI18n }}
</mat-checkbox>
<mat-checkbox
#chkUseAutoLogin
*ngIf="useAutoLogin"
class="auto-login"
aria-label="Auto Login"
[checked]="
!!userStore &&
!!userStore.settings &&
!!userStore.settings.general &&
userStore.settings.general.autoLogin
"
>
{{ 'login.labels.autoLogin' | ucapI18n }}
</mat-checkbox>
</div>
<div class="register" fxLayout="column" fxLayoutAlign="center center">
<button
class="link btn-login-forgot"
(click)="onClickForgotPassword('ko')"
>
Forgot Password? KO
</button>
<button
class="link btn-login-forgot"
(click)="onClickForgotPassword('en')"
>
Forgot Password? EN
</button>
<div ucapAuthenticationLogin="header">
<div class="logo-img">
<img src="../../../assets/images/logo_140.png" alt="" />
</div>
<h1>Welcome to Messenger</h1>
</div>
<div class="policy bg-primary-light">
<a class="link">개인정보 처리방침</a>
<div ucapAuthenticationLogin="footer">
<div class="login-chk-area">
<div>
<mat-checkbox
#chkUseRememberMe
*ngIf="useRememberMe"
aria-label="Remember Me"
[checked]="!!userStore && userStore.rememberMe"
>
{{ 'login.labels.rememberMe' | ucapI18n }}
</mat-checkbox>
</div>
<div>
<mat-checkbox
#chkUseAutoLogin
*ngIf="useAutoLogin"
aria-label="Auto Login"
[checked]="
!!userStore &&
!!userStore.settings &&
!!userStore.settings.general &&
userStore.settings.general.autoLogin
"
>
{{ 'login.labels.autoLogin' | ucapI18n }}
</mat-checkbox>
</div>
</div>
<div class="login-pass-info">
<ul>
<li>
<a href="">{{ 'login.labels.forgotPassword' | ucapI18n }}</a>
</li>
<li>
<a href="" class="fir-pass">{{
'login.labels.resetPassword' | ucapI18n
}}</a>
</li>
</ul>
</div>
<div class="login-button-area">
<button type="button">
{{ 'login.labels.notesOnUse' | ucapI18n }}
</button>
</div>
</div>
</div>
</ucap-authentication-login>
</ucap-authentication-login-local>
</div>

View File

@ -0,0 +1,115 @@
@import '../../../../assets/scss/components';
h1 {
@include font-family($font-light);
font-size: 24px;
text-align: center;
color: $txt-color01;
font-weight: 600;
line-height: 1.2;
@include screen(mid) {
font-size: 19px;
}
@include screen(xs) {
font-size: 14px;
}
}
.login-section-container {
width: 100%;
}
.login-chk-area {
margin-top: 6px;
font-size: 13px;
text-align: left;
@include screen(xs) {
font-size: 12px;
}
}
.login-pass-info {
overflow: hidden;
margin-top: 83px;
ul {
display: flex;
justify-content: center;
li {
height: 24px;
position: relative;
display: inline-flex;
align-items: center;
padding: 0 12% 0 8%;
&::before {
content: '';
height: 11px;
width: 1px;
display: flex;
background-color: $gray-re4a;
position: absolute;
top: 6.5px;
left: 0;
}
&:first-child {
padding-left: 0;
&::before {
display: none;
}
}
&:last-child {
padding-right: 0;
}
a {
line-height: 24px;
font-size: 12px;
color: $gray-re4a;
padding-left: 34px;
position: relative;
white-space: nowrap;
&::before {
font-family: 'material Icons';
font-size: 18px;
text-align: center;
content: 'search';
color: $white;
display: block;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: $black;
position: absolute;
top: 0;
left: 0;
}
&.fir-pass {
&::before {
content: 'sync';
}
}
}
}
}
}
.login-button-area {
margin-top: 14px;
@include screen(xs) {
margin-top: 20px;
}
button {
border: 0;
margin: 0;
width: 100%;
height: 46px;
border-radius: 4px;
background-color: #e0e3e7;
font-size: 12px;
color: $gray-re4a;
cursor: pointer;
@include screen(mid) {
height: 38px;
}
@include screen(xs) {
height: 34px;
}
}
}

View File

@ -1,5 +1,5 @@
import { Subscription } from 'rxjs';
import { take, filter } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core';
@ -56,8 +56,7 @@ export class LoginSectionComponent implements OnInit, OnDestroy {
loginProcessing = false;
loginTry: LoginTry;
private companyListSubscription: Subscription;
private loginTrySubscription: Subscription;
private ngOnDestroySubject = new Subject<boolean>();
constructor(
private piService: PiService,
@ -70,9 +69,17 @@ export class LoginSectionComponent implements OnInit, OnDestroy {
) {}
ngOnInit(): void {
this.loginSession = this.appAuthenticationService.getLoginSession();
this.ngOnDestroySubject = new Subject<boolean>();
this.loginTry = this.sessionStorageService.get<LoginTry>(AppKey.LoginTry);
this.appAuthenticationService
.getLoginSession$()
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe((loginSession) => (this.loginSession = loginSession));
this.sessionStorageService
.get$<LoginTry>(AppKey.LoginTry)
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe((loginTry) => (this.loginTry = loginTry));
this.protocolService.disconnect();
@ -82,25 +89,19 @@ export class LoginSectionComponent implements OnInit, OnDestroy {
})
);
this.companyListSubscription = this.store
.pipe(select(CompanySelector.companyList))
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(CompanySelector.companyList)
)
.subscribe((companyList) => {
this.companyList = companyList;
});
this.loginTrySubscription = this.sessionStorageService.changed$
.pipe(filter((param) => AppKey.LoginTry === param.key))
.subscribe((param) => {
this.loginTry = param.value as LoginTry;
});
}
ngOnDestroy(): void {
if (!!this.companyListSubscription) {
this.companyListSubscription.unsubscribe();
}
if (!!this.loginTrySubscription) {
this.loginTrySubscription.unsubscribe();
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}

View File

@ -0,0 +1,71 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { MatBadgeModule } from '@angular/material/badge';
import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { UiModule } from '@ucap/ng-ui';
import { OrganizationUiModule } from '@ucap/ng-ui-organization';
// import { GroupUiModule } from '@ucap/ng-ui-group';
import { COMPONENTS, DIRECTIVES } from './components';
import { MatButtonModule } from '@angular/material/button';
import { MatRippleModule } from '@angular/material/core';
import { MatTreeModule } from '@angular/material/tree';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
FlexLayoutModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatCardModule,
MatCheckboxModule,
MatFormFieldModule,
MatAutocompleteModule,
MatIconModule,
MatInputModule,
MatSelectModule,
MatBadgeModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatRippleModule,
MatTreeModule,
PerfectScrollbarModule,
ScrollingModule,
I18nModule,
UiModule,
OrganizationUiModule
// GroupUiModule
],
exports: [...COMPONENTS, ...DIRECTIVES],
declarations: [...COMPONENTS, ...DIRECTIVES],
entryComponents: [],
providers: [
{
provide: UCAP_I18N_NAMESPACE,
useValue: ['chat']
}
]
})
export class AppChatSectionModule {}

View File

@ -0,0 +1,21 @@
<div class="chat-list-item">
<div class="profileImage">{{ defaultProfileImage }}</div>
<div class="roomName">
{{ roomName }}
<strong *ngIf="roomInfo.roomType === RoomType.Multi"
>({{ roomInfo.joinUserCount }})</strong
>
</div>
<div class="lastMessage">{{ roomInfo.finalEventMessage }}</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>
</div>

View File

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

View File

@ -0,0 +1,41 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
Input,
OnDestroy
} from '@angular/core';
import { RoomInfo, RoomType } from '@ucap/protocol-room';
import {
RoomUserMap,
RoomUserShortMap
} from '@ucap/ng-store-chat/lib/store/room/state';
import { LoginResponse } from '@ucap/protocol-authentication';
@Component({
selector: 'app-chat-list-item',
templateUrl: './chat-list-item.component.html',
styleUrls: ['./chat-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatListItemComponent implements OnInit, OnDestroy {
@Input()
roomInfo: RoomInfo;
@Input()
defaultProfileImage: string;
@Input()
profileImage: string;
@Input()
roomName: string;
RoomType = RoomType;
constructor() {}
ngOnInit(): void {}
ngOnDestroy(): void {}
}

View File

@ -0,0 +1,85 @@
<div class="ucap-group-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"
*ngIf="!checkable"
>
<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>
<mat-checkbox
*ngIf="checkable"
#checkbox
[checked]="isCheckedGroup(node)"
[disabled]="!isCheckableGroup(node)"
(change)="onChangeCheckGroup(checkbox.checked, node)"
(click)="$event.stopPropagation()"
class="group-check"
>
</mat-checkbox>
<button
mat-icon-button
aria-label="group-header-menu"
*ngIf="!checkable"
(click)="
$event.stopPropagation(); onClickHeaderMenu($event, node)
"
>
<mat-icon>more_vert</mat-icon>
</button>
</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,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { ExpansionComponent } from './expansion.component';
describe('ucap::ui-group::ExpansionComponent', () => {
let component: ExpansionComponent;
let fixture: ComponentFixture<ExpansionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ExpansionComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExpansionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,248 @@
import {
Component,
OnInit,
OnDestroy,
Input,
Output,
EventEmitter,
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 { 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';
import { RoomInfo } from '@ucap/protocol-room';
export interface ChatGroupNode {
nodeType: string;
roomInfo?: RoomInfo;
children?: ChatGroupNode[];
}
export interface FlatNode {
expandable: boolean;
level: number;
node: ChatGroupNode;
}
@Directive({
selector: '[ucapChatExpansionNode]'
})
export class ExpansionNodeDirective {}
@Directive({
selector: '[ucapChatExpansionHeader]'
})
export class ExpansionHeaderDirective {}
@Component({
selector: 'ucap-chat-expansion',
templateUrl: './expansion.component.html',
styleUrls: ['./expansion.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExpansionComponent implements OnInit, OnDestroy {
@Input()
set chatGroup(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: ChatGroupNode = {
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[item.division].push(node);
} else {
this.nodeMap.set(item.division, [node]);
}
}
}
this.refreshNodes();
}
@Input()
checkable = false;
// @Input()
// selectedUserList?: (UserInfo | UserInfoSS | UserInfoF | UserInfoDN)[] = [];
// @Input()
// unselectableUserList?: (
// | UserInfo
// | UserInfoSS
// | UserInfoF
// | UserInfoDN
// )[] = [];
@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
})
nodeTemplate: TemplateRef<ExpansionNodeDirective>;
@ContentChild(ExpansionHeaderDirective, {
read: TemplateRef,
static: false
})
headerTemplate: TemplateRef<ExpansionHeaderDirective>;
treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<ChatGroupNode, FlatNode>;
dataSource: VirtualScrollTreeFlatDataSource<ChatGroupNode, FlatNode>;
private nodeMap: Map<string, ChatGroupNode[]> = 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<ChatGroupNode, FlatNode>(
(node: ChatGroupNode, 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<
ChatGroupNode,
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();
}
}
onClickHeaderMenu(event: MouseEvent, node: FlatNode) {}
isCheckedGroup(node: FlatNode): boolean {
// const groupDetail = node.node.groupDetail;
// if (!groupDetail || groupDetail === undefined) {
// return false;
// }
// if (groupDetail.userSeqs.length === 0) {
// return false;
// }
// if (!!this.selectedUserList && this.selectedUserList.length > 0) {
// let allExist = true;
// groupDetail.userSeqs.some((seq) => {
// if (
// this.selectedUserList.filter((item) => item.seq === seq).length === 0
// ) {
// allExist = false;
// return true;
// }
// });
// return allExist;
// }
return false;
}
isCheckableGroup(node: FlatNode): boolean {
// if (!!this.unselectableUserList && this.unselectableUserList.length > 0) {
// const groupDetail = node.node.groupDetail;
// let allExist = true;
// groupDetail.userSeqs.some((seq) => {
// if (
// this.unselectableUserList.filter((item) => item.seq === seq)
// .length === 0
// ) {
// allExist = false;
// return true;
// }
// });
// if (allExist) {
// return false;
// }
// }
return true;
}
onChangeCheckGroup(value: boolean, node: FlatNode) {}
isHeader = (_: number, node: FlatNode) => 0 === node.level;
private refreshNodes() {
const rootNode: ChatGroupNode[] = [];
this.nodeMap.forEach((node) => rootNode.push(...node));
this.dataSource.data = rootNode;
this.changeDetectorRef.detectChanges();
}
}

View File

@ -0,0 +1,10 @@
import { ChatListItemComponent } from './chat-list-item.component';
import {
ExpansionComponent,
ExpansionNodeDirective,
ExpansionHeaderDirective
} from './expansion.component';
export const COMPONENTS = [ChatListItemComponent, ExpansionComponent];
export const DIRECTIVES = [ExpansionNodeDirective, ExpansionHeaderDirective];

View File

@ -0,0 +1,15 @@
import { SearchSectionComponent } from './search.section.component';
import { ListSectionComponent } from './list.section.component';
import {
COMPONENTS as COMPONENTS_UI,
DIRECTIVES as DIRECTIVES_UI
} from './component-ui';
export const COMPONENTS = [
...COMPONENTS_UI,
SearchSectionComponent,
ListSectionComponent
];
export const DIRECTIVES = [...DIRECTIVES_UI];

View File

@ -0,0 +1,42 @@
<div
*ngIf="!!searchObj && !searchObj.isShowSearch"
fxFlexFill
class="list-container"
>
<!-- <app-chat-list-item
*ngFor="let roomInfo of roomList"
[roomInfo]="roomInfo"
[roomName]="getRoomName(roomInfo)"
defaultProfileImage="assets/images/img_nophoto_50.png"
profileImage=""
></app-chat-list-item> -->
<ucap-chat-expansion [chatGroup]="chatGroup">
<ng-template ucapChatExpansionNode let-node>
<app-chat-list-item
[roomInfo]="node.roomInfo"
[roomName]="getRoomName(node.roomInfo)"
defaultProfileImage="assets/images/img_nophoto_50.png"
profileImage=""
></app-chat-list-item>
</ng-template>
<ng-template ucapChatExpansionHeader let-node>
<span class="header-buddy">
<span>{{ node.nodeType }} {{ node.nodeType | ucapDate: 'dddd' }}</span>
<span *ngIf="isToday(node.nodeType)">
({{ 'room.today' | ucapI18n }})
</span>
</span>
</ng-template>
</ucap-chat-expansion>
</div>
<div *ngIf="!!searchObj && searchObj.isShowSearch">
<app-chat-list-item
*ngFor="let roomInfo of searchRoomList"
[roomInfo]="roomInfo"
[roomName]="getRoomName(roomInfo)"
defaultProfileImage="assets/images/img_nophoto_50.png"
profileImage=""
></app-chat-list-item>
</div>

View File

@ -0,0 +1,2 @@
.list-container {
}

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ListSectionComponent } from './list.section.component';
describe('app::sections::group::ListSectionComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ListSectionComponent]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(ListSectionComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ucap-lg-web'`, () => {
const fixture = TestBed.createComponent(ListSectionComponent);
const app = fixture.componentInstance;
});
it('should render title', () => {
const fixture = TestBed.createComponent(ListSectionComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain(
'ucap-lg-web app is running!'
);
});
});

View File

@ -0,0 +1,300 @@
import { Observable, Subject, combineLatest, of } from 'rxjs';
import { filter, takeUntil, take, map, catchError } from 'rxjs/operators';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
ViewChild
} from '@angular/core';
import { Store, select, State } from '@ngrx/store';
import {
VirtualScrollStrategy,
FixedSizeVirtualScrollStrategy,
VIRTUAL_SCROLL_STRATEGY,
CdkVirtualScrollViewport
} from '@angular/cdk/scrolling';
import { VersionInfo2Response } from '@ucap/api-public';
import { Company } from '@ucap/api-external';
import { LoginResponse } from '@ucap/protocol-authentication';
import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
import { LogService } from '@ucap/ng-logger';
import { NodeType } from '@ucap/ng-ui-group';
import { SessionStorageService } from '@ucap/ng-web-storage';
import {
LoginSelector,
ConfigurationSelector
} from '@ucap/ng-store-authentication';
import { CompanySelector } from '@ucap/ng-store-organization';
import { BuddySelector, GroupSelector } from '@ucap/ng-store-group';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
import { QueryProtocolService } from '@ucap/ng-protocol-query';
import {
UserInfoSS,
DeptSearchType,
SSVC_TYPE_QUERY_DEPT_USER_DATA,
DeptUserData,
SSVC_TYPE_QUERY_DEPT_USER_RES
} from '@ucap/protocol-query';
import { RoomSelector } from '@ucap/ng-store-chat';
import {
RoomInfo,
UserInfo as RoomUserInfo,
UserInfoShort as RoomUserInfoShort,
RoomType
} from '@ucap/protocol-room';
import {
RoomUserMap,
RoomUserShortMap
} from '@ucap/ng-store-chat/lib/store/room/state';
import { Dictionary } from '@ngrx/entity';
import {
TranslatePipe as OrganizationTranslate,
TranslateService
} from '@ucap/ng-ui-organization';
import { I18nService } from '@ucap/ng-i18n';
import { AppChatService } from '@app/services/app-chat.service';
import { DateService } from '@ucap/ng-ui';
import moment from 'moment';
import 'moment-timezone';
export class ChatVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
constructor() {
super(60, 150, 200); // (itemSize, minBufferPx, maxBufferPx)
}
}
@Component({
selector: 'app-sections-chat-list',
templateUrl: './list.section.component.html',
styleUrls: ['./list.section.component.scss'],
providers: [
{
provide: VIRTUAL_SCROLL_STRATEGY,
useClass: ChatVirtualScrollStrategy
}
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListSectionComponent implements OnInit, OnDestroy {
@Input()
set searchObj(obj: { isShowSearch: boolean; searchWord: string }) {
this._searchObj = obj;
console.log(this._searchObj);
if (obj.isShowSearch && obj.searchWord.localeCompare('') !== 0) {
this.onRoomSearch(obj);
} else {
this._searchObj.isShowSearch = false;
this.searchRoomList = [];
}
}
get searchObj() {
return this._searchObj;
}
// tslint:disable-next-line: variable-name
_searchObj: any;
loginRes: LoginResponse;
roomList: RoomInfo[];
roomUsersDictionary: Dictionary<RoomUserMap>;
roomUsersShortDictionary: Dictionary<RoomUserShortMap>;
searchRoomList: RoomInfo[];
chatGroup: { division: string; roomList: RoomInfo[] }[];
organizationTranslate: OrganizationTranslate;
private ngOnDestroySubject = new Subject<boolean>();
constructor(
private appChatService: AppChatService,
private dateService: DateService,
private sessionStorageService: SessionStorageService,
private i18nService: I18nService,
private translateService: TranslateService,
private store: Store<any>,
private changeDetectorRef: ChangeDetectorRef,
private logService: LogService
) {
this.translateService.setDefaultLang(this.i18nService.currentLng);
this.translateService.use(this.i18nService.currentLng);
this.organizationTranslate = new OrganizationTranslate(
this.translateService,
this.changeDetectorRef
);
this.i18nService.setDefaultNamespace('chat');
}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject<boolean>();
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes))
.subscribe((loginRes) => {
this.loginRes = loginRes;
});
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(RoomSelector.rooms))
.subscribe((rooms) => {
rooms = (rooms || []).filter((info) => info.isJoinRoom);
this.roomList = rooms;
// groupping.
this.initGroup();
this.changeDetectorRef.detectChanges();
});
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(
(state: any) =>
state.chat.room.roomUsers.entities as Dictionary<RoomUserMap>
)
)
.subscribe((roomUsers) => {
this.roomUsersDictionary = roomUsers;
this.changeDetectorRef.detectChanges();
});
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(
(state: any) =>
state.chat.room.roomUsersShort.entities as Dictionary<
RoomUserShortMap
>
)
)
.subscribe((roomUsersShort) => {
this.roomUsersShortDictionary = roomUsersShort;
this.changeDetectorRef.detectChanges();
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
initGroup() {
this.chatGroup = [];
this.roomList.forEach((roomInfo) => {
const date = roomInfo.finalEventDate;
let division = '';
try {
const value = this.dateService.get(date, 'LL');
if (value === 'Invalid date') {
division = date;
} else {
division = value;
}
} catch (error) {
division = date;
}
const index = this.chatGroup.findIndex(
(info) => info.division === division
);
if (index > -1) {
this.chatGroup[index] = {
...this.chatGroup[index],
roomList: [...this.chatGroup[index].roomList, roomInfo]
};
} else {
this.chatGroup.push({
division,
roomList: [roomInfo]
});
}
});
}
getRoomName(roomInfo: RoomInfo): string {
if (!roomInfo) {
return '';
}
const roomName = this.appChatService.getRoomName(
this.organizationTranslate,
this.loginRes,
roomInfo,
this.roomUsersDictionary,
this.roomUsersShortDictionary
);
return roomName;
}
isToday(date: any) {
return this.dateService.isToday(date);
}
onRoomSearch(obj: { isShowSearch: boolean; searchWord: string }) {
const searchRoomList: RoomInfo[] = [];
this.roomList.forEach((roomInfo) => {
if (roomInfo.roomName.indexOf(obj.searchWord) > -1) {
searchRoomList.push(roomInfo);
} else {
const roomId = roomInfo.roomId;
const roomUsers = !!this.roomUsersDictionary
? this.roomUsersDictionary[roomId]
: undefined;
const roomUsersShort = !!this.roomUsersShortDictionary
? this.roomUsersShortDictionary[roomId]
: undefined;
let users = [];
let existUsers = false;
if (!!roomUsers && roomUsers.userInfos.length > 0) {
existUsers = true;
users = roomUsers.userInfos.filter(
(userInfo) => userInfo.seq !== Number(this.loginRes.userSeq)
);
} else if (!!roomUsersShort && roomUsersShort.userInfos.length > 0) {
existUsers = true;
users = roomUsersShort.userInfos.filter(
(userInfo) => userInfo.seq !== Number(this.loginRes.userSeq)
);
}
if (
existUsers &&
users.filter(
(userInfo) =>
userInfo.name.indexOf(obj.searchWord) > -1 ||
userInfo.nameEn.indexOf(obj.searchWord) > -1 ||
userInfo.nameCn.indexOf(obj.searchWord) > -1
).length > 0
) {
searchRoomList.push(roomInfo);
}
}
});
this.searchRoomList = searchRoomList;
}
}

View File

@ -0,0 +1,31 @@
import { Observable, Subject } from 'rxjs';
import {
VirtualScrollStrategy,
CdkVirtualScrollViewport
} from '@angular/cdk/scrolling';
import { distinctUntilChanged } from 'rxjs/operators';
export class ChatGroupVirtualScrollStrategy implements VirtualScrollStrategy {
scrolledIndexChange: Observable<number>;
private indexSubject = new Subject<number>();
private viewport: CdkVirtualScrollViewport | null = null;
constructor() {
this.scrolledIndexChange = this.indexSubject.pipe(distinctUntilChanged());
}
attach(viewport: CdkVirtualScrollViewport): void {
this.viewport = viewport;
}
detach(): void {
this.indexSubject.complete();
this.viewport = null;
}
onContentScrolled(): void {}
onDataLengthChanged(): void {}
onContentRendered(): void {}
onRenderedOffsetChanged(): void {}
scrollToIndex(index: number, behavior: ScrollBehavior): void {}
}

View File

@ -0,0 +1,37 @@
<div class="search-container">
<div class="searchbox">
<form [formGroup]="fgSearch">
<mat-form-field floatLabel="never">
<mat-label>{{ 'room.searchRoomByName' | ucapI18n }}</mat-label>
<input
matInput
#searchWordInput
type="text"
maxlength="20"
placeholder="{{ 'room.searchRoomByName' | ucapI18n }}"
formControlName="searchInput"
[matAutocomplete]="auto"
(keydown.enter)="onKeyDownEnter($event, searchWordInput.value)"
/>
<mat-autocomplete #auto="matAutocomplete">
<mat-option
*ngFor="let filteredRecommendedWord of filteredRecommendedWordList"
[value]="filteredRecommendedWord"
>
{{ filteredRecommendedWord }}
</mat-option>
</mat-autocomplete>
<button
mat-button
matSuffix
mat-icon-button
aria-label="Clear"
(click)="searchWordInput.value = ''; onClickCancel()"
>
<mat-icon>close</mat-icon>
</button>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</form>
</div>
</div>

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { SearchSectionComponent } from './search.section.component';
describe('app::sections::group::SearchSectionComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [SearchSectionComponent]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(SearchSectionComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ucap-lg-web'`, () => {
const fixture = TestBed.createComponent(SearchSectionComponent);
const app = fixture.componentInstance;
});
it('should render title', () => {
const fixture = TestBed.createComponent(SearchSectionComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain(
'ucap-lg-web app is running!'
);
});
});

View File

@ -0,0 +1,147 @@
import {
Component,
OnInit,
OnDestroy,
Output,
EventEmitter,
ChangeDetectorRef,
ChangeDetectionStrategy,
ViewChild
} from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Store, select } from '@ngrx/store';
import { LogService } from '@ucap/ng-logger';
import { takeUntil, debounceTime } from 'rxjs/operators';
import { Subject, combineLatest } from 'rxjs';
import { RoomSelector } from '@ucap/ng-store-chat';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { LoginResponse } from '@ucap/protocol-authentication';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
@Component({
selector: 'app-sections-chat-search',
templateUrl: './search.section.component.html',
styleUrls: ['./search.section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchSectionComponent implements OnInit, OnDestroy {
@Output()
keyDownEnter = new EventEmitter<{
searchWord: string;
}>();
@Output()
searchCancel = new EventEmitter<any>();
loginRes: LoginResponse;
fgSearch: FormGroup;
recommendedWordList: string[];
filteredRecommendedWordList: string[];
private ngOnDestroySubject = new Subject<boolean>();
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
constructor(
private store: Store<any>,
private formBuilder: FormBuilder,
private changeDetectorRef: ChangeDetectorRef,
private logService: LogService
) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject<boolean>();
this.fgSearch = this.formBuilder.group({
searchInput: null
});
this.fgSearch
.get('searchInput')
.valueChanges.pipe(debounceTime(100))
.subscribe((value) => {
if (value !== null && value.length > 0) {
this.filteredRecommendedWordList = this.recommendedWordList.filter(
(v) => {
return v.includes(value);
}
);
} else {
this.filteredRecommendedWordList = [];
}
this.changeDetectorRef.detectChanges();
});
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes))
.subscribe((loginRes) => {
this.loginRes = loginRes;
});
combineLatest([
this.store.pipe(select(RoomSelector.rooms)),
this.store.pipe(select(RoomSelector.roomUsers)),
this.store.pipe(select(RoomSelector.roomUsersShort))
])
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe(([rooms, roomUsers, roomUsersShort]) => {
rooms = rooms || [];
roomUsers = roomUsers || [];
roomUsersShort = roomUsersShort || [];
const recommendedWordList = [];
for (const r of rooms) {
if (!!r.roomName && '' !== r.roomName.trim()) {
recommendedWordList.push(r.roomName);
}
}
for (const ru of roomUsers) {
for (const u of ru.userInfos) {
if (!!this.loginRes && u.seq !== Number(this.loginRes.userSeq)) {
if (!!u.name && '' !== u.name.trim() && u.isJoinRoom) {
recommendedWordList.push(u.name);
}
}
}
}
for (const ru of roomUsersShort) {
for (const u of ru.userInfos) {
if (!!this.loginRes && u.seq !== Number(this.loginRes.userSeq)) {
if (!!u.name && '' !== u.name.trim() && u.isJoinRoom) {
recommendedWordList.push(u.name);
}
}
}
}
this.recommendedWordList = [
...recommendedWordList.filter(
(item, index) => recommendedWordList.indexOf(item) === index
)
];
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onKeyDownEnter(event: KeyboardEvent, searchWord: string) {
event.preventDefault();
event.stopPropagation();
this.autocomplete.closePanel();
this.keyDownEnter.emit({ searchWord });
}
onClickCancel() {
this.searchCancel.emit();
}
}

View File

@ -0,0 +1,131 @@
<mat-card class="confirm-card mat-elevation-z dialog-creat-chat">
<mat-card-header>
<mat-card-title
cdkDrag
cdkDragRootElement=".cdk-overlay-pane"
cdkDragHandle
>{{ data.title }}</mat-card-title
>
<button class="icon-button btn-dialog-close" (click)="onClickChoice(false)">
<i class="mdi mdi-window-close"></i>
</button>
</mat-card-header>
<mat-card-content>
<div>
<mat-horizontal-stepper [linear]="isLinear" #stepper>
<mat-step label="Step 1" state="phone">
<ucap-local-organization-select-user
(changeUserList)="onChangeSelectedUserList($event)"
></ucap-local-organization-select-user>
<ng-template
[ngTemplateOutlet]="selectedUserListTemplate"
></ng-template>
<div>
<button mat-button>취소</button>
<button mat-button matStepperNext>
완료
</button>
</div>
</mat-step>
<mat-step label="Step 2" state="chat">
<div>
<mat-label>새 그룹 추가</mat-label>
<input
matInput
#searchWordInput
placeholder="그룹 이름을 입력해주세요."
/>
<button
mat-button
matSuffix
mat-icon-button
aria-label="Clear"
(click)="searchWordInput.value = ''; onClickCancel()"
>
<mat-icon>close</mat-icon>
</button>
</div>
<ng-template
[ngTemplateOutlet]="selectedUserListTemplate"
></ng-template>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button (click)="onClickComplete(searchWordInput.value)">
Next
</button>
</div>
</mat-step>
</mat-horizontal-stepper>
</div>
<!-- <mat-card-actions class="button-form flex-row">
<button
mat-stroked-button
(click)="onClickChoice(false)"
class="mat-primary"
>
{{ 'common.messages.no' | translate }}
</button>
<button
mat-flat-button
[disabled]="getBtnValid()"
(click)="onClickChoice(true)"
class="mat-primary"
>
{{ 'common.messages.yes' | translate }}
</button>
</mat-card-actions> -->
</mat-card-content>
</mat-card>
<ng-template #selectedUserListTemplate>
<div class="list-chip">
<mat-chip-list aria-label="User selection">
<mat-chip
*ngFor="let userInfo of selectedUserList"
[selected]="getChipsRemoveYn(userInfo)"
(removed)="onClickDeleteUser(userInfo)"
>
<!-- {{ userInfo | ucapTranslate: 'name' }} -->
{{ userInfo.name }}
<mat-icon matChipRemove *ngIf="getChipsRemoveYn(userInfo)"
>clear</mat-icon
>
</mat-chip>
</mat-chip-list>
</div>
<ng-container
*ngIf="
SelectUserDialogType.NewChat === SelectUserDialogType.NewChat;
then newchatcount;
else defaultcount
"
></ng-container>
<ng-template #newchatcount>
<span [ngClass]="selectedUserList.length >= 300 ? 'text-warn-color' : ''">
{{ selectedUserList.length }} / 300
<!-- {{ environment.productConfig.CommonSetting.maxChatRoomUser - 1 }} -->
<!-- {{ 'common.units.persons' | translate }} -->
</span>
<span
class="text-warn-color"
style="float: right;"
*ngIf="selectedUserList.length >= 300"
>
<!-- ({{
'chat.errors.maxCountOfRoomMemberWith'
| translate
: {
maxCount:
environment.productConfig.CommonSetting.maxChatRoomUser - 1
}
}}) -->
</span>
</ng-template>
<ng-template #defaultcount>
<span>
{{ selectedUserList.length }}
<!-- {{ 'common.units.persons' | translate }} -->
</span>
</ng-template>
</ng-template>

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

View File

@ -0,0 +1,137 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Inject,
ViewChild
} from '@angular/core';
import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
import {
UserInfoSS,
UserInfoF,
UserInfoDN,
DeptInfo
} from '@ucap/protocol-query';
import { Store, select } from '@ngrx/store';
import { takeUntil } from 'rxjs/operators';
import {
CompanySelector,
DepartmentSelector
} from '@ucap/ng-store-organization';
import { Subject, combineLatest } from 'rxjs';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
import { SelectUserDialogType } from '@app/types';
import { RoomInfo, UserInfo as RoomUserInfo } from '@ucap/protocol-room';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { LoginResponse } from '@ucap/protocol-authentication';
import { environment } from '@environments';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { SelectUserSectionComponent } from '../../select-user.section.component';
import { GroupActions } from '@ucap/ng-store-group';
import { UserInfoTypes } from '../profile-list-item.component';
export interface CreateChatDialogData {
type?: SelectUserDialogType;
title: string;
/** CASE :: EditMember */
group?: GroupDetailData;
/** CASE :: EventForward */
ignoreRoom?: RoomInfo[];
/** CASE :: EditChatMember */
curRoomUser?: (
| UserInfo
| UserInfoSS
| UserInfoF
| UserInfoDN
| RoomUserInfo
)[];
}
export interface CreateChatDialogResult {}
@Component({
selector: 'ucap-local-organization-create-chat.dialog',
templateUrl: './create-chat.dialog.component.html',
styleUrls: ['./create-chat.dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreateChatDialogComponent implements OnInit, OnDestroy {
private ngOnDestroySubject = new Subject<boolean>();
isLinear = false;
firstFormGroup: FormGroup;
secondFormGroup: FormGroup;
selectedUserList: UserInfoTypes[] = [];
SelectUserDialogType = SelectUserDialogType;
constructor(
public dialogRef: MatDialogRef<
CreateChatDialogData,
CreateChatDialogResult
>,
@Inject(MAT_DIALOG_DATA) public data: CreateChatDialogData,
private changeDetectorRef: ChangeDetectorRef,
private store: Store<any>,
private appAuthenticationService: AppAuthenticationService
) {}
@ViewChild('selectBoxUserComponent', { static: false })
selectBoxUserComponent: SelectUserSectionComponent;
ngOnInit(): void {}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onClickCancel() {}
onClickChoice(s: boolean) {}
getBtnValid() {}
getChipsRemoveYn(userInfo: UserInfo) {}
onClickDeleteUser(userInfo: UserInfo) {}
onChangeSelectedUserList(userList: UserInfoTypes[]) {
this.selectedUserList = userList;
this.changeDetectorRef.markForCheck();
}
onClickComplete(groupName: string) {
switch (this.data.type) {
case SelectUserDialogType.NewGroup:
{
const userSeqs: string[] = [];
this.selectedUserList.map((user) =>
userSeqs.push(user.seq.toString())
);
this.store.dispatch(
GroupActions.create({
groupName,
targetUserSeqs: userSeqs
})
);
}
break;
case SelectUserDialogType.NewChat:
{
}
break;
case SelectUserDialogType.EditChatMember:
{
}
break;
case SelectUserDialogType.EditMember:
{
}
break;
case SelectUserDialogType.MessageForward:
{
}
break;
}
this.dialogRef.close();
}
}

View File

@ -0,0 +1,3 @@
import { CreateChatDialogComponent } from './create-chat.dialog.component';
export const DIALOGS = [CreateChatDialogComponent];

View File

@ -0,0 +1,9 @@
import { ProfileListItemComponent } from './profile-list-item.component';
import { ProfileComponent } from './profile.component';
import { TenantSearchComponent } from './tenant-search.component';
export const COMPONENTS = [
ProfileListItemComponent,
ProfileComponent,
TenantSearchComponent
];

View File

@ -0,0 +1,35 @@
<div class="ucap-organization-profile-list-item-container">
<span class="ucap-organization-profile-list-item-presence">bullet</span>
<div class="ucap-organization-profile-list-item-profile-image">
<img
class="thumbnail"
ucapImage
[base]="profileImageRoot"
[path]="userInfo.profileImageFile"
[default]="defaultProfileImage"
(click)="onClickProfileImage($event, userInfo)"
/>
</div>
<div class="user-info">
<div>
<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 class="intro">
{{ userInfo.intro }}
</div>
<div *ngIf="checkable">
<mat-checkbox
#checkbox
(change)="onChangeCheck(checkbox.checked, userInfo)"
></mat-checkbox>
</div>
</div>

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

View File

@ -0,0 +1,64 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from '@angular/core';
import { UserInfo } from '@ucap/protocol-sync';
import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query';
export type UserInfoTypes = UserInfo | UserInfoSS | UserInfoF | UserInfoDN;
@Component({
selector: 'ucap-local-organization-profile-list-item',
templateUrl: './profile-list-item.component.html',
styleUrls: ['./profile-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileListItemComponent implements OnInit, OnDestroy {
@Input()
set userInfo(user: UserInfoTypes) {
this._userInfo = user;
}
get userInfo(): UserInfoTypes {
return this._userInfo;
}
_userInfo: UserInfoTypes;
@Input()
defaultProfileImage: string;
@Input()
profileImageRoot: string;
@Input()
checkable = false;
@Output()
checkUser = new EventEmitter<{
isChecked: boolean;
userInfo: UserInfoTypes;
}>();
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit(): void {}
ngOnDestroy(): void {}
onClickProfileImage(event: Event, userInfo: UserInfoTypes): void {}
onChangeCheck(
value: boolean,
userInfo: UserInfo | UserInfoSS | UserInfoF | UserInfoDN
) {
this.checkUser.emit({
isChecked: value,
userInfo
});
}
}

View File

@ -0,0 +1,71 @@
<div class="mainProfile">
<mat-card class="example-card">
<mat-card-header>
<div mat-card-avatar class="profileImage" style="background-size: cover;">
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 50px; height: auto;"
/>
</div>
<mat-card-title class="name"
>{{ userInfo.name }}
<span class="grade">{{ userInfo.grade }}</span></mat-card-title
>
<mat-card-subtitle>({{ userInfo.nameEn }})</mat-card-subtitle>
<mat-card-subtitle><span>O</span>온라인</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<ng-container *ngIf="isMe; then isMe; else other"></ng-container>
<ng-template #isMe>
<div class="intro">
<mat-form-field class="example-full-width">
<mat-label>이름 부서명, 전화번호, 이메일</mat-label>
<input matInput placeholder="인트로" value="" />
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</ng-template>
<ng-template #other>
<mat-card-actions>
<button mat-button class="info" aria-label="메세지">메세지</button>
<button mat-button class="theme" aria-label="쪽지">쪽지</button>
<button mat-button class="theme" aria-label="휴대폰">휴대폰</button>
<button mat-button class="theme" aria-label="콜"></button>
<button mat-button class="theme" aria-label="화상회의">
화상회의
</button>
</mat-card-actions>
</ng-template>
<ul>
<li class="company">
<label>{{ 'profile.labels.company' | ucapI18n }}</label
>{{ userInfo.companyName }}
</li>
<li class="dept">
<label>{{ 'profile.labels.deptartment' | ucapI18n }}</label
>{{ userInfo.deptName }}
</li>
<li class="email">
<label>{{ 'profile.labels.email' | ucapI18n }}</label
>{{ userInfo.email }}
</li>
<li class="office">
<label>{{ 'profile.labels.officePhoneNumber' | ucapI18n }}</label
>{{ userInfo.lineNumber }}
</li>
<li class="mobile">
<label>{{ 'profile.labels.handphone' | ucapI18n }}</label
>{{ userInfo.hpNumber }}
</li>
</ul>
</mat-card-content>
<mat-card-actions>
<button mat-button class="info">info</button>
<button mat-button class="theme">theme1</button>
<button mat-button class="theme">theme2</button>
<button mat-button class="theme checked">theme3</button>
</mat-card-actions>
</mat-card>
</div>

View File

@ -0,0 +1,2 @@
.profile-container {
}

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ProfileComponent } from './profile.component';
describe('app::sections::group::ProfileComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ProfileComponent]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(ProfileComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ucap-lg-web'`, () => {
const fixture = TestBed.createComponent(ProfileComponent);
const app = fixture.componentInstance;
});
it('should render title', () => {
const fixture = TestBed.createComponent(ProfileComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain(
'ucap-lg-web app is running!'
);
});
});

View File

@ -0,0 +1,280 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
Output,
EventEmitter,
ViewChild,
ElementRef
} from '@angular/core';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
import { Subject } from 'rxjs';
import { UserInfoSS, AuthResponse } from '@ucap/protocol-query';
import { OpenProfileOptions } from '@ucap/protocol-buddy';
import { FileUploadItem } from '@ucap/api';
import { FormControl } from '@angular/forms';
import { WorkStatusType } from '@ucap/protocol';
import { I18nService } from '@ucap/ng-i18n';
@Component({
selector: 'app-group-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileComponent implements OnInit, OnDestroy {
private ngOnDestroySubject = new Subject<boolean>();
@Input()
profileImageRoot: string;
@Input()
isMe: boolean;
@Input()
isBuddy: boolean;
@Input()
isFavorit: boolean;
@Input()
set userInfo(u: UserInfoSS) {
this._userInfo = u;
}
get userInfo(): UserInfoSS {
return this._userInfo;
}
_userInfo: UserInfoSS;
@Input()
myMadn?: string;
@Input()
openProfileOptions?: OpenProfileOptions;
@Input()
useBuddyToggleButton: boolean;
@Input()
authInfo: AuthResponse;
@Output()
profileImageView = new EventEmitter<void>();
@Output()
openChat = new EventEmitter<UserInfoSS>();
@Output()
sendMessage = new EventEmitter<UserInfoSS>();
@Output()
sendCall = new EventEmitter<string>();
@Output()
sendSms = new EventEmitter<string>();
@Output()
createConference = new EventEmitter<number>();
@Output()
toggleFavorit = new EventEmitter<{
userInfo: UserInfoSS;
isFavorit: boolean;
}>();
@Output()
toggleBuddy = new EventEmitter<{
userInfo: UserInfoSS;
isBuddy: boolean;
}>();
@Output()
uploadProfileImage = new EventEmitter<FileUploadItem>();
@Output()
updateIntro = new EventEmitter<string>();
@ViewChild('profileImageFileInput', { static: false })
profileImageFileInput: ElementRef<HTMLInputElement>;
userIntroFormControl = new FormControl('');
profileImageFileUploadItem: FileUploadItem;
constructor(private i18nService: I18nService) {}
ngOnInit(): void {}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onClickProfileImageView() {
this.profileImageView.emit();
}
onClickOpenChat() {
this.openChat.emit(this.userInfo);
}
onClickCall(type: string) {
let calleeNumber = '';
if (type === 'LINE') {
calleeNumber = this.userInfo.lineNumber;
} else {
calleeNumber = this.userInfo.hpNumber;
}
this.sendCall.emit(calleeNumber);
}
onClickSMS() {
this.sendSms.emit(this.userInfo.employeeNum);
}
onClickVideoConference() {
this.createConference.emit(Number(this.userInfo.seq));
}
onClickMessage() {
this.sendMessage.emit(this.userInfo);
}
onToggleFavorit() {
this.isFavorit = !this.isFavorit;
this.toggleFavorit.emit({
userInfo: this.userInfo,
isFavorit: this.isFavorit
});
}
onClickAddBuddy() {
this.toggleBuddy.emit({
userInfo: this.userInfo,
isBuddy: true
});
}
onClickDelBuddy() {
this.toggleBuddy.emit({
userInfo: this.userInfo,
isBuddy: false
});
}
onApplyIntroMessage(intro: string) {
if (intro.trim().length < 1) {
this.updateIntro.emit(' ');
} else {
this.updateIntro.emit(intro);
}
}
onChangeFileInput() {
this.profileImageFileUploadItem = FileUploadItem.fromFiles(
this.profileImageFileInput.nativeElement.files
)[0];
this.uploadProfileImage.emit(this.profileImageFileUploadItem);
this.profileImageFileInput.nativeElement.value = '';
}
getWorkstatus(userInfo: UserInfoSS): string {
let workstatus = '';
if (!!userInfo && !!userInfo.workstatus) {
switch (userInfo.workstatus) {
case WorkStatusType.VacationAM:
workstatus = '오전';
break;
case WorkStatusType.VacationPM:
workstatus = '오후';
break;
case WorkStatusType.VacationAll:
workstatus = '휴가';
break;
case WorkStatusType.LeaveOfAbsence:
workstatus = '휴직';
break;
case WorkStatusType.LongtermRefresh:
workstatus = '장기';
break;
}
}
return workstatus;
}
getWorkstatusStyle(userInfo: UserInfoSS): string {
// morning-off: 오전 afternoon-off: 오후 day-off: 휴가 long-time: 장기 leave-of-absence: 휴직
let style = '';
if (!!userInfo && !!userInfo.workstatus) {
switch (userInfo.workstatus) {
case WorkStatusType.VacationAM:
style = 'morning-off';
break;
case WorkStatusType.VacationPM:
style = 'afternoon-off';
break;
case WorkStatusType.VacationAll:
style = 'day-off';
break;
case WorkStatusType.LeaveOfAbsence:
style = 'leave-of-absence';
break;
case WorkStatusType.LongtermRefresh:
style = 'long-time';
break;
}
}
return style;
}
getDisabledBtn(type: string): boolean {
if (!this.myMadn || this.myMadn.trim().length === 0) {
if (type === 'LINE' || type === 'MOBILE') {
return true;
}
}
if (type === 'LINE') {
if (
!!this.userInfo &&
!!this.userInfo.lineNumber &&
this.userInfo.lineNumber.trim().length > 0
) {
return false;
} else {
return true;
}
} else if (type === 'MOBILE') {
if (
!!this.userInfo &&
!!this.userInfo.hpNumber &&
this.userInfo.hpNumber.trim().length > 0
) {
return false;
} else {
return true;
}
} else if (type === 'SMS') {
// const smsUtils = new SmsUtils(
// this.sessionStorageService,
// this.nativeService
// );
// return !smsUtils.getAuthSms();
}
return true;
}
getShowBuddyToggleBtn(type: 'DEL' | 'ADD'): boolean {
let rtn = false;
if (!this.useBuddyToggleButton) {
return false;
}
if (type === 'ADD') {
if (!this.isBuddy) {
rtn = true;
}
} else if (type === 'DEL') {
if (!!this.isBuddy) {
rtn = true;
}
}
return rtn;
}
}

View File

@ -0,0 +1,32 @@
<div class="search-container">
<div class="selectbox">
<mat-select [(value)]="companyCode" disableOptionCentering>
<mat-option
*ngFor="let company of companyList"
[value]="company.companyCode"
>{{ company.companyName }}
</mat-option>
</mat-select>
</div>
<div class="searchbox">
<mat-form-field>
<mat-label>이름 부서명, 전화번호, 이메일</mat-label>
<input
matInput
#searchWordInput
placeholder="이름 부서명, 전화번호, 이메일"
(keydown.enter)="onKeyDownEnter(searchWordInput.value)"
/>
<button
mat-button
matSuffix
mat-icon-button
aria-label="Clear"
(click)="searchWordInput.value = ''; onClickCancel()"
>
<mat-icon>close</mat-icon>
</button>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
</div>
</div>

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

View File

@ -0,0 +1,73 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from '@angular/core';
import { UserInfo } from '@ucap/protocol-sync';
import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query';
import { Store, select } from '@ngrx/store';
import { takeUntil } from 'rxjs/operators';
import { CompanySelector } from '@ucap/ng-store-organization';
import { Subject } from 'rxjs';
import { Company } from '@ucap/api-external';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
@Component({
selector: 'ucap-local-organization-tenant-search',
templateUrl: './tenant-search.component.html',
styleUrls: ['./tenant-search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TenantSearchComponent implements OnInit, OnDestroy {
companyList: Company[];
companyCode: string;
@Output()
keyDownEnter = new EventEmitter<{
companyCode: string;
searchWord: string;
}>();
@Output()
searchCancel = new EventEmitter<any>();
private ngOnDestroySubject = new Subject<boolean>();
constructor(
private changeDetectorRef: ChangeDetectorRef,
private store: Store<any>,
private appAuthenticationService: AppAuthenticationService
) {}
ngOnInit(): void {
const userStore = this.appAuthenticationService.getUserStore();
this.companyCode = userStore.companyCode;
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(CompanySelector.companyList)
)
.subscribe((companyList) => {
this.companyList = companyList;
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onKeyDownEnter(searchWord: string) {
this.keyDownEnter.emit({ companyCode: this.companyCode, searchWord });
}
onClickCancel() {
this.searchCancel.emit();
}
}

View File

@ -1,4 +1,19 @@
import { ListSectionComponent } from './list.section.component';
import { SearchSectionComponent } from './search.section.component';
import { ProfileSectionComponent } from './profile.section.component';
import { InfoSectionComponent } from './info.section.component';
import { SelectUserSectionComponent } from './select-user.section.component';
export const COMPONENTS = [ListSectionComponent, SearchSectionComponent];
import { COMPONENTS as COMPONENTS_UI } from './component-ui';
import { DIALOGS as DIALOGS_UI } from './component-ui/dialogs';
export const COMPONENTS = [
...COMPONENTS_UI,
ListSectionComponent,
SearchSectionComponent,
ProfileSectionComponent,
InfoSectionComponent,
SelectUserSectionComponent
];
export const DIALOGS = [...DIALOGS_UI];

View File

@ -0,0 +1,99 @@
<div class="info">
<ng-container *ngIf="!!isMe; then myInfo; else otherInfo"> </ng-container>
<ng-template #myInfo>
<div class="bookmark">
<div class="subtitle">Bookmark</div>
<div class="chatlist">
<!-- loop > component > 대화 리스트 공용 -->
<div>
<div class="profileImage">
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 50px; height: 50px;"
/>
</div>
<div class="info">
<div class="roomName">UCAP 프로젝트방</div>
<div class="lastMessage">
대화방의 마지막대화내용이 들어갈껍니다.
</div>
</div>
<div class="subInfo">
<div class="lastDate" matBadge="4">2020.04.05</div>
</div>
</div>
<!--// loop > component > 대화 리스트 공용 -->
<div>
<div class="profileImage">
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 50px; height: 50px;"
/>
</div>
<div class="info">
<div class="roomName">UCAP 프로젝트방</div>
<div class="lastMessage">
대화방의 마지막대화내용이 들어갈껍니다.
</div>
</div>
<div class="subInfo">
<div class="lastDate">2020.04.05</div>
</div>
</div>
</div>
</div>
<div class="allim">
<div class="subtitle">알림봇</div>
<div class="allimList">
<mat-card class="allim-card">
<mat-card-header>
<div
mat-card-avatar
class="profileImage"
style="background-size: cover;"
>
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 50px; height: auto;"
/>
</div>
<mat-card-title>화상회의</mat-card-title>
<mat-card-subtitle>2020.04.05</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="title">화상회의 개설</div>
<div class="contents">화상회의가 개설되었습니다.</div>
</mat-card-content>
<mat-card-actions>
<button mat-button class="more">더보기</button>
</mat-card-actions>
</mat-card>
<mat-card class="allim-card">
<mat-card-header>
<div
mat-card-avatar
class="profileImage"
style="background-size: cover;"
>
<img
src="https://material.angular.io/assets/img/examples/shiba2.jpg"
style="width: 50px; height: auto;"
/>
</div>
<mat-card-title>화상회의</mat-card-title>
<mat-card-subtitle>2020.04.05</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="title">화상회의 개설</div>
<div class="contents">화상회의가 개설되었습니다.</div>
</mat-card-content>
<mat-card-actions>
<button mat-button class="more">더보기</button>
</mat-card-actions>
</mat-card>
</div>
</div>
</ng-template>
<ng-template #otherInfo> </ng-template>
<div class="banner">배너</div>
</div>

View File

@ -0,0 +1,2 @@
.info-container {
}

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { InfoSectionComponent } from './info.section.component';
describe('app::sections::group::InfoSectionComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [InfoSectionComponent]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(InfoSectionComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ucap-lg-web'`, () => {
const fixture = TestBed.createComponent(InfoSectionComponent);
const app = fixture.componentInstance;
});
it('should render title', () => {
const fixture = TestBed.createComponent(InfoSectionComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain(
'ucap-lg-web app is running!'
);
});
});

View File

@ -0,0 +1,58 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input
} from '@angular/core';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
import { Subject } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { takeUntil } from 'rxjs/operators';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { LoginResponse } from '@ucap/protocol-authentication';
@Component({
selector: 'app-sections-group-info',
templateUrl: './info.section.component.html',
styleUrls: ['./info.section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InfoSectionComponent implements OnInit, OnDestroy {
private ngOnDestroySubject = new Subject<boolean>();
@Input()
userSeq: string;
@Input()
isMe: boolean;
loginRes: LoginResponse;
constructor(private store: Store<any>) {}
ngOnInit(): void {
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes))
.subscribe((loginRes) => {
this.loginRes = loginRes;
if (
(!!this.userSeq &&
this.userSeq.localeCompare(loginRes.userSeq) === 0) ||
!this.userSeq
) {
this.isMe = true;
}
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -1,3 +1,59 @@
<div fxFlexFill class="list-container">
<ucap-group-expansion-list></ucap-group-expansion-list>
<div
*ngIf="!!searchObj && !searchObj.isShowSearch"
fxFlexFill
class="list-container"
>
<ucap-group-expansion
[displayOrder]="displayOrder"
[profile]="loginRes?.userInfo"
[favorites]="favorites"
[groupBuddies]="groupBuddies"
>
<ng-template ucapGroupExpansionNode let-node>
<ucap-local-organization-profile-list-item
[userInfo]="node.userInfo"
defaultProfileImage="assets/images/img_nophoto_50.png"
[profileImageRoot]="versionInfo2Res?.profileRoot"
[checkable]="checkable"
(checkUser)="onCheckUser($event)"
></ucap-local-organization-profile-list-item>
</ng-template>
<ng-template ucapGroupExpansionFavoriteHeader let-node>
<span class="header-favorite">
<span>
{{ 'category.favorite' | ucapI18n }}
</span>
<span>{{ node.children?.length }}</span>
</span>
</ng-template>
<ng-template ucapGroupExpansionBuddyHeader let-node>
<span class="header-buddy">
<span>{{ node.groupDetail.name }}</span>
<span>
{{ node.children?.length }}
</span>
</span>
</ng-template>
<ng-template ucapGroupExpansionDefaultHeader let-node>
<span class="header-default">
<span>
{{ 'category.default' | ucapI18n }}
</span>
<span>{{ node.children?.length }}</span>
</span>
</ng-template>
</ucap-group-expansion>
</div>
<div *ngIf="!!searchObj && searchObj.isShowSearch" class="search-wrpper">
<perfect-scrollbar fxFlex="1 1 auto">
<ucap-local-organization-profile-list-item
*ngFor="let userInfo of searchUserInfos"
[userInfo]="userInfo"
[checkable]="checkable"
defaultProfileImage="assets/images/img_nophoto_50.png"
(checkUser)="onCheckUser($event)"
>
</ucap-local-organization-profile-list-item>
</perfect-scrollbar>
</div>

View File

@ -1,2 +1,7 @@
.list-container {
}
.search-wrpper {
overflow: auto;
position: relative;
height: 350px;
}

View File

@ -1,8 +1,20 @@
import { Observable } from 'rxjs';
import { Observable, Subject, combineLatest, of } from 'rxjs';
import { filter, takeUntil, take, map, catchError } from 'rxjs/operators';
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input,
ViewChild,
EventEmitter,
Output
} from '@angular/core';
import { Store, select } from '@ngrx/store';
import { LogService } from '@ucap/ng-logger';
import {
VirtualScrollStrategy,
FixedSizeVirtualScrollStrategy,
@ -10,6 +22,34 @@ import {
CdkVirtualScrollViewport
} from '@angular/cdk/scrolling';
import { VersionInfo2Response } from '@ucap/api-public';
import { Company } from '@ucap/api-external';
import { LoginResponse } from '@ucap/protocol-authentication';
import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
import { LogService } from '@ucap/ng-logger';
import { NodeType } from '@ucap/ng-ui-group';
import { SessionStorageService } from '@ucap/ng-web-storage';
import {
LoginSelector,
ConfigurationSelector
} from '@ucap/ng-store-authentication';
import { CompanySelector } from '@ucap/ng-store-organization';
import { BuddySelector, GroupSelector } from '@ucap/ng-store-group';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
import { QueryProtocolService } from '@ucap/ng-protocol-query';
import {
UserInfoSS,
DeptSearchType,
SSVC_TYPE_QUERY_DEPT_USER_DATA,
DeptUserData,
SSVC_TYPE_QUERY_DEPT_USER_RES
} from '@ucap/protocol-query';
import { UserInfoTypes } from './component-ui/profile-list-item.component';
export class GroupVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
constructor() {
super(60, 150, 200); // (itemSize, minBufferPx, maxBufferPx)
@ -25,12 +65,220 @@ export class GroupVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
provide: VIRTUAL_SCROLL_STRATEGY,
useClass: GroupVirtualScrollStrategy
}
]
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListSectionComponent implements OnInit, OnDestroy {
constructor(private logService: LogService) {}
@Input()
set searchObj(obj: {
isShowSearch: boolean;
companyCode: string;
searchWord: string;
}) {
this._searchObj = obj;
if (obj.isShowSearch && obj.searchWord.localeCompare('') !== 0) {
this.onOrganizationTenantSearch(obj);
} else {
this._searchObj.isShowSearch = false;
this.searchUserInfos = [];
}
}
ngOnInit(): void {}
get searchObj() {
return this._searchObj;
}
_searchObj: any;
ngOnDestroy(): void {}
@Input()
set checkable(check: boolean) {
console.log(check);
this._checkable = check;
}
get checkable(): boolean {
return this._checkable;
}
_checkable = false;
@Output()
checkUser = new EventEmitter<{
isChecked: boolean;
userInfo: UserInfoTypes;
}>();
loginSession: LoginSession;
versionInfo2Res: VersionInfo2Response;
loginRes: LoginResponse;
companyList: Company[];
searchUserInfos: UserInfoSS[] = [];
displayOrder: NodeType[] = [
NodeType.Profile,
NodeType.Favorite,
NodeType.Buddy,
NodeType.Default
];
profile: UserInfo;
favorites: UserInfo[];
groupBuddies: { group: GroupDetailData; buddyList: UserInfo[] }[];
private ngOnDestroySubject = new Subject<boolean>();
constructor(
private appAuthenticationService: AppAuthenticationService,
private sessionStorageService: SessionStorageService,
private store: Store<any>,
private changeDetectorRef: ChangeDetectorRef,
private logService: LogService,
private queryProtocolService: QueryProtocolService
) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject<boolean>();
this.appAuthenticationService
.getLoginSession$()
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe((loginSession) => (this.loginSession = loginSession));
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(ConfigurationSelector.versionInfo2Response)
)
.subscribe((versionInfo2Res) => {
this.versionInfo2Res = versionInfo2Res;
});
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes))
.subscribe((loginRes) => {
this.loginRes = loginRes;
});
this.store
.pipe(
takeUntil(this.ngOnDestroySubject),
select(CompanySelector.companyList)
)
.subscribe((companyList) => {
this.companyList = companyList;
});
combineLatest([
this.store.pipe(select(BuddySelector.buddies)),
this.store.pipe(select(GroupSelector.groups))
])
.pipe(takeUntil(this.ngOnDestroySubject))
.subscribe(([buddies, groups]) => {
buddies = buddies || [];
groups = groups || [];
const favorites = buddies
.filter((buddy) => buddy.isFavorit)
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
if (!!favorites && 0 < favorites.length) {
this.favorites = favorites;
this.changeDetectorRef.markForCheck();
}
const tempOrder: GroupDetailData[] = [];
let defaultGroup: GroupDetailData;
const buddyGroup: GroupDetailData[] = [];
groups.forEach((group) => {
if (0 === group.seq) {
defaultGroup = group;
} else {
buddyGroup.push(group);
}
});
tempOrder.push(
...buddyGroup.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
)
);
if (!!defaultGroup) {
tempOrder.push(defaultGroup);
}
groups = tempOrder;
if (!!groups && 0 < groups.length) {
this.groupBuddies = [];
for (const group of groups) {
this.groupBuddies.push({
group,
buddyList: buddies.filter((buddy) => {
return -1 < group.userSeqs.indexOf(String(buddy.seq));
})
});
}
this.changeDetectorRef.markForCheck();
}
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onOrganizationTenantSearch(obj: {
isShowSearch: boolean;
companyCode: string;
searchWord: string;
}) {
const searchUserInfos: UserInfoSS[] = [];
this.queryProtocolService
.deptUser({
divCd: 'GRP',
companyCode: this._searchObj.companyCode,
searchRange: DeptSearchType.All,
search: this._searchObj.searchWord,
senderCompanyCode: this.loginRes.userInfo.companyCode,
senderEmployeeType: this.loginRes.userInfo.employeeType
})
.pipe(
map((resObj) => {
const userInfos = resObj.userInfos;
searchUserInfos.push(...userInfos);
// 검색 결과 처리.
this.searchUserInfos = searchUserInfos.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
);
// this.searchProcessing = false;
// 검색 결과에 따른 프레즌스 조회.
const userSeqList: number[] = [];
this.searchUserInfos.map((user) =>
userSeqList.push(Number(user.seq))
);
this.changeDetectorRef.markForCheck();
if (userSeqList.length > 0) {
// this.store.dispatch(
// StatusStore.bulkInfo({
// divCd: 'groupSrch',
// userSeqs: userSeqList
// })
// );
}
}),
catchError((error) => {
// this.searchProcessing = false;
return of(error);
})
)
.subscribe();
}
onCheckUser(params: { isChecked: boolean; userInfo: UserInfoTypes }) {
this.checkUser.emit(params);
}
}

View File

@ -0,0 +1,6 @@
<div fxFlexFill class="profile-container">
<app-group-profile
[isMe]="isMe"
[userInfo]="loginRes?.userInfo"
></app-group-profile>
</div>

View File

@ -0,0 +1,2 @@
.profile-container {
}

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ProfileSectionComponent } from './profile.section.component';
describe('app::sections::group::ProfileSectionComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ProfileSectionComponent]
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(ProfileSectionComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'ucap-lg-web'`, () => {
const fixture = TestBed.createComponent(ProfileSectionComponent);
const app = fixture.componentInstance;
});
it('should render title', () => {
const fixture = TestBed.createComponent(ProfileSectionComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain(
'ucap-lg-web app is running!'
);
});
});

View File

@ -0,0 +1,55 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Input
} from '@angular/core';
import { AppKey } from '@app/types/app-key.type';
import { LoginSession } from '@app/models/login-session';
import { Subject } from 'rxjs';
import { Store, select } from '@ngrx/store';
import { takeUntil } from 'rxjs/operators';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { LoginResponse } from '@ucap/protocol-authentication';
@Component({
selector: 'app-sections-group-profile',
templateUrl: './profile.section.component.html',
styleUrls: ['./profile.section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProfileSectionComponent implements OnInit, OnDestroy {
private ngOnDestroySubject = new Subject<boolean>();
@Input()
userSeq: string;
loginRes: LoginResponse;
isMe = false;
constructor(private store: Store<any>) {}
ngOnInit(): void {
this.store
.pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes))
.subscribe((loginRes) => {
this.loginRes = loginRes;
if (
!!this.userSeq &&
this.userSeq.localeCompare(loginRes.userSeq) === 0
) {
this.isMe = true;
}
});
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
}

View File

@ -1,3 +1,5 @@
<div>
search section of group
</div>
<ucap-local-organization-tenant-search
(keyDownEnter)="onKeyDownEnter($event)"
(searchCancel)="onClickCancel()"
>
</ucap-local-organization-tenant-search>

View File

@ -1,16 +1,44 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
Component,
OnInit,
OnDestroy,
Output,
EventEmitter,
ChangeDetectionStrategy
} from '@angular/core';
import { LogService } from '@ucap/ng-logger';
import { I18nService } from '@ucap/ng-i18n';
@Component({
selector: 'app-sections-group-search',
templateUrl: './search.section.component.html',
styleUrls: ['./search.section.component.scss']
styleUrls: ['./search.section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchSectionComponent implements OnInit, OnDestroy {
constructor(private logService: LogService) {}
@Output()
keyDownEnter = new EventEmitter<{
companyCode: string;
searchWord: string;
}>();
@Output()
searchCancel = new EventEmitter<any>();
constructor(
private logService: LogService,
private i18nService: I18nService
) {}
ngOnInit(): void {}
ngOnDestroy(): void {}
onKeyDownEnter(params: { companyCode: string; searchWord: string }) {
this.keyDownEnter.emit(params);
}
onClickCancel() {
this.searchCancel.emit();
}
}

View File

@ -0,0 +1,55 @@
<div fxFlexFill>
<div fxFlex class="container">
<!-- search start-->
<ucap-local-organization-tenant-search
(keyDownEnter)="onKeyDownEnter($event)"
(searchCancel)="onClickCancel()"
></ucap-local-organization-tenant-search>
<!-- search end-->
<mat-tab-group mat-stretch-tabs class="tap-container">
<!--그룹-->
<mat-tab>
<ng-template mat-tab-label>
<!-- <button class="icon-button">
<i class="mid mid-24 mdi-account-multiple"></i>
</button> -->
<p>그룹</p>
</ng-template>
<div fxFlexFill>
<div class="mat-tab-frame dialog-tab-grouplist">
<app-sections-group-list
fxFlexFill
[searchObj]="searchObj"
[checkable]="true"
(checkUser)="onCheckUser($event)"
></app-sections-group-list>
</div>
</div>
</mat-tab>
<!--조직-->
<mat-tab>
<ng-template mat-tab-label>
<!-- <button class="icon-button">
<i class="mid mid-24 mdi-account-multiple"></i>
</button> -->
<p>조직도</p>
</ng-template>
<div fxFlexFill>
<!-- <div class="mat-tab-frame dialog-tab-grouplist">
<app-sections-organization-tree></app-sections-organization-tree>
</div> -->
<div>
<span *ngFor="let breadcrumb of breadcrumbs">
<button mat-raised-button>{{ breadcrumb.label }}</button>
>
</span>
</div>
<div class="organization-tree">
<app-sections-organization-tree></app-sections-organization-tree>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</div>

View File

@ -0,0 +1,12 @@
.container {
overflow: hidden;
.tap-container {
height: 300px;
}
.organization-tree {
width: 100%;
height: calc(100% - 30px);
padding-bottom: 10px;
}
}

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

View File

@ -0,0 +1,107 @@
import {
Component,
OnInit,
OnDestroy,
ChangeDetectionStrategy,
ChangeDetectorRef,
Output,
EventEmitter
} from '@angular/core';
import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
import {
UserInfoSS,
UserInfoF,
UserInfoDN,
DeptInfo
} from '@ucap/protocol-query';
import { Store, select } from '@ngrx/store';
import { takeUntil } from 'rxjs/operators';
import {
CompanySelector,
DepartmentSelector
} from '@ucap/ng-store-organization';
import { Subject, combineLatest } from 'rxjs';
import { AppAuthenticationService } from '@app/services/app-authentication.service';
import { SelectUserDialogType } from '@app/types';
import { RoomInfo, UserInfo as RoomUserInfo } from '@ucap/protocol-room';
import { LoginSelector } from '@ucap/ng-store-authentication';
import { LoginResponse } from '@ucap/protocol-authentication';
import { environment } from '@environments';
import { UserInfoTypes } from './component-ui/profile-list-item.component';
@Component({
selector: 'ucap-local-organization-select-user',
templateUrl: './select-user.section.component.html',
styleUrls: ['./select-user.section.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectUserSectionComponent implements OnInit, OnDestroy {
breadcrumbs: any = [
{
label: 'LGCNS'
},
{
label: 'IT Helpdesk'
},
{
label: 'Issue Log'
}
];
@Output()
changeUserList = new EventEmitter<UserInfoTypes[]>();
searchObj: any = {
isShowSearch: false,
companyCode: '',
searchWord: ''
};
SelectUserDialogType = SelectUserDialogType;
selectedUserList: UserInfoTypes[] = [];
private ngOnDestroySubject = new Subject<boolean>();
constructor(
private changeDetectorRef: ChangeDetectorRef,
private store: Store<any>,
private appAuthenticationService: AppAuthenticationService
) {}
ngOnInit(): void {
this.ngOnDestroySubject = new Subject<boolean>();
}
ngOnDestroy(): void {
if (!!this.ngOnDestroySubject) {
this.ngOnDestroySubject.complete();
}
}
onKeyDownEnter(params: { companyCode: string; searchWord: string }) {
this.searchObj = {
isShowSearch: true,
companyCode: params.companyCode,
searchWord: params.searchWord
};
this.changeDetectorRef.detectChanges();
}
onClickCancel() {
this.searchObj = {
isShowSearch: false,
companyCode: '',
searchWord: ''
};
this.changeDetectorRef.detectChanges();
}
onCheckUser(params: { isChecked: boolean; userInfo: UserInfoTypes }) {
console.log(params);
this.selectedUserList = [...this.selectedUserList, params.userInfo];
this.changeUserList.emit(this.selectedUserList);
}
getSelectedUserList(): UserInfoTypes[] {
return this.selectedUserList;
}
}

View File

@ -1,27 +1,60 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatTabsModule } from '@angular/material/tabs';
import { MatChipsModule } from '@angular/material/chips';
import { MatButtonModule } from '@angular/material/button';
import { MatStepperModule } from '@angular/material/stepper';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n';
import { UiModule } from '@ucap/ng-ui';
import { GroupUiModule } from '@ucap/ng-ui-group';
import { COMPONENTS } from './components';
import { COMPONENTS, DIALOGS } from './components';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { AppOrganizationSectionModule } from '@app/sections/organization/organization.section.module';
import { OrganizationUiModule } from '@ucap/ng-ui-organization';
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
FlexLayoutModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatCardModule,
MatCheckboxModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatTabsModule,
MatChipsModule,
MatButtonModule,
MatStepperModule,
PerfectScrollbarModule,
I18nModule,
UiModule,
AppOrganizationSectionModule,
OrganizationUiModule,
GroupUiModule
],
exports: [...COMPONENTS],
declarations: [...COMPONENTS],
entryComponents: [],
exports: [...COMPONENTS, ...DIALOGS],
declarations: [...COMPONENTS, ...DIALOGS],
entryComponents: [...DIALOGS],
providers: [
{
provide: UCAP_I18N_NAMESPACE,

View File

@ -0,0 +1 @@
export const COMPONENTS = [];

Some files were not shown because too many files have changed in this diff Show More