* [P1-T1] feat(backup): MySQL/SQLite backup models + db_type on SyncOptions (Task 1.1) * [P1-T2] feat: enable tools(mysql,sqlite) + tableManagement(sqlite) + add fflate (Task 1.2) * [P1-T3] feat(cancel): CancelHandle enum + CancelRegistry (Task 1.3) * [P2-T1] feat(export): hand-rolled XLSX writer with inline-string cells (Task 2.1) * [P2-T2] feat(backup): SQLite .dump/restore/sync core, fail-closed virtual tables (Task 2.2) * [P2-T3] feat(backup): MySQL dump/restore/sync arg builders + tool resolution (Task 2.3) * [P2-T4] feat(settings): pure import validator + SettingsExport envelope + Store.apply_settings (Task 2.4) * [P2-T5] feat(table-editor): SQLite create/diff/rebuild SQL generation, fail-closed AUTOINCREMENT (Task 2.5) * [P3-T1] feat(commands): MySQL/SQLite backup + settings export/import commands + wrappers (Task 3.1) * [P3-T2] feat(cancel): capture cancel primitives at connect; cancel_query command; SQLite interrupt test (Task 3.2) * [P3-T3] feat(table-editor): SQLite object-change dispatch + execute_change Ddl/RebuildTable (Task 3.3) * [P4-T1] feat(tools): DB-aware backup/restore/sync pages (Task 4.1) * [P4-T2] feat(export): xlsx export in grid toolbar + overflow menu (Task 4.2) * [P4-T3] feat(query): cancel button wired to cancelQuery (Task 4.3) * [P4-T4] feat(settings): export/import buttons + validation gate (Task 4.4) * [P4-T5] fix(ui): gate macOS overlay drag strip to macOS only (Task 4.5) * [P4-T6] feat(table-editor): SQLite Create/Edit Table mode (Task 4.6) * [P5-T1] chore: bump 0.7.7 -> 0.7.8 + README/AGENTS/ROADMAP status (Task 5.1) * [P5-T2] build(release): bundle mariadb-dump + mariadb client (system-first fallback) (Task 5.2) * fix(cancel): propagate cancellations past wrapped->raw fallback (SQLite/PG/MySQL) + MySQL CONNECTION_ID cast * fix(export): Excel export from overflow menu did nothing + add export success/error toasts * fix(export): tree kebab export fetches table data when rows not loaded * docs(readme): surface v0.7.8 features (MySQL/SQLite backup-sync, Excel export, query cancel, SQLite table editor, settings import/export)
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { buildXlsx } from "./xlsx";
|
|
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 "xlsx": {
|
|
const bytes = buildXlsx(rows, columns);
|
|
const blob = new Blob([bytes], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${tableName}.xlsx`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
return;
|
|
}
|
|
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);
|
|
} |