1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Preserve export type across export source selections (#16922)

This commit is contained in:
John Harrington
2025-10-29 12:49:31 -07:00
committed by GitHub
parent 66052b6dd3
commit e333c0a8bc
4 changed files with 85 additions and 18 deletions

View File

@@ -1,3 +1,5 @@
import { Observable } from "rxjs";
import { UserId, OrganizationId } from "@bitwarden/common/types/guid";
import { ExportedVault } from "../types";
@@ -5,6 +7,24 @@ import { ExportedVault } from "../types";
export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const;
export type ExportFormat = (typeof EXPORT_FORMATS)[number];
/**
* Options that determine which export formats are available
*/
export type FormatOptions = {
/** Whether the export is for the user's personal vault */
isMyVault: boolean;
};
/**
* Metadata describing an available export format
*/
export type ExportFormatMetadata = {
/** Display name for the format (e.g., ".json", ".csv") */
name: string;
/** The export format identifier */
format: ExportFormat;
};
export abstract class VaultExportServiceAbstraction {
abstract getExport: (
userId: UserId,
@@ -18,4 +38,11 @@ export abstract class VaultExportServiceAbstraction {
password: string,
onlyManagedCollections?: boolean,
) => Promise<ExportedVault>;
/**
* Get available export formats based on vault context
* @param options Options determining which formats are available
* @returns Observable stream of available export formats
*/
abstract formats$(options: FormatOptions): Observable<ExportFormatMetadata[]>;
}

View File

@@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, Observable, of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
@@ -9,7 +9,12 @@ import { ExportedVault } from "../types";
import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction";
import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction";
import { ExportFormat, VaultExportServiceAbstraction } from "./vault-export.service.abstraction";
import {
ExportFormat,
ExportFormatMetadata,
FormatOptions,
VaultExportServiceAbstraction,
} from "./vault-export.service.abstraction";
export class VaultExportService implements VaultExportServiceAbstraction {
constructor(
@@ -85,6 +90,26 @@ export class VaultExportService implements VaultExportServiceAbstraction {
);
}
/**
* Get available export formats based on vault context
* @param options Options determining which formats are available
* @returns Observable stream of available export formats
*/
formats$(options: FormatOptions): Observable<ExportFormatMetadata[]> {
const baseFormats: ExportFormatMetadata[] = [
{ name: ".json", format: "json" },
{ name: ".csv", format: "csv" },
{ name: ".json (Encrypted)", format: "encrypted_json" },
];
// ZIP format with attachments is only available for individual vault exports
if (options.isMyVault) {
return of([...baseFormats, { name: ".zip (with attachments)", format: "zip" }]);
}
return of(baseFormats);
}
/** Checks if the provided userId matches the currently authenticated user
* @param userId The userId to check
* @throws Error if the userId does not match the currently authenticated user

View File

@@ -35,7 +35,7 @@
<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-option *ngFor="let f of formatOptions$ | async" [value]="f.format" [label]="f.name" />
</bit-select>
</bit-form-field>

View File

@@ -67,7 +67,11 @@ import {
} from "@bitwarden/components";
import { GeneratorServicesModule } from "@bitwarden/generator-components";
import { CredentialGeneratorService, GenerateRequest, Type } from "@bitwarden/generator-core";
import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import {
ExportedVault,
ExportFormatMetadata,
VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
@@ -231,11 +235,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
fileEncryptionType: [EncryptedExportType.AccountEncrypted],
});
formatOptions = [
{ name: ".json", value: "json" },
{ name: ".csv", value: "csv" },
{ name: ".json (Encrypted)", value: "encrypted_json" },
];
/**
* Observable stream of available export format options
* Dynamically updates based on vault selection (My Vault vs Organization)
*/
formatOptions$: Observable<ExportFormatMetadata[]>;
private destroy$ = new Subject<void>();
private onlyManagedCollections = true;
@@ -338,17 +342,28 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
}
private observeFormSelections(): void {
this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.organizationId = value !== "myVault" ? value : undefined;
// Set up dynamic format options based on vault selection
this.formatOptions$ = this.exportForm.controls.vaultSelector.valueChanges.pipe(
startWith(this.exportForm.controls.vaultSelector.value),
map((vaultSelection) => {
const isMyVault = vaultSelection === "myVault";
// Update organizationId based on vault selection
this.organizationId = isMyVault ? undefined : vaultSelection;
return { isMyVault };
}),
switchMap((options) => this.exportService.formats$(options)),
tap((formats) => {
// Preserve the current format selection if it's still available in the new format list
const currentFormat = this.exportForm.get("format").value;
const isFormatAvailable = formats.some((f) => f.format === currentFormat);
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
this.exportForm.get("format").setValue("json");
if (value === "myVault") {
this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" });
// Only reset to json if the current format is no longer available
if (!isFormatAvailable) {
this.exportForm.get("format").setValue("json");
}
});
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
}
/**