mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[AC-2243] Refactor member modal loading logic (#8087)
* Move loadOrganizationUser into own method * Simplify logic and use observables more * Move init logic into ctor instead of ngOnInit
This commit is contained in:
parent
2181a6d91a
commit
7ced8d5cc9
@ -16,7 +16,10 @@
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
|
||||
<bit-tab-group
|
||||
*ngIf="!loading && organization$ | async as organization"
|
||||
[(selectedIndex)]="tabIndex"
|
||||
>
|
||||
<bit-tab [label]="'role' | i18n">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{ "inviteUserDesc" | i18n }}</p>
|
||||
@ -60,7 +63,10 @@
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div *ngIf="!flexibleCollectionsEnabled" class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<div
|
||||
*ngIf="!organization.flexibleCollections"
|
||||
class="tw-mb-2 tw-flex tw-items-baseline"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeManager"
|
||||
@ -116,11 +122,11 @@
|
||||
formControlName="type"
|
||||
name="type"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
[attr.disabled]="!canUseCustomPermissions || null"
|
||||
[attr.disabled]="!organization.useCustomPermissions || null"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeCustom">
|
||||
{{ "custom" | i18n }}
|
||||
<ng-container *ngIf="!canUseCustomPermissions; else enterprise">
|
||||
<ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "customDescNonEnterpriseStart" | i18n
|
||||
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
|
||||
@ -138,7 +144,7 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<ng-container *ngIf="customUserTypeSelected">
|
||||
<ng-container *ngIf="!flexibleCollectionsEnabled; else customPermissionsFC">
|
||||
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
||||
<h3 class="mt-4 d-flex tw-font-semibold">
|
||||
{{ "permissions" | i18n }}
|
||||
</h3>
|
||||
@ -365,7 +371,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="canUseSecretsManager">
|
||||
<ng-container *ngIf="organization.useSecretsManager">
|
||||
<h3 class="mt-4">
|
||||
{{ "secretsManager" | i18n }}
|
||||
<a
|
||||
@ -401,14 +407,14 @@
|
||||
[columnHeader]="'groups' | i18n"
|
||||
[selectorLabelText]="'selectGroups' | i18n"
|
||||
[emptySelectionText]="'noGroupsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
></bit-access-selector>
|
||||
</bit-tab>
|
||||
<bit-tab [label]="'collections' | i18n">
|
||||
<div *ngIf="organization.useGroups" class="tw-mb-6">
|
||||
{{ "userPermissionOverrideHelper" | i18n }}
|
||||
</div>
|
||||
<div *ngIf="!flexibleCollectionsEnabled" class="tw-mb-6">
|
||||
<div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessAllCollections" />
|
||||
<bit-label>
|
||||
@ -434,7 +440,7 @@
|
||||
[columnHeader]="'collection' | i18n"
|
||||
[selectorLabelText]="'selectCollections' | i18n"
|
||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled"
|
||||
[flexibleCollectionsEnabled]="organization.flexibleCollections"
|
||||
></bit-access-selector
|
||||
></bit-tab>
|
||||
</bit-tab-group>
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import {
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
@ -18,7 +27,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { flagEnabled } from "../../../../../../utils/flags";
|
||||
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
|
||||
import {
|
||||
CollectionAccessSelectionView,
|
||||
@ -66,7 +74,7 @@ export enum MemberDialogResult {
|
||||
@Component({
|
||||
templateUrl: "member-dialog.component.html",
|
||||
})
|
||||
export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
export class MemberDialogComponent implements OnDestroy {
|
||||
loading = true;
|
||||
editMode = false;
|
||||
isRevoked = false;
|
||||
@ -74,12 +82,10 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
access: "all" | "selected" = "selected";
|
||||
collections: CollectionView[] = [];
|
||||
organizationUserType = OrganizationUserType;
|
||||
canUseCustomPermissions: boolean;
|
||||
PermissionMode = PermissionMode;
|
||||
canUseSecretsManager: boolean;
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
protected organization: Organization;
|
||||
protected organization$: Observable<Organization>;
|
||||
protected collectionAccessItems: AccessItemView[] = [];
|
||||
protected groupAccessItems: AccessItemView[] = [];
|
||||
protected tabIndex: MemberDialogTab;
|
||||
@ -130,7 +136,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
private dialogRef: DialogRef<MemberDialogResult>,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationService: OrganizationService,
|
||||
private formBuilder: FormBuilder,
|
||||
// TODO: We should really look into consolidating naming conventions for these services
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
@ -139,28 +144,26 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigServiceAbstraction,
|
||||
) {}
|
||||
organizationService: OrganizationService,
|
||||
) {
|
||||
this.organization$ = organizationService
|
||||
.get$(this.params.organizationId)
|
||||
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
async ngOnInit() {
|
||||
this.editMode = this.params.organizationUserId != null;
|
||||
this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role;
|
||||
this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember");
|
||||
|
||||
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe(
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
const groups$ = organization$.pipe(
|
||||
switchMap((organization) => {
|
||||
if (!organization.useGroups) {
|
||||
return of([] as GroupView[]);
|
||||
}
|
||||
|
||||
return this.groupService.getAll(this.params.organizationId);
|
||||
}),
|
||||
const groups$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
organization.useGroups
|
||||
? this.groupService.getAll(this.params.organizationId)
|
||||
: of([] as GroupView[]),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest({
|
||||
organization: organization$,
|
||||
organization: this.organization$,
|
||||
collections: this.collectionAdminService.getAll(this.params.organizationId),
|
||||
userDetails: this.params.organizationUserId
|
||||
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
|
||||
@ -169,23 +172,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(({ organization, collections, userDetails, groups }) => {
|
||||
this.organization = organization;
|
||||
this.canUseCustomPermissions = organization.useCustomPermissions;
|
||||
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
|
||||
|
||||
const emailsControlValidators = [
|
||||
Validators.required,
|
||||
commaSeparatedEmails,
|
||||
orgSeatLimitReachedValidator(
|
||||
this.organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionUpgrade", organization.seats),
|
||||
),
|
||||
];
|
||||
|
||||
const emailsControl = this.formGroup.get("emails");
|
||||
emailsControl.setValidators(emailsControlValidators);
|
||||
emailsControl.updateValueAndValidity();
|
||||
this.setFormValidators(organization);
|
||||
|
||||
this.collectionAccessItems = [].concat(
|
||||
collections.map((c) => mapCollectionToAccessItemView(c)),
|
||||
@ -196,77 +183,101 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (this.params.organizationUserId) {
|
||||
if (!userDetails) {
|
||||
throw new Error("Could not find user to edit.");
|
||||
}
|
||||
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
|
||||
this.showNoMasterPasswordWarning =
|
||||
userDetails.status > OrganizationUserStatusType.Invited &&
|
||||
userDetails.hasMasterPassword === false;
|
||||
const assignedCollectionsPermissions = {
|
||||
editAssignedCollections: userDetails.permissions.editAssignedCollections,
|
||||
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
|
||||
manageAssignedCollections:
|
||||
userDetails.permissions.editAssignedCollections &&
|
||||
userDetails.permissions.deleteAssignedCollections,
|
||||
};
|
||||
const allCollectionsPermissions = {
|
||||
createNewCollections: userDetails.permissions.createNewCollections,
|
||||
editAnyCollection: userDetails.permissions.editAnyCollection,
|
||||
deleteAnyCollection: userDetails.permissions.deleteAnyCollection,
|
||||
manageAllCollections:
|
||||
userDetails.permissions.createNewCollections &&
|
||||
userDetails.permissions.editAnyCollection &&
|
||||
userDetails.permissions.deleteAnyCollection,
|
||||
};
|
||||
if (userDetails.type === OrganizationUserType.Custom) {
|
||||
this.permissionsGroup.patchValue({
|
||||
accessEventLogs: userDetails.permissions.accessEventLogs,
|
||||
accessImportExport: userDetails.permissions.accessImportExport,
|
||||
accessReports: userDetails.permissions.accessReports,
|
||||
manageGroups: userDetails.permissions.manageGroups,
|
||||
manageSso: userDetails.permissions.manageSso,
|
||||
managePolicies: userDetails.permissions.managePolicies,
|
||||
manageUsers: userDetails.permissions.manageUsers,
|
||||
manageResetPassword: userDetails.permissions.manageResetPassword,
|
||||
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
|
||||
manageAllCollectionsGroup: allCollectionsPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
const collectionsFromGroups = groups
|
||||
.filter((group) => userDetails.groups.includes(group.id))
|
||||
.flatMap((group) =>
|
||||
group.collections.map((accessSelection) => {
|
||||
const collection = collections.find((c) => c.id === accessSelection.id);
|
||||
return { group, collection, accessSelection };
|
||||
}),
|
||||
);
|
||||
|
||||
this.collectionAccessItems = this.collectionAccessItems.concat(
|
||||
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
|
||||
mapCollectionToAccessItemView(collection, accessSelection, group),
|
||||
),
|
||||
);
|
||||
|
||||
const accessSelections = mapToAccessSelections(userDetails);
|
||||
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
|
||||
|
||||
this.formGroup.removeControl("emails");
|
||||
this.formGroup.patchValue({
|
||||
type: userDetails.type,
|
||||
externalId: userDetails.externalId,
|
||||
accessAllCollections: userDetails.accessAll,
|
||||
access: accessSelections,
|
||||
accessSecretsManager: userDetails.accessSecretsManager,
|
||||
groups: groupAccessSelections,
|
||||
});
|
||||
this.loadOrganizationUser(userDetails, groups, collections);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
private setFormValidators(organization: Organization) {
|
||||
const emailsControlValidators = [
|
||||
Validators.required,
|
||||
commaSeparatedEmails,
|
||||
orgSeatLimitReachedValidator(
|
||||
organization,
|
||||
this.params.allOrganizationUserEmails,
|
||||
this.i18nService.t("subscriptionUpgrade", organization.seats),
|
||||
),
|
||||
];
|
||||
|
||||
const emailsControl = this.formGroup.get("emails");
|
||||
emailsControl.setValidators(emailsControlValidators);
|
||||
emailsControl.updateValueAndValidity();
|
||||
}
|
||||
|
||||
private loadOrganizationUser(
|
||||
userDetails: OrganizationUserAdminView,
|
||||
groups: GroupView[],
|
||||
collections: CollectionView[],
|
||||
) {
|
||||
if (!userDetails) {
|
||||
throw new Error("Could not find user to edit.");
|
||||
}
|
||||
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
|
||||
this.showNoMasterPasswordWarning =
|
||||
userDetails.status > OrganizationUserStatusType.Invited &&
|
||||
userDetails.hasMasterPassword === false;
|
||||
const assignedCollectionsPermissions = {
|
||||
editAssignedCollections: userDetails.permissions.editAssignedCollections,
|
||||
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
|
||||
manageAssignedCollections:
|
||||
userDetails.permissions.editAssignedCollections &&
|
||||
userDetails.permissions.deleteAssignedCollections,
|
||||
};
|
||||
const allCollectionsPermissions = {
|
||||
createNewCollections: userDetails.permissions.createNewCollections,
|
||||
editAnyCollection: userDetails.permissions.editAnyCollection,
|
||||
deleteAnyCollection: userDetails.permissions.deleteAnyCollection,
|
||||
manageAllCollections:
|
||||
userDetails.permissions.createNewCollections &&
|
||||
userDetails.permissions.editAnyCollection &&
|
||||
userDetails.permissions.deleteAnyCollection,
|
||||
};
|
||||
if (userDetails.type === OrganizationUserType.Custom) {
|
||||
this.permissionsGroup.patchValue({
|
||||
accessEventLogs: userDetails.permissions.accessEventLogs,
|
||||
accessImportExport: userDetails.permissions.accessImportExport,
|
||||
accessReports: userDetails.permissions.accessReports,
|
||||
manageGroups: userDetails.permissions.manageGroups,
|
||||
manageSso: userDetails.permissions.manageSso,
|
||||
managePolicies: userDetails.permissions.managePolicies,
|
||||
manageUsers: userDetails.permissions.manageUsers,
|
||||
manageResetPassword: userDetails.permissions.manageResetPassword,
|
||||
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
|
||||
manageAllCollectionsGroup: allCollectionsPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
const collectionsFromGroups = groups
|
||||
.filter((group) => userDetails.groups.includes(group.id))
|
||||
.flatMap((group) =>
|
||||
group.collections.map((accessSelection) => {
|
||||
const collection = collections.find((c) => c.id === accessSelection.id);
|
||||
return { group, collection, accessSelection };
|
||||
}),
|
||||
);
|
||||
|
||||
this.collectionAccessItems = this.collectionAccessItems.concat(
|
||||
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
|
||||
mapCollectionToAccessItemView(collection, accessSelection, group),
|
||||
),
|
||||
);
|
||||
|
||||
const accessSelections = mapToAccessSelections(userDetails);
|
||||
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
|
||||
|
||||
this.formGroup.removeControl("emails");
|
||||
this.formGroup.patchValue({
|
||||
type: userDetails.type,
|
||||
externalId: userDetails.externalId,
|
||||
accessAllCollections: userDetails.accessAll,
|
||||
access: accessSelections,
|
||||
accessSecretsManager: userDetails.accessSecretsManager,
|
||||
groups: groupAccessSelections,
|
||||
});
|
||||
}
|
||||
|
||||
check(c: CollectionView, select?: boolean) {
|
||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
||||
if (!(c as any).checked) {
|
||||
@ -335,7 +346,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canUseCustomPermissions && this.customUserTypeSelected) {
|
||||
const organization = await firstValueFrom(this.organization$);
|
||||
|
||||
if (!organization.useCustomPermissions && this.customUserTypeSelected) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
@ -363,8 +376,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
await this.userService.save(userView);
|
||||
} else {
|
||||
userView.id = this.params.organizationUserId;
|
||||
const maxEmailsCount =
|
||||
this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
|
||||
const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
|
||||
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
|
||||
if (emails.length > maxEmailsCount) {
|
||||
this.formGroup.controls.emails.setErrors({
|
||||
@ -373,8 +385,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.organization.hasReseller &&
|
||||
this.params.numConfirmedMembers + emails.length > this.organization.seats
|
||||
organization.hasReseller &&
|
||||
this.params.numConfirmedMembers + emails.length > organization.seats
|
||||
) {
|
||||
this.formGroup.controls.emails.setErrors({
|
||||
tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") },
|
||||
@ -515,10 +527,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
protected get flexibleCollectionsEnabled() {
|
||||
return this.organization?.flexibleCollections;
|
||||
}
|
||||
|
||||
protected readonly ProductType = ProductType;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user