diff --git a/src/components/gui/tabs/query-tab.tsx b/src/components/gui/tabs/query-tab.tsx index 310772c4..99088093 100644 --- a/src/components/gui/tabs/query-tab.tsx +++ b/src/components/gui/tabs/query-tab.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { identify } from "sql-query-identifier"; import { LucideGrid, + LucideLoader, LucideMessageSquareWarning, LucidePlay, LucideSave, @@ -32,11 +33,15 @@ import { useSchema } from "@/context/schema-provider"; interface QueryWindowProps { initialCode?: string; initialName: string; + initialSavedKey?: string; + initialNamespace?: string; } export default function QueryWindow({ initialCode, initialName, + initialSavedKey, + initialNamespace, }: QueryWindowProps) { const { schema } = useAutoComplete(); const { databaseDriver, docDriver } = useDatabaseDriver(); @@ -52,6 +57,12 @@ export default function QueryWindow({ const [name, setName] = useState(initialName); const { renameCurrentTab } = useTabsContext(); + const [namespaceName, setNamespaceName] = useState( + initialNamespace ?? "Unsaved Query" + ); + const [savedKey, setSavedKey] = useState(initialSavedKey); + const [saveLoading, setSaveLoading] = useState(false); + const onRunClicked = (all = false) => { const statements = identify(code, { dialect: "sqlite", @@ -113,12 +124,32 @@ export default function QueryWindow({ const onSaveQuery = useCallback(() => { if (docDriver) { - docDriver.createDoc("sql", docDriver.getCurrentNamespace(), { - content: code, - name: name || "Unnamed Query", - }); + setSaveLoading(true); + if (savedKey) { + docDriver + .updateDoc(savedKey, { + content: code, + name: name || "Unnamed Query", + }) + .finally(() => { + setSaveLoading(false); + }); + } else { + docDriver + .createDoc("sql", docDriver.getCurrentNamespace(), { + content: code, + name: name || "Unnamed Query", + }) + .then((d) => { + setSavedKey(d.id); + setNamespaceName(d.namespace.name); + }) + .finally(() => { + setSaveLoading(false); + }); + } } - }, [docDriver, code, name]); + }, [docDriver, code, name, savedKey]); const windowTab = useMemo(() => { return ( @@ -162,7 +193,7 @@ export default function QueryWindow({
- Unsaved Query / + {namespaceName} /
@@ -227,8 +258,17 @@ export default function QueryWindow({
Col {columnNumber + 1}
-
diff --git a/src/components/gui/tabs/saved-doc-tab.tsx b/src/components/gui/tabs/saved-doc-tab.tsx index 32a5234a..cfb3bddf 100644 --- a/src/components/gui/tabs/saved-doc-tab.tsx +++ b/src/components/gui/tabs/saved-doc-tab.tsx @@ -29,18 +29,33 @@ function mapDoc(data: SavedDocData): ListViewItem { }; } -function SavedDocNamespaceDocList({ namespaceId }: { namespaceId?: string }) { +function SavedDocNamespaceDocList({ + namespaceData, +}: { + namespaceData?: SavedDocNamespace; +}) { const { docDriver } = useDatabaseDriver(); const [selected, setSelected] = useState(); const [docList, setDocList] = useState[]>([]); useEffect(() => { + const namespaceId = namespaceData?.id; + if (docDriver && namespaceId) { - docDriver?.getDocs(namespaceId).then((r) => { + docDriver.getDocs(namespaceId).then((r) => { setDocList(r.map(mapDoc)); }); + + const onDocChange = () => { + docDriver?.getDocs(namespaceId).then((r) => { + setDocList(r.map(mapDoc)); + }); + }; + + docDriver.addChangeListener(onDocChange); + return () => docDriver.removeChangeListener(onDocChange); } - }, [docDriver, namespaceId]); + }, [docDriver, namespaceData]); return ( ([]); useEffect(() => { - docDriver?.getNamespaces().then((r) => { - setNamespaceList(r.map(mapNamespace)); - const firstNamespaceId = r[0].id; - setSelectedNamespace(firstNamespaceId); - docDriver.setCurrentNamespace(firstNamespaceId); - }); + if (docDriver) { + docDriver.getNamespaces().then((r) => { + setNamespaceList(r.map(mapNamespace)); + const firstNamespaceId = r[0].id; + setSelectedNamespace(firstNamespaceId); + docDriver.setCurrentNamespace(firstNamespaceId); + }); + } }, [docDriver]); return ( @@ -88,7 +106,11 @@ export default function SavedDocTab() {
- + n.key === selectedNamespace)?.data + } + />
); diff --git a/src/drivers/saved-doc/remote-saved-doc.ts b/src/drivers/saved-doc/remote-saved-doc.ts index 1002b549..7090886f 100644 --- a/src/drivers/saved-doc/remote-saved-doc.ts +++ b/src/drivers/saved-doc/remote-saved-doc.ts @@ -19,6 +19,7 @@ import { export default class RemoteSavedDocDriver implements SavedDocDriver { protected databaseId: string; protected currentNamespace: string = ""; + protected cb: (() => void)[] = []; constructor(databaseId: string) { this.databaseId = databaseId; @@ -40,24 +41,30 @@ export default class RemoteSavedDocDriver implements SavedDocDriver { return removeDocNamespace(this.databaseId, id); } - createDoc( + async createDoc( type: SavedDocType, namespace: string, data: SavedDocInput ): Promise { - return createSavedDoc(this.databaseId, namespace, type, data); + const r = await createSavedDoc(this.databaseId, namespace, type, data); + this.triggerChange(); + return r; } getDocs(namespaceId: string): Promise { return getSavedDocList(this.databaseId, namespaceId); } - updateDoc(id: string, data: SavedDocInput): Promise { - return updateSavedDoc(this.databaseId, id, data); + async updateDoc(id: string, data: SavedDocInput): Promise { + const r = updateSavedDoc(this.databaseId, id, data); + this.triggerChange(); + return r; } - removeDoc(id: string): Promise { - return removeSavedDoc(this.databaseId, id); + async removeDoc(id: string): Promise { + const r = await removeSavedDoc(this.databaseId, id); + this.triggerChange(); + return r; } setCurrentNamespace(id: string) { @@ -67,4 +74,16 @@ export default class RemoteSavedDocDriver implements SavedDocDriver { getCurrentNamespace(): string { return this.currentNamespace; } + + addChangeListener(cb: () => void): void { + this.cb.push(cb); + } + + removeChangeListener(cb: () => void): void { + this.cb = this.cb.filter((c) => c !== cb); + } + + protected triggerChange() { + this.cb.forEach((c) => c()); + } } diff --git a/src/drivers/saved-doc/saved-doc-actions.ts b/src/drivers/saved-doc/saved-doc-actions.ts index 6123f8d5..20c8ace7 100644 --- a/src/drivers/saved-doc/saved-doc-actions.ts +++ b/src/drivers/saved-doc/saved-doc-actions.ts @@ -7,7 +7,7 @@ import { SavedDocType, } from "./saved-doc-driver"; import { getDatabaseWithAuth } from "@/lib/with-database-ops"; -import { eq } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; import { dbDoc, dbDocNamespace } from "@/db/schema-doc"; import { generateId } from "lucia"; import { ApiError } from "@/lib/api-error"; @@ -163,10 +163,14 @@ export async function getSavedDocList( databaseId: string, namespaceId: string ): Promise { - const { db } = await getNamespaceWithAuth(databaseId, namespaceId); + const { db, namespaceData } = await getNamespaceWithAuth( + databaseId, + namespaceId + ); const docList = await db.query.dbDoc.findMany({ where: eq(dbDoc.namespaceId, namespaceId), + orderBy: desc(dbDoc.createdAt), }); return docList.map((item) => { @@ -177,6 +181,10 @@ export async function getSavedDocList( id: item.id ?? "", name: item.name ?? "", type: (item.type ?? "sql") as SavedDocType, + namespace: { + id: namespaceData.id, + name: namespaceData.name ?? "", + }, }; }); } @@ -214,6 +222,10 @@ export async function createSavedDoc( updatedAt: now, name: input.name, type: type, + namespace: { + id: namespaceData.id, + name: namespaceData.name ?? "", + }, }; } @@ -230,7 +242,10 @@ export async function updateSavedDoc( docId: string, input: SavedDocInput ): Promise { - const { db, docData } = await getDocWithAuth(databaseId, docId); + const { db, docData, namespaceData } = await getDocWithAuth( + databaseId, + docId + ); const now = Math.floor(Date.now() / 1000); await db @@ -249,5 +264,9 @@ export async function updateSavedDoc( updatedAt: now, name: input.name, type: (docData.type ?? "sql") as SavedDocType, + namespace: { + id: namespaceData.id, + name: namespaceData.name ?? "", + }, }; } diff --git a/src/drivers/saved-doc/saved-doc-driver.ts b/src/drivers/saved-doc/saved-doc-driver.ts index 3164fe25..0af4529c 100644 --- a/src/drivers/saved-doc/saved-doc-driver.ts +++ b/src/drivers/saved-doc/saved-doc-driver.ts @@ -18,6 +18,7 @@ export interface SavedDocInput { export interface SavedDocData extends SavedDocInput { id: string; type: SavedDocType; + namespace: { id: string; name: string }; createdAt: number; updatedAt: number; } @@ -41,6 +42,8 @@ export abstract class SavedDocDriver { abstract removeDoc(id: string): Promise; // This is helper to make code easier + abstract addChangeListener(cb: () => void): void; + abstract removeChangeListener(cb: () => void): void; abstract setCurrentNamespace(id: string): void; abstract getCurrentNamespace(): string; } diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index 200f8bc7..e14928d2 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -23,6 +23,7 @@ interface OpenQueryTab { type: "query"; name?: string; saved?: { + namespaceName?: string; key: string; sql: string; }; @@ -91,14 +92,19 @@ function generateTitle(tab: OpenTabsProps) { return tab.name ?? ""; } -function generateComponent(tab: OpenTabsProps) { +function generateComponent(tab: OpenTabsProps, title: string) { if (tab.type === "query") { if (tab.saved) { return ( - + ); } - return ; + return ; } if (tab.type === "table") return ; @@ -126,14 +132,15 @@ export function receiveOpenTabMessage({ return prev; } setSelectedTabIndex(prev.length); + const title = generateTitle(newTab); return [ ...prev, { icon: generateIconFromTab(newTab), - title: generateTitle(newTab), + title, key, - component: generateComponent(newTab), + component: generateComponent(newTab, title), }, ]; });