[PM-1925][PM-2741][AC-1334] flexible collections export page (#5759)

* Use bitTypography for page title

* Replaced app-callout with bit-callout

* Replace button with bit-button

* Update radio buttons to use CL

* Use searchable select for fileFormat dropdown

* Remove unneeded divs (old styling)

* pm-1826 remove eslint-disable tailwindcss/no-custom-classname

* Removed for-attribute from bit-labels

* Removed bitInput from bit-selects

* Removed name-attribute from bit-selects

* Make format a required field

* Removed unused dependency on cryptoService

* Remove unused dependency on BroadcasterService

* Removed dependency on window

* Moved organizationId into BaseExportComponent

* Add vaultSelector

Add organizationService as new dependency
Retrieve organizations a user has access to
Add vaultSelector dropdown
Add `export from` label
Add exportFromHint

* Removed hint as discussed by product&design

* Add function to check for import/export permission

* Export callout should listen to changes

Even though the organizationId was changed, the Input did not trigger changing the scope

* Reading FlexibleCollections feature flag to show the vault-selector on export (#7196)

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
This commit is contained in:
Daniel James Smith 2023-12-14 13:55:54 +01:00 committed by GitHub
parent 12de4b1386
commit 60d9f3d150
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 180 additions and 158 deletions

View File

@ -4,9 +4,9 @@ import { Router } from "@angular/router";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -20,7 +20,6 @@ import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export"
})
export class ExportComponent extends BaseExportComponent {
constructor(
cryptoService: CryptoService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
exportService: VaultExportServiceAbstraction,
@ -32,20 +31,20 @@ export class ExportComponent extends BaseExportComponent {
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
) {
super(
cryptoService,
i18nService,
platformUtilsService,
exportService,
eventCollectionService,
policyService,
window,
logService,
userVerificationService,
formBuilder,
fileDownloadService,
dialogService,
organizationService,
);
}

View File

@ -3,10 +3,9 @@ import { UntypedFormBuilder } from "@angular/forms";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -14,15 +13,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { DialogService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export";
const BroadcasterSubscriptionId = "ExportComponent";
@Component({
selector: "app-export",
templateUrl: "export.component.html",
})
export class ExportComponent extends BaseExportComponent implements OnInit {
constructor(
cryptoService: CryptoService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
exportService: VaultExportServiceAbstraction,
@ -30,28 +26,23 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
policyService: PolicyService,
userVerificationService: UserVerificationService,
formBuilder: UntypedFormBuilder,
private broadcasterService: BroadcasterService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
) {
super(
cryptoService,
i18nService,
platformUtilsService,
exportService,
eventCollectionService,
policyService,
window,
logService,
userVerificationService,
formBuilder,
fileDownloadService,
dialogService,
organizationService,
);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
}

View File

@ -3,10 +3,11 @@ import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -23,7 +24,6 @@ import { ExportComponent } from "../../../../tools/vault-export/export.component
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class OrganizationVaultExportComponent extends ExportComponent {
constructor(
cryptoService: CryptoService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
exportService: VaultExportServiceAbstraction,
@ -35,9 +35,10 @@ export class OrganizationVaultExportComponent extends ExportComponent {
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
configService: ConfigServiceAbstraction,
) {
super(
cryptoService,
i18nService,
platformUtilsService,
exportService,
@ -48,6 +49,8 @@ export class OrganizationVaultExportComponent extends ExportComponent {
formBuilder,
fileDownloadService,
dialogService,
organizationService,
configService,
);
}

View File

@ -1,5 +1,3 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<form
#form
(ngSubmit)="submit()"
@ -7,137 +5,112 @@
[formGroup]="exportForm"
*ngIf="exportForm"
>
<div class="page-header">
<h1>{{ "exportVault" | i18n }}</h1>
</div>
<h1 bitTypography="h1">{{ "exportVault" | i18n }}</h1>
<app-callout type="error" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</app-callout>
</bit-callout>
<app-export-scope-callout
[organizationId]="organizationId"
*ngIf="!disabledByPolicy"
></app-export-scope-callout>
<div class="row">
<div class="col-6">
<bit-form-field>
<bit-label class="tw-text-lg" for="format">{{ "fileFormat" | i18n }}</bit-label>
<select bitInput name="format" formControlName="format">
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
</select>
</bit-form-field>
</div>
</div>
<div class="row">
<div class="form-group col-6">
<ng-container *ngIf="format === 'encrypted_json'">
<div role="radiogroup" aria-labelledby="exportTypeHeading">
<label id="exportTypeHeading" class="tw-semi-bold tw-text-lg">
{{ "exportTypeHeading" | i18n }}
</label>
<bit-form-field *ngIf="flexibleCollectionsEnabled$ | async">
<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>
<div appBoxRow name="FileTypeOptions" class="tw-flex tw-items-center">
<div>
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="AccountEncrypted"
[value]="encryptedExportType.AccountEncrypted"
formControlName="fileEncryptionType"
[checked]="fileEncryptionType === encryptedExportType.AccountEncrypted"
/>
</div>
<div>
<label class="tw-semi-bold tw-text-md tw-my-1 tw-ml-1" for="AccountEncrypted">
{{ "accountRestricted" | i18n }}
</label>
</div>
</div>
<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>
<div class="tw-regular ml-3 pb-2 tw-text-sm">
{{ "accountRestrictedOptionDescription" | i18n }}
</div>
<ng-container *ngIf="format === 'encrypted_json'">
<bit-radio-group formControlName="fileEncryptionType" aria-label="exportTypeHeading">
<bit-label>{{ "exportTypeHeading" | i18n }}</bit-label>
<div class="tw-flex tw-items-center">
<div>
<input
type="radio"
class="radio"
name="fileEncryptionType"
id="FileEncrypted"
[value]="encryptedExportType.FileEncrypted"
formControlName="fileEncryptionType"
[checked]="fileEncryptionType === encryptedExportType.FileEncrypted"
/>
</div>
<div>
<label class="tw-semi-bold tw-text-md tw-my-1 tw-ml-1" for="FileEncrypted">{{
"passwordProtected" | i18n
}}</label>
</div>
</div>
<div class="tw-regular ml-3 tw-text-sm">
{{ "passwordProtectedOptionDescription" | i18n }}
</div>
</div>
<br />
<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>
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="form.loading || disabledByPolicy"
[ngClass]="{ manual: disabledByPolicy }"
<bit-radio-button
id="AccountEncrypted"
name="fileEncryptionType"
class="tw-block"
[value]="encryptedExportType.AccountEncrypted"
checked="fileEncryptionType === encryptedExportType.AccountEncrypted"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "confirmFormat" | i18n }}</span>
</button>
</div>
</div>
<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>
<button
bitButton
type="submit"
buttonType="primary"
[loading]="form.loading"
[disabled]="disabledByPolicy"
>
{{ "confirmFormat" | i18n }}
</button>
</form>

View File

@ -4,9 +4,11 @@ import { firstValueFrom } from "rxjs";
import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -22,12 +24,15 @@ import { openUserVerificationPrompt } from "../../auth/shared/components/user-ve
templateUrl: "export.component.html",
})
export class ExportComponent extends BaseExportComponent {
organizationId: string;
encryptedExportType = EncryptedExportType;
protected showFilePassword: boolean;
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollections,
false,
);
constructor(
cryptoService: CryptoService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
exportService: VaultExportServiceAbstraction,
@ -38,20 +43,21 @@ export class ExportComponent extends BaseExportComponent {
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
protected configService: ConfigServiceAbstraction,
) {
super(
cryptoService,
i18nService,
platformUtilsService,
exportService,
eventCollectionService,
policyService,
window,
logService,
userVerificationService,
formBuilder,
fileDownloadService,
dialogService,
organizationService,
);
}

View File

@ -1074,6 +1074,9 @@
"export": {
"message": "Export"
},
"exportFrom": {
"message": "Export from"
},
"exportVault": {
"message": "Export vault"
},

View File

@ -8,8 +8,6 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
templateUrl: "export-scope-callout.component.html",
})
export class ExportScopeCalloutComponent implements OnInit {
@Input() organizationId: string = null;
show = false;
scopeConfig: {
title: string;
@ -17,6 +15,17 @@ export class ExportScopeCalloutComponent implements OnInit {
scopeIdentifier: string;
};
private _organizationId: string;
get organizationId(): string {
return this._organizationId;
}
@Input() set organizationId(value: string) {
this._organizationId = value;
this.getScopeMessage(this._organizationId);
}
constructor(
protected organizationService: OrganizationService,
protected stateService: StateService,
@ -26,18 +35,23 @@ export class ExportScopeCalloutComponent implements OnInit {
if (!this.organizationService.hasOrganizations()) {
return;
}
await this.getScopeMessage(this.organizationId);
this.show = true;
}
private async getScopeMessage(organizationId: string) {
this.scopeConfig =
this.organizationId != null
organizationId != null
? {
title: "exportingOrganizationVaultTitle",
description: "exportingOrganizationVaultDesc",
scopeIdentifier: this.organizationService.get(this.organizationId).name,
scopeIdentifier: this.organizationService.get(organizationId).name,
}
: {
title: "exportingPersonalVaultTitle",
description: "exportingIndividualVaultDescription",
scopeIdentifier: await this.stateService.getEmail(),
};
this.show = true;
}
}

View File

@ -1,17 +1,22 @@
import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { merge, startWith, Subject, takeUntil } from "rxjs";
import { concat, map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import {
OrganizationService,
canAccessImportExport,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
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 { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export";
@ -26,13 +31,22 @@ export class ExportComponent implements OnInit, OnDestroy {
filePasswordValue: string = null;
formPromise: Promise<string>;
private _disabledByPolicy = false;
protected organizationId: string = null;
organizations$: Observable<Organization[]>;
protected get disabledByPolicy(): boolean {
return this._disabledByPolicy;
}
exportForm = this.formBuilder.group({
format: ["json"],
vaultSelector: [
"myVault",
{
nonNullable: true,
validators: [Validators.required],
},
],
format: ["json", Validators.required],
secret: [""],
filePassword: ["", Validators.required],
confirmFilePassword: ["", Validators.required],
@ -48,21 +62,27 @@ export class ExportComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(
protected cryptoService: CryptoService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected exportService: VaultExportServiceAbstraction,
protected eventCollectionService: EventCollectionService,
private policyService: PolicyService,
protected win: Window,
private logService: LogService,
private userVerificationService: UserVerificationService,
private formBuilder: UntypedFormBuilder,
protected fileDownloadService: FileDownloadService,
protected dialogService: DialogService,
protected organizationService: OrganizationService,
) {}
async ngOnInit() {
this.organizations$ = concat(
this.organizationService.memberOrganizations$.pipe(
canAccessImportExport(this.i18nService),
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))),
),
);
this.policyService
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
.pipe(takeUntil(this.destroy$))
@ -73,6 +93,19 @@ export class ExportComponent implements OnInit, OnDestroy {
}
});
if (this.organizationId) {
this.exportForm.controls.vaultSelector.patchValue(this.organizationId);
this.exportForm.controls.vaultSelector.disable();
} else {
this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.organizationId = value != "myVault" ? value : undefined;
});
this.exportForm.controls.vaultSelector.setValue("myVault");
}
merge(
this.exportForm.get("format").valueChanges,
this.exportForm.get("fileEncryptionType").valueChanges,