From c7494478943a93f5ac139aa5e280221b3e105157 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 28 May 2024 22:32:01 +0200 Subject: [PATCH] [PM-8254] Create shareable export component (#9246) * Make export.component a standalone component Fix lint issue with takeUntil * Create shareable export.component.html Copied existing export.component.html as that has already been migrated to use the component library components Strip the markup from the dialog and the submit-button * Add outputs to inform the hosting component about certain events (submit, loading, disabled) Emit successful Export Expose a form-id so the hosting component can bind to this form Fix name of output * Ensure that the file gets prefixed with `org`when exporting from an organization * When exporting from an organization ensure Organization_ClientExportedVault is collected * Add comments to the components outputs * Better way of addressing the previously fixed lint issue * Fix disabling the form not emitting the formDisabled state * Add better comments to Outputs based on PR feedback --------- Co-authored-by: Daniel James Smith --- .../src/components/export.component.html | 100 ++++++++++++++++++ .../src/components/export.component.ts | 98 +++++++++++++++-- 2 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html new file mode 100644 index 0000000000..8abc0c7755 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.html @@ -0,0 +1,100 @@ + + {{ "personalVaultExportPolicyInEffect" | i18n }} + + + +
+ + + {{ "exportFrom" | i18n }} + + + + + + + + + {{ "fileFormat" | i18n }} + + + + + + + + {{ "exportTypeHeading" | i18n }} + + + {{ "accountRestricted" | i18n }} + {{ "accountRestrictedOptionDescription" | i18n }} + + + + {{ "passwordProtected" | i18n }} + {{ "passwordProtectedOptionDescription" | i18n }} + + + + +
+ + {{ "filePassword" | i18n }} + + + {{ "exportPasswordDescription" | i18n }} + + +
+ + {{ "confirmFilePassword" | i18n }} + + + +
+
+
diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 3e091a2417..d90e069015 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -1,7 +1,9 @@ -import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; -import { UntypedFormBuilder, Validators } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core"; +import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -16,11 +18,70 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum"; -import { DialogService } from "@bitwarden/components"; +import { + AsyncActionsModule, + BitSubmitDirective, + ButtonModule, + CalloutModule, + DialogService, + FormFieldModule, + IconButtonModule, + RadioButtonModule, + SelectModule, +} from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; -@Directive() +import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; + +@Component({ + selector: "tools-export", + templateUrl: "export.component.html", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + SelectModule, + CalloutModule, + RadioButtonModule, + ExportScopeCalloutComponent, + UserVerificationDialogComponent, + ], +}) export class ExportComponent implements OnInit, OnDestroy { + /** + * The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method. + * This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state. + */ + @ViewChild(BitSubmitDirective) + private bitSubmit: BitSubmitDirective; + + /** + * Emits true when the BitSubmitDirective({@link bitSubmit} is executing {@link submit} and false when execution has completed. + * Example: Used to show the loading state of the submit button present on the hosting component + * */ + @Output() + formLoading = new EventEmitter(); + + /** + * Emits true when this form gets disabled and false when enabled. + * Example: Used to disable the submit button, which is present on the hosting component + * */ + @Output() + formDisabled = new EventEmitter(); + + /** + * Emits when the creation and download of the export-file have succeeded + * - Emits an null/empty string when exporting from an individual vault + * - Emits the organizationId when exporting from an organizationl vault + * */ + @Output() + onSuccessfulExport = new EventEmitter(); + @Output() onSaved = new EventEmitter(); @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; @@ -74,6 +135,11 @@ export class ExportComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { + // Setup subscription to emit when this form is enabled/disabled + this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => { + this.formDisabled.emit(c === "DISABLED"); + }); + this.policyService .policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport) .pipe(takeUntil(this.destroy$)) @@ -88,8 +154,7 @@ export class ExportComponent implements OnInit, OnDestroy { this.exportForm.get("format").valueChanges, this.exportForm.get("fileEncryptionType").valueChanges, ) - .pipe(takeUntil(this.destroy$)) - .pipe(startWith(0)) + .pipe(startWith(0), takeUntil(this.destroy$)) .subscribe(() => this.adjustValidators()); if (this.organizationId) { @@ -118,6 +183,12 @@ export class ExportComponent implements OnInit, OnDestroy { this.exportForm.controls.vaultSelector.setValue("myVault"); } + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { + this.formLoading.emit(loading); + }); + } + ngOnDestroy(): void { this.destroy$.next(); } @@ -187,6 +258,7 @@ export class ExportComponent implements OnInit, OnDestroy { protected saved() { this.onSaved.emit(); + this.onSuccessfulExport.emit(this.organizationId); } private async verifyUser(): Promise { @@ -235,6 +307,10 @@ export class ExportComponent implements OnInit, OnDestroy { } protected getFileName(prefix?: string) { + if (this.organizationId) { + prefix = "org"; + } + let extension = this.format; if (this.format === "encrypted_json") { if (prefix == null) { @@ -248,7 +324,15 @@ export class ExportComponent implements OnInit, OnDestroy { } protected async collectEvent(): Promise { - await this.eventCollectionService.collect(EventType.User_ClientExportedVault); + if (this.organizationId) { + return await this.eventCollectionService.collect( + EventType.Organization_ClientExportedVault, + null, + false, + this.organizationId, + ); + } + return await this.eventCollectionService.collect(EventType.User_ClientExportedVault); } get format() {