strapi-plugin-form-builder-cms 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1429 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useFetchClient, Page } from "@strapi/strapi/admin";
3
+ import { useNavigate, useParams, Routes, Route } from "react-router-dom";
4
+ import { useState, useEffect, useCallback } from "react";
5
+ import { Box, Flex, Typography, Button, Table, Thead, Tr, Th, Tbody, Td, Badge, IconButton, Dialog, Field, Textarea, TextInput, SingleSelect, SingleSelectOption, Modal, Loader, Toggle } from "@strapi/design-system";
6
+ import { Plus, Pencil, Eye, Duplicate, Trash, Drag, Check, ArrowLeft } from "@strapi/icons";
7
+ import { P as PLUGIN_ID } from "./index-Cn0nOObD.mjs";
8
+ import { v4 } from "uuid";
9
+ import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from "@dnd-kit/core";
10
+ import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, arrayMove, useSortable } from "@dnd-kit/sortable";
11
+ import { CSS } from "@dnd-kit/utilities";
12
+ const BASE = "/strapi-plugin-form-builder-cms";
13
+ function useFormsApi() {
14
+ const { get, post, put, del } = useFetchClient();
15
+ return {
16
+ async getForms() {
17
+ const { data } = await get(`${BASE}/forms`);
18
+ return data;
19
+ },
20
+ async getForm(id) {
21
+ const { data } = await get(`${BASE}/forms/${id}`);
22
+ return data;
23
+ },
24
+ async createForm(body) {
25
+ const { data } = await post(`${BASE}/forms`, body);
26
+ return data;
27
+ },
28
+ async updateForm(id, body) {
29
+ const { data } = await put(`${BASE}/forms/${id}`, body);
30
+ return data;
31
+ },
32
+ async deleteForm(id) {
33
+ await del(`${BASE}/forms/${id}`);
34
+ },
35
+ async duplicateForm(id) {
36
+ const { data } = await post(`${BASE}/forms/${id}/duplicate`, {});
37
+ return data;
38
+ },
39
+ async getSubmissions(formId, query = {}) {
40
+ const params = new URLSearchParams(query).toString();
41
+ const { data } = await get(`${BASE}/submissions/${formId}${params ? `?${params}` : ""}`);
42
+ return data;
43
+ },
44
+ async getSubmission(id) {
45
+ const { data } = await get(`${BASE}/submissions/entry/${id}`);
46
+ return data;
47
+ },
48
+ async updateSubmissionStatus(id, status) {
49
+ const { data } = await put(`${BASE}/submissions/${id}/status`, { status });
50
+ return data;
51
+ },
52
+ async deleteSubmission(id) {
53
+ await del(`${BASE}/submissions/${id}`);
54
+ },
55
+ async getStats(formId) {
56
+ const { data } = await get(`${BASE}/submissions/${formId}/stats`);
57
+ return data;
58
+ }
59
+ };
60
+ }
61
+ function FormListPage() {
62
+ const navigate = useNavigate();
63
+ const api = useFormsApi();
64
+ const [forms, setForms] = useState([]);
65
+ const [loading, setLoading] = useState(true);
66
+ const [deleteTarget, setDeleteTarget] = useState(null);
67
+ const load = async () => {
68
+ setLoading(true);
69
+ try {
70
+ const data = await api.getForms();
71
+ setForms(Array.isArray(data) ? data : []);
72
+ } catch (e) {
73
+ console.error(e);
74
+ } finally {
75
+ setLoading(false);
76
+ }
77
+ };
78
+ useEffect(() => {
79
+ load();
80
+ }, []);
81
+ const handleDelete = async () => {
82
+ if (!deleteTarget) return;
83
+ await api.deleteForm(deleteTarget);
84
+ setDeleteTarget(null);
85
+ load();
86
+ };
87
+ const handleDuplicate = async (id) => {
88
+ await api.duplicateForm(id);
89
+ load();
90
+ };
91
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
92
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 6, children: [
93
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "Form Builder" }),
94
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: () => navigate(`/plugins/${PLUGIN_ID}/builder/new`), children: "Create form" })
95
+ ] }),
96
+ loading ? /* @__PURE__ */ jsx(Typography, { children: "Loading..." }) : forms.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: 10, background: "neutral100", borderRadius: "4px", textAlign: "center", children: /* @__PURE__ */ jsx(Typography, { variant: "beta", textColor: "neutral600", children: "No forms yet. Create your first one." }) }) : /* @__PURE__ */ jsxs(Table, { colCount: 5, rowCount: forms.length, children: [
97
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
98
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Title" }) }),
99
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Slug" }) }),
100
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
101
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Created" }) }),
102
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
103
+ ] }) }),
104
+ /* @__PURE__ */ jsx(Tbody, { children: forms.map((form) => /* @__PURE__ */ jsxs(Tr, { children: [
105
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", children: form.title }) }),
106
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: form.slug }) }),
107
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { active: !!form.publishedAt, children: form.publishedAt ? "Published" : "Draft" }) }),
108
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: new Date(form.createdAt).toLocaleDateString() }) }),
109
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
110
+ /* @__PURE__ */ jsx(
111
+ IconButton,
112
+ {
113
+ label: "Edit",
114
+ onClick: () => navigate(`/plugins/${PLUGIN_ID}/builder/${form.id}`),
115
+ children: /* @__PURE__ */ jsx(Pencil, {})
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsx(
119
+ IconButton,
120
+ {
121
+ label: "View submissions",
122
+ onClick: () => navigate(`/plugins/${PLUGIN_ID}/submissions/${form.id}`),
123
+ children: /* @__PURE__ */ jsx(Eye, {})
124
+ }
125
+ ),
126
+ /* @__PURE__ */ jsx(IconButton, { label: "Duplicate", onClick: () => handleDuplicate(form.id), children: /* @__PURE__ */ jsx(Duplicate, {}) }),
127
+ /* @__PURE__ */ jsx(
128
+ IconButton,
129
+ {
130
+ label: "Delete",
131
+ onClick: () => setDeleteTarget(form.id),
132
+ children: /* @__PURE__ */ jsx(Trash, {})
133
+ }
134
+ )
135
+ ] }) })
136
+ ] }, form.id)) })
137
+ ] }),
138
+ deleteTarget && /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: () => setDeleteTarget(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
139
+ /* @__PURE__ */ jsx(Dialog.Header, { children: "Confirm delete" }),
140
+ /* @__PURE__ */ jsx(Dialog.Body, { children: /* @__PURE__ */ jsx(Typography, { children: "Are you sure you want to delete this form?" }) }),
141
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
142
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
143
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: handleDelete, children: "Delete" }) })
144
+ ] })
145
+ ] }) })
146
+ ] });
147
+ }
148
+ const PALETTE_ITEMS = [
149
+ // Basic
150
+ { type: "text", label: "Text", icon: "📝", group: "Basic" },
151
+ { type: "email", label: "Email", icon: "✉️", group: "Basic" },
152
+ { type: "number", label: "Number", icon: "#", group: "Basic" },
153
+ { type: "phone", label: "Phone", icon: "📱", group: "Basic" },
154
+ { type: "textarea", label: "Long text", icon: "📄", group: "Basic" },
155
+ { type: "password", label: "Password", icon: "🔒", group: "Basic" },
156
+ // Selection
157
+ { type: "select", label: "Select", icon: "▼", group: "Selection" },
158
+ { type: "radio", label: "Radio", icon: "◉", group: "Selection" },
159
+ { type: "checkbox", label: "Checkbox", icon: "☑", group: "Selection" },
160
+ { type: "checkbox-group", label: "Checkbox group", icon: "☑☑", group: "Selection" },
161
+ // Advanced
162
+ { type: "date", label: "Date", icon: "📅", group: "Advanced" },
163
+ { type: "time", label: "Time", icon: "🕐", group: "Advanced" },
164
+ { type: "url", label: "URL", icon: "🔗", group: "Advanced" },
165
+ { type: "hidden", label: "Hidden", icon: "👁", group: "Advanced" },
166
+ // Layout
167
+ { type: "heading", label: "Heading", icon: "H", group: "Layout" },
168
+ { type: "paragraph", label: "Paragraph", icon: "¶", group: "Layout" },
169
+ { type: "divider", label: "Divider", icon: "—", group: "Layout" }
170
+ ];
171
+ const GROUPS = ["Basic", "Selection", "Advanced", "Layout"];
172
+ function FieldPalette({ onAdd }) {
173
+ const [hovered, setHovered] = useState(null);
174
+ return /* @__PURE__ */ jsxs(
175
+ Box,
176
+ {
177
+ padding: 4,
178
+ background: "neutral100",
179
+ style: { width: 200, minWidth: 200, overflowY: "auto", alignSelf: "stretch", borderRight: "1px solid var(--strapi-neutral-200)" },
180
+ children: [
181
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 3, children: "FIELDS" }),
182
+ GROUPS.map((group) => /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
183
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral500", marginBottom: 2, children: group }),
184
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 1, alignItems: "stretch", children: PALETTE_ITEMS.filter((i) => i.group === group).map((item) => /* @__PURE__ */ jsx(
185
+ Box,
186
+ {
187
+ padding: 2,
188
+ background: hovered === item.type ? "primary100" : "neutral0",
189
+ hasRadius: true,
190
+ style: {
191
+ cursor: "pointer",
192
+ border: "1px solid var(--strapi-neutral-200)",
193
+ userSelect: "none"
194
+ },
195
+ onClick: () => onAdd(item.type),
196
+ onMouseEnter: () => setHovered(item.type),
197
+ onMouseLeave: () => setHovered(null),
198
+ children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
199
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 14 }, children: item.icon }),
200
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: item.label })
201
+ ] })
202
+ },
203
+ item.type
204
+ )) })
205
+ ] }, group))
206
+ ]
207
+ }
208
+ );
209
+ }
210
+ function SortableFieldRow({
211
+ field,
212
+ selected,
213
+ onSelect,
214
+ onDelete
215
+ }) {
216
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: field.id });
217
+ const style = {
218
+ transform: CSS.Transform.toString(transform),
219
+ transition,
220
+ opacity: isDragging ? 0.5 : 1
221
+ };
222
+ return /* @__PURE__ */ jsx(
223
+ Box,
224
+ {
225
+ ref: setNodeRef,
226
+ padding: 3,
227
+ background: selected ? "primary100" : "neutral0",
228
+ marginBottom: 2,
229
+ onClick: onSelect,
230
+ hasRadius: true,
231
+ style: {
232
+ ...style,
233
+ border: `2px solid ${selected ? "var(--strapi-primary-600)" : "transparent"}`,
234
+ outline: selected ? "none" : "1px solid var(--strapi-neutral-200)",
235
+ outlineOffset: "-1px",
236
+ cursor: "pointer"
237
+ },
238
+ children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
239
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
240
+ /* @__PURE__ */ jsx(
241
+ Box,
242
+ {
243
+ ...attributes,
244
+ ...listeners,
245
+ style: { cursor: "grab", color: "var(--strapi-neutral-400)", padding: "0 4px" },
246
+ onClick: (e) => e.stopPropagation(),
247
+ children: /* @__PURE__ */ jsx(Drag, {})
248
+ }
249
+ ),
250
+ /* @__PURE__ */ jsxs(Box, { children: [
251
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", children: field.label || "(no label)" }),
252
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
253
+ " ",
254
+ "— ",
255
+ field.type
256
+ ] })
257
+ ] })
258
+ ] }),
259
+ /* @__PURE__ */ jsx(
260
+ IconButton,
261
+ {
262
+ label: "Delete field",
263
+ onClick: (e) => {
264
+ e.stopPropagation();
265
+ onDelete();
266
+ },
267
+ variant: "ghost",
268
+ children: /* @__PURE__ */ jsx(Trash, {})
269
+ }
270
+ )
271
+ ] })
272
+ }
273
+ );
274
+ }
275
+ function DropZone({ fields, selectedId, onSelect, onDelete, onReorder }) {
276
+ const sensors = useSensors(
277
+ useSensor(PointerSensor),
278
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
279
+ );
280
+ const handleDragEnd = (event) => {
281
+ const { active, over } = event;
282
+ if (over && active.id !== over.id) {
283
+ const oldIndex = fields.findIndex((f) => f.id === active.id);
284
+ const newIndex = fields.findIndex((f) => f.id === over.id);
285
+ onReorder(arrayMove(fields, oldIndex, newIndex));
286
+ }
287
+ };
288
+ if (fields.length === 0) {
289
+ return /* @__PURE__ */ jsx(
290
+ Box,
291
+ {
292
+ padding: 10,
293
+ background: "neutral100",
294
+ hasRadius: true,
295
+ style: { border: "2px dashed var(--strapi-neutral-300)", textAlign: "center", flex: 1 },
296
+ children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Click on a field in the left panel to add it to the form" })
297
+ }
298
+ );
299
+ }
300
+ return /* @__PURE__ */ jsx(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(SortableContext, { items: fields.map((f) => f.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: fields.map((field) => /* @__PURE__ */ jsx(
301
+ SortableFieldRow,
302
+ {
303
+ field,
304
+ selected: selectedId === field.id,
305
+ onSelect: () => onSelect(field.id),
306
+ onDelete: () => onDelete(field.id)
307
+ },
308
+ field.id
309
+ )) }) }) });
310
+ }
311
+ function LabeledInput({
312
+ label,
313
+ value,
314
+ onChange,
315
+ placeholder,
316
+ size
317
+ }) {
318
+ return /* @__PURE__ */ jsxs(Field.Root, { children: [
319
+ /* @__PURE__ */ jsx(Field.Label, { children: label }),
320
+ /* @__PURE__ */ jsx(
321
+ TextInput,
322
+ {
323
+ value,
324
+ onChange: (e) => onChange(e.target.value),
325
+ placeholder,
326
+ size
327
+ }
328
+ )
329
+ ] });
330
+ }
331
+ function FieldSettingsPanel({ field, onChange }) {
332
+ const update = (patch) => onChange({ ...field, ...patch });
333
+ const addOption = () => {
334
+ const options = [...field.options || [], { label: "", value: "" }];
335
+ update({ options });
336
+ };
337
+ const updateOption = (index, patch) => {
338
+ const options = (field.options || []).map(
339
+ (o, i) => i === index ? { ...o, ...patch } : o
340
+ );
341
+ update({ options });
342
+ };
343
+ const removeOption = (index) => {
344
+ update({ options: (field.options || []).filter((_, i) => i !== index) });
345
+ };
346
+ const addValidation = () => {
347
+ const usedTypes = field.validation.map((r) => r.type);
348
+ const defaultType = field.type === "number" ? usedTypes.includes("min") ? usedTypes.includes("max") ? "pattern" : "max" : "min" : field.type === "email" ? usedTypes.includes("email") ? "minLength" : "email" : field.type === "url" ? usedTypes.includes("url") ? "minLength" : "url" : usedTypes.includes("minLength") ? usedTypes.includes("maxLength") ? "pattern" : "maxLength" : "minLength";
349
+ update({ validation: [...field.validation, { type: defaultType }] });
350
+ };
351
+ const updateValidation = (index, patch) => {
352
+ const validation = field.validation.map(
353
+ (v, i) => i === index ? { ...v, ...patch } : v
354
+ );
355
+ update({ validation });
356
+ };
357
+ const removeValidation = (index) => {
358
+ update({ validation: field.validation.filter((_, i) => i !== index) });
359
+ };
360
+ const hasOptions = ["select", "radio", "checkbox-group"].includes(field.type);
361
+ const isDecorative = ["heading", "paragraph", "divider"].includes(field.type);
362
+ return /* @__PURE__ */ jsxs(
363
+ Box,
364
+ {
365
+ padding: 4,
366
+ background: "neutral0",
367
+ style: { width: 280, minWidth: 280, overflowY: "auto", alignSelf: "stretch", borderLeft: "1px solid var(--strapi-neutral-200)" },
368
+ children: [
369
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", marginBottom: 4, children: "Field settings" }),
370
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, alignItems: "stretch", children: [
371
+ !isDecorative && /* @__PURE__ */ jsxs(Fragment, { children: [
372
+ /* @__PURE__ */ jsx(
373
+ LabeledInput,
374
+ {
375
+ label: "Label",
376
+ value: field.label,
377
+ onChange: (v) => update({ label: v })
378
+ }
379
+ ),
380
+ /* @__PURE__ */ jsx(
381
+ LabeledInput,
382
+ {
383
+ label: "Name (technical)",
384
+ value: field.name,
385
+ onChange: (v) => update({ name: v })
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsx(
389
+ LabeledInput,
390
+ {
391
+ label: "Placeholder",
392
+ value: field.placeholder || "",
393
+ onChange: (v) => update({ placeholder: v })
394
+ }
395
+ ),
396
+ /* @__PURE__ */ jsx(
397
+ LabeledInput,
398
+ {
399
+ label: "Help text",
400
+ value: field.helpText || "",
401
+ onChange: (v) => update({ helpText: v })
402
+ }
403
+ ),
404
+ /* @__PURE__ */ jsxs(Box, { children: [
405
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", marginBottom: 1, children: "Required" }),
406
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
407
+ /* @__PURE__ */ jsx(
408
+ Button,
409
+ {
410
+ variant: !field.required ? "default" : "secondary",
411
+ size: "S",
412
+ onClick: () => update({ required: false }),
413
+ children: "No"
414
+ }
415
+ ),
416
+ /* @__PURE__ */ jsx(
417
+ Button,
418
+ {
419
+ variant: field.required ? "default" : "secondary",
420
+ size: "S",
421
+ onClick: () => update({ required: true }),
422
+ children: "Yes"
423
+ }
424
+ )
425
+ ] })
426
+ ] }),
427
+ /* @__PURE__ */ jsxs(Box, { children: [
428
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", marginBottom: 1, children: "Width" }),
429
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
430
+ /* @__PURE__ */ jsx(
431
+ Button,
432
+ {
433
+ variant: field.width === "full" ? "default" : "secondary",
434
+ size: "S",
435
+ onClick: () => update({ width: "full" }),
436
+ children: "Full"
437
+ }
438
+ ),
439
+ /* @__PURE__ */ jsx(
440
+ Button,
441
+ {
442
+ variant: field.width === "half" ? "default" : "secondary",
443
+ size: "S",
444
+ onClick: () => update({ width: "half" }),
445
+ children: "Half"
446
+ }
447
+ )
448
+ ] })
449
+ ] })
450
+ ] }),
451
+ field.type === "heading" && /* @__PURE__ */ jsx(
452
+ LabeledInput,
453
+ {
454
+ label: "Heading text",
455
+ value: field.label,
456
+ onChange: (v) => update({ label: v })
457
+ }
458
+ ),
459
+ field.type === "paragraph" && /* @__PURE__ */ jsxs(Field.Root, { children: [
460
+ /* @__PURE__ */ jsx(Field.Label, { children: "Content" }),
461
+ /* @__PURE__ */ jsx(
462
+ Textarea,
463
+ {
464
+ value: field.label,
465
+ onChange: (e) => update({ label: e.target.value })
466
+ }
467
+ )
468
+ ] }),
469
+ hasOptions && /* @__PURE__ */ jsxs(Box, { children: [
470
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 2, children: [
471
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", children: "Options" }),
472
+ /* @__PURE__ */ jsx(Button, { size: "S", variant: "secondary", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: addOption, children: "Add" })
473
+ ] }),
474
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: (field.options || []).map((opt, i) => /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
475
+ /* @__PURE__ */ jsx(
476
+ TextInput,
477
+ {
478
+ "aria-label": "Label",
479
+ placeholder: "Label",
480
+ value: opt.label,
481
+ onChange: (e) => updateOption(i, { label: e.target.value })
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsx(
485
+ TextInput,
486
+ {
487
+ "aria-label": "Value",
488
+ placeholder: "Value",
489
+ value: opt.value,
490
+ onChange: (e) => updateOption(i, { value: e.target.value })
491
+ }
492
+ ),
493
+ /* @__PURE__ */ jsx(IconButton, { label: "Delete", onClick: () => removeOption(i), variant: "ghost", children: /* @__PURE__ */ jsx(Trash, {}) })
494
+ ] }, i)) })
495
+ ] }),
496
+ !isDecorative && /* @__PURE__ */ jsxs(Box, { children: [
497
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 2, children: [
498
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", children: "Validations" }),
499
+ /* @__PURE__ */ jsx(Button, { size: "S", variant: "secondary", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: addValidation, children: "Add" })
500
+ ] }),
501
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: field.validation.map((rule, i) => {
502
+ const usedTypes = field.validation.filter((_, j) => j !== i).map((r) => r.type);
503
+ const isNumber = field.type === "number";
504
+ const isEmail = field.type === "email";
505
+ const isUrl = field.type === "url";
506
+ const available = [
507
+ !isNumber && { value: "minLength", label: "Min. length" },
508
+ !isNumber && { value: "maxLength", label: "Max. length" },
509
+ isNumber && { value: "min", label: "Min. value" },
510
+ isNumber && { value: "max", label: "Max. value" },
511
+ !isEmail && { value: "email", label: "Email" },
512
+ !isUrl && { value: "url", label: "URL" },
513
+ { value: "pattern", label: "Pattern (regex)" }
514
+ ].filter(
515
+ (opt) => !!opt && (opt.value === rule.type || !usedTypes.includes(opt.value))
516
+ );
517
+ return /* @__PURE__ */ jsxs(Box, { padding: 2, background: "neutral100", hasRadius: true, children: [
518
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", marginBottom: 2, children: [
519
+ /* @__PURE__ */ jsx(Field.Root, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
520
+ SingleSelect,
521
+ {
522
+ value: rule.type,
523
+ onChange: (val) => updateValidation(i, { type: String(val), value: void 0, message: "" }),
524
+ size: "S",
525
+ children: available.map((opt) => /* @__PURE__ */ jsx(SingleSelectOption, { value: opt.value, children: opt.label }, opt.value))
526
+ }
527
+ ) }),
528
+ /* @__PURE__ */ jsx(IconButton, { label: "Delete", onClick: () => removeValidation(i), variant: "ghost", children: /* @__PURE__ */ jsx(Trash, {}) })
529
+ ] }),
530
+ ["minLength", "maxLength", "min", "max", "pattern"].includes(rule.type) && /* @__PURE__ */ jsx(
531
+ LabeledInput,
532
+ {
533
+ label: "Value",
534
+ value: String(rule.value ?? ""),
535
+ onChange: (v) => updateValidation(i, { value: v }),
536
+ size: "S"
537
+ }
538
+ ),
539
+ /* @__PURE__ */ jsx(
540
+ LabeledInput,
541
+ {
542
+ label: "Error message",
543
+ value: rule.message || "",
544
+ onChange: (v) => updateValidation(i, { message: v }),
545
+ placeholder: "Custom message (optional)",
546
+ size: "S"
547
+ }
548
+ )
549
+ ] }, i);
550
+ }) })
551
+ ] }),
552
+ /* @__PURE__ */ jsx(
553
+ LabeledInput,
554
+ {
555
+ label: "CSS class",
556
+ value: field.cssClass || "",
557
+ onChange: (v) => update({ cssClass: v }),
558
+ placeholder: "my-custom-class"
559
+ }
560
+ )
561
+ ] })
562
+ ]
563
+ }
564
+ );
565
+ }
566
+ const TOKEN = {
567
+ border: "1px solid #dcdce4",
568
+ radius: 4,
569
+ accent: "#4945ff",
570
+ text: "#32324d",
571
+ sub: "#666687",
572
+ danger: "#ee5e52",
573
+ bg: "#fff"
574
+ };
575
+ const inputBase = {
576
+ width: "100%",
577
+ height: 40,
578
+ padding: "0 12px",
579
+ border: TOKEN.border,
580
+ borderRadius: TOKEN.radius,
581
+ fontSize: 14,
582
+ color: TOKEN.text,
583
+ background: TOKEN.bg,
584
+ boxSizing: "border-box",
585
+ outline: "none",
586
+ fontFamily: "inherit"
587
+ };
588
+ const fieldWrap = {
589
+ display: "flex",
590
+ flexDirection: "column",
591
+ gap: 4
592
+ };
593
+ const labelStyle = {
594
+ fontSize: 12,
595
+ fontWeight: 600,
596
+ color: TOKEN.text,
597
+ lineHeight: "16px"
598
+ };
599
+ const helpStyle = {
600
+ fontSize: 11,
601
+ color: TOKEN.sub,
602
+ margin: 0
603
+ };
604
+ function Label({ field }) {
605
+ return /* @__PURE__ */ jsxs("span", { style: labelStyle, children: [
606
+ field.label || /* @__PURE__ */ jsx("em", { style: { color: TOKEN.sub }, children: "No label" }),
607
+ field.required && /* @__PURE__ */ jsx("span", { style: { color: TOKEN.danger, marginLeft: 2 }, children: "*" })
608
+ ] });
609
+ }
610
+ function TextField({ field }) {
611
+ const typeMap = {
612
+ text: "text",
613
+ email: "email",
614
+ number: "number",
615
+ phone: "tel",
616
+ password: "password",
617
+ url: "url",
618
+ date: "date",
619
+ time: "time"
620
+ };
621
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
622
+ /* @__PURE__ */ jsx(Label, { field }),
623
+ /* @__PURE__ */ jsx(
624
+ "input",
625
+ {
626
+ type: typeMap[field.type] ?? "text",
627
+ placeholder: field.placeholder,
628
+ style: inputBase,
629
+ readOnly: true
630
+ }
631
+ ),
632
+ field.helpText && /* @__PURE__ */ jsx("p", { style: helpStyle, children: field.helpText })
633
+ ] });
634
+ }
635
+ function TextareaField({ field }) {
636
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
637
+ /* @__PURE__ */ jsx(Label, { field }),
638
+ /* @__PURE__ */ jsx(
639
+ "textarea",
640
+ {
641
+ placeholder: field.placeholder,
642
+ style: { ...inputBase, height: 88, padding: "8px 12px", resize: "vertical" },
643
+ readOnly: true
644
+ }
645
+ ),
646
+ field.helpText && /* @__PURE__ */ jsx("p", { style: helpStyle, children: field.helpText })
647
+ ] });
648
+ }
649
+ function SelectField({ field }) {
650
+ const [val, setVal] = useState("");
651
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
652
+ /* @__PURE__ */ jsx(Label, { field }),
653
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
654
+ /* @__PURE__ */ jsxs(
655
+ "select",
656
+ {
657
+ value: val,
658
+ onChange: (e) => setVal(e.target.value),
659
+ style: {
660
+ ...inputBase,
661
+ appearance: "none",
662
+ paddingRight: 32,
663
+ cursor: "pointer"
664
+ },
665
+ children: [
666
+ /* @__PURE__ */ jsx("option", { value: "", children: field.placeholder || "Select an option…" }),
667
+ (field.options || []).map((o, i) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label || o.value }, i))
668
+ ]
669
+ }
670
+ ),
671
+ /* @__PURE__ */ jsx("span", { style: {
672
+ position: "absolute",
673
+ right: 10,
674
+ top: "50%",
675
+ transform: "translateY(-50%)",
676
+ pointerEvents: "none",
677
+ color: TOKEN.sub,
678
+ fontSize: 10
679
+ }, children: "▼" })
680
+ ] }),
681
+ field.helpText && /* @__PURE__ */ jsx("p", { style: helpStyle, children: field.helpText })
682
+ ] });
683
+ }
684
+ function RadioField({ field }) {
685
+ const [val, setVal] = useState("");
686
+ const options = field.options || [];
687
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
688
+ /* @__PURE__ */ jsx(Label, { field }),
689
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 2 }, children: options.map((o, i) => {
690
+ const checked = val === o.value;
691
+ return /* @__PURE__ */ jsxs(
692
+ "label",
693
+ {
694
+ style: { display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 14, color: TOKEN.text, userSelect: "none" },
695
+ onClick: () => setVal(o.value),
696
+ children: [
697
+ /* @__PURE__ */ jsx("span", { style: {
698
+ width: 18,
699
+ height: 18,
700
+ borderRadius: "50%",
701
+ border: `2px solid ${checked ? TOKEN.accent : "#c0c0cf"}`,
702
+ background: TOKEN.bg,
703
+ display: "flex",
704
+ alignItems: "center",
705
+ justifyContent: "center",
706
+ flexShrink: 0,
707
+ transition: "border-color .15s"
708
+ }, children: checked && /* @__PURE__ */ jsx("span", { style: {
709
+ width: 8,
710
+ height: 8,
711
+ borderRadius: "50%",
712
+ background: TOKEN.accent
713
+ } }) }),
714
+ o.label || o.value
715
+ ]
716
+ },
717
+ i
718
+ );
719
+ }) }),
720
+ field.helpText && /* @__PURE__ */ jsx("p", { style: helpStyle, children: field.helpText })
721
+ ] });
722
+ }
723
+ function CheckboxField({ field }) {
724
+ const [checked, setChecked] = useState(false);
725
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
726
+ /* @__PURE__ */ jsxs(
727
+ "label",
728
+ {
729
+ style: { display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 14, color: TOKEN.text, userSelect: "none" },
730
+ onClick: () => setChecked((v) => !v),
731
+ children: [
732
+ /* @__PURE__ */ jsx("span", { style: {
733
+ width: 18,
734
+ height: 18,
735
+ borderRadius: 3,
736
+ border: `2px solid ${checked ? TOKEN.accent : "#c0c0cf"}`,
737
+ background: checked ? TOKEN.accent : TOKEN.bg,
738
+ display: "flex",
739
+ alignItems: "center",
740
+ justifyContent: "center",
741
+ flexShrink: 0,
742
+ transition: "all .15s"
743
+ }, children: checked && /* @__PURE__ */ jsx("span", { style: { color: "#fff", fontSize: 11, lineHeight: 1, fontWeight: 700 }, children: "✓" }) }),
744
+ /* @__PURE__ */ jsxs("span", { children: [
745
+ field.label,
746
+ field.required && /* @__PURE__ */ jsx("span", { style: { color: TOKEN.danger, marginLeft: 2 }, children: "*" })
747
+ ] })
748
+ ]
749
+ }
750
+ ),
751
+ field.helpText && /* @__PURE__ */ jsx("p", { style: { ...helpStyle, marginLeft: 28 }, children: field.helpText })
752
+ ] });
753
+ }
754
+ function CheckboxGroupField({ field }) {
755
+ const [vals, setVals] = useState(/* @__PURE__ */ new Set());
756
+ const options = field.options || [];
757
+ const toggle = (v) => setVals((prev) => {
758
+ const s = new Set(prev);
759
+ s.has(v) ? s.delete(v) : s.add(v);
760
+ return s;
761
+ });
762
+ return /* @__PURE__ */ jsxs("div", { style: fieldWrap, children: [
763
+ /* @__PURE__ */ jsx(Label, { field }),
764
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 2 }, children: options.map((o, i) => {
765
+ const checked = vals.has(o.value);
766
+ return /* @__PURE__ */ jsxs(
767
+ "label",
768
+ {
769
+ style: { display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 14, color: TOKEN.text, userSelect: "none" },
770
+ onClick: () => toggle(o.value),
771
+ children: [
772
+ /* @__PURE__ */ jsx("span", { style: {
773
+ width: 18,
774
+ height: 18,
775
+ borderRadius: 3,
776
+ border: `2px solid ${checked ? TOKEN.accent : "#c0c0cf"}`,
777
+ background: checked ? TOKEN.accent : TOKEN.bg,
778
+ display: "flex",
779
+ alignItems: "center",
780
+ justifyContent: "center",
781
+ flexShrink: 0,
782
+ transition: "all .15s"
783
+ }, children: checked && /* @__PURE__ */ jsx("span", { style: { color: "#fff", fontSize: 11, lineHeight: 1, fontWeight: 700 }, children: "✓" }) }),
784
+ o.label || o.value
785
+ ]
786
+ },
787
+ i
788
+ );
789
+ }) }),
790
+ field.helpText && /* @__PURE__ */ jsx("p", { style: helpStyle, children: field.helpText })
791
+ ] });
792
+ }
793
+ function PreviewField({ field }) {
794
+ const half = field.width === "half";
795
+ const colSpan = half ? 1 : 2;
796
+ if (field.type === "hidden") return null;
797
+ if (field.type === "divider") {
798
+ return /* @__PURE__ */ jsx("div", { style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: TOKEN.border, margin: "4px 0" } }) });
799
+ }
800
+ if (field.type === "heading") {
801
+ return /* @__PURE__ */ jsx("div", { style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsx("p", { style: { fontSize: 18, fontWeight: 700, color: TOKEN.text, margin: 0 }, children: field.label || "Heading" }) });
802
+ }
803
+ if (field.type === "paragraph") {
804
+ return /* @__PURE__ */ jsx("div", { style: { gridColumn: "1 / -1" }, children: /* @__PURE__ */ jsx("p", { style: { fontSize: 14, color: TOKEN.sub, margin: 0, lineHeight: 1.6 }, children: field.label || "Paragraph text" }) });
805
+ }
806
+ let content;
807
+ switch (field.type) {
808
+ case "textarea":
809
+ content = /* @__PURE__ */ jsx(TextareaField, { field });
810
+ break;
811
+ case "select":
812
+ content = /* @__PURE__ */ jsx(SelectField, { field });
813
+ break;
814
+ case "radio":
815
+ content = /* @__PURE__ */ jsx(RadioField, { field });
816
+ break;
817
+ case "checkbox":
818
+ content = /* @__PURE__ */ jsx(CheckboxField, { field });
819
+ break;
820
+ case "checkbox-group":
821
+ content = /* @__PURE__ */ jsx(CheckboxGroupField, { field });
822
+ break;
823
+ default:
824
+ content = /* @__PURE__ */ jsx(TextField, { field });
825
+ break;
826
+ }
827
+ return /* @__PURE__ */ jsx("div", { style: { gridColumn: `span ${colSpan}` }, children: content });
828
+ }
829
+ function FormPreview({ title, fields, settings, open, onClose }) {
830
+ if (!open) return null;
831
+ return /* @__PURE__ */ jsx(Modal.Root, { open, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(Modal.Content, { style: { maxWidth: 760, width: "100%" }, children: [
832
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsxs(Typography, { variant: "beta", children: [
833
+ "Preview — ",
834
+ title
835
+ ] }) }),
836
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsx(Box, { padding: 6, background: "neutral0", hasRadius: true, style: { border: TOKEN.border }, children: /* @__PURE__ */ jsxs("form", { onSubmit: (e) => e.preventDefault(), children: [
837
+ fields.length === 0 ? /* @__PURE__ */ jsx("p", { style: { textAlign: "center", color: TOKEN.sub, padding: "32px 0", margin: 0 }, children: "No fields added yet." }) : /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }, children: fields.map((field) => /* @__PURE__ */ jsx(PreviewField, { field }, field.id)) }),
838
+ fields.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: 24 }, children: /* @__PURE__ */ jsx(
839
+ "button",
840
+ {
841
+ type: "submit",
842
+ style: {
843
+ background: TOKEN.accent,
844
+ color: "#fff",
845
+ border: "none",
846
+ borderRadius: TOKEN.radius,
847
+ padding: "10px 24px",
848
+ fontSize: 14,
849
+ fontWeight: 600,
850
+ cursor: "pointer",
851
+ fontFamily: "inherit"
852
+ },
853
+ children: settings.submitButtonText || "Submit"
854
+ }
855
+ ) })
856
+ ] }) }) }),
857
+ /* @__PURE__ */ jsx(Modal.Footer, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, children: "Close" }) })
858
+ ] }) });
859
+ }
860
+ function EmbedModal({ formId, open, onClose }) {
861
+ const [copied, setCopied] = useState(false);
862
+ if (!open) return null;
863
+ const origin = window.location.origin;
864
+ const snippet = `<div id="sfb-form-${formId}"></div>
865
+ <script
866
+ src="${origin}/api/strapi-plugin-form-builder-cms/embed.js"
867
+ data-form-id="${formId}"
868
+ async
869
+ ><\/script>`;
870
+ const copy = () => {
871
+ navigator.clipboard.writeText(snippet).then(() => {
872
+ setCopied(true);
873
+ setTimeout(() => setCopied(false), 2e3);
874
+ });
875
+ };
876
+ return /* @__PURE__ */ jsx(Modal.Root, { open, onOpenChange: (v) => !v && onClose(), children: /* @__PURE__ */ jsxs(Modal.Content, { style: { maxWidth: 600, width: "100%" }, children: [
877
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Typography, { variant: "beta", children: "Embed this form" }) }),
878
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
879
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Paste this snippet into your website where you want the form to appear." }),
880
+ /* @__PURE__ */ jsx(
881
+ "pre",
882
+ {
883
+ style: {
884
+ margin: 0,
885
+ background: "#1e1e2e",
886
+ borderRadius: 6,
887
+ padding: "16px 20px",
888
+ overflowX: "auto",
889
+ whiteSpace: "pre",
890
+ color: "#cdd6f4",
891
+ fontFamily: "'Fira Code', 'Cascadia Code', 'Consolas', monospace",
892
+ fontSize: 13,
893
+ lineHeight: 1.7,
894
+ cursor: "text",
895
+ userSelect: "all"
896
+ },
897
+ onClick: (e) => {
898
+ const sel = window.getSelection();
899
+ const range = document.createRange();
900
+ range.selectNodeContents(e.currentTarget);
901
+ sel?.removeAllRanges();
902
+ sel?.addRange(range);
903
+ },
904
+ children: snippet
905
+ }
906
+ )
907
+ ] }) }),
908
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
909
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, children: "Cancel" }),
910
+ /* @__PURE__ */ jsx(
911
+ Button,
912
+ {
913
+ startIcon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Duplicate, {}),
914
+ onClick: copy,
915
+ variant: copied ? "success" : "default",
916
+ children: copied ? "Copied!" : "Copy"
917
+ }
918
+ )
919
+ ] })
920
+ ] }) });
921
+ }
922
+ const DEFAULT_SETTINGS = {
923
+ submitButtonText: "Submit",
924
+ successMessage: "Form submitted successfully",
925
+ enableHoneypot: true,
926
+ enableRateLimit: true,
927
+ maxSubmissionsPerHour: 60,
928
+ notificationEmails: [],
929
+ redirectUrl: "",
930
+ customCss: "",
931
+ publicPage: false
932
+ };
933
+ function createField(type, order) {
934
+ return {
935
+ id: v4(),
936
+ type,
937
+ name: `field_${Date.now()}`,
938
+ label: type.charAt(0).toUpperCase() + type.slice(1),
939
+ placeholder: "",
940
+ helpText: "",
941
+ required: false,
942
+ order,
943
+ width: "full",
944
+ options: ["select", "radio", "checkbox-group"].includes(type) ? [{ label: "Option 1", value: "option_1" }] : void 0,
945
+ validation: []
946
+ };
947
+ }
948
+ function FormBuilderPage() {
949
+ const { id } = useParams();
950
+ const navigate = useNavigate();
951
+ const api = useFormsApi();
952
+ const isNew = !id || id === "new";
953
+ const [loading, setLoading] = useState(!isNew);
954
+ const [saving, setSaving] = useState(false);
955
+ const [publishing, setPublishing] = useState(false);
956
+ const [title, setTitle] = useState("New form");
957
+ const [description, setDescription] = useState("");
958
+ const [fields, setFields] = useState([]);
959
+ const [settings, setSettings] = useState(DEFAULT_SETTINGS);
960
+ const [selectedFieldId, setSelectedFieldId] = useState(null);
961
+ const [showSettings, setShowSettings] = useState(false);
962
+ const [showPreview, setShowPreview] = useState(false);
963
+ const [showEmbed, setShowEmbed] = useState(false);
964
+ const [publishedAt, setPublishedAt] = useState(null);
965
+ const [slug, setSlug] = useState(null);
966
+ useEffect(() => {
967
+ if (!isNew && id) {
968
+ api.getForm(Number(id)).then((form) => {
969
+ setTitle(form.title);
970
+ setDescription(form.description || "");
971
+ setFields(form.fields || []);
972
+ setSettings({ ...DEFAULT_SETTINGS, ...form.settings || {} });
973
+ setPublishedAt(form.publishedAt ?? null);
974
+ setSlug(form.slug ?? null);
975
+ setLoading(false);
976
+ });
977
+ }
978
+ }, [id]);
979
+ const addField = useCallback((type) => {
980
+ const newField = createField(type, fields.length);
981
+ setFields((prev) => [...prev, newField]);
982
+ setSelectedFieldId(newField.id);
983
+ }, [fields.length]);
984
+ const updateField = useCallback((updated) => {
985
+ setFields((prev) => prev.map((f) => f.id === updated.id ? updated : f));
986
+ }, []);
987
+ const deleteField = useCallback((fieldId) => {
988
+ setFields((prev) => prev.filter((f) => f.id !== fieldId));
989
+ setSelectedFieldId((prev) => prev === fieldId ? null : prev);
990
+ }, []);
991
+ const reorderFields = useCallback((reordered) => {
992
+ setFields(reordered.map((f, i) => ({ ...f, order: i })));
993
+ }, []);
994
+ const saveDraft = async () => {
995
+ setSaving(true);
996
+ try {
997
+ const payload = { title, description, fields, settings, conditionalLogic: [], publishedAt: null };
998
+ if (isNew) {
999
+ const created = await api.createForm(payload);
1000
+ setPublishedAt(null);
1001
+ setSlug(created.slug ?? null);
1002
+ navigate(`/plugins/${PLUGIN_ID}/builder/${created.id}`, { replace: true });
1003
+ } else {
1004
+ const updated = await api.updateForm(Number(id), payload);
1005
+ setPublishedAt(updated.publishedAt ?? null);
1006
+ }
1007
+ } finally {
1008
+ setSaving(false);
1009
+ }
1010
+ };
1011
+ const publish = async () => {
1012
+ setPublishing(true);
1013
+ try {
1014
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1015
+ const payload = { title, description, fields, settings, conditionalLogic: [], publishedAt: now };
1016
+ if (isNew) {
1017
+ const created = await api.createForm(payload);
1018
+ setPublishedAt(created.publishedAt ?? now);
1019
+ navigate(`/plugins/${PLUGIN_ID}/builder/${created.id}`, { replace: true });
1020
+ } else {
1021
+ const updated = await api.updateForm(Number(id), payload);
1022
+ setPublishedAt(updated.publishedAt ?? now);
1023
+ }
1024
+ } finally {
1025
+ setPublishing(false);
1026
+ }
1027
+ };
1028
+ const selectedField = fields.find((f) => f.id === selectedFieldId) || null;
1029
+ if (loading) {
1030
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 10, children: /* @__PURE__ */ jsx(Loader, { children: "Loading form..." }) });
1031
+ }
1032
+ return /* @__PURE__ */ jsxs(Box, { style: { display: "flex", flexDirection: "column", height: "100vh" }, children: [
1033
+ /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral0", style: { borderBottom: "1px solid var(--strapi-neutral-200)" }, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1034
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
1035
+ /* @__PURE__ */ jsx(
1036
+ Button,
1037
+ {
1038
+ variant: "ghost",
1039
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
1040
+ onClick: () => navigate(`/plugins/${PLUGIN_ID}`),
1041
+ children: "Back"
1042
+ }
1043
+ ),
1044
+ /* @__PURE__ */ jsx(
1045
+ TextInput,
1046
+ {
1047
+ "aria-label": "Form title",
1048
+ value: title,
1049
+ onChange: (e) => setTitle(e.target.value),
1050
+ style: { minWidth: 300, fontWeight: "bold", fontSize: 18 }
1051
+ }
1052
+ )
1053
+ ] }),
1054
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
1055
+ /* @__PURE__ */ jsx(
1056
+ Typography,
1057
+ {
1058
+ variant: "pi",
1059
+ textColor: publishedAt ? "success600" : "warning600",
1060
+ style: { fontWeight: 600 },
1061
+ children: publishedAt ? "Published" : "Draft"
1062
+ }
1063
+ ),
1064
+ !isNew && /* @__PURE__ */ jsx(
1065
+ Button,
1066
+ {
1067
+ variant: "ghost",
1068
+ startIcon: /* @__PURE__ */ jsx(Duplicate, {}),
1069
+ onClick: () => setShowEmbed(true),
1070
+ children: "Embed"
1071
+ }
1072
+ ),
1073
+ /* @__PURE__ */ jsx(
1074
+ Button,
1075
+ {
1076
+ variant: "ghost",
1077
+ startIcon: /* @__PURE__ */ jsx(Eye, {}),
1078
+ onClick: () => setShowPreview(true),
1079
+ children: "Preview"
1080
+ }
1081
+ ),
1082
+ /* @__PURE__ */ jsx(
1083
+ Button,
1084
+ {
1085
+ variant: "secondary",
1086
+ onClick: () => setShowSettings((v) => !v),
1087
+ children: showSettings ? "Hide settings" : "Settings"
1088
+ }
1089
+ ),
1090
+ /* @__PURE__ */ jsx(
1091
+ Button,
1092
+ {
1093
+ variant: "secondary",
1094
+ startIcon: /* @__PURE__ */ jsx(Pencil, {}),
1095
+ onClick: saveDraft,
1096
+ loading: saving,
1097
+ children: "Save draft"
1098
+ }
1099
+ ),
1100
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Check, {}), onClick: publish, loading: publishing, children: "Publish" })
1101
+ ] })
1102
+ ] }) }),
1103
+ showSettings && /* @__PURE__ */ jsx(Box, { padding: 4, background: "primary100", style: { borderBottom: "1px solid var(--strapi-primary-200)" }, children: /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-start", style: { flexWrap: "wrap" }, children: [
1104
+ /* @__PURE__ */ jsxs(Field.Root, { style: { minWidth: 300 }, children: [
1105
+ /* @__PURE__ */ jsx(Field.Label, { children: "Description" }),
1106
+ /* @__PURE__ */ jsx(
1107
+ TextInput,
1108
+ {
1109
+ value: description,
1110
+ onChange: (e) => setDescription(e.target.value)
1111
+ }
1112
+ )
1113
+ ] }),
1114
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
1115
+ /* @__PURE__ */ jsx(Field.Label, { children: "Submit button" }),
1116
+ /* @__PURE__ */ jsx(
1117
+ TextInput,
1118
+ {
1119
+ value: settings.submitButtonText,
1120
+ onChange: (e) => setSettings((s) => ({ ...s, submitButtonText: e.target.value }))
1121
+ }
1122
+ )
1123
+ ] }),
1124
+ /* @__PURE__ */ jsxs(Field.Root, { style: { minWidth: 300 }, children: [
1125
+ /* @__PURE__ */ jsx(Field.Label, { children: "Success message" }),
1126
+ /* @__PURE__ */ jsx(
1127
+ TextInput,
1128
+ {
1129
+ value: settings.successMessage,
1130
+ onChange: (e) => setSettings((s) => ({ ...s, successMessage: e.target.value }))
1131
+ }
1132
+ )
1133
+ ] }),
1134
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "center", children: [
1135
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1136
+ /* @__PURE__ */ jsx(
1137
+ Toggle,
1138
+ {
1139
+ checked: settings.enableHoneypot,
1140
+ onChange: () => setSettings((s) => ({ ...s, enableHoneypot: !s.enableHoneypot })),
1141
+ onLabel: "Yes",
1142
+ offLabel: "No"
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Honeypot anti-spam" })
1146
+ ] }),
1147
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1148
+ /* @__PURE__ */ jsx(
1149
+ Toggle,
1150
+ {
1151
+ checked: settings.publicPage,
1152
+ onChange: () => setSettings((s) => ({ ...s, publicPage: !s.publicPage })),
1153
+ onLabel: "Yes",
1154
+ offLabel: "No"
1155
+ }
1156
+ ),
1157
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Public page" })
1158
+ ] })
1159
+ ] })
1160
+ ] }) }),
1161
+ settings.publicPage && slug && /* @__PURE__ */ jsx(
1162
+ Box,
1163
+ {
1164
+ paddingTop: 2,
1165
+ paddingBottom: 2,
1166
+ paddingLeft: 4,
1167
+ paddingRight: 4,
1168
+ background: "success100",
1169
+ style: { borderBottom: "1px solid var(--strapi-success-200)", flexShrink: 0 },
1170
+ children: /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
1171
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "success700", fontWeight: "semiBold", children: "Public URL:" }),
1172
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "success600", style: { fontFamily: "monospace" }, children: [
1173
+ window.location.origin,
1174
+ "/api/strapi-plugin-form-builder-cms/page/",
1175
+ slug
1176
+ ] }),
1177
+ /* @__PURE__ */ jsx(
1178
+ Button,
1179
+ {
1180
+ variant: "ghost",
1181
+ size: "S",
1182
+ onClick: () => window.open(`${window.location.origin}/api/strapi-plugin-form-builder-cms/page/${slug}`, "_blank"),
1183
+ children: "Open"
1184
+ }
1185
+ )
1186
+ ] })
1187
+ }
1188
+ ),
1189
+ /* @__PURE__ */ jsxs(Flex, { style: { flex: 1, minHeight: 0 }, children: [
1190
+ /* @__PURE__ */ jsx(FieldPalette, { onAdd: addField }),
1191
+ /* @__PURE__ */ jsx(
1192
+ Box,
1193
+ {
1194
+ padding: 4,
1195
+ style: { flex: 1, overflowY: "auto", minWidth: 0, alignSelf: "stretch" },
1196
+ children: fields.length === 0 ? /* @__PURE__ */ jsx(
1197
+ Box,
1198
+ {
1199
+ padding: 10,
1200
+ background: "neutral100",
1201
+ hasRadius: true,
1202
+ style: { border: "2px dashed var(--strapi-neutral-300)", textAlign: "center" },
1203
+ children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: "Click on a field in the left panel to add it to the form" })
1204
+ }
1205
+ ) : /* @__PURE__ */ jsx(
1206
+ DropZone,
1207
+ {
1208
+ fields,
1209
+ selectedId: selectedFieldId,
1210
+ onSelect: setSelectedFieldId,
1211
+ onDelete: deleteField,
1212
+ onReorder: reorderFields
1213
+ }
1214
+ )
1215
+ }
1216
+ ),
1217
+ selectedField && /* @__PURE__ */ jsx(
1218
+ FieldSettingsPanel,
1219
+ {
1220
+ field: selectedField,
1221
+ onChange: updateField
1222
+ }
1223
+ )
1224
+ ] }),
1225
+ /* @__PURE__ */ jsx(
1226
+ FormPreview,
1227
+ {
1228
+ title,
1229
+ fields,
1230
+ settings,
1231
+ open: showPreview,
1232
+ onClose: () => setShowPreview(false)
1233
+ }
1234
+ ),
1235
+ !isNew && /* @__PURE__ */ jsx(
1236
+ EmbedModal,
1237
+ {
1238
+ formId: id,
1239
+ open: showEmbed,
1240
+ onClose: () => setShowEmbed(false)
1241
+ }
1242
+ )
1243
+ ] });
1244
+ }
1245
+ function SubmissionsPage() {
1246
+ const { formId } = useParams();
1247
+ const navigate = useNavigate();
1248
+ const api = useFormsApi();
1249
+ const [form, setForm] = useState(null);
1250
+ const [submissions, setSubmissions] = useState([]);
1251
+ const [stats, setStats] = useState(null);
1252
+ const [loading, setLoading] = useState(true);
1253
+ const [statusFilter, setStatusFilter] = useState("");
1254
+ const [selected, setSelected] = useState(null);
1255
+ const [deleteTarget, setDeleteTarget] = useState(null);
1256
+ const load = async () => {
1257
+ setLoading(true);
1258
+ const [formData, subData, statsData] = await Promise.all([
1259
+ api.getForm(Number(formId)),
1260
+ api.getSubmissions(Number(formId), statusFilter ? { status: statusFilter } : {}),
1261
+ api.getStats(Number(formId))
1262
+ ]);
1263
+ setForm(formData);
1264
+ setSubmissions(subData?.results || subData || []);
1265
+ setStats(statsData);
1266
+ setLoading(false);
1267
+ };
1268
+ useEffect(() => {
1269
+ load();
1270
+ }, [formId, statusFilter]);
1271
+ const dataFields = (form?.fields || []).filter(
1272
+ (f) => !["heading", "paragraph", "divider"].includes(f.type)
1273
+ );
1274
+ const handleDelete = async () => {
1275
+ if (!deleteTarget) return;
1276
+ await api.deleteSubmission(deleteTarget);
1277
+ setDeleteTarget(null);
1278
+ load();
1279
+ };
1280
+ const statusColor = {
1281
+ new: "success",
1282
+ read: "secondary",
1283
+ archived: "neutral"
1284
+ };
1285
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
1286
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 6, children: [
1287
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
1288
+ /* @__PURE__ */ jsx(
1289
+ Button,
1290
+ {
1291
+ variant: "ghost",
1292
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
1293
+ onClick: () => navigate(`/plugins/${PLUGIN_ID}`),
1294
+ children: "Back"
1295
+ }
1296
+ ),
1297
+ /* @__PURE__ */ jsxs(Typography, { variant: "alpha", children: [
1298
+ "Submissions — ",
1299
+ form?.title || "..."
1300
+ ] })
1301
+ ] }),
1302
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
1303
+ stats && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
1304
+ /* @__PURE__ */ jsxs(Badge, { children: [
1305
+ "Total: ",
1306
+ stats.total
1307
+ ] }),
1308
+ /* @__PURE__ */ jsxs(Badge, { active: true, children: [
1309
+ "New: ",
1310
+ stats.byStatus?.new || 0
1311
+ ] })
1312
+ ] }),
1313
+ /* @__PURE__ */ jsxs(
1314
+ SingleSelect,
1315
+ {
1316
+ "aria-label": "Filter by status",
1317
+ value: statusFilter,
1318
+ onChange: (val) => setStatusFilter(String(val)),
1319
+ placeholder: "All statuses",
1320
+ size: "S",
1321
+ children: [
1322
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "", children: "All" }),
1323
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "new", children: "New" }),
1324
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "read", children: "Read" }),
1325
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "archived", children: "Archived" })
1326
+ ]
1327
+ }
1328
+ )
1329
+ ] })
1330
+ ] }),
1331
+ loading ? /* @__PURE__ */ jsx(Typography, { children: "Loading..." }) : submissions.length === 0 ? /* @__PURE__ */ jsx(Box, { padding: 10, background: "neutral100", borderRadius: "4px", style: { textAlign: "center" }, children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "No submissions yet." }) }) : /* @__PURE__ */ jsxs(Table, { colCount: dataFields.length + 4, rowCount: submissions.length, children: [
1332
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
1333
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "ID" }) }),
1334
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Status" }) }),
1335
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Date" }) }),
1336
+ dataFields.map((f) => /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: f.label }) }, f.id)),
1337
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Actions" }) })
1338
+ ] }) }),
1339
+ /* @__PURE__ */ jsx(Tbody, { children: submissions.map((sub) => /* @__PURE__ */ jsxs(Tr, { children: [
1340
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: sub.id }) }),
1341
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { variant: statusColor[sub.status], children: sub.status }) }),
1342
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: new Date(sub.createdAt).toLocaleString() }) }),
1343
+ dataFields.map((f) => /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { style: { maxWidth: 150, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: String(sub.data?.[f.name] ?? "") }) }, f.id)),
1344
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsxs(Flex, { gap: 1, children: [
1345
+ /* @__PURE__ */ jsx(IconButton, { label: "View details", onClick: () => setSelected(sub), children: /* @__PURE__ */ jsx(Eye, {}) }),
1346
+ /* @__PURE__ */ jsx(IconButton, { label: "Delete", onClick: () => setDeleteTarget(sub.id), children: /* @__PURE__ */ jsx(Trash, {}) })
1347
+ ] }) })
1348
+ ] }, sub.id)) })
1349
+ ] }),
1350
+ selected && /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: () => setSelected(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
1351
+ /* @__PURE__ */ jsxs(Dialog.Header, { children: [
1352
+ "Submission #",
1353
+ selected.id
1354
+ ] }),
1355
+ /* @__PURE__ */ jsx(Dialog.Body, { children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "left" }, children: [
1356
+ /* @__PURE__ */ jsxs("div", { style: {
1357
+ display: "grid",
1358
+ gridTemplateColumns: "80px 1fr",
1359
+ rowGap: 10,
1360
+ columnGap: 16,
1361
+ background: "var(--strapi-neutral-100)",
1362
+ borderRadius: 6,
1363
+ padding: "12px 16px",
1364
+ marginBottom: 16
1365
+ }, children: [
1366
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", textColor: "neutral500", children: "Status" }),
1367
+ /* @__PURE__ */ jsx(Badge, { variant: statusColor[selected.status], children: selected.status.toUpperCase() }),
1368
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", textColor: "neutral500", children: "Date" }),
1369
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: new Date(selected.createdAt).toLocaleString() }),
1370
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", textColor: "neutral500", children: "IP" }),
1371
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: selected.ipAddress || "—" })
1372
+ ] }),
1373
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral400", style: { display: "block", marginBottom: 8 }, children: "SUBMITTED DATA" }),
1374
+ /* @__PURE__ */ jsx("div", { style: {
1375
+ border: "1px solid var(--strapi-neutral-200)",
1376
+ borderRadius: 6,
1377
+ overflow: "hidden"
1378
+ }, children: dataFields.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", style: { padding: "12px 16px", display: "block" }, children: "No data fields." }) : dataFields.map((f, i) => {
1379
+ const val = String(selected.data?.[f.name] ?? "");
1380
+ return /* @__PURE__ */ jsxs("div", { style: {
1381
+ display: "grid",
1382
+ gridTemplateColumns: "140px 1fr",
1383
+ columnGap: 16,
1384
+ padding: "10px 16px",
1385
+ background: i % 2 === 0 ? "#fff" : "var(--strapi-neutral-100)",
1386
+ alignItems: "start"
1387
+ }, children: [
1388
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "semiBold", textColor: "neutral600", style: { whiteSpace: "nowrap" }, children: f.label }),
1389
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: val ? "neutral800" : "neutral400", children: val || "—" })
1390
+ ] }, f.id);
1391
+ }) })
1392
+ ] }) }),
1393
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
1394
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Close" }) }),
1395
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(
1396
+ Button,
1397
+ {
1398
+ onClick: async () => {
1399
+ await api.updateSubmissionStatus(selected.id, "read");
1400
+ setSelected(null);
1401
+ load();
1402
+ },
1403
+ children: "Mark as read"
1404
+ }
1405
+ ) })
1406
+ ] })
1407
+ ] }) }),
1408
+ deleteTarget && /* @__PURE__ */ jsx(Dialog.Root, { open: true, onOpenChange: () => setDeleteTarget(null), children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
1409
+ /* @__PURE__ */ jsx(Dialog.Header, { children: "Confirm delete" }),
1410
+ /* @__PURE__ */ jsx(Dialog.Body, { children: /* @__PURE__ */ jsx(Typography, { children: "Delete this submission?" }) }),
1411
+ /* @__PURE__ */ jsxs(Dialog.Footer, { children: [
1412
+ /* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
1413
+ /* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: handleDelete, children: "Delete" }) })
1414
+ ] })
1415
+ ] }) })
1416
+ ] });
1417
+ }
1418
+ const App = () => {
1419
+ return /* @__PURE__ */ jsxs(Routes, { children: [
1420
+ /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(FormListPage, {}) }),
1421
+ /* @__PURE__ */ jsx(Route, { path: "builder/new", element: /* @__PURE__ */ jsx(FormBuilderPage, {}) }),
1422
+ /* @__PURE__ */ jsx(Route, { path: "builder/:id", element: /* @__PURE__ */ jsx(FormBuilderPage, {}) }),
1423
+ /* @__PURE__ */ jsx(Route, { path: "submissions/:formId", element: /* @__PURE__ */ jsx(SubmissionsPage, {}) }),
1424
+ /* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(Page.Error, {}) })
1425
+ ] });
1426
+ };
1427
+ export {
1428
+ App
1429
+ };