* 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
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import type { ColumnInfo } from "./types";
|
|
|
|
export function exportData(
|
|
rows: unknown[][],
|
|
columns: ColumnInfo[],
|
|
format: string,
|
|
tableName: string,
|
|
) {
|
|
const headers = columns.map((c) => c.name);
|
|
let content: string;
|
|
let mime: string;
|
|
|
|
switch (format) {
|
|
case "json": {
|
|
const jsonRows = rows.map((row) => {
|
|
const obj: Record<string, unknown> = {};
|
|
columns.forEach((c, i) => { obj[c.name] = row[i] ?? null; });
|
|
return obj;
|
|
});
|
|
content = JSON.stringify(jsonRows, null, 2);
|
|
mime = "application/json";
|
|
break;
|
|
}
|
|
case "csv": {
|
|
const csvRows = [headers.map((h) => `"${h.replace(/"/g, '""')}"`).join(",")];
|
|
for (const row of rows) {
|
|
csvRows.push(
|
|
row.map((cell) => {
|
|
const s = cell === null || cell === undefined ? "" : String(cell);
|
|
return `"${s.replace(/"/g, '""')}"`;
|
|
}).join(","),
|
|
);
|
|
}
|
|
content = csvRows.join("\n");
|
|
mime = "text/csv";
|
|
break;
|
|
}
|
|
case "sql": {
|
|
const lines = [`-- ${tableName}`];
|
|
for (const row of rows) {
|
|
const vals = row.map((cell) =>
|
|
cell === null ? "NULL"
|
|
: typeof cell === "number" ? String(cell)
|
|
: `'${String(cell).replace(/'/g, "''")}'`,
|
|
);
|
|
lines.push(`INSERT INTO ${tableName} (${headers.join(", ")}) VALUES (${vals.join(", ")});`);
|
|
}
|
|
content = lines.join("\n");
|
|
mime = "application/sql";
|
|
break;
|
|
}
|
|
case "md": {
|
|
const mdRows = [`| ${headers.join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`];
|
|
for (const row of rows) {
|
|
mdRows.push(`| ${row.map((cell) => cell === null ? "*NULL*" : String(cell)).join(" | ")} |`);
|
|
}
|
|
content = mdRows.join("\n");
|
|
mime = "text/markdown";
|
|
break;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
|
|
const blob = new Blob([content], { type: mime });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${tableName}.${format === "md" ? "md" : format}`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
} |