1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +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:
Thomas Rittson 2024-03-12 13:32:21 +10:00 committed by GitHub
parent 2181a6d91a
commit 7ced8d5cc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 136 additions and 122 deletions

View File

@ -16,7 +16,10 @@
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </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"> <bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode"> <ng-container *ngIf="!editMode">
<p>{{ "inviteUserDesc" | i18n }}</p> <p>{{ "inviteUserDesc" | i18n }}</p>
@ -60,7 +63,10 @@
</div> </div>
</label> </label>
</div> </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 <input
type="radio" type="radio"
id="userTypeManager" id="userTypeManager"
@ -116,11 +122,11 @@
formControlName="type" formControlName="type"
name="type" name="type"
class="tw-relative tw-bottom-[-1px] tw-mr-2" 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"> <label class="tw-m-0" for="userTypeCustom">
{{ "custom" | i18n }} {{ "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"> <div class="text-base tw-block tw-font-normal tw-text-muted">
{{ "customDescNonEnterpriseStart" | i18n {{ "customDescNonEnterpriseStart" | i18n
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{ }}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
@ -138,7 +144,7 @@
</div> </div>
</fieldset> </fieldset>
<ng-container *ngIf="customUserTypeSelected"> <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"> <h3 class="mt-4 d-flex tw-font-semibold">
{{ "permissions" | i18n }} {{ "permissions" | i18n }}
</h3> </h3>
@ -365,7 +371,7 @@
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
<ng-container *ngIf="canUseSecretsManager"> <ng-container *ngIf="organization.useSecretsManager">
<h3 class="mt-4"> <h3 class="mt-4">
{{ "secretsManager" | i18n }} {{ "secretsManager" | i18n }}
<a <a
@ -401,14 +407,14 @@
[columnHeader]="'groups' | i18n" [columnHeader]="'groups' | i18n"
[selectorLabelText]="'selectGroups' | i18n" [selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n" [emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled" [flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
<bit-tab [label]="'collections' | i18n"> <bit-tab [label]="'collections' | i18n">
<div *ngIf="organization.useGroups" class="tw-mb-6"> <div *ngIf="organization.useGroups" class="tw-mb-6">
{{ "userPermissionOverrideHelper" | i18n }} {{ "userPermissionOverrideHelper" | i18n }}
</div> </div>
<div *ngIf="!flexibleCollectionsEnabled" class="tw-mb-6"> <div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
<bit-form-control> <bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessAllCollections" /> <input type="checkbox" bitCheckbox formControlName="accessAllCollections" />
<bit-label> <bit-label>
@ -434,7 +440,7 @@
[columnHeader]="'collection' | i18n" [columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n" [selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled" [flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector ></bit-access-selector
></bit-tab> ></bit-tab>
</bit-tab-group> </bit-tab-group>

View File

@ -1,7 +1,16 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; 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 { 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; 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 { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { flagEnabled } from "../../../../../../utils/flags";
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
import { import {
CollectionAccessSelectionView, CollectionAccessSelectionView,
@ -66,7 +74,7 @@ export enum MemberDialogResult {
@Component({ @Component({
templateUrl: "member-dialog.component.html", templateUrl: "member-dialog.component.html",
}) })
export class MemberDialogComponent implements OnInit, OnDestroy { export class MemberDialogComponent implements OnDestroy {
loading = true; loading = true;
editMode = false; editMode = false;
isRevoked = false; isRevoked = false;
@ -74,12 +82,10 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
access: "all" | "selected" = "selected"; access: "all" | "selected" = "selected";
collections: CollectionView[] = []; collections: CollectionView[] = [];
organizationUserType = OrganizationUserType; organizationUserType = OrganizationUserType;
canUseCustomPermissions: boolean;
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
canUseSecretsManager: boolean;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
protected organization: Organization; protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
protected groupAccessItems: AccessItemView[] = []; protected groupAccessItems: AccessItemView[] = [];
protected tabIndex: MemberDialogTab; protected tabIndex: MemberDialogTab;
@ -130,7 +136,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
private dialogRef: DialogRef<MemberDialogResult>, private dialogRef: DialogRef<MemberDialogResult>,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
// TODO: We should really look into consolidating naming conventions for these services // TODO: We should really look into consolidating naming conventions for these services
private collectionAdminService: CollectionAdminService, private collectionAdminService: CollectionAdminService,
@ -139,28 +144,26 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
private organizationUserService: OrganizationUserService, private organizationUserService: OrganizationUserService,
private dialogService: DialogService, private dialogService: DialogService,
private configService: ConfigServiceAbstraction, 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.editMode = this.params.organizationUserId != null;
this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role;
this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember");
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe( const groups$ = this.organization$.pipe(
shareReplay({ refCount: true, bufferSize: 1 }), switchMap((organization) =>
); organization.useGroups
const groups$ = organization$.pipe( ? this.groupService.getAll(this.params.organizationId)
switchMap((organization) => { : of([] as GroupView[]),
if (!organization.useGroups) { ),
return of([] as GroupView[]);
}
return this.groupService.getAll(this.params.organizationId);
}),
); );
combineLatest({ combineLatest({
organization: organization$, organization: this.organization$,
collections: this.collectionAdminService.getAll(this.params.organizationId), collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: this.params.organizationUserId userDetails: this.params.organizationUserId
? this.userService.get(this.params.organizationId, 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$)) .pipe(takeUntil(this.destroy$))
.subscribe(({ organization, collections, userDetails, groups }) => { .subscribe(({ organization, collections, userDetails, groups }) => {
this.organization = organization; this.setFormValidators(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.collectionAccessItems = [].concat( this.collectionAccessItems = [].concat(
collections.map((c) => mapCollectionToAccessItemView(c)), collections.map((c) => mapCollectionToAccessItemView(c)),
@ -196,6 +183,34 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
); );
if (this.params.organizationUserId) { if (this.params.organizationUserId) {
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) { if (!userDetails) {
throw new Error("Could not find user to edit."); throw new Error("Could not find user to edit.");
} }
@ -263,10 +278,6 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
}); });
} }
this.loading = false;
});
}
check(c: CollectionView, select?: boolean) { check(c: CollectionView, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select; (c as any).checked = select == null ? !(c as any).checked : select;
if (!(c as any).checked) { if (!(c as any).checked) {
@ -335,7 +346,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return; return;
} }
if (!this.canUseCustomPermissions && this.customUserTypeSelected) { const organization = await firstValueFrom(this.organization$);
if (!organization.useCustomPermissions && this.customUserTypeSelected) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
null, null,
@ -363,8 +376,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
await this.userService.save(userView); await this.userService.save(userView);
} else { } else {
userView.id = this.params.organizationUserId; userView.id = this.params.organizationUserId;
const maxEmailsCount = const maxEmailsCount = organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
this.organization.planProductType === ProductType.TeamsStarter ? 10 : 20;
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))]; const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
if (emails.length > maxEmailsCount) { if (emails.length > maxEmailsCount) {
this.formGroup.controls.emails.setErrors({ this.formGroup.controls.emails.setErrors({
@ -373,8 +385,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return; return;
} }
if ( if (
this.organization.hasReseller && organization.hasReseller &&
this.params.numConfirmedMembers + emails.length > this.organization.seats this.params.numConfirmedMembers + emails.length > organization.seats
) { ) {
this.formGroup.controls.emails.setErrors({ this.formGroup.controls.emails.setErrors({
tooManyEmails: { message: this.i18nService.t("seatLimitReachedContactYourProvider") }, 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; protected readonly ProductType = ProductType;
} }