* feat: editor settings model + typed clamped defaults (Task 1) * feat: carry SSH config + ssh_password to backend DbConfig (Task 2) * feat: Change enum bulk/drop/empty + type-specific change payload builder (Task 3) * feat: csv parser + shared export util (Task 4) * feat: rustls TLS connector factory with modes + client auth (Task 5) * feat: real SSH tunnel manager (testable backend) + pool eviction hook (Task 6) * feat: table DDL fetch (sqlite + pg_dump arg builder) (Task 7) * feat: keychain SSH secrets + connection delete purge + tunnel lifecycle (Task 8) * feat: real SSH tunnel + TLS connect path for postgres/mysql (Task 9) * feat: execute_change bulk/drop/empty + get_table_ddl command (Task 10) * feat: fetch SSH secrets into dbConnect + save on connection form (Task 11) * feat: Editor settings tab UI (Task 12) * feat: QueryEditor applies editor settings live (Task 13) * feat: ImportDialog with CSV/JSON preview + column mapping (Task 14) * feat: table-menu export/empty/delete/import + queue labels + payload builder (Task 15) * fix: error sanitization, encrypted-key guard, row-indexed import errors, caps (Task 16) * docs: mark Editor Settings, SSH/SSL runtime, Data Import, table-menu loose ends shipped (Task 17) * feat: auto-refresh schema tree after schema-modifying SQL (query + queue drop) * fix: use theme-consistent red classes for danger menu items (text-error was undefined) * feat: changes queue as tab-bar popover + amber pending border * feat: redesign changes popover (visual/SQL toggle, cards, footer actions, Cmd+S) * refactor: drop per-card status label from changes popover cards * feat: green completion indicator on committed cards + auto-close tabs of dropped tables * docs: changes queue popover UX + auto schema refresh statuses
51 lines
1.5 KiB
TypeScript
51 lines
1.5 KiB
TypeScript
import { parseCsv } from "./csvParser";
|
|
|
|
export interface NormalizedImport {
|
|
headers: string[];
|
|
rows: string[][];
|
|
}
|
|
|
|
export function normalizeImport(text: string): NormalizedImport {
|
|
const trimmed = text.trim();
|
|
if (trimmed === "") throw new Error("Import input is empty");
|
|
|
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(trimmed);
|
|
} catch {
|
|
throw new Error("Invalid JSON");
|
|
}
|
|
|
|
let rows: Record<string, unknown>[];
|
|
if (Array.isArray(parsed)) {
|
|
rows = parsed as Record<string, unknown>[];
|
|
} else if (parsed && typeof parsed === "object") {
|
|
const values = Object.values(parsed);
|
|
const arrays = values.filter((v): v is Record<string, unknown>[] => Array.isArray(v));
|
|
if (arrays.length === 1 && values.length === 1) {
|
|
rows = arrays[0];
|
|
} else {
|
|
rows = [parsed as Record<string, unknown>];
|
|
}
|
|
} else {
|
|
throw new Error("JSON import must be an array of objects or an object");
|
|
}
|
|
|
|
if (rows.length === 0) return { headers: [], rows: [] };
|
|
|
|
const headers = Object.keys(rows[0] ?? {});
|
|
const data = rows.map((r) => headers.map((h) => String((r as Record<string, unknown>)[h] ?? "")));
|
|
return { headers, rows: data };
|
|
}
|
|
|
|
return parseCsv(text);
|
|
}
|
|
|
|
export function coerceRow(v: string): unknown {
|
|
if (v === "") return null;
|
|
if (v === "true") return true;
|
|
if (v === "false") return false;
|
|
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
return v;
|
|
} |