Fix setting values for name and tag selects; some improvements to inferring column types and names if first column does not contain field names

This commit is contained in:
Martin Kleinschrodt 2022-03-12 11:10:34 +01:00
parent 477ab826a4
commit 7e732cba3a
2 changed files with 59 additions and 68 deletions

View File

@ -59,6 +59,7 @@ export class ImportDialog extends Dialog<File, void> {
];
renderContent() {
const csvHasColumnsOnFirstRow = this._csvHasColumnsOnFirstRowButton?.active;
return html`
<div class="padded vertical spacing layout fit-vertically">
<h1 class="big text-centering margined">${$l("Import Data")}</h1>
@ -71,20 +72,18 @@ export class ImportDialog extends Dialog<File, void> {
disabled
></pl-select>
<div class="small padded" ?hidden=${this._formatSelect && this._formatSelect.value !== imp.CSV.value}>
<div class="small padded" ?hidden=${this._formatSelect?.value !== imp.CSV.value}>
${$l("Choose the correct column names and types for each column below.")}
</div>
<pl-scroller
class="stretch"
?hidden=${this._formatSelect && this._formatSelect.value !== imp.CSV.value}
>
<pl-scroller class="stretch" ?hidden=${this._formatSelect?.value !== imp.CSV.value}>
<ul class="vertical spacing layout">
<pl-toggle-button
class="transparent"
id="csvHasColumnsOnFirstRowButton"
.label=${$l("First row contains field names")}
reverse
@change=${() => this._parseData(true)}
>
</pl-toggle-button>
@ -92,7 +91,9 @@ export class ImportDialog extends Dialog<File, void> {
id=${"nameColumnSelect"}
.label=${$l("Name Column")}
.options=${this._itemColumns.map((itemColumn, itemColumnIndex) => ({
label: `${itemColumn.displayName} (${$l("Column {0}", itemColumnIndex.toString())})`,
label: csvHasColumnsOnFirstRow
? `${itemColumn.displayName} (${$l("Column {0}", itemColumnIndex.toString())})`
: $l("Column {0}", itemColumnIndex.toString()),
value: itemColumnIndex,
}))}
.selectedIndex=${this._nameColumnSelect?.selectedIndex}
@ -149,12 +150,22 @@ export class ImportDialog extends Dialog<File, void> {
?hidden=${itemColumn.type === "name" || itemColumn.type === "tags"}
>
<div class="small margined spacing horizontal layout">
<div class="stretch">${itemColumn.name}</div>
<div class="subtle">${$l("Column {0}", itemColumnIndex.toString())}</div>
<div class="stretch">
${csvHasColumnsOnFirstRow
? itemColumn.name
: $l("Column {0}", itemColumnIndex.toString())}
</div>
<div class="subtle" ?hidden=${!csvHasColumnsOnFirstRow}>
${$l("Column {0}", itemColumnIndex.toString())}
</div>
</div>
<div class="tiny horizontally-margined subtle mono">
${itemColumn.exampleValues}
<div class="tiny horizontally-margined subtle mono ellipsis">
${itemColumn.values
.filter((v) => v !== "")
.slice(0, 20)
.map((value) => (value.includes(",") ? `"${value}"` : value))
.join(", ")}
</div>
<pl-input
@ -224,35 +235,14 @@ export class ImportDialog extends Dialog<File, void> {
const importFormat = (await imp.guessFormat(file)) || imp.CSV;
this._formatSelect.value = importFormat.value;
// Select first options properly
if (importFormat.value === imp.CSV.value) {
if (
this._nameColumnSelect &&
(this._nameColumnSelect.selectedIndex === -1 || this._nameColumnSelect.selectedIndex === undefined)
) {
const selectedNameIndex = this._itemColumns.findIndex((itemColumn) => itemColumn.type === "name");
this._nameColumnSelect.value = selectedNameIndex;
this._nameColumnSelect.selectedIndex = selectedNameIndex;
}
await this._parseData(true);
if (
this._tagsColumnSelect &&
(this._tagsColumnSelect.selectedIndex === -1 || this._tagsColumnSelect.selectedIndex === undefined)
) {
// +1 because we have the "none" option first
const selectedTagsIndex = this._itemColumns.findIndex((itemColumn) => itemColumn.type === "tags") + 1;
this._tagsColumnSelect.value = selectedTagsIndex - 1;
this._tagsColumnSelect.selectedIndex = selectedTagsIndex;
}
}
await this._parseData();
this._vaultSelect.value = app.mainVault!;
return result;
}
private async _parseData(): Promise<void> {
private async _parseData(resetCSVColumns = false): Promise<void> {
const file = this._file;
switch (this._formatSelect.value) {
@ -281,9 +271,17 @@ export class ImportDialog extends Dialog<File, void> {
this._items = await imp.asLastPass(file);
break;
case imp.CSV.value:
const result = await imp.asCSV(file, this._itemColumns, this._csvHasColumnsOnFirstRowButton.active);
const result = await imp.asCSV(
file,
resetCSVColumns ? [] : this._itemColumns,
this._csvHasColumnsOnFirstRowButton.active
);
this._items = result.items;
this._itemColumns = result.itemColumns;
console.log("itemcolumns", this._itemColumns);
await this.updateComplete;
this._nameColumnSelect.selectedIndex = this._itemColumns.findIndex(({ type }) => type === "name");
this._tagsColumnSelect.selectedIndex = this._itemColumns.findIndex(({ type }) => type === "tags") + 1;
break;
case imp.ONEPUX.value:
this._items = await imp.as1Pux(file);

View File

@ -18,7 +18,7 @@ export interface ImportCSVColumn {
name: string;
displayName: string;
type: FieldType | "name" | "tags";
exampleValues: string;
values: string[];
}
export const CSV: ImportFormat = {
@ -125,59 +125,52 @@ export async function asCSV(
throw new Err(ErrorCode.INVALID_CSV, "No rows found in .csv file.");
}
const columnNames = rows[0].map((column) => column.toLowerCase());
const columnNames = columnsOnFirstRow
? rows[0].map((column) => column.toLowerCase())
: rows[0].map((_, i) => $l("Column {0}", i.toString()));
// If first row is column names, discard it
if (columnsOnFirstRow) {
rows.shift();
}
let hasNameColumn = false;
let hasTagsColumn = false;
const itemColumns =
mappedItemColumns.length > 0
? mappedItemColumns
: columnNames.map((columnName, columnIndex) => {
const values = rows.map((row) => row[columnIndex] || "");
// Guess field type based on first non-empty value
// TODO: Sample all values for more reliable results?
let type = guessFieldType({
name: columnName,
value: rows[1] ? rows[1][columnIndex] : "",
name: columnsOnFirstRow ? columnName : "",
value: values.find((v) => Boolean(v)),
}) as ImportCSVColumn["type"];
const lowerCaseColumnName = columnName.toLocaleLowerCase();
// If we're not given field names by the first row, base the name on the type
const name = columnsOnFirstRow ? columnName.toLocaleLowerCase() : type;
if (lowerCaseColumnName === "name") {
if (!hasNameColumn && name === "name") {
type = "name";
hasNameColumn = true;
}
if (["tags", "category"].includes(lowerCaseColumnName)) {
if (!hasTagsColumn && ["tags", "category"].includes(name)) {
type = "tags";
hasTagsColumn = true;
}
const exampleValues = [rows[1] ? rows[1][columnIndex] : "", rows[2] ? rows[2][columnIndex] : ""]
.filter((value) => Boolean(value))
.map((value) => (value.includes(",") ? `"${value}"` : value))
.join(", ");
return {
name: columnName,
displayName: capitalize(columnName),
name,
displayName: capitalize(name),
type,
exampleValues,
values,
};
});
// Prevent itemColumns from having more than one "name" or "tags"
let hasNameColumn = false;
let hasTagsColumn = false;
itemColumns.forEach((itemColumn) => {
if (itemColumn.type === "name") {
if (!hasNameColumn) {
hasNameColumn = true;
} else {
itemColumn.type = FieldType.Text;
}
} else if (itemColumn.type === "tags") {
if (!hasTagsColumn) {
hasTagsColumn = true;
} else {
itemColumn.type = FieldType.Text;
}
}
});
// Ensure there's at least one nameColumn
if (!hasNameColumn) {
itemColumns[0].type = "name";