/** Query History (browse / paste / delete) and Import Data from File dialogs. */ import { useEffect, useMemo, useState } from "react"; import { call, commands, type DbHistoryEntry } from "@/platform/ipc"; import { editorsFor } from "@/editor/api"; import { Checkbox, Dialog, EmptyState, IconButton, Input, List, Select, T } from "@/ui"; import { notifications } from "@/workbench/contributions"; import { CodePreview } from "./CodePreview"; import { guessSeparator, importEdits, parseDelimited } from "./model"; import { closeDialog, detailsKey, loadDetails, openConsoleWith, targetLabel, useDatabase } from "./store"; import styles from "./Database.module.css"; import { tx } from "@/platform/i18n"; function when(t: number): string { const d = new Date(t); const p = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; } /** Pastes `text` over the selection of console `path` when it is open, else appends it to the data source's console. */ async function paste(id: string, path: string | null, text: string) { const view = path ? editorsFor(path)[0]?.view : undefined; if (!view) { await openConsoleWith(id, text); return; } const sel = view.state.selection.main; view.dispatch({ changes: { from: sel.from, to: sel.to, insert: text }, selection: { anchor: sel.from, head: sel.from + text.length }, scrollIntoView: true, }); view.focus(); } /** IDEA's ` Query History`: statements newest first, a preview, Paste into the console. */ export function HistoryDialog({ id, path }: { id: string; path: string | null }) { const [entries, setEntries] = useState(null); const [query, setQuery] = useState(""); const [active, setActive] = useState(0); useEffect(() => { void call(commands.dbHistory(id)) .then(setEntries) .catch(() => setEntries([])); }, [id]); const shown = useMemo(() => { const q = query.trim().toLowerCase(); return (entries ?? []).filter((e) => !q || e.text.toLowerCase().includes(q)); }, [entries, query]); const current = shown[Math.min(active, shown.length - 1)]; const remove = async (texts: string[] | null) => { await call(commands.dbRemoveHistory(id, texts)); setEntries((e) => (texts ? (e ?? []).filter((x) => !texts.includes(x.text)) : [])); }; const done = (text: string | undefined) => { closeDialog(); if (text) void paste(id, path, text); }; return ( void remove(null)} /> } buttons={[ { label: "Close", role: "cancel" }, { label: "Paste", role: "default", disabled: !current, onClick: () => done(current?.text) }, ]} >
{ setQuery(e.target.value); setActive(0); }} onKeyDown={(e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(0, Math.min(shown.length - 1, a + (e.key === "ArrowDown" ? 1 : -1)))); } else if (e.key === "Enter") done(current?.text); }} /> current && void remove([current.text])} />
e.text} activeIndex={Math.min(active, shown.length - 1)} onActiveChange={setActive} onActivate={(e) => done(e.text)} renderItem={(e) => ( {e.text.replace(/\s+/g, " ")} )} empty={} />
); } const SEPARATORS = [ { value: ",", label: "Comma" }, { value: "\t", label: "Tab" }, { value: ";", label: "Semicolon" }, { value: "|", label: "Pipe" }, ]; /** Import Data from File: a CSV / TSV file inserted into a table in one transaction. */ export function ImportDialog({ id, schema, table }: { id: string; schema: string; table: string }) { const [file, setFile] = useState(null); const [text, setText] = useState(""); const [sep, setSep] = useState(","); const [header, setHeader] = useState(true); const [emptyAsNull, setEmptyAsNull] = useState(true); const [busy, setBusy] = useState(false); const details = useDatabase((s) => s.details[detailsKey(id, schema, table)]); useEffect(() => { if (details?.status !== "ok") void loadDetails(id, schema, table); // Once per table. // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, schema, table]); const columns = useMemo(() => (details?.status === "ok" ? details.value.columns.map((c) => c.name) : []), [details]); const rows = useMemo(() => (text ? parseDelimited(text, sep) : []), [text, sep]); const { edits, mapped } = useMemo( () => importEdits(rows, columns, header, emptyAsNull), [rows, columns, header, emptyAsNull], ); const choose = async () => { try { const { open } = await import("@tauri-apps/plugin-dialog"); const f = await open({ multiple: false, directory: false, title: "Import Data from File", filters: [{ name: "CSV / TSV", extensions: ["csv", "tsv", "txt"] }], }); if (typeof f !== "string") return; const content = await call(commands.fsReadText(f)); setFile(f); setText(content); setSep(guessSeparator(content)); } catch (e) { notifications().notify({ level: "error", title: "Cannot read the file", body: e instanceof Error ? e.message : String(e), }); } }; const run = async () => { setBusy(true); try { const n = await call(commands.dbApplyEdits(id, schema, table, edits)); notifications().notify({ level: "info", title: `${table}: ${n} row(s) imported`, key: "db-import" }); closeDialog(); } catch (e) { notifications().notify({ level: "error", title: "Nothing was imported", body: e instanceof Error ? e.message : String(e), key: "db-import", }); } finally { setBusy(false); } }; const preview = rows.slice(0, 8); return ( void run(), }, ]} >
File:
void choose()} />
Separator: