SPB Git
5.4 KB · 56 lines tsx
1"use client";2import { useEffect, useMemo, useState } from "react";3import { useQueryClient } from "@tanstack/react-query";4import { Database as DbIcon } from "lucide-react";5import { Spinner, Empty } from "@/components/ui/primitives";6import { toast } from "@/components/ui/toast";7import { api } from "@/lib/client/api";8import type { RowOut, SelectOption, View } from "@/lib/databases/types";9import { sortedProps, newId, pickColor } from "@/lib/databases/values";10import { DbContext, useDatabase, useRows, useRowMutations, useViewMutations, usePropertyMutations, useMedia, type DbCtx } from "./hooks";11import { ViewTabs } from "./ViewTabs";12import { Toolbar } from "./Toolbar";13import { TemplateChooser } from "./TemplateChooser";14import { RowPeek } from "./RowPeek";15import { TableView } from "./views/TableView";16import { BoardView } from "./views/BoardView";17import { CalendarView } from "./views/CalendarView";18import { GalleryView } from "./views/GalleryView";19import { TimelineView } from "./views/TimelineView";20import { ListView } from "./views/ListView";2122const VIEWS = { table: TableView, board: BoardView, calendar: CalendarView, gallery: GalleryView, timeline: TimelineView, list: ListView } as const;23export function DatabaseView({ pageId }: { pageId: string }) {24  const qc = useQueryClient(); const { data: db, isLoading, error } = useDatabase(pageId);25  const [viewId, setViewId] = useState<string | null>(null); const [search, setSearch] = useState(""); const [openRow, setOpenRow] = useState<string | null>(null); const [skipTemplates, setSkipTemplates] = useState(false); const [showTemplates, setShowTemplates] = useState(false);26  const isMobile = useMedia("(max-width: 639px)");27  const view: View | undefined = useMemo(() => db?.views.find((v) => v.id === viewId) ?? db?.views.find((v) => v.isDefault) ?? db?.views[0], [db, viewId]);28  useEffect(() => { if (db && !viewId) { const saved = localStorage.getItem(`db-view-${db.id}`); if (saved && db.views.some((v) => v.id === saved)) setViewId(saved); } }, [db, viewId]);29  const select = (id: string) => { setViewId(id); if (db) localStorage.setItem(`db-view-${db.id}`, id); };30  const rowsQ = useRows(db?.id, view?.id, search); const rm = useRowMutations(db); const vm = useViewMutations(db, pageId); const pm = usePropertyMutations(db, pageId);31  const props = useMemo(() => (db ? sortedProps(db.properties) : []), [db]);32  const ctx: DbCtx | null = useMemo(() => db && view ? {33    db, props, visibleProps: props.filter((p) => p.type === "title" || !(view.config.hidden ?? []).includes(p.id)), view, rows: rowsQ.data?.rows ?? [], total: rowsQ.data?.total ?? 0, loading: rowsQ.isLoading, isMobile, search,34    setCell: (rowId, values) => rm.update.mutate({ rowId, values }),35    addRow: async (values, title) => { const r = await rm.create.mutateAsync({ values, title }); if (view.type !== "table") setOpenRow(r.id); return r; },36    removeRows: async (ids) => { await rm.remove.mutateAsync(ids); },37    open: setOpenRow, patchView: (config) => vm.patchView.mutate({ id: view.id, config }), refresh: () => { qc.invalidateQueries({ queryKey: ["db", pageId] }); qc.invalidateQueries({ queryKey: ["db-rows", db.id] }); },38    regenerate: async (rowId, propertyId) => { try { await api(`/api/databases/${db.id}/ai`, { method: "POST", json: { rowId, propertyId } }); qc.setQueriesData<{ rows: RowOut[] }>({ queryKey: ["db-rows", db.id] }, (old: any) => old ? { ...old, rows: old.rows.map((r: RowOut) => (r.id === rowId ? { ...r, values: { ...r.values, [propertyId]: { pending: true, at: new Date().toISOString() } } } : r)) } : old); } catch (e: any) { toast("Could not queue AI job", { description: e?.message, tone: "danger" }); } },39    addOption: async (propId, name) => { const p = props.find((x) => x.id === propId)!; const o: SelectOption = { id: newId(), name, color: pickColor((p.config.options ?? []).length), ...(p.type === "status" ? { group: "todo" as const } : {}) }; await pm.update.mutateAsync({ id: propId, config: { ...p.config, options: [...(p.config.options ?? []), o] } }); return o; },40  } : null, [db, props, view, rowsQ.data, rowsQ.isLoading, isMobile, search]); // eslint-disable-line react-hooks/exhaustive-deps41  if (isLoading) return <div className="flex items-center gap-2 py-8 text-[13px] text-fg-muted"><Spinner /> Loading database…</div>;42  if (error || !db || !view || !ctx) return <Empty icon={<DbIcon />} title="Database unavailable" hint={(error as any)?.message} />;43  const fresh = db.properties.length <= 1 && (rowsQ.data ? rowsQ.data.total === 0 : false);44  const V = VIEWS[view.type] ?? TableView;45  return (46    <DbContext.Provider value={ctx}>47      <div className="space-y-2">48        <div className="flex flex-wrap items-center gap-x-2 gap-y-1 border-b border-line pb-1"><ViewTabs db={db} view={view} onSelect={select} /><div className="ml-auto"><Toolbar search={search} setSearch={setSearch} onShowTemplates={() => setShowTemplates(true)} /></div></div>49        {(showTemplates || (fresh && !skipTemplates)) ? <TemplateChooser dbId={db.id} onDone={() => { setShowTemplates(false); setSkipTemplates(true); ctx.refresh(); qc.invalidateQueries({ queryKey: ["tree"] }); }} onSkip={() => { setShowTemplates(false); setSkipTemplates(true); }} /> : null}50        {!(fresh && !skipTemplates) && <div className={rowsQ.isFetching && !rowsQ.isLoading ? "opacity-90 transition-opacity" : ""}><V /></div>}51      </div>52      <RowPeek rowId={openRow} onClose={() => setOpenRow(null)} />53    </DbContext.Provider>54  );55}56
Files · main
Menu

Theme