"use client"; import { useRef, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { Search, Filter, ArrowUpDown, Layers, SlidersHorizontal, Plus, MoreHorizontal, Download, Upload, Copy, LayoutTemplate, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Popover, PopoverTrigger, PopoverContent, Menu, MenuTrigger, MenuContent, MenuItem, MenuSeparator } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; import { api } from "@/lib/client/api"; import { cn } from "@/lib/utils"; import { useDb } from "./hooks"; import { FilterBuilder, SortBuilder, describeFilter } from "./FilterBuilder"; import { PropertyManager } from "./PropertyManager"; import { Sel } from "./meta"; export function Toolbar({ search, setSearch, onShowTemplates }: { search: string; setSearch: (s: string) => void; onShowTemplates: () => void }) { const ctx = useDb(); const qc = useQueryClient(); const r = useRouter(); const [open, setOpen] = useState(null); const [showSearch, setShowSearch] = useState(!!search); const file = useRef(null); const nFilters = describeFilter(ctx.view.config.filter); const nSorts = ctx.view.config.sorts?.length ?? 0; const groupable = ctx.props.filter((p) => ["select", "status", "checkbox"].includes(p.type)); const canGroup = ["board", "table", "list"].includes(ctx.view.type); const gb = ctx.view.config.groupBy ?? ""; const importCsv = async (f: File | undefined) => { if (!f) return; const fd = new FormData(); fd.append("file", f); try { const res = await fetch(`/api/databases/${ctx.db.id}/import`, { method: "POST", body: fd, credentials: "same-origin" }); const j = await res.json(); if (!res.ok) throw new Error(j.error); toast(`Imported ${j.created} rows`, { description: j.properties.length ? `New properties: ${j.properties.join(", ")}` : undefined, tone: "success" }); ctx.refresh(); } catch (e: any) { toast("Import failed", { description: e?.message, tone: "danger" }); } finally { if (file.current) file.current.value = ""; } }; const pill = (active: boolean) => cn("flex h-8 items-center gap-1 rounded-md px-2 text-[12px] text-fg-muted hover:bg-bg-sunk hover:text-fg [&_svg]:size-3.5", active && "bg-accent-soft text-accent hover:bg-accent-soft hover:text-accent"); return (
{showSearch ?
setSearch(e.target.value)} placeholder="Search rows…" className="w-28 bg-transparent text-[12px] outline-none sm:w-40" onKeyDown={(e) => { if (e.key === "Escape") { setSearch(""); setShowSearch(false); } }} />
: } setOpen(o ? "filter" : null)}> setOpen(o ? "sort" : null)}> {canGroup && setOpen(o ? "group" : null)}>
Group by
ctx.patchView({ groupBy: e.target.value || null })} className="w-full">{groupable.map((p) => )}{!groupable.length &&
Add a select, status or checkbox property to group.
}
} setOpen(o ? "props" : null)}> setOpen(null)} /> window.open(`/api/databases/${ctx.db.id}/query?format=csv&viewId=${ctx.view.id}`, "_blank")}> Export CSV (this view) file.current?.click()}> Import CSV… { const d = await api(`/api/databases/${ctx.db.id}/duplicate`, { method: "POST", json: { withRows: false } }); qc.invalidateQueries({ queryKey: ["tree"] }); toast("Database structure duplicated"); r.push(`/p/${d.pageId}`); }}> Duplicate (structure) { const d = await api(`/api/databases/${ctx.db.id}/duplicate`, { method: "POST", json: { withRows: true } }); qc.invalidateQueries({ queryKey: ["tree"] }); toast("Database duplicated with rows"); r.push(`/p/${d.pageId}`); }}> Duplicate with rows { await api(`/api/databases/${ctx.db.id}`, { method: "PATCH", json: { isTemplate: !ctx.db.isTemplate } }); ctx.refresh(); toast(ctx.db.isTemplate ? "No longer a template" : "Marked as template"); }}> {ctx.db.isTemplate ? "Unmark template" : "Mark as template"} Apply a built-in template… importCsv(e.target.files?.[0])} />
); }