diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e49e50679995..b29bf8ea33c9 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3460,33 +3460,18 @@ "exportingPersonalVaultTitle": { "message": "Exporting individual vault" }, - "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } - }, - "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } - }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, - "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "exportingIndividualVaultScopeDescription": { + "message": "Only items in your individual vault will export." + }, + "exportingOrganizationVaultScopeDescription": { + "message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.", "placeholders": { "organization": { "content": "$1", - "example": "ACME Moving Co." + "example": "My Org Name" } } }, @@ -4577,6 +4562,18 @@ "exportError": { "message": "An error occurred while exporting your vault data" }, + "exportSuccessSkippedAttachment": { + "message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully." + }, + "exportSuccessSkippedAttachments": { + "message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "typePasskey": { "message": "Passkey" }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8c7093fd78dc..2224d193412b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1261,6 +1261,7 @@ export default class MainBackground { this.kdfConfigService, this.apiService, this.restrictedItemTypesService, + this.logService, ); this.exportApiService = new DefaultVaultExportApiService(this.apiService); diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.html b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.html index 36c84d9b7883..2735b1553383 100644 --- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.html +++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.html @@ -9,6 +9,7 @@ (formDisabled)="this.disabled = $event" (formLoading)="this.loading = $event" (onSuccessfulExport)="this.onSuccessfulExport($event)" + (skippedAttachmentCountChange)="this.skippedAttachmentCount.set($event)" > diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts index 584c7cd3f7c7..25cc8ca35623 100644 --- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -33,10 +33,15 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page export class ExportBrowserV2Component { protected disabled = false; protected loading = false; + protected readonly skippedAttachmentCount = signal(0); constructor(private router: Router) {} protected async onSuccessfulExport(organizationId: string): Promise { + // Skip redirect when attachments were skipped so the user can see the warning callout + if (this.skippedAttachmentCount() > 0) { + return; + } await this.router.navigate(["/tabs/settings"]); } } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 08a90fa6a54b..9e96d96faa62 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -1032,6 +1032,7 @@ export class ServiceContainer { this.kdfConfigService, this.apiService, this.restrictedItemTypesService, + this.logService, ); this.vaultExportApiService = new DefaultVaultExportApiService(this.apiService); diff --git a/apps/desktop/src/app/tools/export/export-desktop.component.html b/apps/desktop/src/app/tools/export/export-desktop.component.html index 5f4f00585776..ff8f294377cb 100644 --- a/apps/desktop/src/app/tools/export/export-desktop.component.html +++ b/apps/desktop/src/app/tools/export/export-desktop.component.html @@ -5,6 +5,7 @@ (formLoading)="this.loading = $event" (formDisabled)="this.disabled = $event" (onSuccessfulExport)="this.onSuccessfulExport($event)" + (skippedAttachmentCountChange)="this.skippedAttachmentCount.set($event)" > diff --git a/apps/desktop/src/app/tools/export/export-desktop.component.ts b/apps/desktop/src/app/tools/export/export-desktop.component.ts index 0ae353729c6b..3fab85a1caa6 100644 --- a/apps/desktop/src/app/tools/export/export-desktop.component.ts +++ b/apps/desktop/src/app/tools/export/export-desktop.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -21,6 +21,7 @@ import { ExportComponent } from "@bitwarden/vault-export-ui"; export class ExportDesktopComponent { protected disabled = false; protected loading = false; + protected readonly skippedAttachmentCount = signal(0); constructor(public dialogRef: DialogRef) {} @@ -28,6 +29,10 @@ export class ExportDesktopComponent { * Callback that is called after a successful export. */ protected async onSuccessfulExport(organizationId: string): Promise { + // Skip closing dialog when attachments were skipped so the user can see the warning callout + if (this.skippedAttachmentCount() > 0) { + return; + } await this.dialogRef.close(); } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index b227439ce312..43b26273ef21 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3025,33 +3025,18 @@ "exportingPersonalVaultTitle": { "message": "Exporting individual vault" }, - "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } - }, - "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } - }, "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, - "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "exportingIndividualVaultScopeDescription": { + "message": "Only items in your individual vault will export." + }, + "exportingOrganizationVaultScopeDescription": { + "message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.", "placeholders": { "organization": { "content": "$1", - "example": "ACME Moving Co." + "example": "My Org Name" } } }, @@ -4010,6 +3995,18 @@ "exportError": { "message": "An error occurred while exporting your vault data" }, + "exportSuccessSkippedAttachment": { + "message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully." + }, + "exportSuccessSkippedAttachments": { + "message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 31b8492247d3..5ea19394a5c9 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2255,6 +2255,18 @@ "exportError": { "message": "An error occurred while exporting your vault data" }, + "exportSuccessSkippedAttachment": { + "message": "1 corrupted attachment was skipped during export. All other vault data and attachments exported successfully." + }, + "exportSuccessSkippedAttachments": { + "message": "$COUNT$ corrupted attachments were skipped during export. All other vault data and attachments exported successfully.", + "placeholders": { + "count": { + "content": "$1", + "example": "3" + } + } + }, "passwordGenerator": { "message": "Password generator" }, @@ -8054,26 +8066,11 @@ "exportingOrganizationVaultTitle": { "message": "Exporting organization vault" }, - "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } - }, - "exportingIndividualVaultWithAttachmentsDescription": { - "message": "Only the individual vault items including attachments associated with $EMAIL$ will be exported. Organization vault items will not be included", - "placeholders": { - "email": { - "content": "$1", - "example": "name@example.com" - } - } + "exportingIndividualVaultScopeDescription": { + "message": "Only items in your individual vault will export." }, - "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "exportingOrganizationVaultScopeDescription": { + "message": "Only items associated with the $ORGANIZATION$ vault will export. Items in individual vaults will not.", "placeholders": { "organization": { "content": "$1", diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9bea9df0adc4..17ef67c9e815 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1051,6 +1051,7 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, ApiServiceAbstraction, RestrictedItemTypesService, + LogService, ], }), safeProvider({ diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 55ad39bc8e0b..d48b7d9b8d59 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -11,6 +11,7 @@ import { EncString, } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherId, emptyGuid, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -175,6 +176,7 @@ describe("VaultExportService", () => { let apiService: MockProxy; let restrictedSubject: BehaviorSubject; let restrictedItemTypesService: Partial; + let logService: MockProxy; let fetchMock: jest.Mock; const userId = emptyGuid as UserId; @@ -188,6 +190,7 @@ describe("VaultExportService", () => { encryptService = mock(); kdfConfigService = mock(); apiService = mock(); + logService = mock(); keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); restrictedSubject = new BehaviorSubject([]); @@ -225,6 +228,7 @@ describe("VaultExportService", () => { kdfConfigService, apiService, restrictedItemTypesService as RestrictedItemTypesService, + logService, ); }); @@ -345,7 +349,7 @@ describe("VaultExportService", () => { }); it.each([[400], [401], [404], [500]])( - "throws error if the http request fails (status === %n)", + "returns skipped attachment if the http request fails (status === %n)", async (status) => { const cipherData = new CipherData(); cipherData.id = "mock-id"; @@ -365,13 +369,13 @@ describe("VaultExportService", () => { ) as any; global.Request = jest.fn(() => {}) as any; - await expect(async () => { - await exportService.getExport(userId, "zip"); - }).rejects.toThrow("Error downloading attachment"); + const result = await exportService.getExport(userId, "zip"); + const blobResult = result as ExportedVaultAsBlob; + expect(blobResult.skippedAttachmentCount).toBe(1); }, ); - it("throws error if decrypting attachment fails", async () => { + it("returns skipped attachment if decrypting attachment fails", async () => { const cipherData = new CipherData(); cipherData.id = "mock-id"; const cipherView = new CipherView(new Cipher(cipherData)); @@ -393,9 +397,33 @@ describe("VaultExportService", () => { ) as any; global.Request = jest.fn(() => {}) as any; - await expect(async () => { - await exportService.getExport(userId, "zip"); - }).rejects.toThrow("Error decrypting attachment"); + const result = await exportService.getExport(userId, "zip"); + const blobResult = result as ExportedVaultAsBlob; + expect(blobResult.skippedAttachmentCount).toBe(1); + }); + + it("returns no skippedAttachments when all attachments succeed", async () => { + const cipherData = new CipherData(); + cipherData.id = "mock-id"; + const cipherView = new CipherView(new Cipher(cipherData)); + const attachmentView = new AttachmentView(new Attachment(new AttachmentData())); + attachmentView.fileName = "mock-file-name"; + cipherView.attachments = [attachmentView]; + + cipherService.getAllDecrypted.mockResolvedValue([cipherView]); + folderService.getAllDecryptedFromState.mockResolvedValue([]); + cipherService.getDecryptedAttachmentBuffer.mockResolvedValue(new Uint8Array(255)); + global.fetch = jest.fn(() => + Promise.resolve({ + status: 200, + arrayBuffer: () => Promise.resolve(new ArrayBuffer(255)), + }), + ) as any; + global.Request = jest.fn(() => {}) as any; + + const result = await exportService.getExport(userId, "zip"); + const blobResult = result as ExportedVaultAsBlob; + expect(blobResult.skippedAttachmentCount).toBeUndefined(); }); it("contains attachments with folders", async () => { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index a5e9f8aea6e1..ff5187288b59 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -9,6 +9,7 @@ import { KeyGenerationService } from "@bitwarden/common/key-management/crypto"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -49,6 +50,7 @@ export class IndividualVaultExportService kdfConfigService: KdfConfigService, private apiService: ApiService, private restrictedItemTypesService: RestrictedItemTypesService, + private logService: LogService, ) { super(keyGenerationService, encryptService, cryptoFunctionService, kdfConfigService); } @@ -105,6 +107,8 @@ export class IndividualVaultExportService } // attachments + let skippedAttachmentCount = 0; + for (const cipher of await this.cipherService.getAllDecrypted(activeUserId)) { if ( !cipher.attachments || @@ -117,9 +121,9 @@ export class IndividualVaultExportService const cipherFolder = attachmentsFolder.folder(cipher.id); for (const attachment of cipher.attachments) { - const response = await this.downloadAttachment(cipher.id, attachment.id); - try { + const response = await this.downloadAttachment(cipher.id, attachment.id); + const decBuf = await this.cipherService.getDecryptedAttachmentBuffer( cipher.id as CipherId, attachment, @@ -128,8 +132,9 @@ export class IndividualVaultExportService ); cipherFolder.file(attachment.fileName, decBuf); - } catch { - throw new Error("Error decrypting attachment"); + } catch (error) { + this.logService.error(`Failed to export attachment: Cipher Id: ${cipher.id}`, error); + skippedAttachmentCount++; } } } @@ -140,6 +145,7 @@ export class IndividualVaultExportService type: "application/zip", data: blobData, fileName: ExportHelper.getFileName("", "zip"), + skippedAttachmentCount: skippedAttachmentCount > 0 ? skippedAttachmentCount : undefined, } as ExportedVaultAsBlob; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/types/exported-vault-type.ts b/libs/tools/export/vault-export/vault-export-core/src/types/exported-vault-type.ts index b7e6d770f0d7..7465a6af489b 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/types/exported-vault-type.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/types/exported-vault-type.ts @@ -2,6 +2,7 @@ export type ExportedVaultAsBlob = { type: "application/zip"; data: Blob; fileName: string; + skippedAttachmentCount?: number; }; export type ExportedVaultAsString = { diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html deleted file mode 100644 index a660219499f3..000000000000 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - {{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }} - - diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.html new file mode 100644 index 000000000000..86066842fa1c --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.html @@ -0,0 +1,5 @@ +@if (show) { + + {{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }} + +} diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.ts similarity index 67% rename from libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts rename to libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.ts index a8a6ee6bb648..cb14188f77a6 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-description.component.ts @@ -1,26 +1,24 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { CommonModule } from "@angular/common"; import { Component, effect, input } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getById } from "@bitwarden/common/platform/misc/rxjs-operators"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CalloutModule } from "@bitwarden/components"; +import { TypographyModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { ExportFormat } from "@bitwarden/vault-export-core"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ - selector: "tools-export-scope-callout", - templateUrl: "export-scope-callout.component.html", - imports: [CommonModule, I18nPipe, CalloutModule], + selector: "tools-export-scope-description", + templateUrl: "export-scope-description.component.html", + imports: [I18nPipe, TypographyModule], }) -export class ExportScopeCalloutComponent { +export class ExportScopeDescriptionComponent { show = false; scopeConfig: { title: string; @@ -30,8 +28,6 @@ export class ExportScopeCalloutComponent { /* Optional OrganizationId, if not provided, it will display individual vault export message */ readonly organizationId = input(); - /* Optional export format, determines which individual export description to display */ - readonly exportFormat = input(); /* The description key to use for organizational exports */ readonly orgExportDescription = input(); @@ -41,18 +37,13 @@ export class ExportScopeCalloutComponent { ) { effect(async () => { this.show = false; - await this.getScopeMessage( - this.organizationId(), - this.exportFormat(), - this.orgExportDescription(), - ); + await this.getScopeMessage(this.organizationId(), this.orgExportDescription()); this.show = true; }); } private async getScopeMessage( organizationId: OrganizationId | undefined, - exportFormat: ExportFormat | undefined, orgExportDescription: string, ): Promise { const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); @@ -75,12 +66,8 @@ export class ExportScopeCalloutComponent { // exporting from individual vault this.scopeConfig = { title: "exportingPersonalVaultTitle", - description: - exportFormat === "zip" - ? "exportingIndividualVaultWithAttachmentsDescription" - : "exportingIndividualVaultDescription", - scopeIdentifier: - (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email)))) ?? "", + description: "exportingIndividualVaultScopeDescription", + scopeIdentifier: "", }; } } 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 index 6f2fcbeafcf7..195d5cdebb91 100644 --- 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 @@ -5,15 +5,9 @@ > {{ "personalVaultExportPolicyInEffect" | i18n }} - -
- + {{ "exportFrom" | i18n }} + @@ -130,3 +128,9 @@ + +@if (skippedAttachmentCount() > 0) { + + {{ skippedAttachmentMessage }} + +} 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 a68001c4d54f..cb8c6a025c83 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 @@ -5,6 +5,7 @@ import { AfterViewInit, Component, EventEmitter, + model, Input, OnDestroy, OnInit, @@ -73,7 +74,7 @@ import { import { EncryptedExportType } from "../enums/encrypted-export-type.enum"; -import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; +import { ExportScopeDescriptionComponent } from "./export-scope-description.component"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -91,7 +92,7 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component"; SelectModule, CalloutModule, RadioButtonModule, - ExportScopeCalloutComponent, + ExportScopeDescriptionComponent, PasswordStrengthV2Component, GeneratorServicesModule, CopyClickDirective, @@ -140,7 +141,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { get orgExportDescription(): string { if (!this._showExcludeMyItems) { - return "exportingOrganizationVaultDesc"; + return "exportingOrganizationVaultScopeDescription"; } return this.isAdminConsoleContext ? "exportingOrganizationVaultFromAdminConsoleWithDataOwnershipDesc" @@ -197,6 +198,8 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { @Output() onSuccessfulExport = new EventEmitter(); + readonly skippedAttachmentCount = model(0); + // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @angular-eslint/prefer-signals @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component; @@ -540,6 +543,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { // Download the export file this.downloadFile(data); + // Track skipped attachments for inline warning callout + if (data.type === "application/zip" && data.skippedAttachmentCount) { + this.skippedAttachmentCount.set(data.skippedAttachmentCount); + } + this.toastService.showToast({ variant: "success", title: null, @@ -670,6 +678,13 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { return this.exportForm.get("fileEncryptionType").value; } + get skippedAttachmentMessage(): string { + const count = this.skippedAttachmentCount(); + return count === 1 + ? this.i18nService.t("exportSuccessSkippedAttachment") + : this.i18nService.t("exportSuccessSkippedAttachments", count); + } + adjustValidators() { this.exportForm.get("confirmFilePassword").reset(); this.exportForm.get("filePassword").reset(); diff --git a/libs/tools/export/vault-export/vault-export-ui/src/index.ts b/libs/tools/export/vault-export/vault-export-ui/src/index.ts index 919bc8b38e5e..b3075b7542c4 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/index.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/index.ts @@ -1,2 +1,2 @@ export { ExportComponent } from "./components/export.component"; -export { ExportScopeCalloutComponent } from "./components/export-scope-callout.component"; +export { ExportScopeDescriptionComponent } from "./components/export-scope-description.component";