From b504becf453adee06f7ff506fe4fea7cb08285d3 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Tue, 2 Jun 2026 18:37:02 +0200 Subject: [PATCH] List skipped item names instead of record types in Keeper import dialog --- apps/browser/src/_locales/en/messages.json | 36 ------------ apps/desktop/src/locales/en/messages.json | 39 ------------- apps/web/src/locales/en/messages.json | 39 ------------- .../partial-import-dialog.component.html | 6 +- .../keeper/partial-import-dialog.component.ts | 57 ++----------------- .../keeper/keeper-direct-importer.spec.ts | 24 +++----- .../keeper/keeper-direct-importer.ts | 16 ++---- .../importers/keeper/keeper-import-error.ts | 4 +- 8 files changed, 20 insertions(+), 201 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f1b3c9138f3..f9368414cb7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1534,9 +1534,6 @@ "file": { "message": "File" }, - "photo": { - "message": "Photo" - }, "unknown": { "message": "Unknown" }, @@ -4644,39 +4641,6 @@ "importPartialErrorDescription": { "message": "The following items could not be read and will not be included in your import:" }, - "contact": { - "message": "Contact" - }, - "membership": { - "message": "Membership" - }, - "passport": { - "message": "Passport" - }, - "driverLicense": { - "message": "Driver license" - }, - "ssnCard": { - "message": "Identity card" - }, - "birthCertificate": { - "message": "Birth certificate" - }, - "databaseCredentials": { - "message": "Database credentials" - }, - "serverCredentials": { - "message": "Server credentials" - }, - "softwareLicense": { - "message": "Software license" - }, - "healthInsurance": { - "message": "Health insurance" - }, - "wifiCredentials": { - "message": "Wi-Fi credentials" - }, "importEncKeyError": { "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 296c05a6f86..2ae4911875a 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1019,9 +1019,6 @@ "file": { "message": "File" }, - "photo": { - "message": "Photo" - }, "selectFile": { "message": "Select a file" }, @@ -4021,42 +4018,6 @@ "importPartialErrorDescription": { "message": "The following items could not be read and will not be included in your import:" }, - "contact": { - "message": "Contact" - }, - "membership": { - "message": "Membership" - }, - "passport": { - "message": "Passport" - }, - "driverLicense": { - "message": "Driver license" - }, - "ssnCard": { - "message": "Identity card" - }, - "birthCertificate": { - "message": "Birth certificate" - }, - "databaseCredentials": { - "message": "Database credentials" - }, - "serverCredentials": { - "message": "Server credentials" - }, - "softwareLicense": { - "message": "Software license" - }, - "healthInsurance": { - "message": "Health insurance" - }, - "wifiCredentials": { - "message": "Wi-Fi credentials" - }, - "general": { - "message": "General" - }, "importEncKeyError": { "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8f37b4ad44d..798d87aae26 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1570,9 +1570,6 @@ "file": { "message": "File" }, - "photo": { - "message": "Photo" - }, "selectFile": { "message": "Select a file." }, @@ -2770,42 +2767,6 @@ "importPartialErrorDescription": { "message": "The following items could not be read and will not be included in your import:" }, - "contact": { - "message": "Contact" - }, - "membership": { - "message": "Membership" - }, - "passport": { - "message": "Passport" - }, - "driverLicense": { - "message": "Driver license" - }, - "ssnCard": { - "message": "Identity card" - }, - "birthCertificate": { - "message": "Birth certificate" - }, - "databaseCredentials": { - "message": "Database credentials" - }, - "serverCredentials": { - "message": "Server credentials" - }, - "softwareLicense": { - "message": "Software license" - }, - "healthInsurance": { - "message": "Health insurance" - }, - "wifiCredentials": { - "message": "Wi-Fi credentials" - }, - "general": { - "message": "General" - }, "importEncKeyError": { "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." }, diff --git a/libs/importer/src/components/keeper/partial-import-dialog.component.html b/libs/importer/src/components/keeper/partial-import-dialog.component.html index 9d0c8a7b832..451a1dc2af8 100644 --- a/libs/importer/src/components/keeper/partial-import-dialog.component.html +++ b/libs/importer/src/components/keeper/partial-import-dialog.component.html @@ -8,14 +8,12 @@ - {{ "type" | i18n }} - {{ "items" | i18n }} + {{ "name" | i18n }} - {{ r.type }} - {{ r.count }} + {{ r.name }} diff --git a/libs/importer/src/components/keeper/partial-import-dialog.component.ts b/libs/importer/src/components/keeper/partial-import-dialog.component.ts index 777f0d2f984..1fdd8da0dba 100644 --- a/libs/importer/src/components/keeper/partial-import-dialog.component.ts +++ b/libs/importer/src/components/keeper/partial-import-dialog.component.ts @@ -12,10 +12,7 @@ import { TableModule, } from "@bitwarden/components"; -import { - ImportRecordError, - ImportRecordErrorReason, -} from "../../importers/keeper/keeper-import-error"; +import { ImportRecordError } from "../../importers/keeper/keeper-import-error"; export interface PartialImportDialogData { errors: ImportRecordError[]; @@ -23,35 +20,9 @@ export interface PartialImportDialogData { } interface SkippedItemRow { - type: string; - count: number; + name: string; } -// Localized label for each Keeper record type. Types not listed here (the pam* family and any custom -// record types) fall back to the "Other" bucket. -const KEEPER_TYPE_I18N_KEYS: Record = { - login: "typeLogin", - bankCard: "typeCard", - bankAccount: "bankAccount", - address: "address", - contact: "contact", - file: "file", - photo: "photo", - encryptedNotes: "typeSecureNote", - ssnCard: "ssnCard", - databaseCredentials: "databaseCredentials", - serverCredentials: "serverCredentials", - sshKeys: "typeSshKey", - softwareLicense: "softwareLicense", - healthInsurance: "healthInsurance", - membership: "membership", - passport: "passport", - driverLicense: "driverLicense", - birthCertificate: "birthCertificate", - general: "general", - wifiCredentials: "wifiCredentials", -}; - // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -68,7 +39,9 @@ export class PartialImportDialogComponent implements OnInit { ) {} ngOnInit(): void { - this.dataSource.data = this.buildRows(this.data.errors); + // Items that could not be read have no decrypted title, so they show as "Unknown". + const unknown = this.i18nService.t("unknown"); + this.dataSource.data = this.data.errors.map((error) => ({ name: error.name || unknown })); } protected continueImport(): void { @@ -78,24 +51,4 @@ export class PartialImportDialogComponent implements OnInit { protected cancel(): void { void this.dialogRef.close(false); } - - private buildRows(errors: ImportRecordError[]): SkippedItemRow[] { - const counts = new Map(); - for (const error of errors) { - const type = this.typeLabel(error); - counts.set(type, (counts.get(type) ?? 0) + 1); - } - return Array.from(counts, ([type, count]) => ({ type, count })); - } - - private typeLabel(error: ImportRecordError): string { - // Folders that could not be decrypted are reported as their own bucket. - if (error.reason === ImportRecordErrorReason.FolderDecryptionFailed) { - return this.i18nService.t("folder"); - } - - // Known Keeper types get their label; unmapped types and unreadable items fall back to "Other". - const mapped = error.type ? KEEPER_TYPE_I18N_KEYS[error.type] : undefined; - return this.i18nService.t(mapped ?? "other"); - } } diff --git a/libs/importer/src/importers/keeper/keeper-direct-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-direct-importer.spec.ts index 215718bd0d3..382dbe7aaec 100644 --- a/libs/importer/src/importers/keeper/keeper-direct-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-direct-importer.spec.ts @@ -191,11 +191,7 @@ describe("Keeper Direct Importer", () => { it("should not import file records and report them as unsupported", () => { expect(result.ciphers.find((c) => c.name === "Project Proposal Document")).toBeUndefined(); expect(errors).toContainEqual( - new ImportRecordError( - "Project Proposal Document", - ImportRecordErrorReason.UnsupportedType, - "file", - ), + new ImportRecordError("Project Proposal Document", ImportRecordErrorReason.UnsupportedType), ); }); @@ -343,11 +339,7 @@ describe("Keeper Direct Importer", () => { it("should not import photo records and report them as unsupported", () => { expect(result.ciphers.find((c) => c.name === "Family Vacation 2024")).toBeUndefined(); expect(errors).toContainEqual( - new ImportRecordError( - "Family Vacation 2024", - ImportRecordErrorReason.UnsupportedType, - "photo", - ), + new ImportRecordError("Family Vacation 2024", ImportRecordErrorReason.UnsupportedType), ); }); @@ -579,24 +571,24 @@ describe("Keeper Direct Importer error handling", () => { return new KeeperDirectImporter().convertVaultToImportResult(vault); } - it("maps an UnsupportedVersion vault error to UnsupportedFeature with the UID as name", () => { + it("maps an UnsupportedVersion vault error to UnsupportedFeature with no readable name", () => { const { errors } = importWith( [], [{ id: "uid-1", reason: VaultRecordErrorReason.UnsupportedVersion }], ); expect(errors).toContainEqual( - new ImportRecordError("uid-1", ImportRecordErrorReason.UnsupportedFeature), + new ImportRecordError("", ImportRecordErrorReason.UnsupportedFeature), ); }); - it("maps a DecryptionFailed vault error to a generic error with the UID as name", () => { + it("maps a DecryptionFailed vault error to a generic error with no readable name", () => { const { errors } = importWith( [], [{ id: "uid-2", reason: VaultRecordErrorReason.DecryptionFailed }], ); - expect(errors).toContainEqual(new ImportRecordError("uid-2", ImportRecordErrorReason.Error)); + expect(errors).toContainEqual(new ImportRecordError("", ImportRecordErrorReason.Error)); }); it("maps a FolderDecryptionFailed vault error and still imports the affected record at the root", () => { @@ -608,7 +600,7 @@ describe("Keeper Direct Importer error handling", () => { ); expect(errors).toContainEqual( - new ImportRecordError("folder-uid", ImportRecordErrorReason.FolderDecryptionFailed), + new ImportRecordError("", ImportRecordErrorReason.FolderDecryptionFailed), ); const cipher = result.ciphers.find((c) => c.name === "Rootless Record"); @@ -635,7 +627,7 @@ describe("Keeper Direct Importer error handling", () => { // The throwing record produces exactly one generic error. expect(errors).toContainEqual( - new ImportRecordError("Throwing Record", ImportRecordErrorReason.Error, "login"), + new ImportRecordError("Throwing Record", ImportRecordErrorReason.Error), ); expect(errors.filter((e) => e.reason === ImportRecordErrorReason.Error).length).toBe(1); diff --git a/libs/importer/src/importers/keeper/keeper-direct-importer.ts b/libs/importer/src/importers/keeper/keeper-direct-importer.ts index 38f327390f9..b5618a4757d 100644 --- a/libs/importer/src/importers/keeper/keeper-direct-importer.ts +++ b/libs/importer/src/importers/keeper/keeper-direct-importer.ts @@ -55,8 +55,8 @@ export class KeeperDirectImporter extends BaseImporter { private mapVaultErrors(vaultErrors: VaultRecordError[], errors: ImportRecordError[]): void { for (const error of vaultErrors) { - // The record/folder name was never decrypted, so the UID is the only identifier we have. - errors.push(new ImportRecordError(error.id, mapVaultErrorReason(error.reason))); + // The record/folder name was never decrypted, so there is no name to show. + errors.push(new ImportRecordError("", mapVaultErrorReason(error.reason))); } } @@ -67,22 +67,14 @@ export class KeeperDirectImporter extends BaseImporter { ): void { for (const item of items) { if (UNSUPPORTED_RECORD_TYPES.has(item.type)) { - errors.push( - new ImportRecordError( - item.title || item.id, - ImportRecordErrorReason.UnsupportedType, - item.type, - ), - ); + errors.push(new ImportRecordError(item.title, ImportRecordErrorReason.UnsupportedType)); continue; } try { this.parseRecord(item, result); } catch { - errors.push( - new ImportRecordError(item.title || item.id, ImportRecordErrorReason.Error, item.type), - ); + errors.push(new ImportRecordError(item.title, ImportRecordErrorReason.Error)); } } } diff --git a/libs/importer/src/importers/keeper/keeper-import-error.ts b/libs/importer/src/importers/keeper/keeper-import-error.ts index 5e1be85910d..18890d45131 100644 --- a/libs/importer/src/importers/keeper/keeper-import-error.ts +++ b/libs/importer/src/importers/keeper/keeper-import-error.ts @@ -9,10 +9,8 @@ export type ImportRecordErrorReason = export class ImportRecordError { constructor( + // The item title, or empty when the item could not be read and its name is unknown. readonly name: string, readonly reason: ImportRecordErrorReason, - // Raw Keeper record type (e.g. "login", "bankCard", "file"). Undefined when the item could not - // be read, so its type is unknown. - readonly type?: string, ) {} }