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:
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user