mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[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 <djsmith85@users.noreply.github.com>
This commit is contained in:
parent
bc170f5207
commit
c749447894
@ -0,0 +1,100 @@
|
|||||||
|
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
|
||||||
|
{{ "personalVaultExportPolicyInEffect" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
<tools-export-scope-callout
|
||||||
|
[organizationId]="organizationId"
|
||||||
|
*ngIf="!disabledByPolicy"
|
||||||
|
></tools-export-scope-callout>
|
||||||
|
|
||||||
|
<form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm">
|
||||||
|
<ng-container *ngIf="organizations$ | async as organizations">
|
||||||
|
<bit-form-field *ngIf="organizations.length > 0">
|
||||||
|
<bit-label>{{ "exportFrom" | i18n }}</bit-label>
|
||||||
|
<bit-select formControlName="vaultSelector">
|
||||||
|
<bit-option [label]="'myVault' | i18n" value="myVault" icon="bwi-user" />
|
||||||
|
<bit-option
|
||||||
|
*ngFor="let o of organizations$ | async"
|
||||||
|
[value]="o.id"
|
||||||
|
[label]="o.name"
|
||||||
|
icon="bwi-business"
|
||||||
|
/>
|
||||||
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
|
||||||
|
<bit-select formControlName="format">
|
||||||
|
<bit-option *ngFor="let f of formatOptions" [value]="f.value" [label]="f.name" />
|
||||||
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<ng-container *ngIf="format === 'encrypted_json'">
|
||||||
|
<bit-radio-group formControlName="fileEncryptionType" aria-label="exportTypeHeading">
|
||||||
|
<bit-label>{{ "exportTypeHeading" | i18n }}</bit-label>
|
||||||
|
|
||||||
|
<bit-radio-button
|
||||||
|
id="AccountEncrypted"
|
||||||
|
name="fileEncryptionType"
|
||||||
|
class="tw-block"
|
||||||
|
[value]="encryptedExportType.AccountEncrypted"
|
||||||
|
checked="fileEncryptionType === encryptedExportType.AccountEncrypted"
|
||||||
|
>
|
||||||
|
<bit-label>{{ "accountRestricted" | i18n }}</bit-label>
|
||||||
|
<bit-hint>{{ "accountRestrictedOptionDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
|
||||||
|
<bit-radio-button
|
||||||
|
id="FileEncrypted"
|
||||||
|
name="fileEncryptionType"
|
||||||
|
class="tw-block"
|
||||||
|
[value]="encryptedExportType.FileEncrypted"
|
||||||
|
checked="fileEncryptionType === encryptedExportType.FileEncrypted"
|
||||||
|
>
|
||||||
|
<bit-label>{{ "passwordProtected" | i18n }}</bit-label>
|
||||||
|
<bit-hint>{{ "passwordProtectedOptionDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
</bit-radio-group>
|
||||||
|
|
||||||
|
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "filePassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
id="filePassword"
|
||||||
|
formControlName="filePassword"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitIconButton
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showFilePassword"
|
||||||
|
></button>
|
||||||
|
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<app-password-strength [password]="filePassword" [showText]="true"> </app-password-strength>
|
||||||
|
</div>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
id="confirmFilePassword"
|
||||||
|
formControlName="confirmFilePassword"
|
||||||
|
name="confirmFilePassword"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitIconButton
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showFilePassword"
|
||||||
|
></button>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</form>
|
@ -1,7 +1,9 @@
|
|||||||
import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
|
import { CommonModule } from "@angular/common";
|
||||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
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 { 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 { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum";
|
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";
|
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 {
|
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<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string>();
|
||||||
|
|
||||||
@Output() onSaved = new EventEmitter();
|
@Output() onSaved = new EventEmitter();
|
||||||
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
||||||
|
|
||||||
@ -74,6 +135,11 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
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
|
this.policyService
|
||||||
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
|
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
@ -88,8 +154,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
this.exportForm.get("format").valueChanges,
|
this.exportForm.get("format").valueChanges,
|
||||||
this.exportForm.get("fileEncryptionType").valueChanges,
|
this.exportForm.get("fileEncryptionType").valueChanges,
|
||||||
)
|
)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(startWith(0), takeUntil(this.destroy$))
|
||||||
.pipe(startWith(0))
|
|
||||||
.subscribe(() => this.adjustValidators());
|
.subscribe(() => this.adjustValidators());
|
||||||
|
|
||||||
if (this.organizationId) {
|
if (this.organizationId) {
|
||||||
@ -118,6 +183,12 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
this.exportForm.controls.vaultSelector.setValue("myVault");
|
this.exportForm.controls.vaultSelector.setValue("myVault");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
|
||||||
|
this.formLoading.emit(loading);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
}
|
}
|
||||||
@ -187,6 +258,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected saved() {
|
protected saved() {
|
||||||
this.onSaved.emit();
|
this.onSaved.emit();
|
||||||
|
this.onSuccessfulExport.emit(this.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyUser(): Promise<boolean> {
|
private async verifyUser(): Promise<boolean> {
|
||||||
@ -235,6 +307,10 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getFileName(prefix?: string) {
|
protected getFileName(prefix?: string) {
|
||||||
|
if (this.organizationId) {
|
||||||
|
prefix = "org";
|
||||||
|
}
|
||||||
|
|
||||||
let extension = this.format;
|
let extension = this.format;
|
||||||
if (this.format === "encrypted_json") {
|
if (this.format === "encrypted_json") {
|
||||||
if (prefix == null) {
|
if (prefix == null) {
|
||||||
@ -248,7 +324,15 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async collectEvent(): Promise<void> {
|
protected async collectEvent(): Promise<void> {
|
||||||
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() {
|
get format() {
|
||||||
|
Loading…
Reference in New Issue
Block a user