1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +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 { UserId, OrganizationId } from "@bitwarden/common/types/guid";
import { ExportedVault } from "../types"; import { ExportedVault } from "../types";
@@ -5,6 +7,24 @@ import { ExportedVault } from "../types";
export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const; export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const;
export type ExportFormat = (typeof EXPORT_FORMATS)[number]; 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 { export abstract class VaultExportServiceAbstraction {
abstract getExport: ( abstract getExport: (
userId: UserId, userId: UserId,
@@ -18,4 +38,11 @@ export abstract class VaultExportServiceAbstraction {
password: string, password: string,
onlyManagedCollections?: boolean, onlyManagedCollections?: boolean,
) => Promise<ExportedVault>; ) => 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/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 { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction";
import { OrganizationVaultExportServiceAbstraction } from "./org-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 { export class VaultExportService implements VaultExportServiceAbstraction {
constructor( 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 /** Checks if the provided userId matches the currently authenticated user
* @param userId The userId to check * @param userId The userId to check
* @throws Error if the userId does not match the currently authenticated user * @throws Error if the userId does not match the currently authenticated user

View File

@@ -35,7 +35,7 @@
<bit-form-field> <bit-form-field>
<bit-label>{{ "fileFormat" | i18n }}</bit-label> <bit-label>{{ "fileFormat" | i18n }}</bit-label>
<bit-select formControlName="format"> <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-select>
</bit-form-field> </bit-form-field>

View File

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