mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-21 21:17:06 +08:00
List skipped item names instead of record types in Keeper import dialog
This commit is contained in:
parent
b7e865eaf8
commit
b504becf45
@ -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."
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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."
|
||||
},
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user