diff --git a/angular.json b/angular.json index 5e26e97..6fd5534 100644 --- a/angular.json +++ b/angular.json @@ -55,8 +55,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" + "maximumWarning": "30kb", + "maximumError": "50kb" } ] }, @@ -86,8 +86,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" + "maximumWarning": "30kb", + "maximumError": "50kb" } ] }, diff --git a/package.json b/package.json index d465b20..66e237c 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,12 @@ "@ngrx/router-store": "^9.0.0", "@ngrx/store": "^9.0.0", "@ucap/api": "~0.0.2", - "@ucap/api-common": "~0.0.3", + "@ucap/api-common": "~0.0.5", "@ucap/api-external": "~0.0.5", "@ucap/api-message": "~0.0.3", "@ucap/api-prompt": "~0.0.3", "@ucap/api-public": "~0.0.4", - "@ucap/core": "~0.0.7", + "@ucap/core": "~0.0.10", "@ucap/logger": "~0.0.12", "@ucap/native": "~0.0.6", "@ucap/native-browser": "~0.0.5", @@ -46,7 +46,7 @@ "@ucap/ng-api-message": "~0.0.1", "@ucap/ng-api-prompt": "~0.0.1", "@ucap/ng-api-public": "~0.0.1", - "@ucap/ng-core": "~0.0.1", + "@ucap/ng-core": "~0.0.7", "@ucap/ng-logger": "~0.0.2", "@ucap/ng-i18n": "~0.0.6", "@ucap/ng-native": "~0.0.1", @@ -69,16 +69,16 @@ "@ucap/ng-protocol-sync": "~0.0.3", "@ucap/ng-protocol-umg": "~0.0.3", "@ucap/ng-store-authentication": "~0.0.11", - "@ucap/ng-store-chat": "~0.0.13", + "@ucap/ng-store-chat": "~0.0.16", "@ucap/ng-store-group": "~0.0.14", "@ucap/ng-store-organization": "~0.0.8", "@ucap/ng-web-socket": "~0.0.2", "@ucap/ng-web-storage": "~0.0.3", "@ucap/ng-ui": "~0.0.19", - "@ucap/ng-ui-organization": "~0.0.55", - "@ucap/ng-ui-authentication": "~0.0.24", + "@ucap/ng-ui-organization": "~0.0.83", + "@ucap/ng-ui-authentication": "~0.0.25", "@ucap/ng-ui-group": "~0.0.33", - "@ucap/ng-ui-chat": "~0.0.9", + "@ucap/ng-ui-chat": "~0.0.12", "@ucap/ng-ui-material": "~0.0.4", "@ucap/ng-ui-skin-default": "~0.0.1", "@ucap/pi": "~0.0.5", @@ -93,12 +93,12 @@ "@ucap/protocol-option": "~0.0.7", "@ucap/protocol-ping": "~0.0.6", "@ucap/protocol-query": "~0.0.5", - "@ucap/protocol-room": "~0.0.5", + "@ucap/protocol-room": "~0.0.6", "@ucap/protocol-service": "~0.0.4", "@ucap/protocol-status": "~0.0.5", "@ucap/protocol-sync": "~0.0.4", "@ucap/protocol-umg": "~0.0.5", - "@ucap/ui-scss": "~0.0.4", + "@ucap/ui-scss": "~0.0.5", "@ucap/web-socket": "~0.0.10", "@ucap/web-storage": "~0.0.9", "autolinker": "^3.13.0", diff --git a/src/app/app-provider.module.ts b/src/app/app-provider.module.ts index f163b09..781e27e 100644 --- a/src/app/app-provider.module.ts +++ b/src/app/app-provider.module.ts @@ -15,6 +15,7 @@ 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'; +import { AppFileService } from './services/app-file.service'; const GUARDS = [AppAuthenticationGuard]; const RESOLVERS = [AppSessionResolver]; @@ -22,6 +23,7 @@ const SERVICES = [ AppService, AppAuthenticationService, AppNativeService, + AppFileService, AppChatService ]; diff --git a/src/app/app.theme.scss b/src/app/app.theme.scss index eaac348..2dbb7d3 100644 --- a/src/app/app.theme.scss +++ b/src/app/app.theme.scss @@ -22,11 +22,11 @@ $typography: mat-typography-config( // 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); +$lgRed-app-primary: mat-palette($ucap-color-primary); +$lgRed-app-accent: mat-palette($ucap-color-accent, 700); // The warn palette is optional (defaults to red). -$lgRed-app-warn: mat-palette($lg-red); +$lgRed-app-warn: mat-palette($ucap-color-warn, 500); // Create the theme object (a Sass map containing all of the palettes). $lgRed-app-theme: mat-light-theme( diff --git a/src/app/layouts/components/default.layout.component.ts b/src/app/layouts/components/default.layout.component.ts index bb098cc..de48dd1 100644 --- a/src/app/layouts/components/default.layout.component.ts +++ b/src/app/layouts/components/default.layout.component.ts @@ -11,6 +11,7 @@ import { MatSidenav } from '@angular/material/sidenav'; import { LogService } from '@ucap/ng-logger'; import { AppSelector } from '@app/store/state'; +import { AppChatService } from '@app/services/app-chat.service'; const NAVS = ['group', 'chat', 'organization', 'message']; @@ -37,6 +38,7 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy { constructor( private router: Router, private store: Store, + private appChatService: AppChatService, private logService: LogService ) {} @@ -175,7 +177,7 @@ export class DefaultLayoutComponent implements OnInit, OnDestroy { break; case 'CAHT_NEW_ADD': { - this.logService.debug('CAHT_NEW_ADD'); + this.appChatService.newOpenRoomDialog(); } break; case 'CHAT_NEW_TIMER_ADD': diff --git a/src/app/layouts/layouts.module.ts b/src/app/layouts/layouts.module.ts index 83663e0..e958ce8 100644 --- a/src/app/layouts/layouts.module.ts +++ b/src/app/layouts/layouts.module.ts @@ -17,6 +17,9 @@ import { UiModule } from '@ucap/ng-ui'; import { COMPONENTS } from './components'; import { DIALOGS } from './dialogs'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n'; +import { MatSelectModule } from '@angular/material/select'; @NgModule({ imports: [ @@ -30,13 +33,21 @@ import { DIALOGS } from './dialogs'; MatSidenavModule, MatTabsModule, MatToolbarModule, + MatSelectModule, PerfectScrollbarModule, + I18nModule, UiModule ], exports: [...COMPONENTS, ...DIALOGS], declarations: [...COMPONENTS, ...DIALOGS], - entryComponents: [...DIALOGS] + entryComponents: [...DIALOGS], + providers: [ + { + provide: UCAP_I18N_NAMESPACE, + useValue: ['chat', 'common'] + } + ] }) export class AppLayoutsModule {} diff --git a/src/app/pages/account/components/login.page.component.scss b/src/app/pages/account/components/login.page.component.scss index ac07d68..1ec2761 100644 --- a/src/app/pages/account/components/login.page.component.scss +++ b/src/app/pages/account/components/login.page.component.scss @@ -7,9 +7,11 @@ $login-bg-h: 100/1080; width: 100%; height: 100%; overflow: auto; - // box-sizing: border-box; - // display: flex; - // flex-direction: column; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; background-color: $bg-gray; background-image: url(../../../../assets/images/bg/bg_login_circle_square01.svg), diff --git a/src/app/pages/chat/chat.page.module.ts b/src/app/pages/chat/chat.page.module.ts index f1fe2d1..d49c340 100644 --- a/src/app/pages/chat/chat.page.module.ts +++ b/src/app/pages/chat/chat.page.module.ts @@ -15,6 +15,7 @@ import { AppChatRoutingPageModule } from './chat-routing.page.module'; import { UiModule } from '@ucap/ng-ui'; import { COMPONENTS } from './components'; +import { UCAP_I18N_NAMESPACE, I18nModule } from '@ucap/ng-i18n'; @NgModule({ imports: [ @@ -30,9 +31,16 @@ import { COMPONENTS } from './components'; AppChatSectionModule, AppChatRoutingPageModule, + I18nModule, UiModule ], declarations: [...COMPONENTS], - entryComponents: [] + entryComponents: [], + providers: [ + { + provide: UCAP_I18N_NAMESPACE, + useValue: ['chat', 'common'] + } + ] }) export class AppChatPageModule {} diff --git a/src/app/pages/chat/components/sidenav.page.component.html b/src/app/pages/chat/components/sidenav.page.component.html index bf10f80..6904461 100644 --- a/src/app/pages/chat/components/sidenav.page.component.html +++ b/src/app/pages/chat/components/sidenav.page.component.html @@ -1,6 +1,6 @@
-

대화

+

{{ 'label.chat' | ucapI18n }}

- +
@@ -29,7 +29,7 @@
diff --git a/src/app/pages/group/components/sidenav.page.component.ts b/src/app/pages/group/components/sidenav.page.component.ts index f72840f..9e0c0f8 100644 --- a/src/app/pages/group/components/sidenav.page.component.ts +++ b/src/app/pages/group/components/sidenav.page.component.ts @@ -1,5 +1,5 @@ -import { Subscription, of } from 'rxjs'; -import { take, map, catchError } from 'rxjs/operators'; +import { of, Subject } from 'rxjs'; +import { take, map, catchError, takeUntil } from 'rxjs/operators'; import { Component, @@ -14,14 +14,14 @@ import { Store } from '@ngrx/store'; import { MatDialog } from '@angular/material/dialog'; +import { ParamsUtil } from '@ucap/ng-core'; import { LogService } from '@ucap/ng-logger'; -import { GroupActions } from '@ucap/ng-store-group'; - -import { SelectUserDialogType } from '@app/types'; +import { I18nService } from '@ucap/ng-i18n'; import { CreateDialogComponent } from '@app/sections/group/dialogs/create.dialog.component'; -import { I18nService } from '@ucap/ng-i18n'; import { ListSectionComponent } from '@app/sections/group/components/list.section.component'; +import { SearchData } from '@app/ucap/organization/models/search-data'; +import { QueryParams } from '@app/pages/organization/types/params.type'; @Component({ selector: 'app-pages-group-sidenav', @@ -32,14 +32,19 @@ export class SidenavPageComponent implements OnInit, OnDestroy { @ViewChild('sectionGroupList', { static: false }) sectionGroupList: ListSectionComponent; - searchObj: any = { - isShowSearch: false, - companyCode: '', - searchWord: '' - }; + set companySearchData(searchData: SearchData) { + this._companySearchData = searchData; + } + get companySearchData() { + return this._companySearchData; + } + // tslint:disable-next-line: variable-name + _companySearchData: SearchData; showType: string; + private ngOnDestroySubject: Subject; + constructor( private activatedRoute: ActivatedRoute, private router: Router, @@ -53,33 +58,14 @@ export class SidenavPageComponent implements OnInit, OnDestroy { } ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + this.showType = 'ALL'; } ngOnDestroy(): void {} onClickFab(event: MouseEvent) {} - onKeyDownSearch(params: { - isShowSearch: boolean; - companyCode: string; - searchWord: string; - }) { - this.searchObj = { - isShowSearch: params.isShowSearch, - companyCode: params.companyCode, - searchWord: params.searchWord - }; - this.changeDetectorRef.detectChanges(); - } - - onClickCancel() { - this.searchObj = { - isShowSearch: false, - companyCode: '', - searchWord: '' - }; - this.changeDetectorRef.detectChanges(); - } onClickGroupMenu(menuType: string) { switch (menuType) { diff --git a/src/app/pages/organization/components/index.page.component.html b/src/app/pages/organization/components/index.page.component.html index fd96073..aedd9e3 100644 --- a/src/app/pages/organization/components/index.page.component.html +++ b/src/app/pages/organization/components/index.page.component.html @@ -1,5 +1,12 @@ -
- +
+ +
+ + +
+ +
+ + +
diff --git a/src/app/pages/organization/components/index.page.component.scss b/src/app/pages/organization/components/index.page.component.scss index e69de29..360f071 100644 --- a/src/app/pages/organization/components/index.page.component.scss +++ b/src/app/pages/organization/components/index.page.component.scss @@ -0,0 +1,4 @@ +.index-page-container { + width: 100%; + height: 100%; +} diff --git a/src/app/pages/organization/components/index.page.component.ts b/src/app/pages/organization/components/index.page.component.ts index 63a1fbf..1393e9c 100644 --- a/src/app/pages/organization/components/index.page.component.ts +++ b/src/app/pages/organization/components/index.page.component.ts @@ -2,10 +2,14 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router, Params } from '@angular/router'; import { Store } from '@ngrx/store'; +import { ParamsUtil } from '@ucap/ng-core'; + +import { SearchData } from '@app/ucap/organization/models/search-data'; + import { QueryParams } from '../types/params.type'; @Component({ @@ -14,12 +18,17 @@ import { QueryParams } from '../types/params.type'; styleUrls: ['./index.page.component.scss'] }) export class IndexPageComponent implements OnInit, OnDestroy { + set companySearchData(searchData: SearchData) { + this._companySearchData = searchData; + this.onChangedCompanySearch(); + } + get companySearchData() { + return this._companySearchData; + } // tslint:disable-next-line: variable-name - _searchData: { - companyCode: string; - searchWord: string; - isSearch: boolean; - }; + _companySearchData: SearchData; + + deptSearchData: SearchData; deptSeq: string; @@ -27,6 +36,7 @@ export class IndexPageComponent implements OnInit, OnDestroy { constructor( private store: Store, + private router: Router, private activatedRoute: ActivatedRoute, private changeDetectorRef: ChangeDetectorRef ) {} @@ -37,14 +47,25 @@ export class IndexPageComponent implements OnInit, OnDestroy { this.activatedRoute.queryParams .pipe(takeUntil(this.ngOnDestroySubject)) .subscribe((params) => { - console.log('activatedRoute.queryParams'); if (!!params) { - const companyCode = params[QueryParams.DEPT_SEQ]; - console.log('activatedRoute.queryParams', companyCode); - this._searchData = { - companyCode, - searchWord: '', - isSearch: false + const deptSeq = params[QueryParams.DEPT_SEQ]; + const companyCode = params[QueryParams.COMPANY_CODE]; + const searchWord = params[QueryParams.SEARCH_WORD]; + const bySearch = ParamsUtil.getParameter( + params, + QueryParams.BY_SEARCH, + false + ); + + this.deptSearchData = { + deptSeq: bySearch ? undefined : deptSeq, + companyCode: bySearch ? companyCode : undefined, + searchWord: bySearch ? searchWord : undefined, + bySearch + }; + + this._companySearchData = { + ...this.deptSearchData }; } }); @@ -55,4 +76,23 @@ export class IndexPageComponent implements OnInit, OnDestroy { this.ngOnDestroySubject.complete(); } } + + onChangedCompanySearch() { + const queryParams: Params = {}; + queryParams[QueryParams.COMPANY_CODE] = this._companySearchData.companyCode; + queryParams[QueryParams.SEARCH_WORD] = this._companySearchData.searchWord; + queryParams[QueryParams.BY_SEARCH] = String(true); + + this.router.navigate( + [ + 'organization', + { + outlets: { content: 'index' } + } + ], + { + queryParams + } + ); + } } diff --git a/src/app/pages/organization/components/sidenav.page.component.ts b/src/app/pages/organization/components/sidenav.page.component.ts index e487348..7b496bb 100644 --- a/src/app/pages/organization/components/sidenav.page.component.ts +++ b/src/app/pages/organization/components/sidenav.page.component.ts @@ -52,6 +52,7 @@ export class SidenavPageComponent implements OnInit, OnDestroy { onClickedTree(node: DeptInfo) { const queryParams: Params = {}; queryParams[QueryParams.DEPT_SEQ] = String(node.seq); + queryParams[QueryParams.BY_SEARCH] = String(false); this.router.navigate( [ diff --git a/src/app/pages/organization/types/params.type.ts b/src/app/pages/organization/types/params.type.ts index dc41404..27d221d 100644 --- a/src/app/pages/organization/types/params.type.ts +++ b/src/app/pages/organization/types/params.type.ts @@ -1,3 +1,6 @@ export enum QueryParams { - DEPT_SEQ = 'dept_seq' + DEPT_SEQ = 'dept_seq', + COMPANY_CODE = 'company_code', + SEARCH_WORD = 'search_word', + BY_SEARCH = 'by_search' } diff --git a/src/app/sections/chat/chat.section.module.ts b/src/app/sections/chat/chat.section.module.ts index b3e1a0a..88a834a 100644 --- a/src/app/sections/chat/chat.section.module.ts +++ b/src/app/sections/chat/chat.section.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; +import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { FlexLayoutModule } from '@angular/flex-layout'; @@ -20,6 +20,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTreeModule } from '@angular/material/tree'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatStepperModule } from '@angular/material/stepper'; import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; @@ -27,12 +28,16 @@ import { I18nModule, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n'; import { UiModule } from '@ucap/ng-ui'; import { ChatUiModule } from '@ucap/ng-ui-chat'; import { AppChatModule } from '@app/ucap/chat/chat.module'; +import { AppLayoutsModule } from '@app/layouts/layouts.module'; +import { AppGroupSectionModule } from '../group/group.section.module'; import { COMPONENTS } from './components'; +import { DIALOGS } from './dialogs'; @NgModule({ imports: [ CommonModule, + FormsModule, ReactiveFormsModule, FlexLayoutModule, @@ -50,6 +55,7 @@ import { COMPONENTS } from './components'; MatRippleModule, MatTreeModule, MatTooltipModule, + MatStepperModule, PerfectScrollbarModule, ScrollingModule, @@ -57,12 +63,14 @@ import { COMPONENTS } from './components'; I18nModule, UiModule, + AppLayoutsModule, + AppGroupSectionModule, ChatUiModule, AppChatModule ], - exports: [...COMPONENTS], - declarations: [...COMPONENTS], - entryComponents: [], + exports: [...COMPONENTS, ...DIALOGS], + declarations: [...COMPONENTS, ...DIALOGS], + entryComponents: [...DIALOGS], providers: [ { provide: UCAP_I18N_NAMESPACE, diff --git a/src/app/sections/chat/components/form.section.component.html b/src/app/sections/chat/components/form.section.component.html index 3d6ec46..ede3691 100644 --- a/src/app/sections/chat/components/form.section.component.html +++ b/src/app/sections/chat/components/form.section.component.html @@ -1,16 +1,23 @@ - + - + @@ -23,13 +30,22 @@ > + +
@@ -47,7 +62,7 @@ aria-label="attachImage" matTooltipPosition="above" matTooltip="{{ 'label.attachImage' | ucapI18n }}" - (click)="onOpenSelector('')" + (click)="onOpenSelector(SelectorType.EMPTY)" > 이미지 @@ -56,7 +71,7 @@ aria-label="screenshot" matTooltipPosition="above" matTooltip="{{ 'label.screenshot' | ucapI18n }}" - (click)="onOpenSelector('')" + (click)="onOpenSelector(SelectorType.EMPTY)" > 캡쳐 화면 전송 @@ -65,7 +80,7 @@ aria-label="imoticon" matTooltipPosition="above" matTooltip="{{ 'label.imoticon' | ucapI18n }}" - (click)="onOpenSelector('STICKER')" + (click)="onOpenSelector(SelectorType.STICKER)" > 이모티콘 @@ -74,7 +89,7 @@ aria-label="emailSend" matTooltipPosition="above" matTooltip="{{ 'label.emailSend' | ucapI18n }}" - (click)="onOpenSelector('EMAILSENDER')" + (click)="onOpenSelector(SelectorType.EMAILSENDER)" > 대화내용 메일 전송 @@ -83,7 +98,7 @@ aria-label="translation" matTooltipPosition="above" matTooltip="{{ 'label.translation' | ucapI18n }}" - (click)="onOpenSelector('TRANSLATION')" + (click)="onOpenSelector(SelectorType.TRANSLATION)" > 대화내용 번역 @@ -92,7 +107,7 @@ aria-label="gams" matTooltipPosition="above" matTooltip="{{ 'label.gams' | ucapI18n }}" - (click)="onOpenSelector('')" + (click)="onOpenSelector(SelectorType.EMPTY)" > +GAMS @@ -101,7 +116,7 @@ color="accent" aria-label="send" matTooltip="{{ 'label.send' | ucapI18n }}" - (click)="onOpenSelector('')" + (click)="send()" > send diff --git a/src/app/sections/chat/components/form.section.component.ts b/src/app/sections/chat/components/form.section.component.ts index da88483..f2f42b3 100644 --- a/src/app/sections/chat/components/form.section.component.ts +++ b/src/app/sections/chat/components/form.section.component.ts @@ -1,5 +1,5 @@ -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Subject, of, Observable, forkJoin } from 'rxjs'; +import { takeUntil, map, catchError, take } from 'rxjs/operators'; import { Component, @@ -7,12 +7,61 @@ import { OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, - Input + Input, + ViewChild, + ElementRef } from '@angular/core'; import { Store, select } from '@ngrx/store'; -import { RoomInfo } from '@ucap/protocol-room'; import { Dictionary } from '@ngrx/entity'; +import { RoomInfo } from '@ucap/protocol-room'; +import { + SendRequest as SendEventRequest, + EventType +} from '@ucap/protocol-event'; +import { LoginResponse } from '@ucap/protocol-authentication'; + +import { ChattingActions } from '@ucap/ng-store-chat'; +import { + LoginSelector, + ConfigurationSelector +} from '@ucap/ng-store-authentication'; +import { StickerFilesInfo, KEY_STICKER_HISTORY } from '@ucap/ng-core'; +import { + AlertDialogComponent, + AlertDialogData, + AlertDialogResult +} from '@ucap/ng-ui'; +import { I18nService } from '@ucap/ng-i18n'; +import { LogService } from '@ucap/ng-logger'; + +import { MatDialog } from '@angular/material/dialog'; +import { + TranslationSaveResponse, + MassTalkSaveRequest, + FileTalkSaveResponse, + FileTalkSaveRequest +} from '@ucap/api-common'; +import { environment } from '@environments'; +import { LocalStorageService } from '@ucap/ng-web-storage'; +import { CommonApiService } from '@ucap/ng-api-common'; +import { LoginSession } from '@app/models/login-session'; +import { AppAuthenticationService } from '@app/services/app-authentication.service'; +import { StatusCode, FileUploadItem } from '@ucap/api'; +import { AppFileService } from '@app/services/app-file.service'; +import { VersionInfo2Response } from '@ucap/api-public'; +import { FileUploadSelectorComponent } from '@app/ucap/chat/components/file-upload.selector.component'; +import { FileUtil } from '@ucap/core'; +import { AppChatService } from '@app/services/app-chat.service'; + +export enum SelectorType { + EMPTY = '', + STICKER = 'STICKER', + TRANSLATION = 'TRANSLATION', + FILEUPLOAD = 'FILEUPLOAD', + EMAILSENDER = 'EMAILSENDER' +} + @Component({ selector: 'app-sections-chat-form', templateUrl: './form.section.component.html', @@ -30,19 +79,70 @@ export class FormSectionComponent implements OnInit, OnDestroy { // tslint:disable-next-line: variable-name _roomId: string; + versionInfo2Res: VersionInfo2Response; + loginSession: LoginSession; + loginRes: LoginResponse; + currentRoomInfo: RoomInfo; - selectorType = ''; + selectorType: SelectorType = SelectorType.EMPTY; + + /** About Sticker */ + selectedSticker: StickerFilesInfo; + + /** About Translation */ + translationSimpleview = false; + translationPreview = false; + destLocale = 'en'; // default English :: en + translationPreviewInfo: { + previewInfo: TranslationSaveResponse | null; + translationType: EventType.Translation | EventType.MassTranslation; + }; + + @ViewChild('messageInput', { static: false }) + messageInput: ElementRef; + @ViewChild('fileInput', { static: false }) + fileInput: ElementRef; + @ViewChild('fileUploadSelector', { static: false }) + fileUploadSelector: FileUploadSelectorComponent; + + SelectorType = SelectorType; private ngOnDestroySubject: Subject; constructor( + private appFileService: AppFileService, + private appChatService: AppChatService, private store: Store, + private i18nService: I18nService, + private dialog: MatDialog, + private localStorageService: LocalStorageService, + private logService: LogService, + private appAuthenticationService: AppAuthenticationService, + private commonApiService: CommonApiService, private changeDetectorRef: ChangeDetectorRef ) {} ngOnInit(): void { this.ngOnDestroySubject = new Subject(); + 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.appAuthenticationService + .getLoginSession$() + .pipe(takeUntil(this.ngOnDestroySubject)) + .subscribe((loginSession) => (this.loginSession = loginSession)); + this.store .pipe( takeUntil(this.ngOnDestroySubject), @@ -65,7 +165,176 @@ export class FormSectionComponent implements OnInit, OnDestroy { } } - onOpenSelector(type: string): void { + /** About Selector */ + onOpenSelector(type: SelectorType): void { this.selectorType = type; + this.changeDetectorRef.detectChanges(); + } + clearSelector(): void { + this.selectorType = SelectorType.EMPTY; + this.selectedSticker = null; + this.changeDetectorRef.detectChanges(); + } + + /** Element Handling */ + focus(clearField: boolean = true): void { + if (!!this.messageInput) { + if (!!clearField) { + this.messageInput.nativeElement.value = ''; + + this.clearSelector(); + } + this.messageInput.nativeElement.focus(); + } + } + + onChangeFileInput(): void { + const self = this; + const fileList = this.fileInput.nativeElement.files; + + this.appFileService + .validUploadFile(fileList, this.versionInfo2Res?.fileAllowSize) + .then((result) => { + if (!result) { + self.fileInput.nativeElement.value = ''; + return; + } else { + // selector open + self.onOpenSelector(SelectorType.FILEUPLOAD); + + // FileuploadItem Init. & FileSelector Init. + const fileUploadItems = FileUploadItem.fromFiles(fileList); + if (!!self.fileUploadSelector) { + self.fileUploadSelector.onFileSelected(fileUploadItems); + } + self.fileInput.nativeElement.value = ''; + + // File Upload.. + self.appChatService + .sendMessageOfAttachFile( + self.loginRes, + self.loginSession.deviceType, + self.currentRoomInfo.roomId, + fileUploadItems + ) + .then((success) => { + if (!!success) { + self.clearSelector(); + if (!!self.fileUploadSelector) { + self.fileUploadSelector.onUploadComplete(); + } + } + }) + .catch((err) => { + alert(err); + if (!!self.fileUploadSelector) { + self.fileUploadSelector.onUploadComplete(); + } + }); + } + }) + .catch((err) => { + self.fileInput.nativeElement.value = ''; + self.logService.error(`validUploadFile ${err}`); + }); + } + + onKeydown(event: KeyboardEvent) { + if (event.key === 'PageUp' || event.key === 'PageDown') { + event.preventDefault(); + return false; + } else if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + this.send(); + } + } + + onSelectedSticker(stickerInfo: StickerFilesInfo) { + this.selectedSticker = stickerInfo; + this.focus(false); + } + + async send() { + const roomId = this.currentRoomInfo.roomId; + const userSeq = this.loginRes.userSeq; + let message = this.messageInput.nativeElement.value; + + if (!!message || message.trim().length > 0) { + message = message.replace(/\t/g, ' '); + } + + // Empty Check. + if (!this.selectedSticker) { + try { + if (!message || message.trim().length === 0) { + const result = await this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + data: { + title: this.i18nService.t('errors.label'), + message: this.i18nService.t('errors.inputChatMessage') + }, + panelClass: '' + }); + return; + } + } catch (e) { + this.logService.debug(e); + } + } + + if ( + this.selectorType === SelectorType.TRANSLATION && + this.destLocale.trim().length > 0 + ) { + /** CASE : Translation */ + // 번역할 대화 없이 스티커만 전송할 경우. + if (!message || message.trim().length === 0) { + this.appChatService.sendMessageOfSticker( + userSeq, + roomId, + this.selectedSticker, + message + ); + + this.clearSelector(); + } else { + this.appChatService.sendMessageOfTranslate( + this.loginRes, + this.loginSession.deviceType, + this.destLocale, + roomId, + message, + this.selectedSticker + ); + } + } else if (!!this.selectedSticker) { + /** CASE : Sticker */ + this.appChatService.sendMessageOfSticker( + userSeq, + roomId, + this.selectedSticker, + message + ); + + this.clearSelector(); + } else if ( + message.trim().length > environment.productConfig.chat.masstextLength + ) { + /** CASE : MASS TEXT */ + this.appChatService.sendMessageOfMassText( + this.loginRes, + this.loginSession.deviceType, + roomId, + message + ); + } else { + /** CASE : Normal Text */ + this.appChatService.sendMessageOfNormal(userSeq, roomId, message); + } + + this.focus(); } } diff --git a/src/app/sections/chat/components/list.section.component.ts b/src/app/sections/chat/components/list.section.component.ts index 1127468..b6d33ee 100644 --- a/src/app/sections/chat/components/list.section.component.ts +++ b/src/app/sections/chat/components/list.section.component.ts @@ -144,6 +144,14 @@ export class ListSectionComponent implements OnInit, OnDestroy { private ngZone: NgZone, private logService: LogService ) { + // default image setting + this.defaultProfileImage = this.appChatService.defaultProfileImage; + this.defaultProfileImageMulti = this.appChatService.defaultProfileImage; + } + + ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + // language setting this.translateService.setDefaultLang(this.i18nService.currentLng); this.translateService.use(this.i18nService.currentLng); @@ -153,14 +161,6 @@ export class ListSectionComponent implements OnInit, OnDestroy { ); this.i18nService.setDefaultNamespace('chat'); - // default image setting - this.defaultProfileImage = this.appChatService.defaultProfileImage; - this.defaultProfileImageMulti = this.appChatService.defaultProfileImage; - } - - ngOnInit(): void { - this.ngOnDestroySubject = new Subject(); - this.store .pipe( takeUntil(this.ngOnDestroySubject), @@ -176,15 +176,20 @@ export class ListSectionComponent implements OnInit, OnDestroy { this.loginRes = loginRes; }); - this.store - .pipe(takeUntil(this.ngOnDestroySubject), select(RoomSelector.rooms)) - .subscribe((rooms) => { - rooms = (rooms || []).filter((info) => info.isJoinRoom); + combineLatest([ + this.store.pipe(select(RoomSelector.rooms)), + this.store.pipe(select(RoomSelector.standbyRooms)) + ]) + .pipe(takeUntil(this.ngOnDestroySubject)) + .subscribe(([rooms, standbyRooms]) => { + rooms = (rooms || []).filter((info) => { + return ( + info.isJoinRoom && + !standbyRooms.find((standbyRoom) => standbyRoom === info.roomId) + ); + }); this.roomList = rooms; - // groupping. - this.initGroup(); - this.changeDetectorRef.detectChanges(); }); @@ -218,41 +223,6 @@ export class ListSectionComponent implements OnInit, OnDestroy { } } - initGroup() { - this.roomGroup = []; - - 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.roomGroup.findIndex( - (info) => info.division === division - ); - if (index > -1) { - this.roomGroup[index] = { - ...this.roomGroup[index], - roomList: [...this.roomGroup[index].roomList, roomInfo] - }; - } else { - this.roomGroup.push({ - division, - roomList: [roomInfo] - }); - } - }); - } - getRoomName(roomInfo: RoomInfo): string { if (!roomInfo) { return ''; @@ -392,8 +362,8 @@ export class ListSectionComponent implements OnInit, OnDestroy { ConfirmDialogResult >(ConfirmDialogComponent, { data: { - title: this.i18nService.t('room.dialog.titleExitFromRoom'), - html: this.i18nService.t('room.dialog.confirmExitFromRoom') + title: this.i18nService.t('dialog.title.exitFromRoom'), + html: this.i18nService.t('dialog.confirmExitFromRoom') } }); diff --git a/src/app/sections/chat/components/message.section.component.html b/src/app/sections/chat/components/message.section.component.html index 539a923..e4001a5 100644 --- a/src/app/sections/chat/components/message.section.component.html +++ b/src/app/sections/chat/components/message.section.component.html @@ -18,6 +18,7 @@ (); + currentRoomInfo: RoomInfo; chatting$: Observable; roomUsers: RoomUserInfoShort[] = []; // eventList$: Observable[]>; @@ -91,6 +94,19 @@ export class MessageSectionComponent implements OnInit, OnDestroy { .subscribe((loginRes) => { this.loginRes = loginRes; }); + + this.store + .pipe( + takeUntil(this.ngOnDestroySubject), + select( + (state: any) => state.chat.room.rooms.entities as Dictionary + ) + ) + .subscribe((rooms) => { + if (!!this.roomId) { + this.currentRoomInfo = rooms[this.roomId]; + } + }); } initializeRoomData() { diff --git a/src/app/sections/chat/dialogs/create.dialog.component.html b/src/app/sections/chat/dialogs/create.dialog.component.html new file mode 100644 index 0000000..433b7a2 --- /dev/null +++ b/src/app/sections/chat/dialogs/create.dialog.component.html @@ -0,0 +1,86 @@ +
+ +
+ {{ 'dialog.title.newChatRoom' | ucapI18n }} +
+
+ + +
+ {{ 'dialog.normalRoom' | ucapI18n }} + +
+
+ +
+ {{ 'dialog.timerRoom' | ucapI18n }} + +
+
+
+ + + +
+
+
+ + + +
+
+
diff --git a/src/app/sections/chat/dialogs/create.dialog.component.scss b/src/app/sections/chat/dialogs/create.dialog.component.scss new file mode 100644 index 0000000..cccbc78 --- /dev/null +++ b/src/app/sections/chat/dialogs/create.dialog.component.scss @@ -0,0 +1,9 @@ +.dialog-container { + width: 100%; + height: 100%; + + .dialog-body { + width: 100%; + height: 100%; + } +} diff --git a/src/app/sections/chat/dialogs/create.dialog.component.spec.ts b/src/app/sections/chat/dialogs/create.dialog.component.spec.ts new file mode 100644 index 0000000..6c2d293 --- /dev/null +++ b/src/app/sections/chat/dialogs/create.dialog.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { CreateDialogComponent } from './create.dialog.component'; + +describe('ucap::ui-organization::CreateChatDialogComponent', () => { + let component: CreateDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CreateDialogComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/sections/chat/dialogs/create.dialog.component.ts b/src/app/sections/chat/dialogs/create.dialog.component.ts new file mode 100644 index 0000000..6db8796 --- /dev/null +++ b/src/app/sections/chat/dialogs/create.dialog.component.ts @@ -0,0 +1,132 @@ +import { Subject } from 'rxjs'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Inject, + Input +} from '@angular/core'; + +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialog +} from '@angular/material/dialog'; + +import { UserInfo } from '@ucap/protocol-sync'; +import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query'; +import { UserInfo as RoomUserInfo } from '@ucap/protocol-room'; +import { MatStepper } from '@angular/material/stepper'; +import { I18nService } from '@ucap/ng-i18n'; +import { GroupActions } from '@ucap/ng-store-group'; +import { + AlertDialogComponent, + AlertDialogData, + AlertDialogResult +} from '@ucap/ng-ui'; +import { environment } from '@environments'; +import { AppChatService } from '@app/services/app-chat.service'; + +export type UserInfoTypes = + | UserInfo + | UserInfoSS + | UserInfoF + | UserInfoDN + | RoomUserInfo; + +export interface CreateDialogData {} +export interface CreateDialogResult { + userSeqs: string[]; + isTimer: boolean | undefined; +} + +@Component({ + selector: 'app-dialog-chat-create', + templateUrl: './create.dialog.component.html', + styleUrls: ['./create.dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CreateDialogComponent implements OnInit, OnDestroy { + currentStep = 0; + maxChatRoomUser: number; + + isTimer: boolean | undefined; + selectedUserList: UserInfoTypes[] = []; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CreateDialogData, + private i18nService: I18nService, + public dialog: MatDialog, + private changeDetectorRef: ChangeDetectorRef + ) { + this.maxChatRoomUser = environment.productConfig.chat.maxChatRoomUser; + } + + ngOnInit(): void {} + + ngOnDestroy(): void {} + + onClosed(event: MouseEvent): void { + this.dialogRef.close(); + } + + onCancel(stepper: MatStepper) { + if (stepper.selectedIndex > 0) { + stepper.previous(); + return; + } + this.dialogRef.close(); + } + onConfirm(stepper: MatStepper) { + // validation. + if (this.isTimer === undefined) { + this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + data: { + title: this.i18nService.t('errors.label'), + html: this.i18nService.t('errors.emptyOpenRoomType', { + maxCount: this.maxChatRoomUser + }) + } + }); + return; + } + stepper.next(); + } + + onOpenRoom(stepper: MatStepper) { + const userSeqs: string[] = []; + + this.selectedUserList.map((user) => userSeqs.push(user.seq.toString())); + + if (this.selectedUserList.length >= this.maxChatRoomUser) { + this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + data: { + title: this.i18nService.t('errors.label'), + html: this.i18nService.t('errors.maxCountOfRoomMemberWith', { + maxCount: this.maxChatRoomUser + }) + } + }); + return; + } + + // Open Room. + this.dialogRef.close({ userSeqs, isTimer: this.isTimer }); + } + + onChangeUserList(selectedUserList: UserInfoTypes[]) { + this.selectedUserList = selectedUserList; + } +} diff --git a/src/app/sections/chat/dialogs/index.ts b/src/app/sections/chat/dialogs/index.ts new file mode 100644 index 0000000..5a5d084 --- /dev/null +++ b/src/app/sections/chat/dialogs/index.ts @@ -0,0 +1,3 @@ +import { CreateDialogComponent } from './create.dialog.component'; + +export const DIALOGS = [CreateDialogComponent]; diff --git a/src/app/sections/group/components/index.ts b/src/app/sections/group/components/index.ts index 84a2bed..01841b9 100644 --- a/src/app/sections/group/components/index.ts +++ b/src/app/sections/group/components/index.ts @@ -3,11 +3,12 @@ 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'; - +import { SelectGroupSectionComponent } from './select-group.section.component'; export const COMPONENTS = [ ListSectionComponent, SearchSectionComponent, ProfileSectionComponent, InfoSectionComponent, - SelectUserSectionComponent + SelectUserSectionComponent, + SelectGroupSectionComponent ]; diff --git a/src/app/sections/group/components/list.section.component.html b/src/app/sections/group/components/list.section.component.html index 5821dba..3e6f0c1 100644 --- a/src/app/sections/group/components/list.section.component.html +++ b/src/app/sections/group/components/list.section.component.html @@ -1,5 +1,5 @@
@@ -8,20 +8,13 @@ [showType]="showType" (clicked)="onClickUser($event)" (selectGroupMenu)="onSelectGroupMenu($event)" + (selectProfileMenu)="onSelectProfileMenu($event)" (profileMenu)="onProfileMenu($event)" >
-
- - - - +
+
diff --git a/src/app/sections/group/components/list.section.component.ts b/src/app/sections/group/components/list.section.component.ts index 9692e36..f966ed0 100644 --- a/src/app/sections/group/components/list.section.component.ts +++ b/src/app/sections/group/components/list.section.component.ts @@ -29,7 +29,7 @@ import { LogService } from '@ucap/ng-logger'; import { ExpansionComponent as AppExpansionComponent } from '@app/ucap/group/components/expansion.component'; import { SessionStorageService } from '@ucap/ng-web-storage'; import { LoginSelector } from '@ucap/ng-store-authentication'; -import { GroupActions } from '@ucap/ng-store-group'; +import { GroupActions, BuddyActions } from '@ucap/ng-store-group'; import { I18nService } from '@ucap/ng-i18n'; import { AppAuthenticationService } from '@app/services/app-authentication.service'; @@ -52,7 +52,22 @@ import { AlertDialogData, AlertDialogResult } from '@ucap/ng-ui'; +import { + ManageDialogComponent, + ManageDialogData, + ManageDialogResult +} from '../dialogs/manage.dialog.component'; + import { PresenceActions, PresenceSelector } from '@ucap/ng-store-organization'; +import { GroupUserDialaogType } from '@app/types'; +import { EditInlineInputDialogComponent } from '../dialogs/edit-inline-input.dialog.component'; +import { + EditUserDialogComponent, + EditUserDialogData, + EditUserDialogResult +} from '../dialogs/edit-user.dialog.component'; +import { SearchData } from '@app/ucap/organization/models/search-data'; +import { UserStore } from '@app/models/user-store'; export type UserInfoTypes = | UserInfo @@ -81,24 +96,34 @@ export class GroupVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy { }) export class ListSectionComponent implements OnInit, OnDestroy { @Input() - set searchObj(obj: { - isShowSearch: boolean; - companyCode: string; - searchWord: string; - }) { - this._searchObj = obj; - if (obj.isShowSearch && obj.searchWord.localeCompare('') !== 0) { - this.onOrganizationTenantSearch(obj); + set searchData(searchData: SearchData) { + this._searchData = searchData; + if (!searchData) { + this._searchData = { + companyCode: this.userStore.companyCode + }; } else { - this._searchObj.isShowSearch = false; - this.searchUserInfos = []; + if (!this._searchData.companyCode) { + this._searchData.companyCode = this.userStore.companyCode; + } + + if ( + searchData.bySearch && + searchData.searchWord.localeCompare('') !== 0 + ) { + this.onOrganizationTenantSearch(searchData); + } else { + this._searchData.isShowSearch = false; + this.searchUserInfos = []; + } } } - get searchObj() { - return this._searchObj; + get searchData() { + return this._searchData; } - _searchObj: any; + // tslint:disable-next-line: variable-name + _searchData: any; @Input() checkable = false; @@ -120,6 +145,7 @@ export class ListSectionComponent implements OnInit, OnDestroy { searchUserInfos: UserInfoSS[] = []; private ngOnDestroySubject = new Subject(); + private userStore: UserStore; constructor( private router: Router, @@ -132,6 +158,7 @@ export class ListSectionComponent implements OnInit, OnDestroy { private queryProtocolService: QueryProtocolService, public dialog: MatDialog ) { + this.userStore = this.appAuthenticationService.getUserStore(); this.i18nService.setDefaultNamespace('group'); } @@ -151,19 +178,15 @@ export class ListSectionComponent implements OnInit, OnDestroy { } } - onOrganizationTenantSearch(obj: { - isShowSearch: boolean; - companyCode: string; - searchWord: string; - }) { + onOrganizationTenantSearch(searchData: SearchData) { const searchUserInfos: UserInfoSS[] = []; this.queryProtocolService .deptUser({ divCd: 'GRP', - companyCode: this._searchObj.companyCode, + companyCode: searchData.companyCode, searchRange: DeptSearchType.All, - search: this._searchObj.searchWord, + search: searchData.searchWord, senderCompanyCode: this.loginRes.userInfo.companyCode, senderEmployeeType: this.loginRes.userInfo.employeeType }) @@ -234,7 +257,11 @@ export class ListSectionComponent implements OnInit, OnDestroy { onProfileMenu(event) { console.log(event); } - onSelectGroupMenu(params: { menuType: string; group: GroupDetailData }) { + onSelectGroupMenu(params: { + menuType: string; + groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; + rect: any; + }) { switch (params.menuType) { case 'CHAT': { @@ -266,10 +293,31 @@ export class ListSectionComponent implements OnInit, OnDestroy { break; case 'RENAME': { + this.renameGroup(params); } break; case 'MANAGE_MEMBER': { + const dialogRef = this.dialog.open< + ManageDialogComponent, + ManageDialogData, + ManageDialogResult + >(ManageDialogComponent, { + width: '100%', + height: '100%', + data: { + title: '그룹 멤버 관리', + groupBuddyList: params.groupBuddyList + } + }); + dialogRef + .afterClosed() + .pipe(take(1)) + .subscribe((result) => { + if (!!result && !!result.type) { + this.manageGroup(result); + } + }); } break; case 'DELETE': @@ -289,7 +337,7 @@ export class ListSectionComponent implements OnInit, OnDestroy { .pipe(take(1)) .subscribe((result) => { if (!!result && !!result.choice) { - this.store.dispatch(GroupActions.del({ group: params.group })); + GroupActions.del({ group: params.groupBuddyList.group }); } }); } @@ -298,41 +346,168 @@ export class ListSectionComponent implements OnInit, OnDestroy { break; } } - // onEditGroupName(params: { editName: string; group: GroupDetailData }) { - // if (params.editName.localeCompare('') === 0) { - // const dialogRef = this.dialog.open< - // AlertDialogComponent, - // AlertDialogData, - // AlertDialogResult - // >(AlertDialogComponent, { - // data: { - // title: this.i18nService.t('moreMenu.error.label'), - // html: this.i18nService.t('moreMenu.error.requireName') - // } - // }); - // dialogRef - // .afterClosed() - // .pipe( - // take(1), - // map((result) => {}), - // catchError((err) => { - // return of(err); - // }) - // ) - // .subscribe(); - // return; - // } - // this.store.dispatch( - // GroupActions.update({ - // req: { - // groupSeq: params.group.seq, - // groupName: params.editName, - // userSeqs: params.group.userSeqs - // } - // }) - // ); - // console.log(params.editName); - // } + + onSelectProfileMenu(params: { + menuType: string; + userInfo: UserInfoF; + group: GroupDetailData; + rect: any; + }) { + switch (params.menuType) { + case 'REGISTER_FAVORITE': + this.store.dispatch( + BuddyActions.update({ + req: { + seq: Number(params.userInfo.seq), + isFavorit: !params.userInfo.isFavorit + } + }) + ); + break; + case 'NICKNAME': + { + this.editNickname(params.userInfo, params.rect); + } + break; + case 'COPY_BUDDY': + this.editUserDialog('COPY_BUDDY', params.group, params.userInfo); + break; + case 'MOVE_BUDDY': + this.editUserDialog('MOVE_BUDDY', params.group, params.userInfo); + break; + case 'REMOVE_BUDDY': + { + this.removeBuddy(params.userInfo, params.group); + } + break; + } + } + + private renameGroup(params: { + menuType: string; + groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; + rect: any; + }) { + const paramGroup = params.groupBuddyList.group; + + const dialogRef = this.dialog.open(EditInlineInputDialogComponent, { + width: params.rect.width, + height: params.rect.height, + panelClass: 'ucap-edit-group-name-dialog', + data: { + curValue: paramGroup.name, + placeholder: '그룹명을 입력하세요.', + left: params.rect.left, + top: params.rect.top + } + }); + + dialogRef + .afterClosed() + .pipe( + take(1), + map((result) => { + if ( + !!result && + result.choice && + result.curValue.localeCompare(paramGroup.name) !== 0 + ) { + this.store.dispatch( + GroupActions.update({ + req: { + groupSeq: paramGroup.seq, + groupName: result.curValue, + userSeqs: paramGroup.userSeqs + } + }) + ); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } + private manageGroup(result: ManageDialogResult) { + let targetGroup: GroupDetailData; + let targetUserSeqs: string[]; + + if (result.type === GroupUserDialaogType.Add) { + targetGroup = result.group; + targetUserSeqs = []; + result.selelctUserList.forEach((userInfo) => { + targetUserSeqs.push(userInfo.seq + ''); + }); + + this.store.dispatch( + GroupActions.updateMember({ targetGroup, targetUserSeqs }) + ); + } else if (result.type === GroupUserDialaogType.Copy) { + if (!!result.selectGroupList && result.selectGroupList.length > 0) { + result.selectGroupList.forEach((g) => { + targetGroup = g; + targetUserSeqs = []; + + g.userSeqs.map((seq) => { + targetUserSeqs.push(seq); + }); + + if (targetUserSeqs.length === 0) { + result.selelctUserList.forEach((user) => { + targetUserSeqs.push(user.seq as any); + }); + } else { + result.selelctUserList.forEach((user) => { + const find = targetUserSeqs.indexOf(user.seq as any); + if (find < 0) { + targetUserSeqs.push(user.seq as any); + } + }); + } + + this.store.dispatch( + GroupActions.updateMember({ targetGroup, targetUserSeqs }) + ); + }); + } + } else if (result.type === GroupUserDialaogType.Move) { + const fromGroup = result.group; + let toGroup: GroupDetailData; + targetUserSeqs = []; + + if (!!result.selectGroupList && result.selectGroupList.length > 0) { + result.selectGroupList.forEach((g) => { + toGroup = g; + targetUserSeqs = []; + + result.selelctUserList.forEach((user) => { + targetUserSeqs.push(user.seq as any); + }); + + this.store.dispatch( + GroupActions.moveMember({ + fromGroup, + toGroup, + targetUserSeq: targetUserSeqs + }) + ); + }); + } + } else if (result.type === GroupUserDialaogType.Create) { + targetUserSeqs = []; + result.selelctUserList.forEach((userInfo) => { + targetUserSeqs.push(userInfo.seq + ''); + }); + + this.store.dispatch( + GroupActions.create({ + groupName: result.groupName, + targetUserSeqs + }) + ); + } + } getStatusBulkInfo(buddy: UserInfoTypes) { return this.store.pipe( @@ -342,4 +517,188 @@ export class ListSectionComponent implements OnInit, OnDestroy { ) ); } + + private editUserDialog( + type: string, + group: GroupDetailData, + userInfo: UserInfoTypes + ) { + let title = ''; + let dialogType: GroupUserDialaogType; + if (type === 'COPY_BUDDY') { + title = '멤버 복사'; + dialogType = GroupUserDialaogType.Copy; + } else { + title = '멤버 이동'; + dialogType = GroupUserDialaogType.Move; + } + const dialogRef = this.dialog.open< + EditUserDialogComponent, + EditUserDialogData, + EditUserDialogResult + >(EditUserDialogComponent, { + width: '100%', + height: '100%', + data: { + title, + type: dialogType, + group, + userInfo + } + }); + dialogRef + .afterClosed() + .pipe( + take(1), + map((result: EditUserDialogResult) => { + let targetGroup: GroupDetailData; + let targetUserSeqs: string[]; + if (result.type === GroupUserDialaogType.Add) { + targetGroup = result.group; + targetUserSeqs = []; + result.selelctUserList.forEach((u) => { + targetUserSeqs.push(u.seq + ''); + }); + this.store.dispatch( + GroupActions.updateMember({ targetGroup, targetUserSeqs }) + ); + } else if (result.type === GroupUserDialaogType.Copy) { + if (!!result.selectGroupList && result.selectGroupList.length > 0) { + result.selectGroupList.forEach((g) => { + targetGroup = g; + targetUserSeqs = []; + g.userSeqs.map((seq) => { + targetUserSeqs.push(seq); + }); + if (targetUserSeqs.length === 0) { + result.selelctUserList.forEach((user) => { + targetUserSeqs.push(user.seq as any); + }); + } else { + result.selelctUserList.forEach((user) => { + const find = targetUserSeqs.indexOf(user.seq as any); + if (find < 0) { + targetUserSeqs.push(user.seq as any); + } + }); + } + this.store.dispatch( + GroupActions.updateMember({ targetGroup, targetUserSeqs }) + ); + }); + } + } else if (result.type === GroupUserDialaogType.Move) { + const fromGroup = result.group; + let toGroup: GroupDetailData; + targetUserSeqs = []; + if (!!result.selectGroupList && result.selectGroupList.length > 0) { + result.selectGroupList.forEach((g) => { + toGroup = g; + targetUserSeqs = []; + result.selelctUserList.forEach((user) => { + targetUserSeqs.push(user.seq as any); + }); + this.store.dispatch( + GroupActions.moveMember({ + fromGroup, + toGroup, + targetUserSeq: targetUserSeqs + }) + ); + }); + } + } else if (result.type === GroupUserDialaogType.Create) { + targetUserSeqs = []; + result.selelctUserList.forEach((u) => { + targetUserSeqs.push(u.seq + ''); + }); + this.store.dispatch( + GroupActions.create({ + groupName: result.groupName, + targetUserSeqs + }) + ); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } + + private removeBuddy(userInfo: UserInfoF, group: GroupDetailData) { + const dialogRef = this.dialog.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + data: { + title: '', + html: this.i18nService.t('label.confirmRemoveBuddy') + } + }); + dialogRef + .afterClosed() + .pipe( + take(1), + map((result) => { + if (!!result && result.choice) { + const trgtUserSeq = group.userSeqs.filter( + (user) => user + '' !== userInfo.seq + '' + ); + + this.store.dispatch( + GroupActions.updateMember({ + targetGroup: group, + targetUserSeqs: trgtUserSeq + }) + ); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } + private editNickname(userInfo: UserInfoF, rect: any) { + const dialogRef = this.dialog.open(EditInlineInputDialogComponent, { + width: rect.width - 30 + '', + height: rect.height, + panelClass: 'ucap-edit-group-name-dialog', + data: { + curValue: userInfo.nickName, + placeholder: '닉네임을 설정하세요.', + left: rect.left + 70, + top: rect.top + } + }); + + dialogRef + .afterClosed() + .pipe( + take(1), + map((result) => { + if ( + !!result && + result.choice && + result.curValue.localeCompare(userInfo.nickName) !== 0 + ) { + this.store.dispatch( + BuddyActions.nickname({ + req: { + userSeq: Number(userInfo.seq), + nickname: result.curValue + } + }) + ); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } } diff --git a/src/app/sections/group/components/select-group.section.component.html b/src/app/sections/group/components/select-group.section.component.html new file mode 100644 index 0000000..69d8753 --- /dev/null +++ b/src/app/sections/group/components/select-group.section.component.html @@ -0,0 +1,70 @@ +
+
+ +
+ + +
+ + +
+
+ + + {{ input.value?.length || 0 }}/20 + + + + + + + + + +
+ +
+ 기존 그룹 지정 +
+
+
+ {{ group.name }} +
+ +
+
+
+
+
+
+
+ +
+
+
diff --git a/src/app/sections/group/components/select-group.section.component.scss b/src/app/sections/group/components/select-group.section.component.scss new file mode 100644 index 0000000..8a387b9 --- /dev/null +++ b/src/app/sections/group/components/select-group.section.component.scss @@ -0,0 +1,2 @@ +.profile-container { +} diff --git a/src/app/sections/group/components/select-group.section.component.spec.ts b/src/app/sections/group/components/select-group.section.component.spec.ts new file mode 100644 index 0000000..6b3a9c5 --- /dev/null +++ b/src/app/sections/group/components/select-group.section.component.spec.ts @@ -0,0 +1,32 @@ +import { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SelectGroupSectionComponent } from './select-group.section.component'; + +describe('app::sections::group::SelectGroupSectionComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [SelectGroupSectionComponent] + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(SelectGroupSectionComponent); + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'ucap-lg-web'`, () => { + const fixture = TestBed.createComponent(SelectGroupSectionComponent); + const app = fixture.componentInstance; + }); + + it('should render title', () => { + const fixture = TestBed.createComponent(SelectGroupSectionComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(compiled.querySelector('.content span').textContent).toContain( + 'ucap-lg-web app is running!' + ); + }); +}); diff --git a/src/app/sections/group/components/select-group.section.component.ts b/src/app/sections/group/components/select-group.section.component.ts new file mode 100644 index 0000000..c85021e --- /dev/null +++ b/src/app/sections/group/components/select-group.section.component.ts @@ -0,0 +1,226 @@ +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Input, + Output, + EventEmitter +} from '@angular/core'; + +import { Subject, of } from 'rxjs'; +import { Store, select } from '@ngrx/store'; +import { takeUntil, take, map, catchError } from 'rxjs/operators'; +import { + LoginSelector, + AuthorizationSelector +} from '@ucap/ng-store-authentication'; +import { LoginResponse } from '@ucap/protocol-authentication'; +import { QueryProtocolService } from '@ucap/ng-protocol-query'; +import { + UserInfoSS, + AuthResponse, + DeptSearchType, + UserInfoF, + UserInfoDN +} from '@ucap/protocol-query'; +import { GroupSelector } from '@ucap/ng-store-group'; +import { GroupDetailData, UserInfo } from '@ucap/protocol-sync'; +import { PresenceActions } from '@ucap/ng-store-organization'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { UserInfo as RoomUserInfo } from '@ucap/protocol-room'; +import { MatDialog } from '@angular/material/dialog'; +import { + AlertDialogComponent, + AlertDialogData, + AlertDialogResult +} from '@ucap/ng-ui'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { SearchData } from '@app/ucap/organization/models/search-data'; + +export type UserInfoTypes = + | UserInfo + | UserInfoSS + | UserInfoF + | UserInfoDN + | RoomUserInfo; + +@Component({ + selector: 'app-sections-select-group', + templateUrl: './select-group.section.component.html', + styleUrls: ['./select-group.section.component.scss'], + + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SelectGroupSectionComponent implements OnInit, OnDestroy { + @Input() + isMemberMove: boolean; + + @Input() + isDialog = false; + + @Input() + checkable = false; + + @Input() + curGroup: GroupDetailData; + + @Output() + changeUserList: EventEmitter<{ + checked: boolean; + userInfo: UserInfoSS; + }> = new EventEmitter(); + + @Output() + changeGroupList: EventEmitter = new EventEmitter(); + + @Output() + changeGroupName: EventEmitter = new EventEmitter(); + + set companySearchData(searchData: SearchData) { + this._companySearchData = searchData; + this.onChangedCompanySearch(); + } + get companySearchData() { + return this._companySearchData; + } + // tslint:disable-next-line: variable-name + _companySearchData: SearchData; + + private ngOnDestroySubject = new Subject(); + + constructor( + private store: Store, + private queryProtocolService: QueryProtocolService, + private changeDetectorRef: ChangeDetectorRef, + private formBuilder: FormBuilder, + public dialog: MatDialog + ) {} + + loginRes: LoginResponse; + isSearch = false; + searchWord: string; + searchUserInfos: UserInfoSS[] = []; + groupList: GroupDetailData[]; + selectedUserList: UserInfoTypes[] = []; + selectedGroupList: GroupDetailData[] = []; + inputForm: FormGroup; + groupChecked = false; + + groupName: string; + + ngOnInit(): void { + this.store + .pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes)) + .subscribe((loginRes) => { + this.loginRes = loginRes; + }); + + this.store + .pipe(takeUntil(this.ngOnDestroySubject), select(GroupSelector.groups)) + .subscribe((groups) => { + this.groupList = groups; + }); + + this.inputForm = this.formBuilder.group({ + groupName: [ + this.groupName, + [ + // Validators.required + // StringUtil.includes(, CharactorType.Special), + // this.checkBanWords(), + // this.checkSameName() + ] + ] + }); + } + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.complete(); + } + } + + onKeyupGroupName() { + this.inputForm.get('groupName').markAsTouched(); + this.changeGroupName.emit(this.inputForm.get('groupName').value); + } + + onCheckForGroup(checbox: MatCheckbox, group: GroupDetailData) { + if ( + this.isMemberMove && + !!this.selectedGroupList && + this.selectedGroupList.length > 0 && + this.selectedGroupList[0].seq !== group.seq + ) { + this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + data: { + title: '멤버이동', + html: '멤버이동은 그룹 여러개를 선택할 수 없습니다.' + } + }); + + checbox.checked = false; + return; + } + if ( + this.selectedGroupList.filter((g) => g.seq === group.seq).length === 0 + ) { + this.selectedGroupList = [...this.selectedGroupList, group]; + } else { + this.selectedGroupList = this.selectedGroupList.filter( + (g) => g.seq !== group.seq + ); + } + + this.changeGroupList.emit(this.selectedGroupList); + } + + // onCheckForUser(params: { isChecked: boolean; userInfo: UserInfoTypes }) { + // if ( + // this.selectedUserList.filter((user) => user.seq === params.userInfo.seq) + // .length === 0 + // ) { + // this.selectedUserList = [...this.selectedUserList, params.userInfo]; + // } else { + // this.selectedUserList = this.selectedUserList.filter( + // (item) => item.seq !== params.userInfo.seq + // ); + // } + + // this.changeUserList.emit(this.selectedUserList); + // } + + onToggleCheck(data: { checked: boolean; userInfo: UserInfoSS }) { + this.changeUserList.emit(data); + } + + onChangedCompanySearch() { + this.isSearch = true; + } + + onCanceled() { + this.isSearch = false; + } + + getCheckedUser(userInfo: UserInfoTypes) { + if (!!this.selectedUserList && this.selectedUserList.length > 0) { + return ( + this.selectedUserList.filter((item) => item.seq === userInfo.seq) + .length > 0 + ); + } + return false; + } + checkVisible(group: GroupDetailData): boolean { + if (!!this.curGroup && this.curGroup.seq === group.seq) { + return false; + } + return true; + } +} diff --git a/src/app/sections/group/components/select-user.section.component.html b/src/app/sections/group/components/select-user.section.component.html index a93b680..0cf34fb 100644 --- a/src/app/sections/group/components/select-user.section.component.html +++ b/src/app/sections/group/components/select-user.section.component.html @@ -4,7 +4,7 @@
@@ -24,12 +24,13 @@
@@ -59,73 +60,14 @@
-
- - - - +
+
-
- -
- - -
- - - - {{ userInfo.name }} - clear - - -
- - - - {{ selectedUserList.length }} / 300 - - - - - - - - - - {{ selectedUserList.length }} - - - -
diff --git a/src/app/sections/group/components/select-user.section.component.ts b/src/app/sections/group/components/select-user.section.component.ts index e853572..877c80c 100644 --- a/src/app/sections/group/components/select-user.section.component.ts +++ b/src/app/sections/group/components/select-user.section.component.ts @@ -27,6 +27,7 @@ import { import { UserInfo as RoomUserInfo } from '@ucap/protocol-room'; import { LoginResponse } from '@ucap/protocol-authentication'; import { LoginSelector } from '@ucap/ng-store-authentication'; +import { SearchData } from '@app/ucap/organization/models/search-data'; export type UserInfoTypes = | UserInfo @@ -61,16 +62,22 @@ export class SelectUserSectionComponent implements OnInit, OnDestroy { existUsers: UserInfoTypes[]; @Input() - set checkable(check: boolean) { - this._checkable = check; - } - get checkable(): boolean { - return this._checkable; - } - _checkable = false; + isSelectionOff = true; + + @Input() + checkable = false; @Output() - changeUserList = new EventEmitter(); + toggleCheckUser: EventEmitter<{ + checked: boolean; + userInfo: UserInfoSS; + }> = new EventEmitter(); + + @Output() + toggleCheckGroup: EventEmitter<{ + isChecked: boolean; + groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; + }> = new EventEmitter(); @Output() cancel = new EventEmitter(); @@ -78,11 +85,23 @@ export class SelectUserSectionComponent implements OnInit, OnDestroy { @Output() confirm = new EventEmitter(); + set companySearchData(searchData: SearchData) { + this._companySearchData = searchData; + + this.onChangedCompanySearch(); + } + get companySearchData() { + return this._companySearchData; + } + // tslint:disable-next-line: variable-name + _companySearchData: SearchData; + isSearch = false; searchWord: string; private ngOnDestroySubject = new Subject(); + @Input() selectedUserList: UserInfoTypes[] = []; searchUserInfos: UserInfoSS[] = []; @@ -113,20 +132,15 @@ export class SelectUserSectionComponent implements OnInit, OnDestroy { } } - onCheckUser(params: { isChecked: boolean; userInfo: UserInfoTypes }) { - console.log(params); - if ( - this.selectedUserList.filter((user) => user.seq === params.userInfo.seq) - .length === 0 - ) { - this.selectedUserList = [...this.selectedUserList, params.userInfo]; - } else { - this.selectedUserList = this.selectedUserList.filter( - (item) => item.seq !== params.userInfo.seq - ); - } + onToggleCheckGroup(params: { + isChecked: boolean; + groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; + }) { + this.toggleCheckGroup.emit(params); + } - this.changeUserList.emit(this.selectedUserList); + onToggleCheckUser(data: { checked: boolean; userInfo: UserInfoSS }) { + this.toggleCheckUser.emit(data); } getCheckedUser(userInfo: UserInfoTypes) { @@ -139,96 +153,8 @@ export class SelectUserSectionComponent implements OnInit, OnDestroy { return false; } - onCheckGroup(params: { - isChecked: boolean; - groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; - }) { - if (params.isChecked) { - params.groupBuddyList.buddyList.forEach((item) => { - if ( - this.selectedUserList.filter((user) => user.seq === item.seq) - .length === 0 - ) { - this.selectedUserList = [...this.selectedUserList, item]; - } - }); - } else { - this.selectedUserList = this.selectedUserList.filter( - (item) => - params.groupBuddyList.buddyList.filter((del) => del.seq === item.seq) - .length === 0 - ); - } - this.changeDetectorRef.markForCheck(); - } - getSelectedUserList(): any[] { - return this.selectedUserList; - } - - getChipsRemoveYn(userInfo: UserInfoTypes) { - if (!!this.existUsers && this.existUsers.length > 0) { - return !( - this.existUsers.filter((user) => user.seq === userInfo.seq).length > 0 - ); - } else { - return true; - } - } - - onClickDeleteUser(userInfo: UserInfoTypes) { - this.selectedUserList = this.selectedUserList.filter( - (item) => item.seq !== userInfo.seq - ); - this.changeDetectorRef.markForCheck(); - } - - onChanged(data: { companyCode: string; searchWord: string }) { + onChangedCompanySearch() { this.isSearch = true; - this.searchWord = data.searchWord; - - const searchUserInfos: UserInfoSS[] = []; - - this.queryProtocolService - .deptUser({ - divCd: 'GRP', - companyCode: data.companyCode, - searchRange: DeptSearchType.All, - search: data.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(); } onCanceled() { diff --git a/src/app/sections/group/dialogs/create.dialog.component.html b/src/app/sections/group/dialogs/create.dialog.component.html index 1f1ec97..f3b7f4f 100644 --- a/src/app/sections/group/dialogs/create.dialog.component.html +++ b/src/app/sections/group/dialogs/create.dialog.component.html @@ -43,12 +43,25 @@ + +
+ + +
diff --git a/src/app/sections/group/dialogs/create.dialog.component.ts b/src/app/sections/group/dialogs/create.dialog.component.ts index 072ce45..bbcce68 100644 --- a/src/app/sections/group/dialogs/create.dialog.component.ts +++ b/src/app/sections/group/dialogs/create.dialog.component.ts @@ -147,7 +147,63 @@ export class CreateDialogComponent implements OnInit, OnDestroy { this.inputForm.get('groupName').markAsTouched(); } - onChangeUserList(selectedUserList: UserInfoTypes[]) { - this.selectedUserList = selectedUserList; + onChangeUserList(data: { checked: boolean; userInfo: UserInfoSS }) { + const i = this.selectedUserList.findIndex( + (u) => u.seq === data.userInfo.seq + ); + + if (data.checked) { + if (-1 === i) { + this.selectedUserList = [...this.selectedUserList, data.userInfo]; + } + } else { + if (-1 < i) { + this.selectedUserList = this.selectedUserList.filter( + (u) => u.seq !== data.userInfo.seq + ); + } + } } + + onChangeGroupList(params: { + isChecked: boolean; + groupBuddyList: { group: GroupDetailData; buddyList: UserInfo[] }; + }) { + if (params.isChecked) { + params.groupBuddyList.buddyList.forEach((item) => { + if ( + this.selectedUserList.filter((user) => user.seq === item.seq) + .length === 0 + ) { + this.selectedUserList = [...this.selectedUserList, item]; + } + }); + } else { + this.selectedUserList = this.selectedUserList.filter( + (item) => + params.groupBuddyList.buddyList.filter((del) => del.seq === item.seq) + .length === 0 + ); + } + } + + onRemovedProfileSelection(userInfo: UserInfo) { + const i = this.selectedUserList.findIndex( + (u) => (u.seq as any) === (userInfo.seq as any) + ); + + if (-1 < i) { + this.selectedUserList = this.selectedUserList.filter( + (u) => (u.seq as any) !== (userInfo.seq as any) + ); + } + } + + removableForSelection = (userInfo: UserInfo) => { + return true; + }; + + colorForSelection = (userInfo: UserInfo) => { + return 'accent'; + }; } diff --git a/src/app/sections/group/dialogs/edit-user.dialog.component.html b/src/app/sections/group/dialogs/edit-user.dialog.component.html new file mode 100644 index 0000000..bf356da --- /dev/null +++ b/src/app/sections/group/dialogs/edit-user.dialog.component.html @@ -0,0 +1,36 @@ +
+ +
+ {{ data.title }} +
+
+ +
+ + +
+
+
+ + +
+
+
diff --git a/src/app/sections/group/dialogs/edit-user.dialog.component.scss b/src/app/sections/group/dialogs/edit-user.dialog.component.scss new file mode 100644 index 0000000..cccbc78 --- /dev/null +++ b/src/app/sections/group/dialogs/edit-user.dialog.component.scss @@ -0,0 +1,9 @@ +.dialog-container { + width: 100%; + height: 100%; + + .dialog-body { + width: 100%; + height: 100%; + } +} diff --git a/src/app/sections/group/dialogs/edit-user.dialog.component.spec.ts b/src/app/sections/group/dialogs/edit-user.dialog.component.spec.ts new file mode 100644 index 0000000..d23ce90 --- /dev/null +++ b/src/app/sections/group/dialogs/edit-user.dialog.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { EditUserDialogComponent } from './edit-user.dialog.component'; + +describe('ucap::ui-organization::EditUserDialogComponent', () => { + let component: EditUserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [EditUserDialogComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditUserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/sections/group/dialogs/edit-user.dialog.component.ts b/src/app/sections/group/dialogs/edit-user.dialog.component.ts new file mode 100644 index 0000000..cdfa0bb --- /dev/null +++ b/src/app/sections/group/dialogs/edit-user.dialog.component.ts @@ -0,0 +1,149 @@ +import { Subject } from 'rxjs'; + +import { + Component, + OnInit, + OnDestroy, + ChangeDetectionStrategy, + ChangeDetectorRef, + Inject, + Input +} from '@angular/core'; + +import { Store } from '@ngrx/store'; + +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialog +} from '@angular/material/dialog'; + +import { UserInfo, GroupDetailData } from '@ucap/protocol-sync'; +import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query'; +import { UserInfo as RoomUserInfo } from '@ucap/protocol-room'; +import { I18nService } from '@ucap/ng-i18n'; + +import { SelectUserDialogType, GroupUserDialaogType } from '@app/types'; + +export type UserInfoTypes = + | UserInfo + | UserInfoSS + | UserInfoF + | UserInfoDN + | RoomUserInfo; + +export interface EditUserDialogData { + title: string; + type: GroupUserDialaogType; + group: GroupDetailData; + userInfo: UserInfoTypes; +} +export interface EditUserDialogResult { + type: GroupUserDialaogType; + groupName: string; + group: GroupDetailData; + selelctUserList?: UserInfoTypes[]; + selectGroupList?: GroupDetailData[]; +} + +@Component({ + selector: 'app-dialog-edit-user', + templateUrl: './edit-user.dialog.component.html', + styleUrls: ['./edit-user.dialog.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditUserDialogComponent implements OnInit, OnDestroy { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: EditUserDialogData, + private changeDetectorRef: ChangeDetectorRef, + private store: Store, + private i18nService: I18nService, + public dialog: MatDialog + ) {} + + private ngOnDestroySubject: Subject; + + currentType: GroupUserDialaogType; + selectedUserList: UserInfoTypes[] = []; + selectedGroupList: GroupDetailData[] = []; + groupName = ''; + SelectUserDialogType = SelectUserDialogType; + ngOnInit(): void { + this.ngOnDestroySubject = new Subject(); + + this.selectedUserList.push(this.data.userInfo); + this.currentType = this.data.type; + } + + ngOnDestroy(): void { + if (!!this.ngOnDestroySubject) { + this.ngOnDestroySubject.complete(); + } + } + + onClosed(event: MouseEvent): void { + this.dialogRef.close(); + } + + onConfirm() { + if (!!this.groupName && this.groupName.trim().localeCompare('') !== 0) { + this.currentType = GroupUserDialaogType.Create; + } else { + this.groupName = undefined; + } + + this.dialogRef.close({ + type: this.currentType, + groupName: this.groupName, + group: this.data.group, + selelctUserList: this.selectedUserList, + selectGroupList: this.selectedGroupList + }); + } + onChangeUserList(data: { checked: boolean; userInfo: UserInfoSS }) { + const i = this.selectedUserList.findIndex( + (u) => u.seq === data.userInfo.seq + ); + + if (data.checked) { + if (-1 === i) { + this.selectedUserList = [...this.selectedUserList, data.userInfo]; + } + } else { + if (-1 < i) { + this.selectedUserList = this.selectedUserList.filter( + (u) => u.seq !== data.userInfo.seq + ); + } + } + } + + onChangeGroupList(selectedGroupList: GroupDetailData[]) { + this.selectedGroupList = selectedGroupList; + } + + onChangeGroupName(name: string) { + this.groupName = name; + } + + onRemovedProfileSelection(userInfo: UserInfo) { + const i = this.selectedUserList.findIndex( + (u) => (u.seq as any) === (userInfo.seq as any) + ); + + if (-1 < i) { + this.selectedUserList = this.selectedUserList.filter( + (u) => (u.seq as any) !== (userInfo.seq as any) + ); + } + } + + removableForSelection = (userInfo: UserInfo) => { + return true; + }; + + colorForSelection = (userInfo: UserInfo) => { + return 'accent'; + }; +} diff --git a/src/app/sections/group/dialogs/index.ts b/src/app/sections/group/dialogs/index.ts index 3b6f510..449db10 100644 --- a/src/app/sections/group/dialogs/index.ts +++ b/src/app/sections/group/dialogs/index.ts @@ -1,4 +1,11 @@ import { CreateDialogComponent } from './create.dialog.component'; import { EditInlineInputDialogComponent } from './edit-inline-input.dialog.component'; +import { ManageDialogComponent } from './manage.dialog.component'; +import { EditUserDialogComponent } from './edit-user.dialog.component'; -export const DIALOGS = [CreateDialogComponent, EditInlineInputDialogComponent]; +export const DIALOGS = [ + CreateDialogComponent, + EditInlineInputDialogComponent, + ManageDialogComponent, + EditUserDialogComponent +]; diff --git a/src/app/sections/group/dialogs/manage.dialog.component.html b/src/app/sections/group/dialogs/manage.dialog.component.html index 78a0d92..f356a73 100644 --- a/src/app/sections/group/dialogs/manage.dialog.component.html +++ b/src/app/sections/group/dialogs/manage.dialog.component.html @@ -12,28 +12,68 @@ #stepper [selectedIndex]="currentStep" > - - +
+
+ {{ data.groupBuddyList.group.name }} +
+
+ +
+
+
+ +
+
+ + +
+ + +
- - - +
+ + + +
+
+ + +
diff --git a/src/app/sections/group/dialogs/manage.dialog.component.ts b/src/app/sections/group/dialogs/manage.dialog.component.ts index f20486d..b09f804 100644 --- a/src/app/sections/group/dialogs/manage.dialog.component.ts +++ b/src/app/sections/group/dialogs/manage.dialog.component.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs'; +import { Subject, of } from 'rxjs'; import { Component, @@ -7,7 +7,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Inject, - Input + Input, + ComponentFactoryResolver, + ViewChild, + ViewContainerRef, + ComponentRef } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -28,8 +32,15 @@ import { GroupActions } from '@ucap/ng-store-group'; import { AlertDialogComponent, AlertDialogData, - AlertDialogResult + AlertDialogResult, + ConfirmDialogComponent, + ConfirmDialogResult, + ConfirmDialogData } from '@ucap/ng-ui'; +import { SelectUserSectionComponent } from '../components/select-user.section.component'; +import { take, map, catchError } from 'rxjs/operators'; +import { SelectGroupSectionComponent } from '../components/select-group.section.component'; +import { SelectUserDialogType, GroupUserDialaogType } from '@app/types'; export type UserInfoTypes = | UserInfo @@ -40,8 +51,15 @@ export type UserInfoTypes = export interface ManageDialogData { title: string; + groupBuddyList?: { group: GroupDetailData; buddyList: UserInfo[] }; +} +export interface ManageDialogResult { + type: GroupUserDialaogType; + groupName: string; + group?: GroupDetailData; + selelctUserList?: UserInfoTypes[]; + selectGroupList?: GroupDetailData[]; } -export interface ManageDialogResult {} @Component({ selector: 'app-dialog-group-manage', @@ -57,16 +75,29 @@ export class ManageDialogComponent implements OnInit, OnDestroy { private store: Store, private formBuilder: FormBuilder, private i18nService: I18nService, - public dialog: MatDialog + public dialog: MatDialog, + private cfResolver: ComponentFactoryResolver ) {} - private ngOnDestroySubject: Subject; - currentStep = 0; + @ViewChild('dialogContainer', { static: true, read: ViewContainerRef }) + dialogContainer: ViewContainerRef; + componentRef: ComponentRef; + private ngOnDestroySubject: Subject; + currentType: GroupUserDialaogType; + SelectUserDialogType = SelectUserDialogType; + GroupUserDialaogType = GroupUserDialaogType; + + currentStep = 0; + groupName = ''; + + delteUserList: UserInfoTypes[] = []; selectedUserList: UserInfoTypes[] = []; + selectedGroupList: GroupDetailData[] = []; ngOnInit(): void { this.ngOnDestroySubject = new Subject(); + // this.selectedUserList = this.data.groupBuddyList.buddyList; } ngOnDestroy(): void { @@ -80,14 +111,111 @@ export class ManageDialogComponent implements OnInit, OnDestroy { } onDelete(stepper: MatStepper) { - this.dialogRef.close(); + if ( + !!this.delteUserList && + this.delteUserList.length > 0 && + this.currentStep === 0 + ) { + let titleStr = ''; + this.delteUserList.forEach((user, idx) => { + let userTitle = user.name + ' ' + user.grade; + if (idx < this.delteUserList.length) { + userTitle = userTitle + ', '; + } + titleStr = titleStr.concat('', userTitle); + }); + const dialogRef = this.dialog.open< + ConfirmDialogComponent, + ConfirmDialogData, + ConfirmDialogResult + >(ConfirmDialogComponent, { + data: { + title: '동료 삭제', + html: titleStr + '을 삭제하시겠습니까?' + } + }); + dialogRef + .afterClosed() + .pipe( + take(1), + map((result) => { + if (!!result && result.choice) { + const trgtUserSeq: string[] = []; + + this.delteUserList.forEach((userIfno) => { + const tempSeq = this.data.groupBuddyList.group.userSeqs.filter( + (seq) => seq === userIfno.seq + ); + + trgtUserSeq.push(tempSeq[0]); + }); + + console.log(trgtUserSeq); + + this.store.dispatch( + GroupActions.updateMember({ + targetGroup: this.data.groupBuddyList.group, + targetUserSeqs: trgtUserSeq + }) + ); + this.dialogRef.close(); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } } - onCopy(stepper: MatStepper) { - this.dialogRef.close(); + onUpdateMember(stepper: MatStepper, type: GroupUserDialaogType) { + this.dialogContainer.clear(); + this.currentType = type; + const isMemberMove = type === GroupUserDialaogType.Copy ? false : true; + // const title = type === GroupUserDialaogType.Copy ? '멤버 복사' : '멤버 이동'; + + const factory = this.cfResolver.resolveComponentFactory( + SelectGroupSectionComponent + ); + + this.componentRef = this.dialogContainer.createComponent(factory); + const cpInstance = this.componentRef.instance; + // cpInstance.title = title; + cpInstance.isMemberMove = isMemberMove; + cpInstance.isDialog = true; + cpInstance.checkable = true; + cpInstance.curGroup = this.data.groupBuddyList.group; + cpInstance.changeUserList.subscribe((val) => { + this.selectedUserList = val; + }); + cpInstance.changeGroupList.subscribe((groupList) => { + this.selectedGroupList = groupList; + }); + cpInstance.changeGroupName.subscribe((groupName) => { + this.groupName = groupName; + }); + this.currentStep++; + stepper.next(); } - onMove(stepper: MatStepper) {} onAdd(stepper: MatStepper) { + this.dialogContainer.clear(); + this.currentType = GroupUserDialaogType.Add; + // this.selectedUserList = this.data.groupBuddyList.buddyList; + + const factory = this.cfResolver.resolveComponentFactory( + SelectUserSectionComponent + ); + + this.componentRef = this.dialogContainer.createComponent(factory); + const cpInstance = this.componentRef.instance; + cpInstance.isDialog = true; + cpInstance.checkable = true; + cpInstance.selectedUserList = this.data.groupBuddyList.buddyList; + cpInstance.isSelectionOff = false; + cpInstance.changeUserList.subscribe((val) => { + this.selectedUserList = val; + }); this.currentStep++; stepper.next(); } @@ -95,4 +223,119 @@ export class ManageDialogComponent implements OnInit, OnDestroy { onChangeUserList(selectedUserList: UserInfoTypes[]) { this.selectedUserList = selectedUserList; } + + onCnacel(stepper: MatStepper) { + if (!!this.selectedUserList && this.selectedUserList.length > 0) { + this.selectedUserList = []; + } + this.currentStep--; + stepper.previous(); + } + onConfirm(stepper: MatStepper) { + switch (this.currentType) { + case GroupUserDialaogType.Add: + { + if (!!this.selectedUserList && this.selectedUserList.length > 0) { + this.doAction(); + } + } + break; + case GroupUserDialaogType.Copy: + case GroupUserDialaogType.Move: + { + if (!!this.selectedUserList && this.selectedUserList.length === 0) { + this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + data: { + title: 'Error', + html: '선택된 유저가 없습니다.' + } + }); + + return; + } + + this.doAction(); + } + break; + } + } + + doAction() { + this.dialogContainer.clear(); + if (!!this.groupName && this.groupName.trim().localeCompare('') !== 0) { + this.currentType = GroupUserDialaogType.Create; + } else { + this.groupName = undefined; + } + + this.dialogRef.close({ + type: this.currentType, + groupName: this.groupName, + group: this.data.groupBuddyList.group, + selelctUserList: this.selectedUserList, + selectGroupList: this.selectedGroupList + }); + } + + /** 개별 체크여부 */ + getCheckedUser(userInfo: UserInfoSS) { + if (!!this.selectedUserList && this.selectedUserList.length > 0) { + return ( + this.selectedUserList.filter( + (item) => (item.seq as any) === (userInfo.seq as any) + ).length > 0 + ); + } + return false; + } + + onToggleCheckForDelete(data: { checked: boolean; userInfo: UserInfoSS }) { + if (data.checked) { + this.delteUserList.push(data.userInfo); + } else { + const index = this.delteUserList.findIndex( + (userInfo) => userInfo.seq === data.userInfo.seq + ); + if (index > -1) { + this.delteUserList.splice(index, 1); + } + } + this.onToggleCheck(data); + } + onToggleCheck(data: { checked: boolean; userInfo: UserInfoSS }) { + if (data.checked) { + this.selectedUserList.push(data.userInfo); + } else { + const index = this.selectedUserList.findIndex( + (userInfo) => userInfo.seq === data.userInfo.seq + ); + if (index > -1) { + this.selectedUserList.splice(index, 1); + } + } + } + + onRemovedProfileSelection(userInfo: UserInfo) { + const i = this.selectedUserList.findIndex( + (u) => (u.seq as any) === (userInfo.seq as any) + ); + + if (-1 < i) { + this.selectedUserList = this.selectedUserList.filter( + (u) => (u.seq as any) !== (userInfo.seq as any) + ); + } + } + + removableForSelection = (userInfo: UserInfo) => { + return true; + }; + + colorForSelection = (userInfo: UserInfo) => { + return 'accent'; + }; } diff --git a/src/app/sections/group/group.section.module.ts b/src/app/sections/group/group.section.module.ts index bd2aa1b..9e10705 100644 --- a/src/app/sections/group/group.section.module.ts +++ b/src/app/sections/group/group.section.module.ts @@ -43,8 +43,6 @@ import { DIALOGS } from './dialogs'; MatIconModule, MatCardModule, MatCheckboxModule, - MatFormFieldModule, - MatInputModule, MatSelectModule, MatTabsModule, MatChipsModule, diff --git a/src/app/sections/organization/components/detail-table.component.html b/src/app/sections/organization/components/detail-table.component.html index 048f5ec..92947f4 100644 --- a/src/app/sections/organization/components/detail-table.component.html +++ b/src/app/sections/organization/components/detail-table.component.html @@ -2,7 +2,9 @@
{{ selectedDeptInfo | ucapOrganizationTranslate: 'name' }}{{ - !!searchData.isSearch ? searchUserList.length : departmentUserList.length + !!searchObj.isShowSearch + ? searchUserList.length + : departmentUserList.length }}{{ 'common.units.persons' | ucapI18n }}
@@ -27,9 +29,28 @@
- + + + + + +
diff --git a/src/app/sections/organization/components/detail-table.component.ts b/src/app/sections/organization/components/detail-table.component.ts index a930a19..c147aaf 100644 --- a/src/app/sections/organization/components/detail-table.component.ts +++ b/src/app/sections/organization/components/detail-table.component.ts @@ -59,7 +59,26 @@ export class DepartmentUserVirtualScrollStrategy extends FixedSizeVirtualScrollS }) export class DetailTableComponent implements OnInit, OnDestroy { @Input() - searchData: any; + 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.searchUserList = []; + this.changeDetectorRef.detectChanges(); + } + } + + get searchObj() { + return this._searchObj; + } + // tslint:disable-next-line: variable-name + _searchObj: any; @Input() set deptSeq(deptSeq: string) { @@ -198,13 +217,59 @@ export class DetailTableComponent implements OnInit, OnDestroy { } } + onOrganizationTenantSearch(obj: { + isShowSearch: boolean; + companyCode: string; + searchWord: string; + }) { + this.departmentUserListProcessing = true; + + this.queryProtocolService + .deptUser({ + divCd: 'ORGS', + companyCode: this._searchObj.companyCode, + searchRange: DeptSearchType.All, + search: this._searchObj.searchWord, + senderCompanyCode: this.loginRes.userInfo.companyCode, + senderEmployeeType: this.loginRes.userInfo.employeeType + }) + .pipe( + map((resObj) => { + // 검색 결과 처리. + this.searchUserList = resObj.userInfos.sort((a, b) => + a.name < b.name ? -1 : a.name > b.name ? 1 : 0 + ); + this.changeDetectorRef.detectChanges(); + this.departmentUserListProcessing = false; + + // 검색 결과에 따른 프레즌스 조회. + const userSeqList: string[] = []; + this.searchUserList.map((user) => userSeqList.push(user.seq)); + + if (userSeqList.length > 0) { + this.store.dispatch( + PresenceActions.bulkInfo({ + divCd: 'orgSrch', + userSeqs: userSeqList + }) + ); + } + }), + catchError((error) => { + this.departmentUserListProcessing = false; + return of(error); + }) + ) + .subscribe(); + } + /** 전체선택 체크여부 */ getCheckedAllUser() { if (!this.loginRes) { return false; } - const compareList: UserInfoSS[] = !!this.searchData.isSearch + const compareList: UserInfoSS[] = !!this.searchObj.isShowSearch ? this.searchUserList : this.departmentUserList; if ( @@ -233,7 +298,7 @@ export class DetailTableComponent implements OnInit, OnDestroy { return false; } - const trgtUserList = !!this.searchData.isSearch + const trgtUserList = !!this.searchObj.isShowSearch ? this.searchUserList : this.departmentUserList; diff --git a/src/app/sections/organization/components/member-list.component.html b/src/app/sections/organization/components/member-list.component.html index b90e9af..79324c4 100644 --- a/src/app/sections/organization/components/member-list.component.html +++ b/src/app/sections/organization/components/member-list.component.html @@ -1,34 +1,99 @@
- -
- - -
- - -
-
-
-
아키텍처솔루션팀 20
-
-
- 이름 - - format_line_spacing - - - -
+
+
+
+ + {{ selectedDeptInfo | ucapOrganizationTranslate: 'name' }} + + + {{ selectedCompanyInfo.companyName }} + + {{ searchedProfileLength }}명 +
+
+ 이름 + + + + +
+
+
- +
+
+ + + + + 선택한 대화상대 + {{ + !!selectedUserInfos ? selectedUserInfos.length : 0 + }} + + + + +
+ + +
+
+ + + + + +
+
+
+
diff --git a/src/app/sections/organization/components/member-list.component.scss b/src/app/sections/organization/components/member-list.component.scss index 21bee5a..b3c8e10 100644 --- a/src/app/sections/organization/components/member-list.component.scss +++ b/src/app/sections/organization/components/member-list.component.scss @@ -1,34 +1,59 @@ +@import '~@ucap/lg-scss/mixins'; + .member-list-container { width: 100%; height: 100%; - .member-list-body { - align-content: space-between; - padding: 0 30px; - background-color: white; + align-content: space-between; + padding: 0 30px; + background-color: white; - .list-header { - justify-content: space-between; - align-items: center; - border-bottom: 2px solid #999999; - padding: 12px 0 13px; + .list-header { + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #999999; + padding: 12px 0 13px; - .list-header-title { - h5 { - font-size: 13px; - align-items: center; - font-weight: 600; - color: #333333; - flex-grow: 1; - strong { - color: #e42f66; - } + .list-header-title { + h5 { + font-size: 13px; + align-items: center; + font-weight: 600; + color: #333333; + flex-grow: 1; + strong { + color: #e42f66; } } - .list-header-toolbox { - position: absolute; - right: 0px; + } + .list-header-toolbox { + right: 0px; + } + } + + .selected-users { + flex-grow: 0.8; + .organization-accordion-head { + background-color: #f1f2f6; + } + .select-user-title { + strong { + color: $lipstick; + margin-left: 8px; } } + + .selected-user-list { + width: 150px; + } + + .btn-box { + margin-top: 10px; + padding-right: 8px; + display: flex; + flex-direction: row; + align-content: center; + justify-content: space-between; + } } } diff --git a/src/app/sections/organization/components/member-list.component.ts b/src/app/sections/organization/components/member-list.component.ts index d7b381b..d5b9128 100644 --- a/src/app/sections/organization/components/member-list.component.ts +++ b/src/app/sections/organization/components/member-list.component.ts @@ -1,89 +1,83 @@ +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + import { Component, - ViewChild, OnInit, ChangeDetectorRef, Input, OnDestroy, - ChangeDetectionStrategy + ChangeDetectionStrategy, + ViewChild } from '@angular/core'; -import { MatTableDataSource } from '@angular/material/table'; -import { SelectionModel } from '@angular/cdk/collections'; -import { MatSort } from '@angular/material/sort'; -import { Subject, of } from 'rxjs'; -import { map, takeUntil, take, catchError } from 'rxjs/operators'; -import { - DeptInfo, - DeptSearchType, - DeptUserRequest, - UserInfoSS, - AuthResponse -} from '@ucap/protocol-query'; + +import { select, Store } from '@ngrx/store'; + +import { SortOrder } from '@ucap/core'; +import { LoginResponse } from '@ucap/protocol-authentication'; +import { DeptInfo, UserInfoSS } from '@ucap/protocol-query'; +import { UserInfo } from '@ucap/protocol-sync'; + +import { LoginSelector } from '@ucap/ng-store-authentication'; import { DepartmentSelector, - PresenceActions + CompanySelector } from '@ucap/ng-store-organization'; -import { select, Store } from '@ngrx/store'; -import { QueryProtocolService } from '@ucap/ng-protocol-query'; -import { - LoginSelector, - AuthorizationSelector, - ConfigurationSelector -} from '@ucap/ng-store-authentication'; -import { LoginResponse } from '@ucap/protocol-authentication'; -import { - FixedSizeVirtualScrollStrategy, - VIRTUAL_SCROLL_STRATEGY, - CdkVirtualScrollViewport -} from '@angular/cdk/scrolling'; -import { VersionInfo2Response } from '@ucap/api-public'; -import { PerfectScrollbarDirective } from 'ngx-perfect-scrollbar'; -export class MemberVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy { - constructor() { - super(184, 150, 200); - } -} +import { SearchData } from '@app/ucap/organization/models/search-data'; +import { Company } from '@ucap/api-external'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { ProfileListComponent as AppProfileListComponent } from '@app/ucap/organization/components/profile-list.component'; @Component({ selector: 'app-sections-organization-member-list', templateUrl: './member-list.component.html', styleUrls: ['./member-list.component.scss'], - providers: [ - { - provide: VIRTUAL_SCROLL_STRATEGY, - useClass: MemberVirtualScrollStrategy - } - ], changeDetection: ChangeDetectionStrategy.OnPush }) export class MemberListComponent implements OnInit, OnDestroy { @Input() - set searchData(data: { - companyCode: string; - searchWord: string; - isSearch: boolean; - }) { - this._searchData = data; + set searchData(searchData: SearchData) { + this._searchData = searchData; + + if (searchData.bySearch) { + this.setCompanyInfo(searchData.companyCode); + } else { + this.setDeptInfo(searchData.deptSeq); + } } + @ViewChild('profileList', { static: false }) + profileList: AppProfileListComponent; + // tslint:disable-next-line: variable-name - _searchData: { - companyCode: string; - searchWord: string; - isSearch: boolean; + _searchData: SearchData; + loginRes: LoginResponse; + selectedDeptInfo: DeptInfo; + selectedCompanyInfo: Company; + searchedProfileLength: number; + selectedUserInfos: UserInfoSS[] = []; + isExpanded = false; + sortOrderForProfileList: SortOrder = { + property: 'name', + ascending: true }; private ngOnDestroySubject: Subject; constructor( private store: Store, - private queryProtocolService: QueryProtocolService, private changeDetectorRef: ChangeDetectorRef ) {} ngOnInit() { this.ngOnDestroySubject = new Subject(); + + this.store + .pipe(takeUntil(this.ngOnDestroySubject), select(LoginSelector.loginRes)) + .subscribe((loginRes) => { + this.loginRes = loginRes; + }); } ngOnDestroy(): void { @@ -92,11 +86,131 @@ export class MemberListComponent implements OnInit, OnDestroy { } } - onChangedSearch(data: { - isSearch: boolean; - companyCode: string; - searchWord: string; - }) { - this._searchData = data; + onChangedSearch(data: { deptSeq: string; searchWord: string }) { + this._searchData = { + bySearch: true, + ...data + }; + this.setDeptInfo(data.deptSeq); + } + + onSearchedProfileList(userInfos: UserInfoSS[]) { + this.searchedProfileLength = !!userInfos ? userInfos.length : 0; + } + + onChangedCheckProfileList( + datas: { checked: boolean; userInfo: UserInfoSS }[] + ) { + if (!datas || 0 === datas.length) { + return; + } + + const pushs: UserInfoSS[] = []; + const pops: UserInfoSS[] = []; + + datas.forEach((d) => { + const i = this.selectedUserInfos.findIndex( + (u) => u.seq === d.userInfo.seq + ); + if (d.checked) { + if (-1 === i) { + pushs.push(d.userInfo); + } + } else { + if (-1 < i) { + pops.push(d.userInfo); + } + } + }); + + if (0 < pushs.length) { + this.selectedUserInfos = [...this.selectedUserInfos, ...pushs]; + } + + if (0 < pops.length) { + this.selectedUserInfos = this.selectedUserInfos.filter( + (u) => -1 === pops.findIndex((p) => p.seq === u.seq) + ); + } + } + + onRemovedProfileSelection(userInfo: UserInfo) { + const i = this.selectedUserInfos.findIndex( + (u) => u.seq === String(userInfo.seq) + ); + + if (-1 < i) { + this.selectedUserInfos = this.selectedUserInfos.filter( + (u) => u.seq !== String(userInfo.seq) + ); + } + } + + removableForSelection = (userInfo: UserInfo) => { + return true; + }; + + colorForSelection = (userInfo: UserInfo) => { + return 'accent'; + }; + + onOpenedSelection() { + this.isExpanded = true; + } + + onClosedSelection() { + this.isExpanded = false; + } + + onClickToggleSort() { + this.sortOrderForProfileList = { + ...this.sortOrderForProfileList, + ascending: !this.sortOrderForProfileList.ascending + }; + } + + onChangeSelectAll(event: MatCheckboxChange) { + if (event.checked) { + this.profileList.checkAll(); + } else { + this.selectedUserInfos = []; + } + } + + private setCompanyInfo(companyCode: string) { + const destroySubject: Subject = new Subject(); + this.store + .pipe(takeUntil(destroySubject), select(CompanySelector.companyList)) + .subscribe((companyList) => { + if (!companyList) { + return; + } + this.selectedCompanyInfo = companyList.find( + (c) => c.companyCode === companyCode + ); + this.changeDetectorRef.markForCheck(); + destroySubject.next(); + destroySubject.complete(); + }); + } + + private setDeptInfo(seq: string) { + const destroySubject: Subject = new Subject(); + this.store + .pipe( + takeUntil(destroySubject), + select(DepartmentSelector.departmentInfoList) + ) + .subscribe((departmentInfoList) => { + if (!departmentInfoList) { + return; + } + this.selectedDeptInfo = departmentInfoList.find( + (d) => String(d.seq) === seq + ); + this.changeDetectorRef.markForCheck(); + destroySubject.next(); + destroySubject.complete(); + }); } } diff --git a/src/app/sections/organization/organization.section.module.ts b/src/app/sections/organization/organization.section.module.ts index 5d0664c..4261d8e 100644 --- a/src/app/sections/organization/organization.section.module.ts +++ b/src/app/sections/organization/organization.section.module.ts @@ -15,6 +15,7 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatTableModule } from '@angular/material/table'; import { MatButtonModule } from '@angular/material/button'; import { MatExpansionModule } from '@angular/material/expansion'; +import { MatSidenavModule } from '@angular/material/sidenav'; import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; @@ -42,6 +43,7 @@ import { COMPONENTS } from './components'; MatTableModule, MatButtonModule, MatExpansionModule, + MatSidenavModule, PerfectScrollbarModule, diff --git a/src/app/services/app-chat.service.ts b/src/app/services/app-chat.service.ts index 5738027..44970ba 100644 --- a/src/app/services/app-chat.service.ts +++ b/src/app/services/app-chat.service.ts @@ -1,11 +1,11 @@ -import { Observable } from 'rxjs'; -import { take, concatMap, map } from 'rxjs/operators'; +import { Observable, of, forkJoin } from 'rxjs'; +import { take, concatMap, map, catchError } from 'rxjs/operators'; import { Injectable, Inject, ChangeDetectorRef } from '@angular/core'; import { Store } from '@ngrx/store'; -import { LocaleCode } from '@ucap/core'; +import { LocaleCode, DeviceType, FileUtil } from '@ucap/core'; import { PasswordUtil } from '@ucap/pi'; import { LoginResponse, SSOMode } from '@ucap/protocol-authentication'; @@ -26,7 +26,12 @@ import { UserStore } from '@app/models/user-store'; import { AppKey } from '@app/types/app-key.type'; import { environment } from '@environments'; -import { RoomInfo, RoomType } from '@ucap/protocol-room'; +import { + RoomInfo, + RoomType, + OpenRequest, + Open3Request +} from '@ucap/protocol-room'; import { Dictionary } from '@ngrx/entity'; import { RoomUserMap, @@ -38,6 +43,31 @@ import { TranslateService } from '@ucap/ng-ui-organization'; import { I18nService } from '@ucap/ng-i18n'; +import { ChattingActions, RoomActions } from '@ucap/ng-store-chat'; +import { + SendRequest as SendEventRequest, + EventType +} from '@ucap/protocol-event'; +import { + MassTalkSaveRequest, + FileTalkSaveResponse, + FileTalkSaveRequest +} from '@ucap/api-common'; +import { CommonApiService } from '@ucap/ng-api-common'; +import { StatusCode, FileUploadItem } from '@ucap/api'; +import { LogService } from '@ucap/ng-logger'; +import { MatDialog } from '@angular/material/dialog'; +import { + AlertDialogComponent, + AlertDialogData, + AlertDialogResult +} from '@ucap/ng-ui'; +import { StickerFilesInfo, KEY_STICKER_HISTORY } from '@ucap/ng-core'; +import { + CreateDialogComponent, + CreateDialogData, + CreateDialogResult +} from '@app/sections/chat/dialogs/create.dialog.component'; @Injectable({ providedIn: 'root' @@ -46,8 +76,21 @@ export class AppChatService { defaultProfileImage = 'assets/images/ico/img_nophoto.svg'; defaultProfileImageMulti = 'assets/images/ico/img_nophoto.svg'; - constructor(private i18nService: I18nService) {} + constructor( + private i18nService: I18nService, + private dialog: MatDialog, + private localStorageService: LocalStorageService, + private store: Store, + private commonApiService: CommonApiService, + private logService: LogService + ) { + this.i18nService.setDefaultNamespace('chat'); + } + /** + * 방이름 생성. + * cf) 방이름이 지정되어 있다면 방이름 리턴, 지정되어 있지 않으면 방참여인원의 이름 조합. + */ getRoomName( organizationTranslate: OrganizationTranslate, loginRes: LoginResponse, @@ -109,6 +152,12 @@ export class AppChatService { return roomName; } + /** + * 방 프로필 이미지 생성. + * cf) 방 참여인원의 프로필을 리턴. + * 없으면, defaultProfileImage, defaultProfileImageMulti, + * 멀티룸은 기본 defaultProfileImageMulti + */ getRoomProfileImage( roomInfo: RoomInfo, loginRes: LoginResponse, @@ -164,6 +213,11 @@ export class AppChatService { return roomImage; } + /** + * 방 인원 정보 수집. + * cf) roomUserShort, roomUser 데이터가 쌍을 이루는데 수집방법에 따라 short 만 존재할 수 있어, 수집할 수 있는 방인원을 리턴. + * roomUser 가 detail 정보라 우선함. + */ getRoomUserList( loginRes: LoginResponse, roomId: string, @@ -197,4 +251,431 @@ export class AppChatService { users }; } + + /** + * 스티커 히스토리 관리 in localstorage. + * cf) 스티커 전송 성공분만 처리. + */ + setStickerHistory(sticker: StickerFilesInfo) { + const history = this.localStorageService.get(KEY_STICKER_HISTORY); + + if (!!history && history.length > 0) { + const stickers: string[] = []; + [sticker.index, ...history.filter((hist) => hist !== sticker.index)].map( + (s, i) => { + if (i < 10) { + stickers.push(s); + } + } + ); + + this.localStorageService.set(KEY_STICKER_HISTORY, stickers); + } else { + this.localStorageService.set(KEY_STICKER_HISTORY, [ + sticker.index + ]); + } + } + + /** + * Event Send Protocol Service. + */ + protected sendEvent( + senderSeq: string, + roomId: string, + eventType: EventType, + sentMessage: string + ) { + this.store.dispatch( + ChattingActions.send({ + senderSeq, + req: { + roomId, + eventType, + sentMessage + } as SendEventRequest + }) + ); + } + + /** Send Normal message */ + sendMessageOfNormal(senderSeq: string, roomId: string, sentMessage: string) { + this.sendEvent(senderSeq, roomId, EventType.Character, sentMessage); + } + + /** Send Masstext message */ + sendMessageOfMassText( + loginRes: LoginResponse, + deviceType: DeviceType, + roomId: string, + sentMessage: string + ) { + const req: MassTalkSaveRequest = { + userSeq: loginRes.userSeq, + deviceType, + token: loginRes.tokenString, + content: sentMessage.replace(/"/g, '\\"'), + roomId + }; + + this.commonApiService + .massTalkSave(req) + .pipe( + take(1), + map((res) => { + if (res.statusCode === StatusCode.Success) { + this.sendEvent( + loginRes.userSeq, + roomId, + EventType.MassText, + res.returnJson + ); + } else { + this.logService.error( + `commonApiService] massTalkSave ${res?.errorMessage}` + ); + } + }), + catchError((error) => of({ error })) + ) + .subscribe(); + } + + async sendMessageOfSticker( + senderSeq: string, + roomId: string, + selectedSticker: StickerFilesInfo, + sentMessage: string + ) { + // Validation + if ( + !!sentMessage && + sentMessage.trim().length > environment.productConfig.chat.masstextLength + ) { + const result = await this.dialog.open< + AlertDialogComponent, + AlertDialogData, + AlertDialogResult + >(AlertDialogComponent, { + panelClass: 'miniSize-dialog', + data: { + title: this.i18nService.t('errors.label'), + message: this.i18nService.t('errors.maxLengthOfMassText', { + maxLength: environment.productConfig.chat.masstextLength + }) + } + }); + return; + } + + // Send + this.sendEvent( + senderSeq, + roomId, + EventType.Sticker, + JSON.stringify({ + name: '스티커', + file: selectedSticker.index, + chat: !!sentMessage ? sentMessage.trim() : '' + }) + ); + + // set sticker's history in localstorage + this.setStickerHistory(selectedSticker); + } + + /** Send Translation message */ + sendMessageOfTranslate( + loginRes: LoginResponse, + deviceType: DeviceType, + destLocale: string, + roomId: string, + sentMessage: string, + selectedSticker?: StickerFilesInfo + ) { + // const destLocale = this.destLocale; + // const original = message; + // const roomSeq = this.roomInfoSubject.value.roomSeq; + // if (!!this.isTranslationProcess) { + // return; + // } + // this.isTranslationProcess = true; + // this.commonApiService + // .translationSave({ + // userSeq: this.loginResSubject.value.userSeq, + // deviceType: this.environmentsInfo.deviceType, + // token: this.loginResSubject.value.tokenString, + // roomSeq, + // original, + // srcLocale: '', + // destLocale + // } as TranslationSaveRequest) + // .pipe( + // take(1), + // map((res) => { + // if (res.statusCode === StatusCode.Success) { + // let sentMessage = ''; + // let eventType = EventType.Translation; + // let previewObject: TranslationEventJson | MassTranslationEventJson; + // if (res.translationSeq > 0) { + // // Mass Text Translation + // previewObject = res; + // sentMessage = res.returnJson; + // eventType = EventType.MassTranslation; + // } else { + // // Normal Text Translation + // previewObject = { + // locale: destLocale, + // original, + // translation: res.translation, + // stickername: '', + // stickerfile: !!this.selectedSticker + // ? this.selectedSticker.index + // : '' + // }; + // sentMessage = JSON.stringify(previewObject); + // eventType = EventType.Translation; + // } + // if (!!this.translationPreview) { + // // preview + // this.translationPreviewInfo = { + // previewInfo: res, + // translationType: eventType + // }; + // this.changeDetectorRef.detectChanges(); + // } else { + // // direct send + // this.store.dispatch( + // EventStore.send({ + // senderSeq: this.loginResSubject.value.userSeq, + // req: { + // roomSeq, + // eventType, + // sentMessage + // } + // }) + // ); + // if (!!this.translationPreviewInfo) { + // this.translationPreviewInfo = null; + // } + // } + // if (!!this.selectedSticker) { + // this.isShowStickerSelector = false; + // this.setStickerHistory(this.selectedSticker); + // this.selectedSticker = null; + // } + // } else { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // panelClass: 'miniSize-dialog', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // this.logger.error('res', res); + // } + // }), + // catchError((error) => { + // this.isTranslationProcess = false; + // this.dialogService.open< + // AlertDialogComponent, + // AlertDialogData, + // AlertDialogResult + // >(AlertDialogComponent, { + // panelClass: 'miniSize-dialog', + // data: { + // title: '', + // message: this.translateService.instant( + // 'chat.error.translateServerError' + // ) + // } + // }); + // return of(this.logger.error('error', error)); + // }) + // ) + // .subscribe(() => { + // this.isTranslationProcess = false; + // }); + } + + /** Send AttachFile message */ + sendMessageOfAttachFile( + loginRes: LoginResponse, + deviceType: DeviceType, + roomId: string, + fileUploadItems: FileUploadItem[] + ): Promise { + const executor = async ( + resolve: (value?: boolean | PromiseLike) => void, + reject: (reason?: any) => void + ) => { + const allObservables: Observable[] = []; + + for (const fileUploadItem of fileUploadItems) { + let thumbnail: File; + if ( + -1 !== + [ + '3gp', + 'avi', + 'm4v', + 'mkv', + 'mov', + 'mp4', + 'mpeg', + 'mpg', + 'rv', + 'ts', + 'webm', + 'wmv' + ].indexOf(FileUtil.getExtension(fileUploadItem.file.name)) + ) { + try { + thumbnail = await FileUtil.thumbnail(fileUploadItem.file); + } catch (err) { + this.logService.error('video thumbnail error.', err); + } + this.logService.debug('thumbnail', thumbnail); + } + + const req: FileTalkSaveRequest = { + userSeq: loginRes.userSeq, + deviceType, + token: loginRes.tokenString, + roomId, + file: fileUploadItem.file, + fileName: fileUploadItem.file.name, + thumb: thumbnail, + fileUploadItem + }; + + allObservables.push( + this.commonApiService.fileTalkSave(req).pipe( + map((res) => { + if (!res) { + return; + } + if (StatusCode.Success === res.statusCode) { + return res; + } else { + throw res; + } + }) + ) + ); + } + + forkJoin(allObservables) + .pipe(take(1)) + .subscribe( + (resList) => { + for (const res of resList) { + this.store.dispatch( + ChattingActions.send({ + senderSeq: loginRes.userSeq, + req: { + roomId, + eventType: EventType.File, + sentMessage: JSON.stringify(res.returnJson) + } as SendEventRequest + }) + ); + } + }, + (error) => { + this.logService.debug('onFileSelected error', error); + const msg = this.i18nService.t('common.file.errors.failToUpload'); + alert(msg); + + reject(msg); + }, + () => { + resolve(true); + } + ); + }; + + return new Promise(executor); + } + + /** + * Open Dialog for 'New Room Open'. + */ + newOpenRoomDialog(): void { + const dialogRef = this.dialog.open< + CreateDialogComponent, + CreateDialogData, + CreateDialogResult + >(CreateDialogComponent, { + width: '100%', + height: '100%', + data: {} + }); + + dialogRef + .afterClosed() + .pipe( + take(1), + map((result) => { + if (!!result && !!result.userSeqs && result.userSeqs.length > 0) { + this.newOpenRoom(result.userSeqs, result.isTimer); + } + }), + catchError((err) => { + return of(err); + }) + ) + .subscribe(); + } + + newOpenRoom( + userSeqs: string[], + isTimerRoom: boolean, + loginRes?: LoginResponse + ) { + if (!userSeqs || userSeqs.length === 0) { + return; + } + isTimerRoom = isTimerRoom || false; + + if (!!isTimerRoom) { + /** Timer Room Open. */ + const req: Open3Request = { + divCd: 'OPROOMT', + roomName: '', + isTimerRoom, + timerRoomInterval: + environment.productConfig.chat.timerRoomDefaultInterval, + userSeqs + }; + this.store.dispatch(RoomActions.createTimer({ req })); + } else { + /** Normal Room Open */ + let req: OpenRequest; + if ( + userSeqs.length === 1 && + !!loginRes && + userSeqs[0] === loginRes.userSeq + ) { + // MyTalk Open. + req = { + divCd: 'OPMYTALK', + userSeqs: [loginRes.talkWithMeBotSeq + ''] + }; + } else { + req = { + divCd: 'OPROOM', + userSeqs + }; + } + this.store.dispatch(RoomActions.create({ req })); + } + } } diff --git a/src/app/services/app-file.service.ts b/src/app/services/app-file.service.ts new file mode 100644 index 0000000..0dc2f68 --- /dev/null +++ b/src/app/services/app-file.service.ts @@ -0,0 +1,86 @@ +import { Injectable, Inject } from '@angular/core'; + +import { FileUtil } from '@ucap/core'; +import { I18nService, UCAP_I18N_NAMESPACE } from '@ucap/ng-i18n'; +import { CommonApiService } from '@ucap/ng-api-common'; +import { Store } from '@ngrx/store'; + +@Injectable({ + providedIn: 'root' +}) +export class AppFileService { + constructor( + private store: Store, + private i18nService: I18nService, + private commonApiService: CommonApiService + ) { + this.i18nService.setDefaultNamespace('common'); + } + + async validUploadFile( + fileList: FileList, + fileAllowSize: number = 50 + ): Promise { + let valid = true; + + // File size check. + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < fileList.length; i++) { + const file = fileList[i]; + if (file.size > fileAllowSize * 1024 * 1024) { + const msg = this.i18nService.t('common.file.errors.oversize', { + maxSize: fileAllowSize + }); + alert(msg); + + valid = false; + return valid; + } + } + + // Extention check. + const checkExt = this.commonApiService.acceptableExtensionForFileTalk( + FileUtil.getExtensions(fileList) + ); + if (!!checkExt) { + const msg = this.i18nService.t('common.file.errors.notSupporedType', { + supporedType: checkExt.join(',') + }); + alert(msg); + // this.snackBarService.openFromComponent< + // AlertSnackbarComponent, + // AlertSnackbarData + // >(AlertSnackbarComponent, { + // duration: 1000, + // verticalPosition: 'bottom', + // horizontalPosition: 'center', + // data: { + // html: this.translateService.instant( + // 'common.file.errors.notSupporedType', + // { + // supporedType: checkExt.join(',') + // } + // ) + // } + // }); + valid = false; + return valid; + } + + // Fake media file check. + const fakeMedia = await this.commonApiService.checkInvalidMediaMimeForFileTalkForFileList( + fileList + ); + if (!!fakeMedia) { + const msg = this.i18nService.t('common.file.errors.notAcceptableMime', { + supporedType: fakeMedia.join(',') + }); + alert(msg); + + valid = false; + return valid; + } + + return valid; + } +} diff --git a/src/app/types/group-user.dialog.type.ts b/src/app/types/group-user.dialog.type.ts new file mode 100644 index 0000000..23fb2ce --- /dev/null +++ b/src/app/types/group-user.dialog.type.ts @@ -0,0 +1,6 @@ +export enum GroupUserDialaogType { + Create = 'CREATE_GROUP', + Add = 'ADD_GROUP', + Copy = 'COPY_GROUP', + Move = 'MOVE_GROUP' +} diff --git a/src/app/types/index.ts b/src/app/types/index.ts index 8a2e241..04138f8 100644 --- a/src/app/types/index.ts +++ b/src/app/types/index.ts @@ -1,3 +1,4 @@ export * from './app-key.type'; export * from './select-user.dialog.type'; export * from './tokens'; +export * from './group-user.dialog.type'; diff --git a/src/app/ucap/authentication/components/login.component.html b/src/app/ucap/authentication/components/login.component.html index ce65d43..bdf12cb 100644 --- a/src/app/ucap/authentication/components/login.component.html +++ b/src/app/ucap/authentication/components/login.component.html @@ -1,4 +1,4 @@ -