"use client"; /** * ButtonsPanel — database action buttons (PRODUCT-SPEC: "buttons" property * type / DatabaseButtonDef). * * - : renders the view's `config.buttons` row and executes * each action on the current record / selection: * update → PATCH the target property (selected records, else the * current record); * create → POST a new record from the template; * navigate → open a page/url; * formula → run a formula (deferred to the formula engine, shown as a * placeholder action in this phase). * - : dialog to add/remove/rename/reorder buttons and pick * their action + target property/value. * * Buttons live on the *view* config, so saving patches the view. */ import { useState } from "react"; import { useRouter } from "next/navigation"; import { ArrowDown, ArrowUp, Loader2, Play, Plus, Save, Trash2, X, Zap } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "@/stores/ui-store"; /** Toast adapter: call sites use variant:"destructive"; core store uses kind. */ const notify = (t: { title: string; description?: string; variant?: string }) => toast(t.variant === "destructive" ? "error" : "success", t.title, t.description); import { DatabaseButtonDef, DatabaseProperty, DatabaseView, RecordDto, RecordValue, useCreateRecord, useUpdateRecord, useUpdateView } from "@/hooks/use-database"; // --------------------------------------------------------------------------- // ButtonsBar (display + run) // --------------------------------------------------------------------------- export interface DatabaseButtonsProps { databaseId: string; view: DatabaseView; properties: DatabaseProperty[]; /** The record currently focused (record panel open), if any. */ record?: RecordDto | null; /** Records selected in the active view. */ selected?: RecordDto[]; onOpenEditor: () => void; } export function DatabaseButtons({ databaseId, view, properties, record, selected = [], onOpenEditor }: DatabaseButtonsProps) { const router = useRouter(); const updateRecord = useUpdateRecord(databaseId); const createRecord = useCreateRecord(databaseId); const buttons: DatabaseButtonDef[] = (view.config.buttons ?? []) as DatabaseButtonDef[]; const execute = (button: DatabaseButtonDef) => { if (button.prompt) { const ok = window.confirm(button.prompt); if (!ok) return; } switch (button.action.type) { case "navigate": if (button.action.pageId) router.push(`/p/${button.action.pageId}`); else if (button.action.url) window.open(button.action.url, "_blank", "noopener,noreferrer"); return; case "create": { createRecord.mutate( { properties: { ...(button.action.template ?? {}) } }, { onSuccess: () => notify({ title: "Record created" }), onError: (e) => notify({ title: "Create failed", description: (e as Error).message, variant: "destructive" }) } ); return; } case "update": { const propId = button.action.propertyId; const value: RecordValue = button.action.value; const targets = selected.length ? selected : record ? [record] : []; if (targets.length === 0) { notify({ title: "No record selected", description: "Select a row or open a record first.", variant: "destructive" }); return; } targets.forEach((r) => updateRecord.mutate({ recordId: r.id, version: r.version, properties: { [propId]: value } }) ); notify({ title: `Updated ${targets.length} record(s)` }); return; } case "formula": notify({ title: "Formula buttons arrive in a later phase" }); return; } }; if (buttons.length === 0) return null; return (
{buttons.map((b, i) => ( ))}
); } // --------------------------------------------------------------------------- // ButtonsEditor // --------------------------------------------------------------------------- export interface ButtonsEditorProps { databaseId: string; view: DatabaseView; properties: DatabaseProperty[]; onClose: () => void; } interface DraftButton { name: string; icon?: string; prompt?: string; actionType: DatabaseButtonDef["action"]["type"]; propertyId?: string; valueText?: string; } export function ButtonsEditor({ databaseId, view, properties, onClose }: ButtonsEditorProps) { const updateView = useUpdateView(databaseId); const [drafts, setDrafts] = useState(() => ((view.config.buttons ?? []) as DatabaseButtonDef[]).map((b) => ({ name: b.name, icon: b.icon, prompt: b.prompt, actionType: b.action.type, propertyId: b.action.type === "update" ? b.action.propertyId : undefined })) ); const [error, setError] = useState(null); const setDraft = (i: number, patch: Partial) => setDrafts((d) => d.map((x, idx) => (idx === i ? { ...x, ...patch } : x))); const remove = (i: number) => setDrafts((d) => d.filter((_, idx) => idx !== i)); const move = (i: number, dir: -1 | 1) => setDrafts((d) => { const j = i + dir; if (j < 0 || j >= d.length) return d; const copy = [...d]; [copy[i], copy[j]] = [copy[j], copy[i]]; return copy; }); const settable = properties.filter((p) => p.type !== "button" && p.type !== "created_time" && p.type !== "created_by" && p.type !== "last_edited_time" && p.type !== "last_edited_by" && p.type !== "unique_id"); const save = () => { const buttons: DatabaseButtonDef[] = drafts.map((d, i) => { const base: DatabaseButtonDef = { name: d.name || `Button ${i + 1}`, icon: d.icon, prompt: d.prompt || undefined, action: { type: "formula" } }; switch (d.actionType) { case "update": base.action = { type: "update", propertyId: d.propertyId ?? settable[0]?.id ?? "", value: parseValue(settable.find((p) => p.id === d.propertyId), d.valueText ?? "") }; break; case "create": base.action = { type: "create", template: {} }; break; case "navigate": base.action = { type: "navigate" }; break; case "formula": default: base.action = { type: "formula" }; break; } return base; }); updateView.mutate( { viewId: view.id, config: { ...view.config, buttons } }, { onSuccess: () => { notify({ title: "Buttons saved" }); onClose(); }, onError: (e) => setError((e as Error).message) } ); }; return (
Buttons
{drafts.length === 0 && (

No buttons yet. Add one below.

)} {drafts.map((d, i) => (
setDraft(i, { name: e.target.value })} />
{d.actionType === "update" && ( <> setDraft(i, { valueText: e.target.value })} /> )}
setDraft(i, { prompt: e.target.value })} />
))} {error &&

{error}

}
); } // ---- helpers ---------------------------------------------------------------- function parseValue(prop: DatabaseProperty | undefined, text: string): RecordValue { if (!prop) return { text }; switch (prop.type) { case "checkbox": return { value: text.toLowerCase() === "true" || text === "1" || text === "yes" || text === "on" }; case "number": return { value: Number(text) || 0 }; case "select": case "status": return { optionId: text || undefined }; case "multi_select": return { optionIds: text ? [text] : [] }; case "date": return { start: text ? new Date(text).toISOString() : null, end: null }; default: return { text }; } }