[AC-561] Refactor delete organization component (#5007)

* [AC-561] Rename DeleteOrganizationComponent to DeleteOrganizationDialogComponent

* [AC-561] Refactor delete organization dialog to use dialog service

- Use new bit-dialog
- Use reactive form and bitSubmit directives
- Add injected dialog params
- Switch to observable pattern
- Use dialog result instead of success event emitter
- Add helper method to open dialog using dialog service
- Update usage in families-for-enterprise-setup.component.ts and account.component.ts

* [AC-561] Create a UserVerification module

Move the user verification components into their own module that can be imported in multiple modules without conflict and allow tree shaking.

* [AC-561] Move delete-organization-dialog into its own folder

* [AC-561] Create delete organization dialog module

* [AC-561] Cleanup delete org dialog import statements

* [AC-561] Remove unused property

* [AC-561] Use organization observable from organizationService

* [AC-561] Use organization object instead of pull out storing the name individually

* [AC-561] Make the delete organization dialog a standalone component

- Remove the delete organization dialog module
- Move the dialog component up a directory
- Remove references to the deleted module

* [AC-561] Fix DialogServiceAbstraction references after merge

* [AC-561] Cleanup dialog loading spinner and cancel button

* [AC-561] Fix broken barrel file after merge
This commit is contained in:
Shane Melton 2023-05-25 07:49:30 -07:00 committed by GitHub
parent 1a9a328d39
commit 86471790ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 179 additions and 142 deletions

View File

@ -91,7 +91,6 @@
</button>
</div>
</div>
<ng-template #deleteOrganizationTemplate></ng-template>
<ng-template #purgeOrganizationTemplate></ng-template>
<ng-template #apiKeyTemplate></ng-template>
<ng-template #rotateApiKeyTemplate></ng-template>

View File

@ -1,6 +1,8 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { lastValueFrom } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@ -15,7 +17,7 @@ import { OrganizationResponse } from "@bitwarden/common/admin-console/models/res
import { ApiKeyComponent } from "../../../settings/api-key.component";
import { PurgeVaultComponent } from "../../../settings/purge-vault.component";
import { DeleteOrganizationComponent } from "./delete-organization.component";
import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./components";
@Component({
selector: "app-org-account",
@ -23,8 +25,6 @@ import { DeleteOrganizationComponent } from "./delete-organization.component";
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccountComponent {
@ViewChild("deleteOrganizationTemplate", { read: ViewContainerRef, static: true })
deleteModalRef: ViewContainerRef;
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
purgeModalRef: ViewContainerRef;
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
@ -51,7 +51,8 @@ export class AccountComponent {
private logService: LogService,
private router: Router,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogServiceAbstraction
) {}
async ngOnInit() {
@ -100,17 +101,18 @@ export class AccountComponent {
}
async deleteOrganization() {
await this.modalService.openViewRef(
DeleteOrganizationComponent,
this.deleteModalRef,
(comp) => {
comp.organizationId = this.organizationId;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSuccess.subscribe(() => {
this.router.navigate(["/"]);
});
}
);
const dialog = openDeleteOrganizationDialog(this.dialogService, {
data: {
organizationId: this.organizationId,
requestType: "RegularDelete",
},
});
const result = await lastValueFrom(dialog.closed);
if (result === DeleteOrganizationDialogResult.Deleted) {
this.router.navigate(["/"]);
}
}
async purgeVault() {

View File

@ -0,0 +1,40 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [loading]="!loaded">
<span bitDialogTitle>{{ "deleteOrganization" | i18n }}</span>
<div bitDialogContent>
<app-callout type="warning">{{
"deletingOrganizationIsPermanentWarning" | i18n : organization?.name
}}</app-callout>
<p id="organizationDeleteDescription">
<ng-container
*ngIf="
deleteOrganizationRequestType === 'InvalidFamiliesForEnterprise';
else regularDelete
"
>
{{ "orgCreatedSponsorshipInvalid" | i18n }}
</ng-container>
<ng-template #regularDelete>
<ng-container *ngIf="organizationContentSummary.totalItemCount > 0">
{{ "deletingOrganizationContentWarning" | i18n : organization?.name }}
<ul>
<li *ngFor="let type of organizationContentSummary.itemCountByType">
{{ type.count }} {{ type.localizationKey | i18n }}
</li>
</ul>
{{ "deletingOrganizationActiveUserAccountsWarning" | i18n }}
</ng-container>
</ng-template>
</p>
<app-user-verification formControlName="secret"> </app-user-verification>
</div>
<div bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="danger" [disabled]="!loaded">
{{ "deleteOrganization" | i18n }}
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</div>
</bit-dialog>
</form>

View File

@ -1,17 +1,25 @@
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { combineLatest, Subject, takeUntil } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Utils } from "@bitwarden/common/misc/utils";
import { Verification } from "@bitwarden/common/types/verification";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { UserVerificationModule } from "../../../../shared/components/user-verification";
import { SharedModule } from "../../../../shared/shared.module";
class CountBasedLocalizationKey {
singular: string;
plural: string;
@ -43,63 +51,90 @@ class OrganizationContentSummary {
itemCountByType: OrganizationContentSummaryItem[] = [];
}
export interface DeleteOrganizationDialogParams {
organizationId: string;
requestType: "InvalidFamiliesForEnterprise" | "RegularDelete";
}
export enum DeleteOrganizationDialogResult {
Deleted = "deleted",
Canceled = "canceled",
}
@Component({
selector: "app-delete-organization",
templateUrl: "delete-organization.component.html",
standalone: true,
imports: [SharedModule, UserVerificationModule],
templateUrl: "delete-organization-dialog.component.html",
})
export class DeleteOrganizationComponent implements OnInit {
organizationId: string;
export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
loaded: boolean;
deleteOrganizationRequestType: "InvalidFamiliesForEnterprise" | "RegularDelete" = "RegularDelete";
organizationName: string;
organization: Organization;
organizationContentSummary: OrganizationContentSummary = new OrganizationContentSummary();
@Output() onSuccess: EventEmitter<void> = new EventEmitter();
secret: Verification;
masterPassword: Verification;
protected formGroup = this.formBuilder.group({
secret: new FormControl<Verification>(null, [Validators.required]),
});
formPromise: Promise<void>;
constructor(
@Inject(DIALOG_DATA) private params: DeleteOrganizationDialogParams,
private dialogRef: DialogRef<DeleteOrganizationDialogResult>,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private userVerificationService: UserVerificationService,
private logService: LogService,
private cipherService: CipherService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
private organizationApiService: OrganizationApiServiceAbstraction,
private formBuilder: FormBuilder
) {}
async ngOnInit(): Promise<void> {
await this.load();
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async submit() {
async ngOnInit(): Promise<void> {
this.deleteOrganizationRequestType = this.params.requestType;
combineLatest([
this.organizationService.get$(this.params.organizationId),
this.cipherService.getAllFromApiForOrganization(this.params.organizationId),
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, ciphers]) => {
this.organization = organization;
this.organizationContentSummary = this.buildOrganizationContentSummary(ciphers);
this.loaded = true;
});
}
protected submit = async () => {
try {
this.formPromise = this.userVerificationService
.buildRequest(this.masterPassword)
.then((request) => this.organizationApiService.delete(this.organizationId, request));
.buildRequest(this.formGroup.value.secret)
.then((request) => this.organizationApiService.delete(this.organization.id, request));
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("organizationDeleted"),
this.i18nService.t("organizationDeletedDesc")
);
this.onSuccess.emit();
this.dialogRef.close(DeleteOrganizationDialogResult.Deleted);
} catch (e) {
this.logService.error(e);
}
}
};
private async load() {
this.organizationName = (await this.organizationService.get(this.organizationId)).name;
this.organizationContentSummary = await this.buildOrganizationContentSummary();
this.loaded = true;
}
private async buildOrganizationContentSummary(): Promise<OrganizationContentSummary> {
private buildOrganizationContentSummary(ciphers: CipherView[]): OrganizationContentSummary {
const organizationContentSummary = new OrganizationContentSummary();
const organizationItems = (
await this.cipherService.getAllFromApiForOrganization(this.organizationId)
).filter((item) => item.deletedDate == null);
const organizationItems = ciphers.filter((item) => item.deletedDate == null);
if (organizationItems.length < 1) {
return organizationContentSummary;
@ -129,3 +164,18 @@ export class DeleteOrganizationComponent implements OnInit {
return new CountBasedLocalizationKey(`type${type}`, `type${type}Plural`);
}
}
/**
* Strongly typed helper to open a Delete Organization dialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export function openDeleteOrganizationDialog(
dialogService: DialogServiceAbstraction,
config: DialogConfig<DeleteOrganizationDialogParams>
) {
return dialogService.open<DeleteOrganizationDialogResult, DeleteOrganizationDialogParams>(
DeleteOrganizationDialogComponent,
config
);
}

View File

@ -0,0 +1 @@
export * from "./delete-organization-dialog.component";

View File

@ -1,61 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deleteOrganizationTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="loaded"
>
<div class="modal-header">
<h1 class="modal-title" id="deleteOrganizationTitle">{{ "deleteOrganization" | i18n }}</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<app-callout type="warning">{{
"deletingOrganizationIsPermanentWarning" | i18n : organizationName
}}</app-callout>
<p id="organizationDeleteDescription">
<ng-container
*ngIf="
deleteOrganizationRequestType === 'InvalidFamiliesForEnterprise';
else regularDelete
"
>
{{ "orgCreatedSponsorshipInvalid" | i18n }}
</ng-container>
<ng-template #regularDelete>
<ng-container *ngIf="organizationContentSummary.totalItemCount > 0">
{{ "deletingOrganizationContentWarning" | i18n : organizationName }}
<ul>
<li *ngFor="let type of organizationContentSummary.itemCountByType">
{{ type.count }} {{ type.localizationKey | i18n }}
</li>
</ul>
{{ "deletingOrganizationActiveUserAccountsWarning" | i18n }}
</ng-container>
</ng-template>
</p>
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
</app-user-verification>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "deleteOrganization" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@ -1,2 +1,2 @@
export * from "./organization-settings.module";
export { DeleteOrganizationComponent } from "./delete-organization.component";
export { DeleteOrganizationDialogComponent } from "./components/delete-organization-dialog.component";

View File

@ -4,18 +4,12 @@ import { LooseComponentsModule, SharedModule } from "../../../shared";
import { PoliciesModule } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { DeleteOrganizationComponent } from "./delete-organization.component";
import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module";
import { SettingsComponent } from "./settings.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule],
declarations: [
SettingsComponent,
AccountComponent,
DeleteOrganizationComponent,
TwoFactorSetupComponent,
],
declarations: [SettingsComponent, AccountComponent, TwoFactorSetupComponent],
})
export class OrganizationSettingsModule {}

View File

@ -50,4 +50,3 @@
</div>
</form>
</div>
<ng-template #deleteOrganizationTemplate></ng-template>

View File

@ -1,9 +1,9 @@
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Observable, Subject } from "rxjs";
import { lastValueFrom, Observable, Subject } from "rxjs";
import { first, map, takeUntil } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@ -11,12 +11,15 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/admin-console/models/request/organization/organization-sponsorship-redeem.request";
import { PlanType, PlanSponsorshipType } from "@bitwarden/common/billing/enums";
import { PlanSponsorshipType, PlanType } from "@bitwarden/common/billing/enums";
import { ProductType } from "@bitwarden/common/enums";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { OrganizationPlansComponent } from "../../../billing/settings/organization-plans.component";
import { DeleteOrganizationComponent } from "../../organizations/settings";
import {
DeleteOrganizationDialogResult,
openDeleteOrganizationDialog,
} from "../settings/components";
@Component({
selector: "families-for-enterprise-setup",
@ -36,9 +39,6 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this));
}
@ViewChild("deleteOrganizationTemplate", { read: ViewContainerRef, static: true })
deleteModalRef: ViewContainerRef;
loading = true;
badToken = false;
formPromise: Promise<any>;
@ -62,7 +62,7 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
private syncService: SyncService,
private validationService: ValidationService,
private organizationService: OrganizationService,
private modalService: ModalService
private dialogService: DialogServiceAbstraction
) {}
async ngOnInit() {
@ -136,18 +136,18 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
this.router.navigate(["/"]);
} catch (e) {
if (this.showNewOrganization) {
await this.modalService.openViewRef(
DeleteOrganizationComponent,
this.deleteModalRef,
(comp) => {
comp.organizationId = organizationId;
comp.deleteOrganizationRequestType = "InvalidFamiliesForEnterprise";
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
comp.onSuccess.subscribe(() => {
this.router.navigate(["/"]);
});
}
);
const dialog = openDeleteOrganizationDialog(this.dialogService, {
data: {
organizationId: organizationId,
requestType: "InvalidFamiliesForEnterprise",
},
});
const result = await lastValueFrom(dialog.closed);
if (result === DeleteOrganizationDialogResult.Deleted) {
this.router.navigate(["/"]);
}
}
this.validationService.showError(this.i18nService.t("sponsorshipTokenHasExpired"));
}

View File

@ -9,7 +9,6 @@ import { SubscriptionRoutingModule } from "../app/billing/settings/subscription-
import { flagEnabled, Flags } from "../utils/flags";
import { TrialInitiationComponent } from "./accounts/trial-initiation/trial-initiation.component";
import { OrganizationModule } from "./admin-console/organizations/organization.module";
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
@ -249,7 +248,8 @@ const routes: Routes = [
},
{
path: "organizations",
loadChildren: () => OrganizationModule,
loadChildren: () =>
import("./admin-console/organizations/organization.module").then((m) => m.OrganizationModule),
},
];

View File

@ -0,0 +1,3 @@
export * from "./user-verification.module";
export * from "./user-verification-prompt.component";
export * from "./user-verification.component";

View File

@ -0,0 +1,13 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared.module";
import { UserVerificationPromptComponent } from "./user-verification-prompt.component";
import { UserVerificationComponent } from "./user-verification.component";
@NgModule({
imports: [SharedModule],
declarations: [UserVerificationComponent, UserVerificationPromptComponent],
exports: [UserVerificationComponent, UserVerificationPromptComponent],
})
export class UserVerificationModule {}

View File

@ -64,8 +64,6 @@ import { TaxInfoComponent } from "../billing/settings/tax-info.component";
import { UserSubscriptionComponent } from "../billing/settings/user-subscription.component";
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
import { UserVerificationPromptComponent } from "../components/user-verification-prompt.component";
import { UserVerificationComponent } from "../components/user-verification.component";
import { FooterComponent } from "../layouts/footer.component";
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
import { NavbarComponent } from "../layouts/navbar.component";
@ -110,6 +108,7 @@ import { AddEditComponent as OrgAddEditComponent } from "../vault/org-vault/add-
import { AttachmentsComponent as OrgAttachmentsComponent } from "../vault/org-vault/attachments.component";
import { CollectionsComponent as OrgCollectionsComponent } from "../vault/org-vault/collections.component";
import { UserVerificationModule } from "./components/user-verification";
import { SharedModule } from "./shared.module";
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
@ -120,6 +119,7 @@ import { SharedModule } from "./shared.module";
OrganizationCreateModule,
RegisterFormModule,
ProductSwitcherModule,
UserVerificationModule,
ChangeKdfModule,
DynamicAvatarComponent,
],
@ -178,7 +178,6 @@ import { SharedModule } from "./shared.module";
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordRepromptComponent,
UserVerificationPromptComponent,
PaymentComponent,
PaymentMethodComponent,
PreferencesComponent,
@ -224,7 +223,6 @@ import { SharedModule } from "./shared.module";
BillingHistoryViewComponent,
UserLayoutComponent,
UserSubscriptionComponent,
UserVerificationComponent,
VaultTimeoutInputComponent,
VerifyEmailComponent,
VerifyEmailTokenComponent,
@ -330,7 +328,6 @@ import { SharedModule } from "./shared.module";
BillingHistoryViewComponent,
UserLayoutComponent,
UserSubscriptionComponent,
UserVerificationComponent,
VaultTimeoutInputComponent,
VerifyEmailComponent,
VerifyEmailTokenComponent,

View File

@ -15,7 +15,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { EncryptedExportType } from "@bitwarden/common/enums";
import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export";
import { UserVerificationPromptComponent } from "../../components/user-verification-prompt.component";
import { UserVerificationPromptComponent } from "../../shared/components/user-verification";
@Component({
selector: "app-export",

View File

@ -6,7 +6,7 @@ import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/components/user-verification-prompt.component";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/shared/components/user-verification";
import { AccessTokenView } from "../models/view/access-token.view";

View File

@ -9,7 +9,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/components/user-verification-prompt.component";
import { UserVerificationPromptComponent } from "@bitwarden/web-vault/app/shared/components/user-verification";
import { SecretsManagerPortingApiService } from "../services/sm-porting-api.service";
import { SecretsManagerPortingService } from "../services/sm-porting.service";