From 6224ddd8127a0ab94b07ebb2a93dac56d7180aea Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:05:13 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- .../forms/form-field-defs-dialog.tsx | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/components/forms/form-field-defs-dialog.tsx diff --git a/src/components/forms/form-field-defs-dialog.tsx b/src/components/forms/form-field-defs-dialog.tsx new file mode 100644 index 0000000..d13f3cf --- /dev/null +++ b/src/components/forms/form-field-defs-dialog.tsx @@ -0,0 +1,252 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Switch } from "@/components/ui/switch"; +import { Trash2, Plus, ArrowUp, ArrowDown } from "lucide-react"; +import type { FormFieldDef, FormFieldType } from "@/lib/forms-shared"; +import { toast } from "sonner"; + +const TYPE_LABEL: Record = { + text: "Text", + textarea: "Long text", + number: "Number", + date: "Date", + dropdown: "Dropdown", + county: "Florida County", + client: "Client / Association", + homeowner: "Homeowner", +}; + +function slug(s: string): string { + return s + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .slice(0, 40); +} + +export function FormFieldDefsDialog({ + open, + onOpenChange, + fields, + onChange, +}: { + open: boolean; + onOpenChange: (v: boolean) => void; + fields: FormFieldDef[]; + onChange: (next: FormFieldDef[]) => void; +}) { + const [draft, setDraft] = useState(fields); + + // re-sync when reopened + const reset = () => setDraft(fields); + + const update = (i: number, patch: Partial) => { + setDraft((prev) => prev.map((f, idx) => (idx === i ? { ...f, ...patch } : f))); + }; + const remove = (i: number) => setDraft((prev) => prev.filter((_, idx) => idx !== i)); + const move = (i: number, dir: -1 | 1) => { + setDraft((prev) => { + const next = [...prev]; + const j = i + dir; + if (j < 0 || j >= next.length) return prev; + [next[i], next[j]] = [next[j], next[i]]; + return next; + }); + }; + const add = () => { + const base = "field"; + let n = 1; + const used = new Set(draft.map((d) => d.key)); + while (used.has(`${base}_${n}`)) n++; + setDraft((prev) => [ + ...prev, + { key: `${base}_${n}`, label: `Field ${n}`, type: "text", required: false }, + ]); + }; + + const save = () => { + // de-dupe + slug keys + const seen = new Set(); + for (const f of draft) { + if (!f.label.trim()) { + toast.error("Each field needs a label"); + return; + } + const key = slug(f.key || f.label); + if (!key) { + toast.error(`Invalid key for "${f.label}"`); + return; + } + if (seen.has(key)) { + toast.error(`Duplicate field key: ${key}`); + return; + } + seen.add(key); + f.key = key; + } + onChange(draft); + onOpenChange(false); + toast.success("Fields saved"); + }; + + return ( + { + onOpenChange(v); + if (v) reset(); + }} + > + + + Form fillable fields + + Define inputs that will appear when generating this form. Insert them in the body using{" "} + {"{{field.key}}"}. + + + +
+ {draft.length === 0 && ( +

+ No fields yet. Add one to start collecting input. +

+ )} + {draft.map((f, i) => ( +
+
+
+ + update(i, { label: e.target.value })} + /> +
+
+ + update(i, { key: e.target.value })} + onBlur={(e) => update(i, { key: slug(e.target.value || f.label) })} + /> +
+
+ + +
+
+ + + +
+
+ + {f.type === "dropdown" && ( +
+ +