From 5b3c1fcd01a8f3771f7e073ffc67aec4f03c735a Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 6 Dec 2024 15:51:54 -0500 Subject: [PATCH] [PM-13755] Decouple Invite and Edit User Flows --- .../member-dialog/member-dialog.component.ts | 136 +++++++++++------- .../members/members.component.ts | 86 ++++++----- 2 files changed, 136 insertions(+), 86 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index fe84c5097de..4956c2e67be 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -158,8 +158,21 @@ export class MemberDialogComponent implements OnDestroy { .pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.editMode = this.params.organizationUserId != null; + + let userDetails$; + if (this.editMode) { + this.title = this.i18nService.t("editMember"); + userDetails$ = this.userService.get( + this.params.organizationId, + this.params.organizationUserId, + ); + } else { + this.title = this.i18nService.t("inviteMember"); + userDetails$ = of(null); + } + this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; - this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); + this.isOnSecretsManagerStandalone = this.params.isOnSecretsManagerStandalone; if (this.isOnSecretsManagerStandalone) { @@ -176,10 +189,6 @@ export class MemberDialogComponent implements OnDestroy { ), ); - const userDetails$ = this.params.organizationUserId - ? this.userService.get(this.params.organizationId, this.params.organizationUserId) - : of(null); - this.allowAdminAccessToAllCollectionItems$ = this.organization$.pipe( map((organization) => { return organization.allowAdminAccessToAllCollectionItems; @@ -276,6 +285,72 @@ export class MemberDialogComponent implements OnDestroy { emailsControl.updateValueAndValidity(); } + private async handleInviteUsers(userView: OrganizationUserAdminView, organization: Organization) { + const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; + + if (this.enforceEmailCountLimit(emails, organization)) { + return; + } + + await this.userService.invite(emails, userView); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("invitedUsers"), + }); + this.close(MemberDialogResult.Saved); + } + + private enforceEmailCountLimit(emails: string[], organization: Organization): boolean { + const maxEmailsCount = organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20; + + if (emails.length > maxEmailsCount) { + this.formGroup.controls.emails.setErrors({ + tooManyEmails: { message: this.i18nService.t("tooManyEmails", maxEmailsCount) }, + }); + return true; + } + + return false; + } + + private async handleEditUser(userView: OrganizationUserAdminView) { + userView.id = this.params.organizationUserId; + await this.userService.save(userView); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedUserId", this.params.name), + }); + + this.close(MemberDialogResult.Saved); + } + + private async getUserView(): Promise { + const userView = new OrganizationUserAdminView(); + userView.organizationId = this.params.organizationId; + userView.type = this.formGroup.value.type; + + userView.permissions = this.setRequestPermissions( + userView.permissions ?? new PermissionsApi(), + userView.type !== OrganizationUserType.Custom, + ); + + userView.collections = this.formGroup.value.access + .filter((v) => v.type === AccessItemType.Collection) + .map(convertToSelectionView); + + userView.groups = (await firstValueFrom(this.restrictEditingSelf$)) + ? null + : this.formGroup.value.groups.map((m) => m.id); + + userView.accessSecretsManager = this.formGroup.value.accessSecretsManager; + + return userView; + } + private loadOrganizationUser( userDetails: OrganizationUserAdminView, groups: GroupDetailsView[], @@ -418,58 +493,13 @@ export class MemberDialogComponent implements OnDestroy { return; } - const userView = new OrganizationUserAdminView(); - userView.id = this.params.organizationUserId; - userView.organizationId = this.params.organizationId; - userView.type = this.formGroup.value.type; - userView.permissions = this.setRequestPermissions( - userView.permissions ?? new PermissionsApi(), - userView.type !== OrganizationUserType.Custom, - ); - userView.collections = this.formGroup.value.access - .filter((v) => v.type === AccessItemType.Collection) - .map(convertToSelectionView); - - userView.groups = (await firstValueFrom(this.restrictEditingSelf$)) - ? null - : this.formGroup.value.groups.map((m) => m.id); - - userView.accessSecretsManager = this.formGroup.value.accessSecretsManager; + const userView = await this.getUserView(); if (this.editMode) { - await this.userService.save(userView); + await this.handleEditUser(userView); } else { - userView.id = this.params.organizationUserId; - const maxEmailsCount = - organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20; - const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; - if (emails.length > maxEmailsCount) { - this.formGroup.controls.emails.setErrors({ - tooManyEmails: { message: this.i18nService.t("tooManyEmails", maxEmailsCount) }, - }); - return; - } - if ( - organization.hasReseller && - this.params.numConfirmedMembers + emails.length > organization.seats - ) { - this.formGroup.controls.emails.setErrors({ - tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, - }); - return; - } - await this.userService.invite(emails, userView); + await this.handleInviteUsers(userView, organization); } - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t( - this.editMode ? "editedUserId" : "invitedUsers", - this.params.name, - ), - }); - this.close(MemberDialogResult.Saved); }; remove = async () => { diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 26e27e1249b..fe37b443384 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -462,9 +462,51 @@ export class MembersComponent extends BaseMembersComponent firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this)); } - async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) { + private async handleInviteDialog(initialTab: MemberDialogTab) { + const dialog = openUserAddEditDialog(this.dialogService, { + data: { + name: null, + organizationId: this.organization.id, + organizationUserId: null, + allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], + usesKeyConnector: null, + isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, + initialTab: initialTab, + numConfirmedMembers: this.dataSource.confirmedUserCount, + managedByOrganization: null, + }, + }); + + const result = await lastValueFrom(dialog.closed); + + if (result === MemberDialogResult.Saved) { + await this.load(); + } + } + + private async handleSeatLimitForFixedTiers() { + if (!this.organization.canEditSubscription) { + await this.showSeatLimitReachedDialog(); + return; + } + + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: this.organization.id, + subscription: null, + productTierType: this.organization.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + + if (result === ChangePlanDialogResultType.Submitted) { + await this.load(); + } + } + + async invite(initialTab: MemberDialogTab = MemberDialogTab.Role) { if ( - !user && this.organization.hasReseller && this.organization.seats === this.dataSource.confirmedUserCount ) { @@ -473,52 +515,30 @@ export class MembersComponent extends BaseMembersComponent title: this.i18nService.t("seatLimitReached"), message: this.i18nService.t("contactYourProvider"), }); - return; - } - - // Invite User: Add Flow - // Click on user email: Edit Flow - - // User attempting to invite new users in a free org with max users - if ( - !user && + } else if ( this.dataSource.data.length === this.organization.seats && (this.organization.productTierType === ProductTierType.Free || this.organization.productTierType === ProductTierType.TeamsStarter || this.organization.productTierType === ProductTierType.Families) ) { - if (!this.organization.canEditSubscription) { - await this.showSeatLimitReachedDialog(); - return; - } - - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: this.organization.id, - subscription: null, - productTierType: this.organization.productTierType, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === ChangePlanDialogResultType.Submitted) { - await this.load(); - } - return; + await this.handleSeatLimitForFixedTiers(); + } else { + await this.handleInviteDialog(initialTab); } + } + async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) { const dialog = openUserAddEditDialog(this.dialogService, { data: { name: this.userNamePipe.transform(user), organizationId: this.organization.id, - organizationUserId: user != null ? user.id : null, + organizationUserId: user.id, allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], - usesKeyConnector: user?.usesKeyConnector, + usesKeyConnector: user.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: initialTab, numConfirmedMembers: this.dataSource.confirmedUserCount, - managedByOrganization: user?.managedByOrganization, + managedByOrganization: user.managedByOrganization, }, });