List skipped item names instead of record types in Keeper import dialog

This commit is contained in:
Dmitry Yakimenko 2026-06-02 18:37:02 +02:00
parent b7e865eaf8
commit b504becf45
No known key found for this signature in database
8 changed files with 20 additions and 201 deletions

View File

@ -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."
},

View File

@ -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."
},

View File

@ -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."
},

View File

@ -8,14 +8,12 @@
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell>{{ "type" | i18n }}</th>
<th bitCell>{{ "items" | i18n }}</th>
<th bitCell>{{ "name" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>{{ r.type }}</td>
<td bitCell>{{ r.count }}</td>
<td bitCell>{{ r.name }}</td>
</tr>
</ng-template>
</bit-table>

View File

@ -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<string, string> = {
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<string, number>();
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");
}
}

View File

@ -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);

View File

@ -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));
}
}
}

View File

@ -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,
) {}
}