1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

[PM-13755] Decouple Invite and Edit User Flows

This commit is contained in:
Jimmy Vo
2024-12-06 15:51:54 -05:00
parent 8d68a2dd58
commit 5b3c1fcd01
2 changed files with 136 additions and 86 deletions

View File

@@ -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<OrganizationUserAdminView> {
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 () => {

View File

@@ -462,9 +462,51 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
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<OrganizationUserView>
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,
},
});