import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { RoleOrganizationType } from '@app/core/common/enum/role-organization-type'; import { CultureInfo } from '@app/core/model/culture-info'; import { Reference } from '@app/core/model/reference/reference'; import { User, UserCredential } from '@app/core/model/user/user'; import { AuthService } from '@app/core/services/auth/auth.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { CultureService } from '@app/core/services/culture/culture-service'; import { LanguageService } from '@app/core/services/language/language.service'; import { MatomoService } from '@app/core/services/matomo/matomo-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { UserService } from '@app/core/services/user/user.service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; import { PopupNotificationDialogComponent } from "@app/library/notification/popup/popup-notification.component"; import { BaseComponent } from '@common/base/base.component'; import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component'; import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; import { TranslateService } from '@ngx-translate/core'; import * as moment from 'moment-timezone'; import { Observable, of } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { AddAccountDialogComponent } from './add-account/add-account-dialog.component'; import { UserProfileEditorModel } from './user-profile-editor.model'; import { nameof } from 'ts-simple-nameof'; import { Guid } from '@common/types/guid'; @Component({ selector: 'app-user-profile', templateUrl: './user-profile.component.html', styleUrls: ['./user-profile.component.scss'], }) export class UserProfileComponent extends BaseComponent implements OnInit, OnDestroy { userProfileEditorModel: UserProfileEditorModel; user: Observable; //TODO: refactor userCredentials: Observable; firstEmail: String; userLanguage: String; currentUserId: string; cultures: Observable; timezones: Observable; editMode = false; languages = []; roleOrganizationEnum = RoleOrganizationType; errorMessages = []; nestedCount = []; nestedIndex = 0; organisationsAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = { filterFn: this.filterOrganisations.bind(this), initialItems: (excludedItems: any[]) => this.filterOrganisations('').pipe(map(result => result.filter(resultItem => (excludedItems || []).map(x => x.id).indexOf(resultItem.id) === -1))), displayFn: (item) => item['name'], titleFn: (item) => item['name'], subtitleFn: (item) => item['tag'] ? this.language.instant('TYPES.EXTERNAL-DATASET-TYPE.SOURCE:') + item['tag'] : (item['key'] ? this.language.instant('TYPES.EXTERNAL-DATASET-TYPE.SOURCE:') + item['key'] : this.language.instant('TYPES.EXTERNAL-DATASET-TYPE.NO-SOURCE')) }; formGroup: UntypedFormGroup; constructor( private userService: UserService, private route: ActivatedRoute, private router: Router, private authService: AuthService, private language: TranslateService, private cultureService: CultureService, private authentication: AuthService, private languageService: LanguageService, private configurationService: ConfigurationService, private uiNotificationService: UiNotificationService, private dialog: MatDialog, public enumUtils: EnumUtils, private httpClient: HttpClient, private matomoService: MatomoService ) { super(); this.languages = this.languageService.getAvailableLanguagesCodes(); } public getProviderIcons(userCredential: UserCredential, culture:string): string[] { if (userCredential.data.externalProviderNames === undefined || userCredential.data.externalProviderNames?.length === 0) { return [this.configurationService.authProviders.defaultAuthProvider.providerClass]; } const providerNames: string[] = []; for (let providerName of userCredential.data.externalProviderNames) { const providerImage = this.configurationService.authProviders.findOrGetDefault(providerName.toString(), culture).providerClass; if (providerImage !== null) { providerNames.push(providerImage); } } return providerNames; } public getProviderIcon(providerName:string, culture:string): string { return this.configurationService.authProviders.find(providerName, culture).providerClass; } ngOnInit() { this.matomoService.trackPageView('User Profile'); this.route.params .pipe(takeUntil(this._destroyed)) .subscribe((params: Params) => { this.currentUserId = this.authService.userId()?.toString(); this.user = this.userService.getSingle( Guid.parse(this.currentUserId), [ nameof(x => x.id), nameof(x => x.name), nameof(x => x.additionalInfo.language), nameof(x => x.additionalInfo.timezone), nameof(x => x.additionalInfo.culture), nameof(x => x.additionalInfo.organization), nameof(x => x.additionalInfo.roleOrganization), `${nameof(x => x.credentials)}.${nameof(x => x.data.email)}`, `${nameof(x => x.credentials)}.${nameof(x => x.data.externalProviderNames)}`, ] ) .pipe(map(result => { //tested ui with fake data // const fakecredentials: UserCredential = { // "id": Guid.createEmpty(), // "externalId": "123", // "user": null, // "createdAt": new Date(), // "data": { // "email": "dmpadmin@dmp.com", // "externalProviderNames": ["Google", "Facebook"] // } // }; // result.credentials.push(fakecredentials); // result.credentials[0].data.externalProviderNames = ['Google']; this.userLanguage = result.additionalInfo.language; this.firstEmail = result.credentials[0].data.email; this.userCredentials = of(result.credentials); this.userProfileEditorModel = new UserProfileEditorModel().fromModel(result); this.formGroup = this.userProfileEditorModel.buildForm(this.languageService.getAvailableLanguagesCodes()); //this.formGroup.get('language').valueChanges.pipe(takeUntil(this._destroyed)).subscribe(x => { if (x) this.translate.use(x.value) }) this.formGroup.get('timezone').valueChanges .pipe(takeUntil(this._destroyed)) .subscribe(x => { if (x) { this.timezones = this._filterTimezone(x); } }); this.formGroup.get('culture').valueChanges .pipe(takeUntil(this._destroyed)) .subscribe(x => { if (x) { this.cultures = this._filterCulture(x); } }); // this.initializeDisabledFormGroup(); this.unlock(); return result; })); //TODO: refactor // this.userService.getEmails(userId).pipe(takeUntil(this._destroyed)) // .subscribe(result => { // this.user.subscribe(x => { // const mainEmail = result.filter(el => el.email === x.email) // const otherEmails = result.filter(el => el.email !== x.email) // this.userCredentials = [...mainEmail, ...otherEmails]; // } // ) // }); }); } ngOnDestroy(): void { } public hasProvider(userCredential: UserCredential, provider: string): boolean { const result = userCredential.data.externalProviderNames?.find(x => x == provider); return result != null; } private filterOrganisations(value: string): Observable { //TODO: refactor // return this.externalSourcesService.searchDMPOrganizations(value); return null; } private _filterTimezone(value: string): Observable { if (value && typeof value === 'string') { const filterValue = value.toLowerCase(); return of(moment.tz.names().filter(option => option.toLowerCase().includes(filterValue))); } else { return of(moment.tz.names()); } } private _filterCulture(value: string): Observable { if (value && typeof value === 'string') { const filterValue = value.toLowerCase(); return of(this.cultureService.getCultureValues().filter(option => option.displayName.toLowerCase().includes(filterValue))); } else { return of(this.cultureService.getCultureValues()); } } displayFn(culture?: CultureInfo): string | undefined { if (culture == null || culture.displayName == null || culture.nativeName == null) return undefined; return culture.displayName + '-' + culture.nativeName; } save() { //TODO: refactor // if (!this.formGroup.valid) { // this.printErrors(this.formGroup); // this.showValidationErrorsDialog(); // this.nestedCount = []; // this.nestedIndex = 0; // this.errorMessages = []; // return; // } // this.userService.updateUserSettings(this.formGroup.value) // .pipe(takeUntil(this._destroyed)) // .subscribe( // x => { // this.editMode = false; // this.languageService.changeLanguage(this.formGroup.value.language.value); // this.authService.refresh() // .pipe(takeUntil(this._destroyed)) // .subscribe(result => { // this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); // this.router.navigate(['/profile']); // }); // // .subscribe(result => window.location.reload()); // }, // error => { // console.log(error); // }); } public unlock() { this.editMode = true; this.formGroup.enable(); } public initializeDisabledFormGroup() { this.formGroup.disable(); } public lock() { if (!this.formGroup.valid) { return; } //TODO: refactor // this.userService.updateUserSettings(this.formGroup.value) // .pipe(takeUntil(this._destroyed)) // .subscribe( // x => { // this.editMode = false; // this.languageService.changeLanguage(this.formGroup.value.language.value); // this.formGroup.disable(); // this.authService.refresh() // .pipe(takeUntil(this._destroyed)) // .subscribe(result => this.router.navigate(['/profile'])); // // .subscribe(result => window.location.reload()); // }, // error => { // console.log(error); // }); } private showValidationErrorsDialog(projectOnly?: boolean) { const dialogRef = this.dialog.open(FormValidationErrorsDialogComponent, { disableClose: true, autoFocus: false, restoreFocus: false, data: { errorMessages: this.errorMessages, projectOnly: projectOnly }, }); } public applyFallbackAvatar(ev: Event) { (ev.target as HTMLImageElement).src = 'assets/images/profile-placeholder.png'; } public removeAccount(userCredential: any) { this.dialog.open(ConfirmationDialogComponent, { data: { message: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT-DIALOG.MESSAGE'), confirmButton: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT-DIALOG.CONFIRM'), cancelButton: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT-DIALOG.CANCEL') }, maxWidth: '35em' }) .afterClosed() .subscribe(confirm => { if (confirm) { //TODO: refactor console.log('removed!'); // const unlinkAccountModel: UnlinkAccountRequestModel = { // userId: this.currentUserId, // email: userCredential.email, // provider: userCredential.provider // }; // this.unlinkAccountEmailConfirmation.sendConfirmationEmail(unlinkAccountModel).pipe(takeUntil(this._destroyed)).subscribe( // result => { // this.dialog.open(PopupNotificationDialogComponent, { // data: { // title: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.TITLE'), // message: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.MESSAGE', { 'accountToBeUnlinked': userCredential.email }) // }, maxWidth: '35em' // }); // }, // error => { this.onCallbackError(error); } // ); } }); } onCallbackError(errorResponse: HttpErrorResponse) { this.uiNotificationService.snackBarNotification(errorResponse.message, SnackBarNotificationLevel.Warning); } public addAccount() { const dialogRef = this.dialog.open(AddAccountDialogComponent, { restoreFocus: false, autoFocus: false, // width: '653px', // maxHeight: '90vh', data: { email: '' } }); dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { if (result) { this.userService.mergeAccount({ email: result.email }) .subscribe(result => { if (result) { this.dialog.open(PopupNotificationDialogComponent, { data: { title: this.language.instant('USER-PROFILE.MERGING-EMAILS-DIALOG.TITLE'), message: this.language.instant('USER-PROFILE.MERGING-EMAILS-DIALOG.MESSAGE') }//, maxWidth: '30em' }); } }, error => console.error(error)); //TODO how to handle this //TODO refactor // this.mergeEmailConfirmation.sendConfirmationEmail(result).pipe(takeUntil(this._destroyed)) // .subscribe(res => { // this.dialog.open(PopupNotificationDialogComponent, { // data: { // title: this.language.instant('USER-PROFILE.MERGING-EMAILS-DIALOG.TITLE'), // message: this.language.instant('USER-PROFILE.MERGING-EMAILS-DIALOG.MESSAGE') // }, maxWidth: '30em' // }); // }, err => { }); } }); } printErrors(rootform: UntypedFormGroup) { if (!rootform.valid) { Object.keys(rootform.controls).forEach(key => { const errors = rootform.get(key).errors; if (errors !== null) { let numbering: string = ''; for (let j = 0; j < this.nestedCount.length; j++) { numbering += this.nestedCount[j]; if (j < this.nestedIndex) { numbering += '.'; } else { break; } } Object.keys(errors).forEach(keyError => { if (typeof errors[keyError] === 'boolean') { this.errorMessages.push(numbering + ' ' + key + ' is ' + keyError); } else { this.errorMessages.push(numbering + ' ' + key + ': ' + keyError + ': ' + JSON.stringify(errors[keyError])); } }); } else { if (rootform.get(key) instanceof UntypedFormGroup) { this.printErrors(rootform.get(key)); } else if (rootform.get(key) instanceof UntypedFormArray) { this.nestedIndex++; this.nestedCount[this.nestedIndex] = 0; for (let childForm of (rootform.get(key)).controls) { this.nestedCount[this.nestedIndex]++; this.printErrors(childForm); } this.nestedCount[this.nestedIndex] = 0; this.nestedIndex--; } } }); } } }