diff --git a/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java b/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java index 651eed085..28f7d489b 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java +++ b/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java @@ -21,7 +21,8 @@ public class NotificationProperties { private UUID descriptionTemplateInvitationType; private UUID contactSupportType; private UUID publicContactSupportType; - private UUID tenantSpecificInvitationUserType; + private UUID tenantSpecificInvitationExternalUserType; + private UUID tenantSpecificInvitationExistingUserType; private int emailExpirationTimeSeconds; private String contactSupportEmail; @@ -153,11 +154,19 @@ public class NotificationProperties { this.descriptionAnnotationCreated = descriptionAnnotationCreated; } - public UUID getTenantSpecificInvitationUserType() { - return tenantSpecificInvitationUserType; + public UUID getTenantSpecificInvitationExternalUserType() { + return tenantSpecificInvitationExternalUserType; } - public void setTenantSpecificInvitationUserType(UUID tenantSpecificInvitationUserType) { - this.tenantSpecificInvitationUserType = tenantSpecificInvitationUserType; + public void setTenantSpecificInvitationExternalUserType(UUID tenantSpecificInvitationExternalUserType) { + this.tenantSpecificInvitationExternalUserType = tenantSpecificInvitationExternalUserType; + } + + public UUID getTenantSpecificInvitationExistingUserType() { + return tenantSpecificInvitationExistingUserType; + } + + public void setTenantSpecificInvitationExistingUserType(UUID tenantSpecificInvitationExistingUserType) { + this.tenantSpecificInvitationExistingUserType = tenantSpecificInvitationExistingUserType; } } diff --git a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java index 0c91c9ac3..548909b83 100644 --- a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java @@ -914,19 +914,23 @@ public class UserServiceImpl implements UserService { public void sendUserToTenantInvitation(UserTenantUsersInviteRequest users) throws InvalidApplicationException, JAXBException { String tenantName = null; - String tenantCode = null; + String tenantCode; if (this.tenantScope.getTenantCode() != null && !this.tenantScope.getTenantCode().equals(this.tenantScope.getDefaultTenantCode())) { TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(this.tenantScope.getTenantCode()).isActive(IsActive.Active).first(); if (tenantEntity == null) throw new MyApplicationException("Tenant not found"); tenantName = tenantEntity.getName(); tenantCode = tenantEntity.getCode(); } else { - tenantName = "OpenCDMP"; tenantCode = this.tenantScope.getDefaultTenantCode(); } for (UserInviteToTenantRequestPersist user: users.getUsers()) { String token = this.createUserInviteToTenantConfirmation(user, tenantCode); - this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName); + UserContactInfoEntity contactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).disableTracking().values(user.getEmail()).types(ContactInfoType.Email).first(); + if (contactInfoEntity != null){ + this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName, contactInfoEntity.getUserId()); + } else { + this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName, null); + } } } @@ -952,26 +956,32 @@ public class UserServiceImpl implements UserService { return persist.getToken(); } - private void createTenantSpecificInvitationUserNotificationEvent(String token, String email, String tenantName) throws InvalidApplicationException { - UserEntity currentUser = this.entityManager.find(UserEntity.class, this.userScope.getUserIdSafe()); - if (currentUser == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{ this.userScope.getUserIdSafe(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); + private void createTenantSpecificInvitationUserNotificationEvent(String token, String email, String tenantName, UUID existingRecipient) throws InvalidApplicationException { + UserEntity sender = this.entityManager.find(UserEntity.class, this.userScope.getUserIdSafe()); + if (sender == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{ this.userScope.getUserIdSafe(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); NotifyIntegrationEvent event = new NotifyIntegrationEvent(); - List contactPairs = new ArrayList<>(); - contactPairs.add(new ContactPair(ContactInfoType.Email, email)); + if (existingRecipient == null) { + List contactPairs = new ArrayList<>(); + contactPairs.add(new ContactPair(ContactInfoType.Email, email)); - NotificationContactData contactData = new NotificationContactData(contactPairs, null, null); - event.setContactHint(this.jsonHandlingService.toJsonSafe(contactData)); - event.setContactTypeHint(NotificationContactType.EMAIL); + NotificationContactData contactData = new NotificationContactData(contactPairs, null, null); + event.setContactHint(this.jsonHandlingService.toJsonSafe(contactData)); + event.setContactTypeHint(NotificationContactType.EMAIL); + + event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationExternalUserType()); + } else { + event.setUserId(existingRecipient); + event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationExistingUserType()); + } - event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationUserType()); NotificationFieldData data = new NotificationFieldData(); List fieldInfoList = new ArrayList<>(); - fieldInfoList.add(new FieldInfo("{userName}", DataType.String, currentUser.getName())); + fieldInfoList.add(new FieldInfo("{userName}", DataType.String, sender.getName())); fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token)); fieldInfoList.add(new FieldInfo("{expiration_time}", DataType.String, this.secondsToTime(this.notificationProperties.getEmailExpirationTimeSeconds()))); - fieldInfoList.add(new FieldInfo("{tenantName}", DataType.String, tenantName)); + if (!this.conventionService.isNullOrEmpty(tenantName)) fieldInfoList.add(new FieldInfo("{tenantName}", DataType.String, tenantName)); data.setFields(fieldInfoList); event.setData(this.jsonHandlingService.toJsonSafe(data)); @@ -991,10 +1001,14 @@ public class UserServiceImpl implements UserService { this.checkActionState(action); UserInviteToTenantRequestEntity userInviteToTenantRequest = this.xmlHandlingService.fromXmlSafe(UserInviteToTenantRequestEntity.class, action.getData()); - if (userInviteToTenantRequest == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), UserInviteToTenantRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (userInviteToTenantRequest == null) + throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), UserInviteToTenantRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); - TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(userInviteToTenantRequest.getTenantCode()).isActive(IsActive.Active).first(); - if (tenantEntity == null) throw new MyApplicationException("Tenant not found"); + TenantEntity tenantEntity = null; + if (!userInviteToTenantRequest.getTenantCode().equals(this.tenantScope.getTenantCode())) { + tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(userInviteToTenantRequest.getTenantCode()).isActive(IsActive.Active).first(); + if (tenantEntity == null) throw new MyApplicationException("Tenant not found"); + } this.addUserToTenant(tenantEntity, userInviteToTenantRequest); } @@ -1014,21 +1028,25 @@ public class UserServiceImpl implements UserService { UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).disableTracking().userIds(userId).first(); if (userCredential == null) throw new MyApplicationException(); - TenantUserEntity tenantUserEntity = new TenantUserEntity(); - tenantUserEntity.setId(UUID.randomUUID()); - tenantUserEntity.setUserId(userId); - tenantUserEntity.setIsActive(IsActive.Active); - tenantUserEntity.setTenantId(tenant.getId()); - tenantUserEntity.setCreatedAt(Instant.now()); - tenantUserEntity.setUpdatedAt(Instant.now()); - this.entityManager.persist(tenantUserEntity); - this.eventBroker.emit(new UserAddedToTenantEvent(tenantUserEntity.getUserId(), tenantUserEntity.getTenantId())); + if (tenant != null){ + TenantUserEntity tenantUserEntity = new TenantUserEntity(); + tenantUserEntity.setId(UUID.randomUUID()); + tenantUserEntity.setUserId(userId); + tenantUserEntity.setIsActive(IsActive.Active); + tenantUserEntity.setTenantId(tenant.getId()); + tenantUserEntity.setCreatedAt(Instant.now()); + tenantUserEntity.setUpdatedAt(Instant.now()); + this.entityManager.persist(tenantUserEntity); + this.eventBroker.emit(new UserAddedToTenantEvent(tenantUserEntity.getUserId(), tenantUserEntity.getTenantId())); + } + for (String role: userInviteToTenantRequest.getRoles()) { UserRoleEntity item = new UserRoleEntity(); item.setId(UUID.randomUUID()); item.setUserId(userId); - item.setTenantId(tenant.getId()); + if (tenant != null) item.setTenantId(tenant.getId()); + else item.setTenantId(null); item.setRole(role); item.setCreatedAt(Instant.now()); this.entityManager.persist(item); @@ -1043,7 +1061,8 @@ public class UserServiceImpl implements UserService { this.entityManager.flush(); for (String role: userInviteToTenantRequest.getRoles()) { - this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenant.getCode(), role); + if (tenant != null && !this.conventionService.isNullOrEmpty(tenant.getCode())) this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenant.getCode(), role); + else this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenantScope.getDefaultTenantCode(), role); } } diff --git a/backend/web/src/main/resources/config/notification-devel.yml b/backend/web/src/main/resources/config/notification-devel.yml index fd2e7f976..2ef74e6f8 100644 --- a/backend/web/src/main/resources/config/notification-devel.yml +++ b/backend/web/src/main/resources/config/notification-devel.yml @@ -13,5 +13,6 @@ notification: descriptionTemplateInvitationType: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B contactSupportType: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD publicContactSupportType: B542B606-ACC6-4629-ADEF-4D8EE2F01222 - tenantSpecificInvitationUserType: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be + tenantSpecificInvitationExternalUserType: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be + tenantSpecificInvitationExistingUserType: b3809c17-d1e4-420a-919c-828564114191 contactSupportEmail: support@dmp.com \ No newline at end of file diff --git a/dmp-frontend/src/app/core/model/user/user.ts b/dmp-frontend/src/app/core/model/user/user.ts index 7e6b6be34..b42925dfd 100644 --- a/dmp-frontend/src/app/core/model/user/user.ts +++ b/dmp-frontend/src/app/core/model/user/user.ts @@ -84,3 +84,12 @@ export interface UserMergeRequestPersist { export interface RemoveCredentialRequestPersist { credentialId: Guid; } + +export interface UserTenantUsersInviteRequest { + users: UserInviteToTenantRequest[]; +} + +export interface UserInviteToTenantRequest { + email: string; + roles: string[]; +} diff --git a/dmp-frontend/src/app/core/services/user/user.service.ts b/dmp-frontend/src/app/core/services/user/user.service.ts index 38a820e77..5fbc5fbd6 100644 --- a/dmp-frontend/src/app/core/services/user/user.service.ts +++ b/dmp-frontend/src/app/core/services/user/user.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { IsActive } from '@app/core/common/enum/is-active.enum'; -import { DmpAssociatedUser, RemoveCredentialRequestPersist, User, UserMergeRequestPersist, UserPersist, UserRolePatchPersist } from '@app/core/model/user/user'; +import { DmpAssociatedUser, RemoveCredentialRequestPersist, User, UserMergeRequestPersist, UserPersist, UserRolePatchPersist, UserTenantUsersInviteRequest } from '@app/core/model/user/user'; import { UserLookup } from '@app/core/query/user.lookup'; import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; @@ -139,6 +139,22 @@ export class UserService { catchError((error: any) => throwError(error))); } + inviteUsersToTenant(item: UserTenantUsersInviteRequest): Observable { + const url = `${this.apiBase}/invite-users-to-tenant`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(error))); + } + + confirmInviteUser(token: Guid): Observable { + const url = `${this.apiBase}/confirm-invite-user-to-tenant/token/${token}`; + + return this.http + .get(url).pipe( + catchError((error: any) => throwError(error))); + } + // // Autocomplete Commons // diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog-editor.model.ts b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog-editor.model.ts new file mode 100644 index 000000000..3b89049d3 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog-editor.model.ts @@ -0,0 +1,117 @@ +import { UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { UserInviteToTenantRequest, UserTenantUsersInviteRequest } from "@app/core/model/user/user"; +import { BackendErrorValidator } from '@common/forms/validation/custom-validator'; +import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model'; +import { Validation, ValidationContext } from '@common/forms/validation/validation-context'; + +export class UserTenantUsersInviteRequestEditorModel implements UserTenantUsersInviteRequest { + users: UserInviteToTenantRequestEditorModel[] = []; + + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel(); + protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder(); + + constructor() { } + + buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup { + if (context == null) { context = this.createValidationContext(); } + + return this.formBuilder.group({ + users: this.formBuilder.array( + (this.users ?? []).map( + (item, index) => item.buildForm({ + rootPath: `userInviteToTenantRequest[${index}].`, + disabled: disabled + }) + ), context.getValidation('users').validators + ), + }); + } + + createValidationContext(): ValidationContext { + const baseContext: ValidationContext = new ValidationContext(); + const baseValidationArray: Validation[] = new Array(); + baseValidationArray.push({ key: 'users', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'userInviteToTenantRequest')] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } + + static reapplyValidators(params: { + formArray: UntypedFormArray, + validationErrorModel: ValidationErrorModel, + }): void { + const { validationErrorModel, formArray } = params; + formArray?.controls?.forEach( + (control, index) => UserInviteToTenantRequestEditorModel.reapplyValidators({ + formGroup: control as UntypedFormGroup, + rootPath: `userInviteToTenantRequest[${index}].`, + validationErrorModel: validationErrorModel + }) + ); + } +} + +export class UserInviteToTenantRequestEditorModel implements UserInviteToTenantRequest { + email: string; + roles: string[]; + + protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder(); + + constructor( + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel() + ) { } + + buildForm(params?: { + context?: ValidationContext, + disabled?: boolean, + rootPath?: string + }): UntypedFormGroup { + let { context = null, disabled = false, rootPath } = params ?? {} + if (context == null) { + context = UserInviteToTenantRequestEditorModel.createValidationContext({ + validationErrorModel: this.validationErrorModel, + rootPath + }); + } + + return this.formBuilder.group({ + email: [{ value: this.email, disabled: disabled }, context.getValidation('email').validators], + roles: [{ value: this.roles, disabled: disabled }, context.getValidation('roles').validators], + }); + } + + static createValidationContext(params: { + rootPath?: string, + validationErrorModel: ValidationErrorModel + }): ValidationContext { + const { rootPath = '', validationErrorModel } = params; + + const baseContext: ValidationContext = new ValidationContext(); + const baseValidationArray: Validation[] = new Array(); + + baseValidationArray.push({ key: 'email', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}email`)] }); + baseValidationArray.push({ key: 'roles', validators: [Validators.required, BackendErrorValidator(validationErrorModel, `${rootPath}roles`)] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } + + static reapplyValidators(params: { + formGroup: UntypedFormGroup, + validationErrorModel: ValidationErrorModel, + rootPath: string + }): void { + + const { formGroup, rootPath, validationErrorModel } = params; + const context = UserInviteToTenantRequestEditorModel.createValidationContext({ + rootPath, + validationErrorModel + }); + + ['email', 'roles'].forEach(keyField => { + const control = formGroup?.get(keyField); + control?.clearValidators(); + control?.addValidators(context.getValidation(keyField).validators); + }) + } +} diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.html b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.html new file mode 100644 index 000000000..7f2a1a9dc --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.html @@ -0,0 +1,57 @@ +
+
+
+

{{'USER-INVITE-TO-TENANT-DIALOG.TITLE' | translate}}

+
+
+ close +
+
+
+
+
+ + {{'USER-INVITE-TO-TENANT-DIALOG.FIELDS.EMAIL' | translate}} + + {{user.get('email').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+ +
+ + {{'USER-INVITE-TO-TENANT-DIALOG.FIELDS.ROLES' | translate}} + + {{enumUtils.toAppRoleString(appRoleEnum.TenantAdmin)}} + {{enumUtils.toAppRoleString(appRoleEnum.TenantPlanManager)}} + {{enumUtils.toAppRoleString(appRoleEnum.TenantConfigManager)}} + {{enumUtils.toAppRoleString(appRoleEnum.TenantUser)}} + + {{user.get('roles').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ +
+
+
+ {{'USER-INVITE-TO-TENANT-DIALOG.USERS-REQUIRED' | translate}} + {{formGroup.get('users').getError('backendError').message}} +
+
+
+ +
+
+
+
+ + {{formGroup.get('users').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} +
+
diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.scss b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.scss new file mode 100644 index 000000000..e25cf8eb0 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.scss @@ -0,0 +1,150 @@ +.user-invite-to-tenant-dialog { + padding: 2.5rem; + + .form-container { + width: 33em; + min-height: 14em; + padding: 0.28em 0.34em 0em 1.125em; + } + + .close-icon { + cursor: pointer; + // margin-right: 20px; + padding: .4rem; + width: auto !important; + height: auto !important; + } + + .close-icon:hover { + background-color: #ECECED !important; + border-radius: 50%; + } + + .title { + font-size: 2.375em; + font-weight: lighter; + color: #000000; + opacity: 0.8; + margin-bottom: 0.842em; + } + + .content { + width: 31em; + margin: 0px; + padding: 0px; + } + + // .mat-form-field { + // background: #fafafa; + // border: 1px solid #d1d1d1; + // border-radius: 4px; + // } + + .hint { + font-size: 0.875rem; + font-weight: 500; + color: #212121; + opacity: 0.81; + } + + ::ng-deep .mat-dialog-container { + border-radius: 8px; + } + + .search { + padding: 2px !important; + } + + ::ng-deep .search { + .mat-form-field-infix { + font-size: 1rem; + padding: 0.6em 0 1em 0 !important; + } + + // .mat-form-field-underline { + // display: none; + // } + // .mat-form-field-flex { + // padding: 0em; + // } + // .align-arrow-right { + // display: none; + // } + } + + .select-role { + width: 50% !important; + font-size: 14px; + color: #848484; + height: min-content; + margin-right: 2rem; + border: none; + background-color: transparent; + } + + ::ng-deep .select-role { + + .mat-form-field-outline-start, + .mat-form-field-outline-gap, + .mat-form-field-outline-end { + border: none !important; + } + + .mat-select-arrow-wrapper { + transform: none !important; + } + } + + ::ng-deep .select-role .mat-form-field-wrapper { + padding-bottom: 0 !important; + } + + .invite-btn { + background: #ffffff 0% 0% no-repeat padding-box; + border: 1px solid var(--primary-color); + border-radius: 30px; + opacity: 1; + min-width: 101px; + height: 43px; + color: var(--primary-color); + font-weight: 500; + } + + .invite-btn-disabled { + min-width: 6.64em; + height: 2.93em; + background: #ffffff; + border: 1px solid #b5b5b5; + border-radius: 30px; + font-weight: bold; + letter-spacing: -0.35px; + color: #b5b5b5; + margin-bottom: 0.25em; + cursor: default; + } + + .invite-btn:hover { + background: var(--primary-color); + color: #ffffff; + } + + @keyframes blink { + 0% { + border: 1px solid rgba(255, 0, 0, 0.2); + } + + 50% { + border: 1px solid rgba(255, 0, 0, 0.5); + } + + 100% { + border: 1px solid rgba(255, 0, 0, 0.2); + } + } + + :host ::ng-deep .invalid-email { + border: 1px solid red; + // animation: blink 1.4s infinite; + // animation-fill-mode: both; + } +} diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.ts b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.ts new file mode 100644 index 000000000..fdfdab57d --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.component.ts @@ -0,0 +1,110 @@ + +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, Inject, OnInit } from '@angular/core'; +import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { BaseComponent } from '@common/base/base.component'; +import { FormService } from '@common/forms/form-service'; +import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { TranslateService } from '@ngx-translate/core'; +import { takeUntil } from 'rxjs/operators'; +import { UserInviteToTenantRequestEditorModel, UserTenantUsersInviteRequestEditorModel } from './user-invite-to-tenant-dialog-editor.model'; +import { UserTenantUsersInviteRequest } from '@app/core/model/user/user'; +import { UserService } from '@app/core/services/user/user.service'; +import { AppRole } from '@app/core/common/enum/app-role'; + +@Component({ + selector: 'app-user-invite-to-tenant-dialog.component', + templateUrl: 'user-invite-to-tenant-dialog.component.html', + styleUrls: ['./user-invite-to-tenant-dialog.component.scss'] +}) +export class UserInviteToTenantDialogComponent extends BaseComponent implements OnInit { + + editorModel: UserTenantUsersInviteRequestEditorModel; + formGroup: any; + inProgressSendButton = false; + appRoleEnum = AppRole; + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + + constructor( + public enumUtils: EnumUtils, + public route: ActivatedRoute, + public router: Router, + private language: TranslateService, + public dialogRef: MatDialogRef, + private uiNotificationService: UiNotificationService, + private httpErrorHandlingService: HttpErrorHandlingService, + private userService: UserService, + private formService: FormService, + ) { + super(); + } + + ngOnInit() { + this.editorModel = new UserTenantUsersInviteRequestEditorModel(); + this.formGroup = this.editorModel.buildForm(); + this.addUser(); + } + + addUser(): void { + const formArray = this.formGroup.get("users") as UntypedFormArray; + const user: UserInviteToTenantRequestEditorModel = new UserInviteToTenantRequestEditorModel(this.editorModel.validationErrorModel); + formArray.push(user.buildForm({ rootPath: "userInviteToTenantRequest[" + formArray.length + "]." })); + } + + removeUser(userIndex: number): void { + (this.formGroup.get("users") as UntypedFormArray).removeAt(userIndex); + + UserTenantUsersInviteRequestEditorModel.reapplyValidators( + { + formArray: this.formGroup.get("users") as UntypedFormArray, + validationErrorModel: this.editorModel.validationErrorModel, + } + ); + (this.formGroup.get("users") as UntypedFormArray).markAsDirty(); + } + + send() { + this.formService.removeAllBackEndErrors(this.formGroup); + this.formService.touchAllFormFields(this.formGroup); + + if (!this.formGroup.valid) { return; } + this.inProgressSendButton = true; + const userFormData = this.formGroup.value as UserTenantUsersInviteRequest; + + this.userService.inviteUsersToTenant(userFormData) + .pipe(takeUntil(this._destroyed)) + .subscribe( + complete => { + this.dialogRef.close(); + this.onCallbackSuccess(); + }, + error => this.onCallbackError(error) + ); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('DMP-USER-INVITATION-DIALOG.SUCCESS'), SnackBarNotificationLevel.Success); + } + + onCallbackError(errorResponse: HttpErrorResponse) { + this.inProgressSendButton = false; + let errorOverrides = new Map(); + errorOverrides.set(-1, this.language.instant('DMP-USER-INVITATION-DIALOG.ERROR')); + this.httpErrorHandlingService.handleBackedRequestError(errorResponse, errorOverrides, SnackBarNotificationLevel.Error); + + const error: HttpError = this.httpErrorHandlingService.getError(errorResponse); + if (error.statusCode === 400) { + this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error); + this.formService.validateAllFormFields(this.formGroup); + } + } +} diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.module.ts b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.module.ts new file mode 100644 index 000000000..f20b3340c --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-invite-to-tenant-dialog/user-invite-to-tenant-dialog.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CommonUiModule } from '@common/ui/common-ui.module'; +import { UserInviteToTenantDialogComponent } from './user-invite-to-tenant-dialog.component'; + +@NgModule({ + imports: [CommonUiModule, FormsModule, ReactiveFormsModule], + declarations: [UserInviteToTenantDialogComponent], + exports: [UserInviteToTenantDialogComponent] +}) +export class UserInviteToTenantDialogModule { + constructor() { } +} \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.html b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.html index 10869f6e6..8f5981787 100644 --- a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.html +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.html @@ -6,6 +6,11 @@ +
+ +