"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) => (
))}
{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 };
}
}