@nextlyhq/plugin-form-builder 0.0.2-alpha.60 → 0.0.2-alpha.64

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.
@@ -1,10 +1,15 @@
1
1
  import {
2
2
  DEFAULT_FORM_SETTINGS,
3
+ buildFieldReferenceMap,
4
+ documentReachability,
5
+ enforcedValidationRules,
3
6
  evaluateConditions,
4
7
  formatExportValue,
5
8
  isKnownFormField,
6
- normalizeFormSettings
7
- } from "../chunk-XCICDELK.js";
9
+ normalizeFormSettings,
10
+ parseFieldRef,
11
+ parseRedirectReference
12
+ } from "../chunk-W6NLZFHH.js";
8
13
 
9
14
  // src/admin/index.ts
10
15
  import {
@@ -872,23 +877,24 @@ function SubmissionsView({ collectionSlug }) {
872
877
  // src/admin/FormBuilderView.tsx
873
878
  import {
874
879
  Button as Button8,
880
+ Card as Card3,
875
881
  FieldShell as FieldShell2,
876
882
  FormActions,
877
- FormLayout,
878
- Input as Input6,
883
+ Input as Input7,
884
+ Textarea as Textarea4,
879
885
  toast,
880
886
  Tabs as Tabs4,
881
887
  TabsList as TabsList4,
882
888
  TabsTrigger as TabsTrigger4,
883
- Select as Select8,
884
- SelectTrigger as SelectTrigger8,
885
- SelectValue as SelectValue8,
886
- SelectContent as SelectContent8,
887
- SelectItem as SelectItem8
889
+ Select as Select9,
890
+ SelectTrigger as SelectTrigger9,
891
+ SelectValue as SelectValue9,
892
+ SelectContent as SelectContent9,
893
+ SelectItem as SelectItem9
888
894
  } from "@nextlyhq/ui";
889
895
  import { useQueryClient } from "@tanstack/react-query";
890
896
  import { FORM_FIELD_TYPE_CATALOG as FORM_FIELD_TYPE_CATALOG3 } from "nextly/field-catalog";
891
- import { useCallback as useCallback7, useEffect as useEffect4, useRef as useRef2, useState as useState9 } from "react";
897
+ import { useCallback as useCallback7, useEffect as useEffect5, useRef as useRef2, useState as useState10 } from "react";
892
898
 
893
899
  // src/admin/components/builder/FieldCards.tsx
894
900
  import {
@@ -922,52 +928,6 @@ import * as Lucide from "lucide-react";
922
928
  import { FORM_FIELD_TYPE_CATALOG as FORM_FIELD_TYPE_CATALOG2 } from "nextly/field-catalog";
923
929
  import { useCallback as useCallback5, useMemo as useMemo5, useState as useState6 } from "react";
924
930
 
925
- // src/utils/field-references.ts
926
- function referencesInTemplate(template, fieldName) {
927
- const pattern = new RegExp(
928
- `\\{\\{\\s*${fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\}\\}`
929
- );
930
- return pattern.test(template);
931
- }
932
- function findFieldReferences(fieldName, fields, notifications) {
933
- const references = [];
934
- for (const field of fields) {
935
- if (field.name === fieldName) continue;
936
- if (!field.conditionalLogic?.enabled) continue;
937
- const conditions = field.conditionalLogic.conditions ?? [];
938
- if (conditions.some((condition) => condition.field === fieldName)) {
939
- references.push({ kind: "condition", label: field.label || field.name });
940
- }
941
- }
942
- for (const notification of notifications) {
943
- if (!notification.enabled) continue;
944
- const templates = [
945
- notification.to,
946
- ...notification.cc ?? [],
947
- ...notification.bcc ?? [],
948
- notification.replyTo
949
- ].filter((value) => typeof value === "string");
950
- const referencedByTemplate = templates.some(
951
- (template) => referencesInTemplate(template, fieldName)
952
- );
953
- const referencedByCondition = notification.condition?.field === fieldName;
954
- if (referencedByTemplate || referencedByCondition) {
955
- references.push({
956
- kind: "notification",
957
- label: notification.name || "Notification"
958
- });
959
- }
960
- }
961
- return references;
962
- }
963
- function buildFieldReferenceMap(fields, notifications) {
964
- const map = /* @__PURE__ */ new Map();
965
- for (const field of fields) {
966
- map.set(field.name, findFieldReferences(field.name, fields, notifications));
967
- }
968
- return map;
969
- }
970
-
971
931
  // src/admin/context/FormBuilderContext.tsx
972
932
  import { FORM_FIELD_TYPE_CATALOG } from "nextly/field-catalog";
973
933
  import {
@@ -978,6 +938,7 @@ import {
978
938
  useState as useState3
979
939
  } from "react";
980
940
  import { jsx as jsx3 } from "react/jsx-runtime";
941
+ var READS_ONLY = /* @__PURE__ */ new Set(["selectField", "setActiveTab", "markAsSaved"]);
981
942
  var DEFAULT_SETTINGS = DEFAULT_FORM_SETTINGS;
982
943
  function createNotification() {
983
944
  const id = `notif_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 6)}`;
@@ -1061,7 +1022,8 @@ function createFieldFromType(type, existingNames = []) {
1061
1022
  }
1062
1023
  function FormBuilderProvider({
1063
1024
  initialData,
1064
- children
1025
+ children,
1026
+ readOnly = false
1065
1027
  }) {
1066
1028
  const [fields, setFieldsState] = useState3(
1067
1029
  initialData?.fields || []
@@ -1074,7 +1036,8 @@ function FormBuilderProvider({
1074
1036
  name: initialData?.name || "",
1075
1037
  slug: initialData?.slug || "",
1076
1038
  description: initialData?.description,
1077
- status: initialData?.status || "draft"
1039
+ status: initialData?.status || "draft",
1040
+ closedMessage: initialData?.closedMessage
1078
1041
  });
1079
1042
  const [settings, setSettings] = useState3(
1080
1043
  () => normalizeFormSettings(initialData?.settings)
@@ -1264,7 +1227,15 @@ function FormBuilderProvider({
1264
1227
  markAsSaved
1265
1228
  ]
1266
1229
  );
1267
- return /* @__PURE__ */ jsx3(FormBuilderContext.Provider, { value, children });
1230
+ const guarded = useMemo2(() => {
1231
+ if (!readOnly) return value;
1232
+ return Object.fromEntries(
1233
+ Object.entries(value).map(
1234
+ ([name, member]) => typeof member === "function" && !READS_ONLY.has(name) ? [name, () => void 0] : [name, member]
1235
+ )
1236
+ );
1237
+ }, [value, readOnly]);
1238
+ return /* @__PURE__ */ jsx3(FormBuilderContext.Provider, { value: guarded, children });
1268
1239
  }
1269
1240
  function useFormBuilder() {
1270
1241
  const context = useContext(FormBuilderContext);
@@ -1341,8 +1312,9 @@ function AddFieldDialog({
1341
1312
 
1342
1313
  // src/admin/components/builder/FieldEditor.tsx
1343
1314
  import {
1315
+ drawsAnyValidationRule,
1344
1316
  FieldOptionsEditor,
1345
- ValidationNumberField,
1317
+ ValidationRulesEditor,
1346
1318
  withOptionIds
1347
1319
  } from "@nextlyhq/plugin-sdk/admin";
1348
1320
  import {
@@ -1359,6 +1331,7 @@ import {
1359
1331
  SelectTrigger as SelectTrigger4,
1360
1332
  SelectValue as SelectValue4
1361
1333
  } from "@nextlyhq/ui";
1334
+ import { validationRulesForFieldType } from "nextly/field-catalog";
1362
1335
  import { useState as useState5, useCallback as useCallback4, useMemo as useMemo4 } from "react";
1363
1336
 
1364
1337
  // src/admin/components/builder/ConditionalLogicEditor.tsx
@@ -1592,7 +1565,11 @@ function ConditionalLogicEditor({
1592
1565
  }
1593
1566
  ) : (
1594
1567
  // Full-strength warning border so this notice boundary is perceivable over its tinted fill.
1595
- /* @__PURE__ */ jsx5("p", { className: "mt-4 text-center text-xs text-warning font-medium bg-warning/10 p-2 rounded-none border border-warning", children: "Add more fields to the form to create conditions." })
1568
+ // The 600 shade rather than the base token: the base measures 4.37:1
1569
+ // once its own 10% fill composites over the page container, short of
1570
+ // the 4.5:1 text needs. The 600 shade holds 5.13:1 at its worst
1571
+ // surface in either mode.
1572
+ /* @__PURE__ */ jsx5("p", { className: "mt-4 text-center text-xs text-warning-600 font-medium bg-warning/10 p-2 rounded-none border border-warning", children: "Add more fields to the form to create conditions." })
1596
1573
  )
1597
1574
  ] })
1598
1575
  ] });
@@ -1982,100 +1959,49 @@ function ValidationTab({
1982
1959
  field,
1983
1960
  onUpdate
1984
1961
  }) {
1985
- const validation = useMemo4(() => field.validation || {}, [field.validation]);
1986
- const updateValidation = useCallback4(
1987
- (key, value) => {
1962
+ const validation = useMemo4(
1963
+ () => field.validation ?? {},
1964
+ [field.validation]
1965
+ );
1966
+ const allowed = useMemo4(() => {
1967
+ const enforced = enforcedValidationRules(field.type);
1968
+ return validationRulesForFieldType(field.type).filter(
1969
+ (rule) => enforced.includes(rule)
1970
+ );
1971
+ }, [field.type]);
1972
+ const applyRules = useCallback4(
1973
+ (next) => {
1974
+ const { message, ...rules } = next;
1988
1975
  onUpdate({
1989
- validation: { ...validation, [key]: value }
1976
+ validation: {
1977
+ ...validation,
1978
+ ...rules,
1979
+ // This surface's own key for the rule core calls `message`.
1980
+ ...message === void 0 ? {} : { errorMessage: message }
1981
+ }
1990
1982
  });
1991
1983
  },
1992
1984
  [validation, onUpdate]
1993
1985
  );
1994
1986
  return /* @__PURE__ */ jsxs5("div", { className: "space-y-6 pt-2", children: [
1995
- /* @__PURE__ */ jsxs5("div", { className: "space-y-2", children: [
1996
- /* @__PURE__ */ jsx6(
1997
- FormLabelWithTooltip2,
1998
- {
1999
- label: "Custom Error Message",
2000
- htmlFor: "error-message",
2001
- description: "Override the default browser validation message."
2002
- }
2003
- ),
2004
- /* @__PURE__ */ jsx6(
2005
- Input2,
2006
- {
2007
- id: "error-message",
2008
- type: "text",
2009
- value: validation.errorMessage || "",
2010
- onChange: (e) => updateValidation("errorMessage", e.target.value),
2011
- placeholder: "This field is required",
2012
- className: "bg-transparent"
2013
- }
2014
- )
2015
- ] }),
2016
- (field.type === "text" || field.type === "textarea") && /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-4", children: [
2017
- /* @__PURE__ */ jsx6(
2018
- ValidationNumberField,
2019
- {
2020
- label: "Min Length",
2021
- description: "Minimum characters required.",
2022
- counts: true,
2023
- value: field.validation?.minLength,
2024
- onChange: (n) => updateValidation("minLength", n)
2025
- }
2026
- ),
2027
- /* @__PURE__ */ jsx6(
2028
- ValidationNumberField,
2029
- {
2030
- label: "Max Length",
2031
- description: "Maximum characters allowed.",
2032
- counts: true,
2033
- value: field.validation?.maxLength,
2034
- onChange: (n) => updateValidation("maxLength", n)
2035
- }
2036
- )
2037
- ] }),
2038
- field.type === "number" && /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-4", children: [
2039
- /* @__PURE__ */ jsx6(
2040
- ValidationNumberField,
2041
- {
2042
- label: "Min Value",
2043
- description: "Minimum numerical value.",
2044
- value: field.validation?.min,
2045
- onChange: (n) => updateValidation("min", n)
2046
- }
2047
- ),
2048
- /* @__PURE__ */ jsx6(
2049
- ValidationNumberField,
2050
- {
2051
- label: "Max Value",
2052
- description: "Maximum numerical value.",
2053
- value: field.validation?.max,
2054
- onChange: (n) => updateValidation("max", n)
2055
- }
2056
- )
2057
- ] }),
2058
- (field.type === "text" || field.type === "email" || field.type === "phone" || field.type === "url") && /* @__PURE__ */ jsxs5("div", { className: "space-y-2", children: [
2059
- /* @__PURE__ */ jsx6(
2060
- FormLabelWithTooltip2,
2061
- {
2062
- label: "Pattern (Regex)",
2063
- htmlFor: "val-pattern",
2064
- description: "Regular expression for advanced validation."
2065
- }
2066
- ),
2067
- /* @__PURE__ */ jsx6(
2068
- Input2,
2069
- {
2070
- id: "val-pattern",
2071
- type: "text",
2072
- value: field.validation?.pattern || "",
2073
- onChange: (e) => updateValidation("pattern", e.target.value),
2074
- placeholder: "^[A-Za-z]+$",
2075
- className: "bg-transparent"
2076
- }
2077
- )
2078
- ] }),
1987
+ /* @__PURE__ */ jsx6(
1988
+ ValidationRulesEditor,
1989
+ {
1990
+ allowed,
1991
+ value: {
1992
+ minLength: validation.minLength,
1993
+ maxLength: validation.maxLength,
1994
+ // No row bounds here: this surface neither stores nor enforces them,
1995
+ // and `allowed` above never admits them, so passing them would be a
1996
+ // value the editor could not draw and the runtime could not read.
1997
+ min: validation.min,
1998
+ max: validation.max,
1999
+ pattern: validation.pattern,
2000
+ message: validation.errorMessage
2001
+ },
2002
+ onChange: applyRules
2003
+ }
2004
+ ),
2079
2005
  field.type === "file" && /* @__PURE__ */ jsxs5("div", { className: "space-y-2", children: [
2080
2006
  /* @__PURE__ */ jsx6(
2081
2007
  FormLabelWithTooltip2,
@@ -2100,7 +2026,7 @@ function ValidationTab({
2100
2026
  }
2101
2027
  )
2102
2028
  ] }),
2103
- (field.type === "checkbox" || field.type === "hidden") && // Semantic border token so this info-box boundary is visible at the 3:1 UI minimum.
2029
+ !drawsAnyValidationRule(allowed) && field.type !== "file" && // Semantic border token so this info-box boundary is visible at the 3:1 UI minimum.
2104
2030
  /* @__PURE__ */ jsx6("div", { className: "p-3 bg-muted rounded-none text-xs text-muted-foreground text-center border border-dashed border-border", children: "No additional validation options for this field type." })
2105
2031
  ] });
2106
2032
  }
@@ -2485,6 +2411,7 @@ function FieldCards({
2485
2411
  // src/admin/components/builder/FormNotificationsTab.tsx
2486
2412
  import {
2487
2413
  Badge as Badge3,
2414
+ Card,
2488
2415
  Button as Button6,
2489
2416
  DropdownMenu as DropdownMenu3,
2490
2417
  DropdownMenuContent as DropdownMenuContent3,
@@ -2500,11 +2427,6 @@ import {
2500
2427
  SelectItem as SelectItem5,
2501
2428
  SelectTrigger as SelectTrigger5,
2502
2429
  SelectValue as SelectValue5,
2503
- Sheet as Sheet2,
2504
- SheetContent as SheetContent2,
2505
- SheetDescription as SheetDescription2,
2506
- SheetHeader as SheetHeader2,
2507
- SheetTitle as SheetTitle2,
2508
2430
  Switch as Switch2
2509
2431
  } from "@nextlyhq/ui";
2510
2432
  import {
@@ -2519,7 +2441,51 @@ import {
2519
2441
  X
2520
2442
  } from "lucide-react";
2521
2443
  import { useCallback as useCallback6, useEffect as useEffect3, useMemo as useMemo6, useState as useState7 } from "react";
2522
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2444
+
2445
+ // src/admin/components/builder/notification-addresses.ts
2446
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2447
+ function isValidEmail(value) {
2448
+ return EMAIL_PATTERN.test(value.trim());
2449
+ }
2450
+ var MESSAGE = "Enter a valid email address.";
2451
+ function addressError(field, value) {
2452
+ if (field === "replyTo" && parseFieldRef(value) !== null) return void 0;
2453
+ if (!value?.trim()) return void 0;
2454
+ return isValidEmail(value) ? void 0 : MESSAGE;
2455
+ }
2456
+ function addressErrorsIn(notification) {
2457
+ const errors = {};
2458
+ const sender = addressError("senderEmail", notification.senderEmail);
2459
+ if (sender) errors.senderEmail = sender;
2460
+ if (notification.recipientType === "static") {
2461
+ const to = addressError("to", notification.to);
2462
+ if (to) errors.to = to;
2463
+ }
2464
+ const replyTo = addressError("replyTo", notification.replyTo);
2465
+ if (replyTo) errors.replyTo = replyTo;
2466
+ return errors;
2467
+ }
2468
+ function notificationsWithBadAddresses(notifications) {
2469
+ return notifications.filter((n) => Object.keys(addressErrorsIn(n)).length > 0).map((n) => n.name.trim() || "Untitled notification");
2470
+ }
2471
+ function badAddressMessage(notifications) {
2472
+ const names = notificationsWithBadAddresses(notifications);
2473
+ if (names.length === 0) return null;
2474
+ return names.length === 1 ? `${names[0]} has an invalid email address` : `${names.length} notifications have invalid email addresses`;
2475
+ }
2476
+ function notificationsBlockingSave(notifications) {
2477
+ const unnamed = unnamedNotificationCount(notifications);
2478
+ if (unnamed > 0) {
2479
+ return unnamed === 1 ? "A notification needs a name" : `${unnamed} notifications need a name`;
2480
+ }
2481
+ return badAddressMessage(notifications);
2482
+ }
2483
+ function unnamedNotificationCount(notifications) {
2484
+ return notifications.filter((n) => !n.name.trim()).length;
2485
+ }
2486
+
2487
+ // src/admin/components/builder/FormNotificationsTab.tsx
2488
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
2523
2489
  async function fetchProviders() {
2524
2490
  try {
2525
2491
  const res = await fetch("/admin/api/email-providers?pageSize=100&page=1", {
@@ -2544,15 +2510,9 @@ async function fetchTemplates() {
2544
2510
  return [];
2545
2511
  }
2546
2512
  }
2547
- var FIELD_REF_PATTERN = /^\{\{(\w+)\}\}$/;
2548
2513
  function toFieldRef(fieldName) {
2549
2514
  return `{{${fieldName}}}`;
2550
2515
  }
2551
- function parseFieldRef(value) {
2552
- if (!value) return null;
2553
- const match = value.match(FIELD_REF_PATTERN);
2554
- return match ? match[1] : null;
2555
- }
2556
2516
  function buildEmailFieldOptions(fields, currentRef) {
2557
2517
  const emailFields = fields.filter((field) => field.type === "email");
2558
2518
  if (currentRef && !emailFields.some((field) => field.name === currentRef)) {
@@ -2561,10 +2521,6 @@ function buildEmailFieldOptions(fields, currentRef) {
2561
2521
  }
2562
2522
  return emailFields;
2563
2523
  }
2564
- var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2565
- function isValidEmail(value) {
2566
- return EMAIL_PATTERN.test(value.trim());
2567
- }
2568
2524
  var COMPARISON_LABELS = {
2569
2525
  equals: "Equals",
2570
2526
  notEquals: "Does not equal",
@@ -2600,10 +2556,13 @@ function NotificationCard({
2600
2556
  notification,
2601
2557
  providerName,
2602
2558
  fields,
2603
- onEdit,
2559
+ expanded,
2560
+ editorId,
2561
+ onToggle,
2604
2562
  onDuplicate,
2605
2563
  onDelete,
2606
- onToggleEnabled
2564
+ onToggleEnabled,
2565
+ children
2607
2566
  }) {
2608
2567
  const recipient = describeRecipient(notification, fields);
2609
2568
  const ccCount = (notification.cc?.length ?? 0) + (notification.bcc?.length ?? 0);
@@ -2617,120 +2576,140 @@ function NotificationCard({
2617
2576
  if (hasStaleReference) {
2618
2577
  warnings.push("References a deleted field \u2014 edit to repair");
2619
2578
  }
2620
- return /* @__PURE__ */ jsx8(
2621
- "div",
2622
- {
2623
- className: `border border-border bg-background ${notification.enabled ? "" : "opacity-60"}`,
2624
- children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-2.5", children: [
2625
- /* @__PURE__ */ jsx8("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center bg-primary/5 text-primary", children: /* @__PURE__ */ jsx8(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }),
2626
- /* @__PURE__ */ jsxs7(
2627
- "button",
2628
- {
2629
- type: "button",
2630
- onClick: onEdit,
2631
- className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
2632
- "aria-label": `Edit notification ${notification.name}`,
2633
- children: [
2634
- /* @__PURE__ */ jsxs7("span", { className: "min-w-0 flex-1", children: [
2635
- /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-2", children: [
2636
- /* @__PURE__ */ jsx8("span", { className: "truncate text-sm font-medium text-foreground", children: notification.name }),
2637
- notification.condition && /* @__PURE__ */ jsxs7(
2638
- Badge3,
2579
+ return (
2580
+ // `Card` rather than its classes written out again: the primitive owns the
2581
+ // surface colour, the radius, the foreground and the transition, and a
2582
+ // second spelling of them drifts the moment the shared surface changes.
2583
+ //
2584
+ // `overflow-hidden` because an expanded editor paints a full-width tinted
2585
+ // child; without clipping it squares off the corners the card just rounded.
2586
+ /* @__PURE__ */ jsxs7(Card, { className: "overflow-hidden p-0", children: [
2587
+ /* @__PURE__ */ jsxs7(
2588
+ "div",
2589
+ {
2590
+ className: `flex items-center gap-2 px-3 py-2.5 ${notification.enabled ? "" : "opacity-60"}`,
2591
+ children: [
2592
+ /* @__PURE__ */ jsx8("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center bg-primary/5 text-primary", children: /* @__PURE__ */ jsx8(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }),
2593
+ /* @__PURE__ */ jsxs7(
2594
+ "button",
2595
+ {
2596
+ type: "button",
2597
+ onClick: onToggle,
2598
+ "aria-expanded": expanded,
2599
+ "aria-controls": editorId,
2600
+ className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
2601
+ "aria-label": `Edit notification ${notification.name}`,
2602
+ children: [
2603
+ /* @__PURE__ */ jsxs7("span", { className: "min-w-0 flex-1", children: [
2604
+ /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-2", children: [
2605
+ /* @__PURE__ */ jsx8("span", { className: "truncate text-sm font-medium text-foreground", children: notification.name }),
2606
+ notification.condition && /* @__PURE__ */ jsxs7(
2607
+ Badge3,
2608
+ {
2609
+ variant: "outline",
2610
+ className: "shrink-0 gap-1 rounded-none border-border px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
2611
+ children: [
2612
+ /* @__PURE__ */ jsx8(Filter, { className: "h-2.5 w-2.5", "aria-hidden": "true" }),
2613
+ "Conditional"
2614
+ ]
2615
+ }
2616
+ )
2617
+ ] }),
2618
+ /* @__PURE__ */ jsxs7("span", { className: "block truncate text-xs text-muted-foreground", children: [
2619
+ /* @__PURE__ */ jsx8("span", { className: recipient.missing ? "text-destructive" : "", children: recipient.text }),
2620
+ ccCount > 0 && /* @__PURE__ */ jsxs7("span", { children: [
2621
+ /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2622
+ "+",
2623
+ ccCount,
2624
+ " cc/bcc"
2625
+ ] }),
2626
+ /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2627
+ providerName
2628
+ ] })
2629
+ ] }),
2630
+ warnings.length > 0 && /* @__PURE__ */ jsx8("span", { className: "flex shrink-0 flex-col items-end gap-0.5", children: warnings.map((warning) => /* @__PURE__ */ jsxs7(
2631
+ "span",
2639
2632
  {
2640
- variant: "outline",
2641
- className: "shrink-0 gap-1 rounded-none border-border px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground",
2633
+ className: "flex items-center gap-1 text-xs font-medium text-destructive",
2642
2634
  children: [
2643
- /* @__PURE__ */ jsx8(Filter, { className: "h-2.5 w-2.5", "aria-hidden": "true" }),
2644
- "Conditional"
2635
+ /* @__PURE__ */ jsx8(TriangleAlert2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2636
+ warning
2645
2637
  ]
2646
- }
2647
- )
2648
- ] }),
2649
- /* @__PURE__ */ jsxs7("span", { className: "block truncate text-xs text-muted-foreground", children: [
2650
- /* @__PURE__ */ jsx8("span", { className: recipient.missing ? "text-destructive" : "", children: recipient.text }),
2651
- ccCount > 0 && /* @__PURE__ */ jsxs7("span", { children: [
2652
- /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2653
- "+",
2654
- ccCount,
2655
- " cc/bcc"
2656
- ] }),
2657
- /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2658
- providerName
2659
- ] })
2660
- ] }),
2661
- warnings.length > 0 && /* @__PURE__ */ jsx8("span", { className: "flex shrink-0 flex-col items-end gap-0.5", children: warnings.map((warning) => /* @__PURE__ */ jsxs7(
2662
- "span",
2638
+ },
2639
+ warning
2640
+ )) })
2641
+ ]
2642
+ }
2643
+ ),
2644
+ /* @__PURE__ */ jsx8(
2645
+ Switch2,
2646
+ {
2647
+ checked: notification.enabled,
2648
+ onCheckedChange: onToggleEnabled,
2649
+ "aria-label": `${notification.enabled ? "Disable" : "Enable"} notification ${notification.name}`
2650
+ }
2651
+ ),
2652
+ /* @__PURE__ */ jsxs7(DropdownMenu3, { children: [
2653
+ /* @__PURE__ */ jsx8(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx8(
2654
+ Button6,
2655
+ {
2656
+ type: "button",
2657
+ variant: "ghost",
2658
+ size: "icon",
2659
+ className: "h-8 w-8 shrink-0 rounded-none text-muted-foreground hover:text-foreground",
2660
+ "aria-label": `Notification actions for ${notification.name}`,
2661
+ children: /* @__PURE__ */ jsx8(MoreHorizontal2, { className: "h-4 w-4", "aria-hidden": "true" })
2662
+ }
2663
+ ) }),
2664
+ /* @__PURE__ */ jsxs7(
2665
+ DropdownMenuContent3,
2663
2666
  {
2664
- className: "flex items-center gap-1 text-xs font-medium text-destructive",
2667
+ align: "end",
2668
+ className: "w-56 shadow-none border-border",
2665
2669
  children: [
2666
- /* @__PURE__ */ jsx8(TriangleAlert2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2667
- warning
2670
+ /* @__PURE__ */ jsxs7(
2671
+ DropdownMenuItem3,
2672
+ {
2673
+ onClick: onToggle,
2674
+ className: "gap-2 cursor-pointer",
2675
+ children: [
2676
+ /* @__PURE__ */ jsx8(Pencil2, { className: "h-4 w-4 text-muted-foreground" }),
2677
+ expanded ? "Collapse" : "Edit"
2678
+ ]
2679
+ }
2680
+ ),
2681
+ /* @__PURE__ */ jsxs7(
2682
+ DropdownMenuItem3,
2683
+ {
2684
+ onClick: onDuplicate,
2685
+ className: "gap-2 cursor-pointer",
2686
+ children: [
2687
+ /* @__PURE__ */ jsx8(Copy2, { className: "h-4 w-4 text-muted-foreground" }),
2688
+ "Duplicate"
2689
+ ]
2690
+ }
2691
+ ),
2692
+ /* @__PURE__ */ jsx8(DropdownMenuSeparator2, {}),
2693
+ /* @__PURE__ */ jsxs7(
2694
+ DropdownMenuItem3,
2695
+ {
2696
+ onClick: onDelete,
2697
+ className: "gap-2 cursor-pointer text-destructive focus:text-destructive",
2698
+ children: [
2699
+ /* @__PURE__ */ jsx8(Trash22, { className: "h-4 w-4" }),
2700
+ "Delete"
2701
+ ]
2702
+ }
2703
+ )
2668
2704
  ]
2669
- },
2670
- warning
2671
- )) })
2672
- ]
2673
- }
2674
- ),
2675
- /* @__PURE__ */ jsx8(
2676
- Switch2,
2677
- {
2678
- checked: notification.enabled,
2679
- onCheckedChange: onToggleEnabled,
2680
- "aria-label": `${notification.enabled ? "Disable" : "Enable"} notification ${notification.name}`
2681
- }
2682
- ),
2683
- /* @__PURE__ */ jsxs7(DropdownMenu3, { children: [
2684
- /* @__PURE__ */ jsx8(DropdownMenuTrigger3, { asChild: true, children: /* @__PURE__ */ jsx8(
2685
- Button6,
2686
- {
2687
- type: "button",
2688
- variant: "ghost",
2689
- size: "icon",
2690
- className: "h-8 w-8 shrink-0 rounded-none text-muted-foreground hover:text-foreground",
2691
- "aria-label": `Notification actions for ${notification.name}`,
2692
- children: /* @__PURE__ */ jsx8(MoreHorizontal2, { className: "h-4 w-4", "aria-hidden": "true" })
2693
- }
2694
- ) }),
2695
- /* @__PURE__ */ jsxs7(
2696
- DropdownMenuContent3,
2697
- {
2698
- align: "end",
2699
- className: "w-56 shadow-none border-border",
2700
- children: [
2701
- /* @__PURE__ */ jsxs7(DropdownMenuItem3, { onClick: onEdit, className: "gap-2 cursor-pointer", children: [
2702
- /* @__PURE__ */ jsx8(Pencil2, { className: "h-4 w-4 text-muted-foreground" }),
2703
- "Edit"
2704
- ] }),
2705
- /* @__PURE__ */ jsxs7(
2706
- DropdownMenuItem3,
2707
- {
2708
- onClick: onDuplicate,
2709
- className: "gap-2 cursor-pointer",
2710
- children: [
2711
- /* @__PURE__ */ jsx8(Copy2, { className: "h-4 w-4 text-muted-foreground" }),
2712
- "Duplicate"
2713
- ]
2714
- }
2715
- ),
2716
- /* @__PURE__ */ jsx8(DropdownMenuSeparator2, {}),
2717
- /* @__PURE__ */ jsxs7(
2718
- DropdownMenuItem3,
2719
- {
2720
- onClick: onDelete,
2721
- className: "gap-2 cursor-pointer text-destructive focus:text-destructive",
2722
- children: [
2723
- /* @__PURE__ */ jsx8(Trash22, { className: "h-4 w-4" }),
2724
- "Delete"
2725
- ]
2726
- }
2727
- )
2728
- ]
2729
- }
2730
- )
2731
- ] })
2732
- ] })
2733
- }
2705
+ }
2706
+ )
2707
+ ] })
2708
+ ]
2709
+ }
2710
+ ),
2711
+ expanded && children
2712
+ ] })
2734
2713
  );
2735
2714
  }
2736
2715
  function AddressChipList({
@@ -2812,588 +2791,571 @@ function AddressChipList({
2812
2791
  error && /* @__PURE__ */ jsx8("p", { id: `${id}-error`, className: "text-xs text-destructive", children: error })
2813
2792
  ] });
2814
2793
  }
2815
- function initialReplyToMode(replyTo) {
2816
- if (!replyTo) return "none";
2817
- return parseFieldRef(replyTo) ? "field" : "custom";
2818
- }
2819
- function NotificationSheet({
2820
- initial,
2821
- isEditing,
2822
- providers,
2823
- templates,
2794
+ function RecipientFields({
2795
+ form,
2824
2796
  fields,
2825
2797
  defaults,
2826
- onSave,
2827
- onClose
2798
+ update,
2799
+ onRecipientTypeChange,
2800
+ addressErrors,
2801
+ validateAddress,
2802
+ replyToMode,
2803
+ setReplyToMode
2828
2804
  }) {
2829
- const [form, setForm] = useState7(initial);
2830
- const [replyToMode, setReplyToMode] = useState7(
2831
- initialReplyToMode(initial.replyTo)
2832
- );
2833
- const [addressErrors, setAddressErrors] = useState7({});
2834
- const update = useCallback6(
2835
- (key, value) => {
2836
- setForm((prev) => ({ ...prev, [key]: value }));
2837
- setAddressErrors(
2838
- (prev) => key in prev ? { ...prev, [key]: void 0 } : prev
2839
- );
2840
- },
2841
- []
2842
- );
2843
- const handleSave = useCallback6(() => {
2844
- const errors = {};
2845
- const invalid = (value) => Boolean(value?.trim()) && !isValidEmail(value);
2846
- if (invalid(form.senderEmail)) {
2847
- errors.senderEmail = "Enter a valid email address.";
2848
- }
2849
- if (form.recipientType === "static" && invalid(form.to)) {
2850
- errors.to = "Enter a valid email address.";
2851
- }
2852
- if (!parseFieldRef(form.replyTo) && invalid(form.replyTo)) {
2853
- errors.replyTo = "Enter a valid email address.";
2854
- }
2855
- if (Object.values(errors).some(Boolean)) {
2856
- setAddressErrors(errors);
2857
- return;
2858
- }
2859
- onSave(form);
2860
- }, [form, onSave]);
2861
- const defaultProvider = providers.find((p) => p.isDefault);
2862
- const defaultProviderLabel = defaultProvider ? `System default (${defaultProvider.name})` : "System default";
2863
2805
  const toRef = parseFieldRef(form.to);
2864
- const toFieldOptions = buildEmailFieldOptions(
2865
- fields,
2866
- form.recipientType === "field" ? toRef : null
2867
- );
2806
+ const toFieldOptions = buildEmailFieldOptions(fields, toRef);
2868
2807
  const replyToRef = parseFieldRef(form.replyTo);
2869
2808
  const replyToFieldOptions = buildEmailFieldOptions(fields, replyToRef);
2870
- const senderPlaceholder = defaults?.defaultFrom || "Provider default";
2871
- const senderHelp = defaults?.defaultFrom ? `Leave blank to send from ${defaults.defaultFrom} (the configured default).` : "Leave blank to use the template or provider default address.";
2872
- const condition = form.condition;
2873
- return /* @__PURE__ */ jsx8(
2874
- Sheet2,
2875
- {
2876
- open: true,
2877
- onOpenChange: (open) => {
2878
- if (!open) onClose();
2879
- },
2880
- children: /* @__PURE__ */ jsxs7(
2881
- SheetContent2,
2809
+ return /* @__PURE__ */ jsx8(Fragment3, { children: /* @__PURE__ */ jsxs7("div", { className: "space-y-4 pt-4 border-t border-border", children: [
2810
+ /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
2811
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Send to", htmlFor: "notification-recipient-type", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2812
+ Select5,
2882
2813
  {
2883
- side: "right",
2884
- className: "w-[560px] sm:max-w-[560px] p-0 flex flex-col",
2814
+ value: form.recipientType,
2815
+ onValueChange: (value) => {
2816
+ onRecipientTypeChange(value);
2817
+ },
2885
2818
  children: [
2886
- /* @__PURE__ */ jsxs7(SheetHeader2, { className: "p-4 border-b border-border", children: [
2887
- /* @__PURE__ */ jsx8(SheetTitle2, { children: isEditing ? "Edit notification" : "New notification" }),
2888
- /* @__PURE__ */ jsx8(SheetDescription2, { children: "Sent when someone submits this form." })
2889
- ] }),
2890
- /* @__PURE__ */ jsxs7("div", { className: "flex-1 overflow-y-auto p-4 space-y-6", children: [
2891
- /* @__PURE__ */ jsx8(FieldShell, { label: "Name", htmlFor: "notification-name", width: "half", children: /* @__PURE__ */ jsx8(
2892
- Input3,
2893
- {
2894
- type: "text",
2895
- value: form.name,
2896
- onChange: (e) => update("name", e.target.value),
2897
- placeholder: "e.g. Admin notification"
2898
- }
2899
- ) }),
2900
- /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
2901
- /* @__PURE__ */ jsx8(FieldShell, { label: "Email provider", htmlFor: "notification-provider", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2902
- Select5,
2903
- {
2904
- value: form.providerId ?? "__default",
2905
- onValueChange: (value) => update(
2906
- "providerId",
2907
- value === "__default" ? void 0 : value
2908
- ),
2909
- children: [
2910
- /* @__PURE__ */ jsx8(
2911
- SelectTrigger5,
2912
- {
2913
- id,
2914
- "aria-describedby": describedBy,
2915
- "aria-invalid": invalid,
2916
- className: "w-full bg-transparent border-input dark:bg-muted/50",
2917
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
2918
- }
2919
- ),
2920
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2921
- /* @__PURE__ */ jsx8(SelectItem5, { value: "__default", children: defaultProviderLabel }),
2922
- providers.map((p) => /* @__PURE__ */ jsxs7(SelectItem5, { value: p.id, children: [
2923
- p.name,
2924
- p.isDefault ? " (Default)" : ""
2925
- ] }, p.id))
2926
- ] })
2927
- ]
2928
- }
2929
- ) }),
2930
- /* @__PURE__ */ jsxs7("div", { className: "space-y-1.5", children: [
2931
- /* @__PURE__ */ jsx8(
2932
- FieldShell,
2933
- {
2934
- label: "Email template",
2935
- htmlFor: "notification-template",
2936
- children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2937
- Select5,
2938
- {
2939
- value: form.templateSlug ?? "__none",
2940
- onValueChange: (value) => update(
2941
- "templateSlug",
2942
- value === "__none" ? void 0 : value
2943
- ),
2944
- children: [
2945
- /* @__PURE__ */ jsx8(
2946
- SelectTrigger5,
2947
- {
2948
- id,
2949
- "aria-describedby": describedBy,
2950
- "aria-invalid": invalid,
2951
- className: "w-full bg-transparent border-input dark:bg-muted/50",
2952
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
2953
- }
2954
- ),
2955
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2956
- /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a template" }),
2957
- templates.filter((t) => t.isActive && t.kind !== "layout").map((t) => /* @__PURE__ */ jsx8(SelectItem5, { value: t.slug, children: t.name }, t.id))
2958
- ] })
2959
- ]
2960
- }
2961
- )
2962
- }
2963
- ),
2964
- !form.templateSlug && /* @__PURE__ */ jsxs7("p", { className: "flex items-center gap-1 text-xs text-destructive", children: [
2965
- /* @__PURE__ */ jsx8(TriangleAlert2, { className: "h-3 w-3", "aria-hidden": "true" }),
2966
- "Required \u2014 a notification without a template is never sent."
2967
- ] })
2968
- ] })
2969
- ] }),
2819
+ /* @__PURE__ */ jsx8(
2820
+ SelectTrigger5,
2821
+ {
2822
+ id,
2823
+ "aria-describedby": describedBy,
2824
+ "aria-invalid": invalid,
2825
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
2826
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
2827
+ }
2828
+ ),
2829
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2830
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "static", children: "A specific address" }),
2831
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "field", children: "The visitor (email field)" })
2832
+ ] })
2833
+ ]
2834
+ }
2835
+ ) }),
2836
+ form.recipientType === "field" ? /* @__PURE__ */ jsxs7("div", { className: "space-y-1.5", children: [
2837
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Visitor email field", htmlFor: "notification-to", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2838
+ Select5,
2839
+ {
2840
+ value: toRef ?? "__none",
2841
+ onValueChange: (value) => update("to", value === "__none" ? "" : toFieldRef(value)),
2842
+ children: [
2970
2843
  /* @__PURE__ */ jsx8(
2971
- FieldShell,
2844
+ SelectTrigger5,
2972
2845
  {
2973
- label: "Sender email",
2974
- htmlFor: "notification-sender",
2975
- width: "half",
2976
- description: addressErrors.senderEmail ? void 0 : senderHelp,
2977
- error: addressErrors.senderEmail,
2978
- children: /* @__PURE__ */ jsx8(
2979
- Input3,
2980
- {
2981
- type: "email",
2982
- value: form.senderEmail ?? "",
2983
- onChange: (e) => update("senderEmail", e.target.value || void 0),
2984
- placeholder: senderPlaceholder
2985
- }
2986
- )
2846
+ id,
2847
+ "aria-describedby": describedBy,
2848
+ "aria-invalid": invalid,
2849
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
2850
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
2987
2851
  }
2988
2852
  ),
2989
- /* @__PURE__ */ jsxs7("div", { className: "space-y-4 pt-4 border-t border-border", children: [
2990
- /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
2991
- /* @__PURE__ */ jsx8(FieldShell, { label: "Send to", htmlFor: "notification-recipient-type", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2992
- Select5,
2993
- {
2994
- value: form.recipientType,
2995
- onValueChange: (value) => {
2996
- setForm((prev) => ({
2997
- ...prev,
2998
- recipientType: value,
2999
- to: ""
3000
- }));
3001
- },
3002
- children: [
3003
- /* @__PURE__ */ jsx8(
3004
- SelectTrigger5,
3005
- {
3006
- id,
3007
- "aria-describedby": describedBy,
3008
- "aria-invalid": invalid,
3009
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3010
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3011
- }
3012
- ),
3013
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3014
- /* @__PURE__ */ jsx8(SelectItem5, { value: "static", children: "A specific address" }),
3015
- /* @__PURE__ */ jsx8(SelectItem5, { value: "field", children: "The visitor (email field)" })
3016
- ] })
3017
- ]
3018
- }
3019
- ) }),
3020
- form.recipientType === "field" ? /* @__PURE__ */ jsxs7("div", { className: "space-y-1.5", children: [
3021
- /* @__PURE__ */ jsx8(
3022
- FieldShell,
3023
- {
3024
- label: "Visitor email field",
3025
- htmlFor: "notification-to",
3026
- children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3027
- Select5,
3028
- {
3029
- value: toRef ?? "__none",
3030
- onValueChange: (value) => update(
3031
- "to",
3032
- value === "__none" ? "" : toFieldRef(value)
3033
- ),
3034
- children: [
3035
- /* @__PURE__ */ jsx8(
3036
- SelectTrigger5,
3037
- {
3038
- id,
3039
- "aria-describedby": describedBy,
3040
- "aria-invalid": invalid,
3041
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3042
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3043
- }
3044
- ),
3045
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3046
- /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
3047
- toFieldOptions.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
3048
- ] })
3049
- ]
3050
- }
3051
- )
3052
- }
3053
- ),
3054
- toFieldOptions.length === 0 && /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Add an email field to the form first." })
3055
- ] }) : /* @__PURE__ */ jsx8(
3056
- FieldShell,
3057
- {
3058
- label: "Recipient address",
3059
- htmlFor: "notification-to",
3060
- width: "half",
3061
- error: addressErrors.to,
3062
- children: /* @__PURE__ */ jsx8(
3063
- Input3,
3064
- {
3065
- type: "email",
3066
- value: form.to,
3067
- onChange: (e) => update("to", e.target.value),
3068
- placeholder: defaults?.defaultToEmail || "admin@example.com"
3069
- }
3070
- )
3071
- }
3072
- )
3073
- ] }),
3074
- /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
3075
- /* @__PURE__ */ jsx8(FieldShell, { label: "Reply-To", htmlFor: "notification-replyto-mode", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3076
- Select5,
3077
- {
3078
- value: replyToMode,
3079
- onValueChange: (value) => {
3080
- const mode = value;
3081
- setReplyToMode(mode);
3082
- update("replyTo", void 0);
3083
- },
3084
- children: [
3085
- /* @__PURE__ */ jsx8(
3086
- SelectTrigger5,
3087
- {
3088
- id,
3089
- "aria-describedby": describedBy,
3090
- "aria-invalid": invalid,
3091
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3092
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3093
- }
3094
- ),
3095
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3096
- /* @__PURE__ */ jsx8(SelectItem5, { value: "none", children: "None" }),
3097
- /* @__PURE__ */ jsx8(SelectItem5, { value: "field", children: "The visitor (email field)" }),
3098
- /* @__PURE__ */ jsx8(SelectItem5, { value: "custom", children: "A custom address" })
3099
- ] })
3100
- ]
3101
- }
3102
- ) }),
3103
- replyToMode === "field" && /* @__PURE__ */ jsx8(
3104
- FieldShell,
3105
- {
3106
- label: "Visitor email field",
3107
- htmlFor: "notification-replyto",
3108
- children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3109
- Select5,
3110
- {
3111
- value: replyToRef ?? "__none",
3112
- onValueChange: (value) => update(
3113
- "replyTo",
3114
- value === "__none" ? void 0 : toFieldRef(value)
3115
- ),
3116
- children: [
3117
- /* @__PURE__ */ jsx8(
3118
- SelectTrigger5,
3119
- {
3120
- id,
3121
- "aria-describedby": describedBy,
3122
- "aria-invalid": invalid,
3123
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3124
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3125
- }
3126
- ),
3127
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3128
- /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
3129
- replyToFieldOptions.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
3130
- ] })
3131
- ]
3132
- }
3133
- )
3134
- }
3135
- ),
3136
- replyToMode === "custom" && /* @__PURE__ */ jsx8(
3137
- FieldShell,
3138
- {
3139
- label: "Reply-To address",
3140
- htmlFor: "notification-replyto",
3141
- width: "half",
3142
- error: addressErrors.replyTo,
3143
- children: /* @__PURE__ */ jsx8(
3144
- Input3,
3145
- {
3146
- type: "email",
3147
- value: form.replyTo ?? "",
3148
- onChange: (e) => update("replyTo", e.target.value || void 0),
3149
- placeholder: "replies@example.com"
3150
- }
3151
- )
3152
- }
3153
- )
3154
- ] }),
3155
- replyToMode === "field" && /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground -mt-2", children: "Replying to this email answers the person who submitted the form." }),
2853
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2854
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
2855
+ toFieldOptions.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
2856
+ ] })
2857
+ ]
2858
+ }
2859
+ ) }),
2860
+ toFieldOptions.length === 0 && /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Add an email field to the form first." })
2861
+ ] }) : /* @__PURE__ */ jsx8(
2862
+ FieldShell,
2863
+ {
2864
+ label: "Recipient address",
2865
+ htmlFor: "notification-to",
2866
+ width: "half",
2867
+ error: addressErrors.to,
2868
+ children: /* @__PURE__ */ jsx8(
2869
+ Input3,
2870
+ {
2871
+ type: "email",
2872
+ value: form.to,
2873
+ onChange: (e) => update("to", e.target.value),
2874
+ onBlur: (e) => validateAddress("to", e.target.value),
2875
+ placeholder: defaults?.defaultToEmail || "admin@example.com"
2876
+ }
2877
+ )
2878
+ }
2879
+ )
2880
+ ] }),
2881
+ /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
2882
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Reply-To", htmlFor: "notification-replyto-mode", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2883
+ Select5,
2884
+ {
2885
+ value: replyToMode,
2886
+ onValueChange: (value) => {
2887
+ const mode = value;
2888
+ setReplyToMode(mode);
2889
+ update("replyTo", void 0);
2890
+ },
2891
+ children: [
2892
+ /* @__PURE__ */ jsx8(
2893
+ SelectTrigger5,
2894
+ {
2895
+ id,
2896
+ "aria-describedby": describedBy,
2897
+ "aria-invalid": invalid,
2898
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
2899
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
2900
+ }
2901
+ ),
2902
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2903
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "none", children: "None" }),
2904
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "field", children: "The visitor (email field)" }),
2905
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "custom", children: "A custom address" })
2906
+ ] })
2907
+ ]
2908
+ }
2909
+ ) }),
2910
+ replyToMode === "field" && /* @__PURE__ */ jsx8(
2911
+ FieldShell,
2912
+ {
2913
+ label: "Visitor email field",
2914
+ htmlFor: "notification-replyto",
2915
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
2916
+ Select5,
2917
+ {
2918
+ value: replyToRef ?? "__none",
2919
+ onValueChange: (value) => update(
2920
+ "replyTo",
2921
+ value === "__none" ? void 0 : toFieldRef(value)
2922
+ ),
2923
+ children: [
3156
2924
  /* @__PURE__ */ jsx8(
3157
- AddressChipList,
2925
+ SelectTrigger5,
3158
2926
  {
3159
- id: "notification-cc",
3160
- label: "CC (optional)",
3161
- addresses: form.cc,
3162
- placeholder: "cc@example.com",
3163
- onChange: (cc) => update("cc", cc)
2927
+ id,
2928
+ "aria-describedby": describedBy,
2929
+ "aria-invalid": invalid,
2930
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
2931
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
3164
2932
  }
3165
2933
  ),
2934
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
2935
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
2936
+ replyToFieldOptions.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
2937
+ ] })
2938
+ ]
2939
+ }
2940
+ )
2941
+ }
2942
+ ),
2943
+ replyToMode === "custom" && /* @__PURE__ */ jsx8(
2944
+ FieldShell,
2945
+ {
2946
+ label: "Reply-To address",
2947
+ htmlFor: "notification-replyto",
2948
+ width: "half",
2949
+ error: addressErrors.replyTo,
2950
+ children: /* @__PURE__ */ jsx8(
2951
+ Input3,
2952
+ {
2953
+ type: "email",
2954
+ value: form.replyTo ?? "",
2955
+ onBlur: (e) => validateAddress("replyTo", e.target.value),
2956
+ onChange: (e) => update("replyTo", e.target.value || void 0),
2957
+ placeholder: "replies@example.com"
2958
+ }
2959
+ )
2960
+ }
2961
+ )
2962
+ ] }),
2963
+ replyToMode === "field" && /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground -mt-2", children: "Replying to this email answers the person who submitted the form." }),
2964
+ /* @__PURE__ */ jsx8(
2965
+ AddressChipList,
2966
+ {
2967
+ id: "notification-cc",
2968
+ label: "CC (optional)",
2969
+ addresses: form.cc,
2970
+ placeholder: "cc@example.com",
2971
+ onChange: (cc) => update("cc", cc)
2972
+ }
2973
+ ),
2974
+ /* @__PURE__ */ jsx8(
2975
+ AddressChipList,
2976
+ {
2977
+ id: "notification-bcc",
2978
+ label: "BCC (optional)",
2979
+ addresses: form.bcc,
2980
+ placeholder: "bcc@example.com",
2981
+ onChange: (bcc) => update("bcc", bcc)
2982
+ }
2983
+ )
2984
+ ] }) });
2985
+ }
2986
+ function SendConditionField({
2987
+ condition,
2988
+ fields,
2989
+ update
2990
+ }) {
2991
+ return /* @__PURE__ */ jsx8(Fragment3, { children: /* @__PURE__ */ jsxs7("div", { className: "space-y-3 pt-4 border-t border-border", children: [
2992
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
2993
+ /* @__PURE__ */ jsxs7("div", { children: [
2994
+ /* @__PURE__ */ jsx8("p", { className: "text-sm font-medium text-foreground", children: "Send condition" }),
2995
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Only send this notification when a submitted value matches." })
2996
+ ] }),
2997
+ !condition && /* @__PURE__ */ jsxs7(
2998
+ Button6,
2999
+ {
3000
+ type: "button",
3001
+ variant: "outline",
3002
+ size: "sm",
3003
+ onClick: () => update("condition", {
3004
+ field: fields[0]?.name ?? "",
3005
+ comparison: "equals",
3006
+ value: ""
3007
+ }),
3008
+ disabled: fields.length === 0,
3009
+ children: [
3010
+ /* @__PURE__ */ jsx8(Plus2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3011
+ "Add condition"
3012
+ ]
3013
+ }
3014
+ )
3015
+ ] }),
3016
+ condition && /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-end gap-2 border border-border bg-muted/40 p-3", children: [
3017
+ /* @__PURE__ */ jsx8(
3018
+ FieldShell,
3019
+ {
3020
+ label: "Field",
3021
+ htmlFor: "notification-condition-field",
3022
+ className: "min-w-36 flex-1",
3023
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3024
+ Select5,
3025
+ {
3026
+ value: condition.field || "__none",
3027
+ onValueChange: (value) => update("condition", {
3028
+ ...condition,
3029
+ field: value === "__none" ? "" : value
3030
+ }),
3031
+ children: [
3166
3032
  /* @__PURE__ */ jsx8(
3167
- AddressChipList,
3033
+ SelectTrigger5,
3168
3034
  {
3169
- id: "notification-bcc",
3170
- label: "BCC (optional)",
3171
- addresses: form.bcc,
3172
- placeholder: "bcc@example.com",
3173
- onChange: (bcc) => update("bcc", bcc)
3035
+ id,
3036
+ "aria-describedby": describedBy,
3037
+ "aria-invalid": invalid,
3038
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
3039
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
3174
3040
  }
3175
- )
3176
- ] }),
3177
- /* @__PURE__ */ jsxs7("div", { className: "space-y-3 pt-4 border-t border-border", children: [
3178
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
3179
- /* @__PURE__ */ jsxs7("div", { children: [
3180
- /* @__PURE__ */ jsx8("p", { className: "text-sm font-medium text-foreground", children: "Send condition" }),
3181
- /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Only send this notification when a submitted value matches." })
3182
- ] }),
3183
- !condition && /* @__PURE__ */ jsxs7(
3184
- Button6,
3185
- {
3186
- type: "button",
3187
- variant: "outline",
3188
- size: "sm",
3189
- onClick: () => update("condition", {
3190
- field: fields[0]?.name ?? "",
3191
- comparison: "equals",
3192
- value: ""
3193
- }),
3194
- disabled: fields.length === 0,
3195
- children: [
3196
- /* @__PURE__ */ jsx8(Plus2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3197
- "Add condition"
3198
- ]
3199
- }
3200
- )
3201
- ] }),
3202
- condition && /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-end gap-2 border border-border bg-muted/40 p-3", children: [
3203
- /* @__PURE__ */ jsx8(
3204
- FieldShell,
3205
- {
3206
- label: "Field",
3207
- htmlFor: "notification-condition-field",
3208
- className: "min-w-36 flex-1",
3209
- children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3210
- Select5,
3211
- {
3212
- value: condition.field || "__none",
3213
- onValueChange: (value) => update("condition", {
3214
- ...condition,
3215
- field: value === "__none" ? "" : value
3216
- }),
3217
- children: [
3218
- /* @__PURE__ */ jsx8(
3219
- SelectTrigger5,
3220
- {
3221
- id,
3222
- "aria-describedby": describedBy,
3223
- "aria-invalid": invalid,
3224
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3225
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3226
- }
3227
- ),
3228
- /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3229
- /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
3230
- fields.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
3231
- ] })
3232
- ]
3233
- }
3234
- )
3235
- }
3236
- ),
3237
- /* @__PURE__ */ jsx8(
3238
- FieldShell,
3239
- {
3240
- label: "Comparison",
3241
- htmlFor: "notification-condition-comparison",
3242
- className: "min-w-36 flex-1",
3243
- children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3244
- Select5,
3245
- {
3246
- value: condition.comparison,
3247
- onValueChange: (value) => update("condition", {
3248
- ...condition,
3249
- comparison: value
3250
- }),
3251
- children: [
3252
- /* @__PURE__ */ jsx8(
3253
- SelectTrigger5,
3254
- {
3255
- id,
3256
- "aria-describedby": describedBy,
3257
- "aria-invalid": invalid,
3258
- className: "w-full bg-transparent border-input dark:bg-muted/50",
3259
- children: /* @__PURE__ */ jsx8(SelectValue5, {})
3260
- }
3261
- ),
3262
- /* @__PURE__ */ jsx8(SelectContent5, { children: Object.entries(COMPARISON_LABELS).map(
3263
- ([value, label]) => /* @__PURE__ */ jsx8(SelectItem5, { value, children: label }, value)
3264
- ) })
3265
- ]
3266
- }
3267
- )
3268
- }
3269
- ),
3270
- !VALUELESS_COMPARISONS.has(condition.comparison) && /* @__PURE__ */ jsx8(
3271
- FieldShell,
3272
- {
3273
- label: "Value",
3274
- htmlFor: "notification-condition-value",
3275
- className: "min-w-36 flex-1",
3276
- children: /* @__PURE__ */ jsx8(
3277
- Input3,
3278
- {
3279
- type: "text",
3280
- value: typeof condition.value === "string" || typeof condition.value === "number" ? String(condition.value) : "",
3281
- onChange: (e) => update("condition", {
3282
- ...condition,
3283
- value: e.target.value
3284
- })
3285
- }
3286
- )
3287
- }
3288
- ),
3289
- /* @__PURE__ */ jsx8(
3290
- Button6,
3291
- {
3292
- type: "button",
3293
- variant: "ghost",
3294
- size: "icon",
3295
- className: "h-9 w-9 rounded-none text-muted-foreground hover:text-destructive",
3296
- onClick: () => update("condition", void 0),
3297
- "aria-label": "Remove send condition",
3298
- children: /* @__PURE__ */ jsx8(Trash22, { className: "h-4 w-4", "aria-hidden": "true" })
3299
- }
3300
- )
3041
+ ),
3042
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3043
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a field" }),
3044
+ fields.map((f) => /* @__PURE__ */ jsx8(SelectItem5, { value: f.name, children: f.label }, f.name))
3301
3045
  ] })
3302
- ] }),
3303
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between pt-4 border-t border-border", children: [
3304
- /* @__PURE__ */ jsxs7("div", { children: [
3305
- /* @__PURE__ */ jsx8(Label2, { htmlFor: "notification-enabled", children: "Enabled" }),
3306
- /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Turn off to keep the rule without sending." })
3307
- ] }),
3046
+ ]
3047
+ }
3048
+ )
3049
+ }
3050
+ ),
3051
+ /* @__PURE__ */ jsx8(
3052
+ FieldShell,
3053
+ {
3054
+ label: "Comparison",
3055
+ htmlFor: "notification-condition-comparison",
3056
+ className: "min-w-36 flex-1",
3057
+ children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3058
+ Select5,
3059
+ {
3060
+ value: condition.comparison,
3061
+ onValueChange: (value) => update("condition", {
3062
+ ...condition,
3063
+ comparison: value
3064
+ }),
3065
+ children: [
3308
3066
  /* @__PURE__ */ jsx8(
3309
- Switch2,
3067
+ SelectTrigger5,
3310
3068
  {
3311
- id: "notification-enabled",
3312
- checked: form.enabled,
3313
- onCheckedChange: (checked) => update("enabled", checked)
3069
+ id,
3070
+ "aria-describedby": describedBy,
3071
+ "aria-invalid": invalid,
3072
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
3073
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
3314
3074
  }
3315
- )
3316
- ] })
3317
- ] }),
3318
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-end gap-3 border-t border-border bg-muted p-4", children: [
3319
- /* @__PURE__ */ jsx8(Button6, { type: "button", variant: "outline", onClick: onClose, children: "Cancel" }),
3320
- /* @__PURE__ */ jsx8(
3321
- Button6,
3322
- {
3323
- type: "button",
3324
- onClick: handleSave,
3325
- disabled: !form.name.trim(),
3326
- children: isEditing ? "Save changes" : "Add notification"
3327
- }
3328
- )
3329
- ] })
3330
- ]
3075
+ ),
3076
+ /* @__PURE__ */ jsx8(SelectContent5, { children: Object.entries(COMPARISON_LABELS).map(([value, label]) => /* @__PURE__ */ jsx8(SelectItem5, { value, children: label }, value)) })
3077
+ ]
3078
+ }
3079
+ )
3080
+ }
3081
+ ),
3082
+ !VALUELESS_COMPARISONS.has(condition.comparison) && /* @__PURE__ */ jsx8(
3083
+ FieldShell,
3084
+ {
3085
+ label: "Value",
3086
+ htmlFor: "notification-condition-value",
3087
+ className: "min-w-36 flex-1",
3088
+ children: /* @__PURE__ */ jsx8(
3089
+ Input3,
3090
+ {
3091
+ type: "text",
3092
+ value: typeof condition.value === "string" || typeof condition.value === "number" ? String(condition.value) : "",
3093
+ onChange: (e) => update("condition", {
3094
+ ...condition,
3095
+ value: e.target.value
3096
+ })
3097
+ }
3098
+ )
3099
+ }
3100
+ ),
3101
+ /* @__PURE__ */ jsx8(
3102
+ Button6,
3103
+ {
3104
+ type: "button",
3105
+ variant: "ghost",
3106
+ size: "icon",
3107
+ className: "h-9 w-9 rounded-none text-muted-foreground hover:text-destructive",
3108
+ onClick: () => update("condition", void 0),
3109
+ "aria-label": "Remove send condition",
3110
+ children: /* @__PURE__ */ jsx8(Trash22, { className: "h-4 w-4", "aria-hidden": "true" })
3331
3111
  }
3332
3112
  )
3333
- }
3334
- );
3113
+ ] })
3114
+ ] }) });
3335
3115
  }
3336
- function FormNotificationsTab({ defaults }) {
3337
- const {
3338
- notifications,
3339
- fields,
3340
- addNotification,
3341
- duplicateNotification,
3342
- updateNotification,
3343
- deleteNotification
3344
- } = useFormBuilder();
3345
- const [providers, setProviders] = useState7([]);
3346
- const [templates, setTemplates] = useState7([]);
3347
- const [sheetState, setSheetState] = useState7({ open: false, editing: null });
3348
- useEffect3(() => {
3349
- void fetchProviders().then(setProviders);
3350
- void fetchTemplates().then(setTemplates);
3351
- }, []);
3352
- const fieldList = useMemo6(
3353
- () => fields.map((f) => ({ name: f.name, label: f.label, type: f.type })),
3354
- [fields]
3355
- );
3356
- const openAddSheet = useCallback6(() => {
3357
- setSheetState({ open: true, editing: null });
3358
- }, []);
3359
- const openEditSheet = useCallback6((notification) => {
3360
- setSheetState({ open: true, editing: notification });
3361
- }, []);
3362
- const closeSheet = useCallback6(() => {
3363
- setSheetState({ open: false, editing: null });
3364
- }, []);
3365
- const handleSave = useCallback6(
3366
- (notification) => {
3367
- if (sheetState.editing) {
3368
- updateNotification(notification.id, notification);
3369
- } else {
3370
- addNotification(notification);
3371
- }
3372
- closeSheet();
3373
- },
3374
- [sheetState.editing, addNotification, updateNotification, closeSheet]
3375
- );
3376
- const getProviderName = useCallback6(
3377
- (providerId) => {
3378
- if (!providerId) {
3379
- const def = providers.find((p) => p.isDefault);
3380
- return def ? `${def.name} (default)` : "System default";
3116
+ function NotificationIdentityFields({
3117
+ form,
3118
+ providers,
3119
+ defaults,
3120
+ templates,
3121
+ update,
3122
+ addressErrors,
3123
+ validateAddress
3124
+ }) {
3125
+ const defaultProvider = providers.find((p) => p.isDefault);
3126
+ const defaultProviderLabel = defaultProvider ? `System default (${defaultProvider.name})` : "System default";
3127
+ const senderPlaceholder = defaults?.defaultFrom || "Provider default";
3128
+ const senderHelp = defaults?.defaultFrom ? `Leave blank to send from ${defaults.defaultFrom} (the configured default).` : "Leave blank to use the template or provider default address.";
3129
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
3130
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Name", htmlFor: "notification-name", width: "half", children: /* @__PURE__ */ jsx8(
3131
+ Input3,
3132
+ {
3133
+ type: "text",
3134
+ value: form.name,
3135
+ onChange: (e) => update("name", e.target.value),
3136
+ placeholder: "e.g. Admin notification"
3381
3137
  }
3382
- return providers.find((p) => p.id === providerId)?.name ?? "Unknown";
3383
- },
3384
- [providers]
3385
- );
3386
- const initialNotification = sheetState.editing ?? createNotification();
3387
- return (
3388
- // The measure belongs to `FormLayout` now, not to this tab.
3389
- /* @__PURE__ */ jsxs7("div", { children: [
3390
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between mb-8 pb-4 border-b border-border", children: [
3391
- /* @__PURE__ */ jsxs7("div", { children: [
3392
- /* @__PURE__ */ jsx8("h3", { className: "text-xl font-semibold text-foreground", children: "Notifications" }),
3393
- /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground mt-1", children: "Emails sent when someone submits this form." })
3394
- ] }),
3395
- /* @__PURE__ */ jsxs7(Button6, { type: "button", onClick: openAddSheet, children: [
3396
- /* @__PURE__ */ jsx8(Plus2, { className: "h-4 w-4", "aria-hidden": "true" }),
3138
+ ) }),
3139
+ /* @__PURE__ */ jsxs7(Grid, { cols: 2, responsive: true, children: [
3140
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Email provider", htmlFor: "notification-provider", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3141
+ Select5,
3142
+ {
3143
+ value: form.providerId ?? "__default",
3144
+ onValueChange: (value) => update("providerId", value === "__default" ? void 0 : value),
3145
+ children: [
3146
+ /* @__PURE__ */ jsx8(
3147
+ SelectTrigger5,
3148
+ {
3149
+ id,
3150
+ "aria-describedby": describedBy,
3151
+ "aria-invalid": invalid,
3152
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
3153
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
3154
+ }
3155
+ ),
3156
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3157
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "__default", children: defaultProviderLabel }),
3158
+ providers.map((p) => /* @__PURE__ */ jsxs7(SelectItem5, { value: p.id, children: [
3159
+ p.name,
3160
+ p.isDefault ? " (Default)" : ""
3161
+ ] }, p.id))
3162
+ ] })
3163
+ ]
3164
+ }
3165
+ ) }),
3166
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-1.5", children: [
3167
+ /* @__PURE__ */ jsx8(FieldShell, { label: "Email template", htmlFor: "notification-template", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs7(
3168
+ Select5,
3169
+ {
3170
+ value: form.templateSlug ?? "__none",
3171
+ onValueChange: (value) => update("templateSlug", value === "__none" ? void 0 : value),
3172
+ children: [
3173
+ /* @__PURE__ */ jsx8(
3174
+ SelectTrigger5,
3175
+ {
3176
+ id,
3177
+ "aria-describedby": describedBy,
3178
+ "aria-invalid": invalid,
3179
+ className: "w-full bg-transparent border-input dark:bg-muted/50",
3180
+ children: /* @__PURE__ */ jsx8(SelectValue5, {})
3181
+ }
3182
+ ),
3183
+ /* @__PURE__ */ jsxs7(SelectContent5, { children: [
3184
+ /* @__PURE__ */ jsx8(SelectItem5, { value: "__none", children: "Select a template" }),
3185
+ templates.filter((t) => t.isActive && t.kind !== "layout").map((t) => /* @__PURE__ */ jsx8(SelectItem5, { value: t.slug, children: t.name }, t.id))
3186
+ ] })
3187
+ ]
3188
+ }
3189
+ ) }),
3190
+ !form.templateSlug && /* @__PURE__ */ jsxs7("p", { className: "flex items-center gap-1 text-xs text-destructive", children: [
3191
+ /* @__PURE__ */ jsx8(TriangleAlert2, { className: "h-3 w-3", "aria-hidden": "true" }),
3192
+ "Required \u2014 a notification without a template is never sent."
3193
+ ] })
3194
+ ] })
3195
+ ] }),
3196
+ /* @__PURE__ */ jsx8(
3197
+ FieldShell,
3198
+ {
3199
+ label: "Sender email",
3200
+ htmlFor: "notification-sender",
3201
+ width: "half",
3202
+ description: addressErrors.senderEmail ? void 0 : senderHelp,
3203
+ error: addressErrors.senderEmail,
3204
+ children: /* @__PURE__ */ jsx8(
3205
+ Input3,
3206
+ {
3207
+ type: "email",
3208
+ value: form.senderEmail ?? "",
3209
+ onChange: (e) => update("senderEmail", e.target.value || void 0),
3210
+ onBlur: (e) => validateAddress("senderEmail", e.target.value),
3211
+ placeholder: senderPlaceholder
3212
+ }
3213
+ )
3214
+ }
3215
+ )
3216
+ ] });
3217
+ }
3218
+ function initialReplyToMode(replyTo) {
3219
+ if (!replyTo) return "none";
3220
+ return parseFieldRef(replyTo) ? "field" : "custom";
3221
+ }
3222
+ function NotificationEditor({
3223
+ notification,
3224
+ editorId,
3225
+ providers,
3226
+ templates,
3227
+ fields,
3228
+ defaults,
3229
+ onChange
3230
+ }) {
3231
+ const form = notification;
3232
+ const [replyToMode, setReplyToMode] = useState7(
3233
+ initialReplyToMode(notification.replyTo)
3234
+ );
3235
+ const [addressErrors, setAddressErrors] = useState7(
3236
+ () => addressErrorsIn(notification)
3237
+ );
3238
+ const update = useCallback6(
3239
+ (key, value) => {
3240
+ onChange({ ...notification, [key]: value });
3241
+ setAddressErrors(
3242
+ (prev) => key in prev ? { ...prev, [key]: void 0 } : prev
3243
+ );
3244
+ },
3245
+ [notification, onChange]
3246
+ );
3247
+ const changeRecipientType = useCallback6(
3248
+ (kind) => {
3249
+ onChange({ ...notification, recipientType: kind, to: "" });
3250
+ },
3251
+ [notification, onChange]
3252
+ );
3253
+ const validateAddress = useCallback6(
3254
+ (key, value) => {
3255
+ setAddressErrors((prev) => ({ ...prev, [key]: addressError(key, value) }));
3256
+ },
3257
+ []
3258
+ );
3259
+ const condition = form.condition;
3260
+ return /* @__PURE__ */ jsx8("div", { id: editorId, className: "border-t border-border bg-muted/20 px-4 py-5", children: /* @__PURE__ */ jsxs7("div", { className: "space-y-6", children: [
3261
+ /* @__PURE__ */ jsx8(
3262
+ NotificationIdentityFields,
3263
+ {
3264
+ form,
3265
+ providers,
3266
+ defaults,
3267
+ templates,
3268
+ update,
3269
+ addressErrors,
3270
+ validateAddress
3271
+ }
3272
+ ),
3273
+ /* @__PURE__ */ jsx8(
3274
+ RecipientFields,
3275
+ {
3276
+ form,
3277
+ fields,
3278
+ defaults,
3279
+ update,
3280
+ onRecipientTypeChange: changeRecipientType,
3281
+ addressErrors,
3282
+ validateAddress,
3283
+ replyToMode,
3284
+ setReplyToMode
3285
+ }
3286
+ ),
3287
+ /* @__PURE__ */ jsx8(
3288
+ SendConditionField,
3289
+ {
3290
+ condition,
3291
+ fields,
3292
+ update
3293
+ }
3294
+ ),
3295
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between pt-4 border-t border-border", children: [
3296
+ /* @__PURE__ */ jsxs7("div", { children: [
3297
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "notification-enabled", children: "Enabled" }),
3298
+ /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: "Turn off to keep the rule without sending." })
3299
+ ] }),
3300
+ /* @__PURE__ */ jsx8(
3301
+ Switch2,
3302
+ {
3303
+ id: "notification-enabled",
3304
+ checked: form.enabled,
3305
+ onCheckedChange: (checked) => update("enabled", checked)
3306
+ }
3307
+ )
3308
+ ] })
3309
+ ] }) });
3310
+ }
3311
+ function FormNotificationsTab({ defaults }) {
3312
+ const {
3313
+ notifications,
3314
+ fields,
3315
+ addNotification,
3316
+ duplicateNotification,
3317
+ updateNotification,
3318
+ deleteNotification
3319
+ } = useFormBuilder();
3320
+ const [providers, setProviders] = useState7([]);
3321
+ const [templates, setTemplates] = useState7([]);
3322
+ const [expandedId, setExpandedId] = useState7(null);
3323
+ useEffect3(() => {
3324
+ void fetchProviders().then(setProviders);
3325
+ void fetchTemplates().then(setTemplates);
3326
+ }, []);
3327
+ const fieldList = useMemo6(
3328
+ () => fields.map((f) => ({ name: f.name, label: f.label, type: f.type })),
3329
+ [fields]
3330
+ );
3331
+ const addAndOpen = useCallback6(() => {
3332
+ const created = createNotification();
3333
+ addNotification(created);
3334
+ setExpandedId(created.id);
3335
+ }, [addNotification]);
3336
+ const toggleExpanded = useCallback6((id) => {
3337
+ setExpandedId((current) => current === id ? null : id);
3338
+ }, []);
3339
+ const getProviderName = useCallback6(
3340
+ (providerId) => {
3341
+ if (!providerId) {
3342
+ const def = providers.find((p) => p.isDefault);
3343
+ return def ? `${def.name} (default)` : "System default";
3344
+ }
3345
+ return providers.find((p) => p.id === providerId)?.name ?? "Unknown";
3346
+ },
3347
+ [providers]
3348
+ );
3349
+ return (
3350
+ // The measure belongs to the page's shell, not to this tab.
3351
+ /* @__PURE__ */ jsxs7("div", { children: [
3352
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between mb-8 pb-4 border-b border-border", children: [
3353
+ /* @__PURE__ */ jsxs7("div", { children: [
3354
+ /* @__PURE__ */ jsx8("h3", { className: "text-xl font-semibold text-foreground", children: "Notifications" }),
3355
+ /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground mt-1", children: "Emails sent when someone submits this form." })
3356
+ ] }),
3357
+ /* @__PURE__ */ jsxs7(Button6, { type: "button", onClick: addAndOpen, children: [
3358
+ /* @__PURE__ */ jsx8(Plus2, { className: "h-4 w-4", "aria-hidden": "true" }),
3397
3359
  "Add notification"
3398
3360
  ] })
3399
3361
  ] }),
@@ -3401,7 +3363,7 @@ function FormNotificationsTab({ defaults }) {
3401
3363
  /* @__PURE__ */ jsx8("div", { className: "flex items-center justify-center w-12 h-12 rounded-none border border-border bg-background mb-4 text-primary", children: /* @__PURE__ */ jsx8(Mail, { className: "h-6 w-6", "aria-hidden": "true" }) }),
3402
3364
  /* @__PURE__ */ jsx8("p", { className: "text-base font-medium text-foreground mb-1", children: "No notifications yet" }),
3403
3365
  /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground mb-6 text-center max-w-sm", children: "Add a notification to email someone when this form is submitted." }),
3404
- /* @__PURE__ */ jsxs7(Button6, { type: "button", onClick: openAddSheet, children: [
3366
+ /* @__PURE__ */ jsxs7(Button6, { type: "button", onClick: addAndOpen, children: [
3405
3367
  /* @__PURE__ */ jsx8(Plus2, { className: "h-4 w-4", "aria-hidden": "true" }),
3406
3368
  "Add notification"
3407
3369
  ] })
@@ -3411,52 +3373,771 @@ function FormNotificationsTab({ defaults }) {
3411
3373
  notification,
3412
3374
  providerName: getProviderName(notification.providerId),
3413
3375
  fields: fieldList,
3414
- onEdit: () => openEditSheet(notification),
3376
+ expanded: expandedId === notification.id,
3377
+ editorId: `notification-editor-${notification.id}`,
3378
+ onToggle: () => toggleExpanded(notification.id),
3415
3379
  onDuplicate: () => duplicateNotification(notification.id),
3416
3380
  onDelete: () => deleteNotification(notification.id),
3417
- onToggleEnabled: (enabled) => updateNotification(notification.id, { enabled })
3381
+ onToggleEnabled: (enabled) => updateNotification(notification.id, { enabled }),
3382
+ children: /* @__PURE__ */ jsx8(
3383
+ NotificationEditor,
3384
+ {
3385
+ notification,
3386
+ editorId: `notification-editor-${notification.id}`,
3387
+ providers,
3388
+ templates,
3389
+ fields: fieldList,
3390
+ defaults,
3391
+ onChange: (next) => updateNotification(notification.id, next)
3392
+ }
3393
+ )
3418
3394
  },
3419
3395
  notification.id
3420
- )) }),
3421
- sheetState.open && /* @__PURE__ */ jsx8(
3422
- NotificationSheet,
3396
+ )) })
3397
+ ] })
3398
+ );
3399
+ }
3400
+
3401
+ // src/admin/components/builder/FormPreview.tsx
3402
+ import {
3403
+ Badge as Badge4,
3404
+ Button as Button7,
3405
+ Checkbox as Checkbox3,
3406
+ Input as Input6,
3407
+ Card as Card2,
3408
+ Label as Label4,
3409
+ RadioGroup as RadioGroup2,
3410
+ RadioGroupItem as RadioGroupItem2,
3411
+ Select as Select8,
3412
+ SelectContent as SelectContent8,
3413
+ SelectItem as SelectItem8,
3414
+ SelectTrigger as SelectTrigger8,
3415
+ SelectValue as SelectValue8,
3416
+ Tabs as Tabs3,
3417
+ TabsList as TabsList3,
3418
+ TabsTrigger as TabsTrigger3,
3419
+ Textarea as Textarea3
3420
+ } from "@nextlyhq/ui";
3421
+ import { Eye, Monitor, Paperclip, RotateCcw, Smartphone } from "lucide-react";
3422
+ import { useMemo as useMemo7, useState as useState9 } from "react";
3423
+
3424
+ // src/admin/components/builder/FormSettingsTab.tsx
3425
+ import {
3426
+ FormSection,
3427
+ Input as Input5,
3428
+ Label as Label3,
3429
+ RadioGroup,
3430
+ RadioGroupItem,
3431
+ Select as Select7,
3432
+ SelectContent as SelectContent7,
3433
+ SelectItem as SelectItem7,
3434
+ SelectTrigger as SelectTrigger7,
3435
+ SelectValue as SelectValue7,
3436
+ Switch as Switch3,
3437
+ Textarea as Textarea2
3438
+ } from "@nextlyhq/ui";
3439
+
3440
+ // src/admin/components/builder/RedirectPagePicker.tsx
3441
+ import {
3442
+ Input as Input4,
3443
+ Select as Select6,
3444
+ SelectContent as SelectContent6,
3445
+ SelectGroup,
3446
+ SelectItem as SelectItem6,
3447
+ SelectLabel,
3448
+ SelectTrigger as SelectTrigger6,
3449
+ SelectValue as SelectValue6
3450
+ } from "@nextlyhq/ui";
3451
+ import { encodeSelectParam } from "nextly/query";
3452
+ import { useEffect as useEffect4, useState as useState8 } from "react";
3453
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3454
+ var PAGE_SIZE2 = 50;
3455
+ var FALLBACK_TITLE_FIELDS = ["title", "name", "label", "slug"];
3456
+ function labelFieldsFor(useAsTitle) {
3457
+ const configured = useAsTitle?.trim();
3458
+ if (!configured) return ["id", ...FALLBACK_TITLE_FIELDS];
3459
+ return [
3460
+ "id",
3461
+ configured,
3462
+ ...FALLBACK_TITLE_FIELDS.filter((field) => field !== configured)
3463
+ ];
3464
+ }
3465
+ function selectFieldsFor(useAsTitle) {
3466
+ return [...labelFieldsFor(useAsTitle), "status", "firstPublishedAt"];
3467
+ }
3468
+ function selectParam(fields) {
3469
+ return encodeURIComponent(encodeSelectParam(fields));
3470
+ }
3471
+ function selectionKey(value, collections) {
3472
+ const reference = parseRedirectReference(value, collections);
3473
+ return reference ? `${reference.collection}:${reference.id}` : void 0;
3474
+ }
3475
+ function documentLabel(row, fields = labelFieldsFor()) {
3476
+ for (const field of fields.filter((name) => name !== "id")) {
3477
+ const candidate = row[field];
3478
+ const text = typeof candidate === "string" ? candidate : typeof candidate === "number" && Number.isFinite(candidate) ? String(candidate) : typeof candidate === "boolean" ? String(candidate) : void 0;
3479
+ if (text !== void 0 && text.trim()) return text;
3480
+ }
3481
+ return typeof row.id === "string" ? row.id : "Untitled";
3482
+ }
3483
+ async function fetchChoices(collection, query, page, meta) {
3484
+ const search = query ? `&search=${encodeURIComponent(query)}` : "";
3485
+ try {
3486
+ const response = await fetch(
3487
+ `/admin/api/collections/${collection}/entries?limit=${PAGE_SIZE2}&page=${page}&status=all&depth=0&select=${selectParam(selectFieldsFor(meta.titleField))}${search}`,
3488
+ { credentials: "include" }
3489
+ );
3490
+ if (!response.ok) return null;
3491
+ const body = await response.json();
3492
+ return {
3493
+ rows: (body.items ?? []).filter((row) => typeof row.id === "string").map((row) => ({
3494
+ collection,
3495
+ id: row.id,
3496
+ label: documentLabel(row, labelFieldsFor(meta.titleField)),
3497
+ reachability: documentReachability(
3498
+ row,
3499
+ meta.localized
3500
+ )
3501
+ })),
3502
+ // Reported rather than assumed, so the control can offer the rest
3503
+ // instead of truncating where the author cannot see it.
3504
+ hasMore: page < (body.meta?.totalPages ?? 1)
3505
+ };
3506
+ } catch {
3507
+ return null;
3508
+ }
3509
+ }
3510
+ function mergeChoices(existing, incoming) {
3511
+ const seen = new Set(existing.map((c) => `${c.collection}:${c.id}`));
3512
+ const merged = [...existing];
3513
+ for (const choice of incoming) {
3514
+ const key = `${choice.collection}:${choice.id}`;
3515
+ if (seen.has(key)) continue;
3516
+ seen.add(key);
3517
+ merged.push(choice);
3518
+ }
3519
+ return merged;
3520
+ }
3521
+ function renderChoice(choice) {
3522
+ return /* @__PURE__ */ jsx9(
3523
+ SelectItem6,
3524
+ {
3525
+ value: `${choice.collection}:${choice.id}`,
3526
+ children: /* @__PURE__ */ jsxs8("span", { className: "inline-flex items-center gap-2", children: [
3527
+ /* @__PURE__ */ jsx9("span", { children: choice.label }),
3528
+ choice.reachability === "unreachable" && /* @__PURE__ */ jsx9("span", { className: "shrink-0 rounded-sm border px-1 py-px text-[10px] uppercase tracking-wide text-muted-foreground", children: "Draft" })
3529
+ ] })
3530
+ },
3531
+ `${choice.collection}:${choice.id}`
3532
+ );
3533
+ }
3534
+ function metadataCollections(collections, value) {
3535
+ const stored = selectionKey(value, collections)?.split(":")[0];
3536
+ const wanted = stored && !collections.includes(stored) ? [...collections, stored] : collections;
3537
+ return wanted.join(",");
3538
+ }
3539
+ async function fetchCollectionMeta(collection) {
3540
+ try {
3541
+ const response = await fetch(`/admin/api/collections/${collection}`, {
3542
+ credentials: "include"
3543
+ });
3544
+ if (!response.ok) return { titleField: void 0, localized: void 0 };
3545
+ const body = await response.json();
3546
+ const configured = body.admin?.useAsTitle;
3547
+ return {
3548
+ titleField: typeof configured === "string" && configured.trim() ? configured.trim() : void 0,
3549
+ localized: typeof body.localized === "boolean" ? body.localized : void 0
3550
+ };
3551
+ } catch {
3552
+ return { titleField: void 0, localized: void 0 };
3553
+ }
3554
+ }
3555
+ function useTitleFields(key) {
3556
+ const [fields, setFields] = useState8(
3557
+ null
3558
+ );
3559
+ useEffect4(() => {
3560
+ let cancelled = false;
3561
+ const wanted = key ? key.split(",") : [];
3562
+ setFields(null);
3563
+ void Promise.all(
3564
+ wanted.map(async (collection) => {
3565
+ return [collection, await fetchCollectionMeta(collection)];
3566
+ })
3567
+ ).then((pairs) => {
3568
+ if (!cancelled) setFields(Object.fromEntries(pairs));
3569
+ });
3570
+ return () => {
3571
+ cancelled = true;
3572
+ };
3573
+ }, [key]);
3574
+ return fields;
3575
+ }
3576
+ function useChoices(key, applied, titleFields) {
3577
+ const [choices, setChoices] = useState8(null);
3578
+ const [failed, setFailed] = useState8([]);
3579
+ const [hasMore, setHasMore] = useState8(false);
3580
+ const [loading, setLoading] = useState8(false);
3581
+ const [pages, setPages] = useState8({});
3582
+ useEffect4(() => {
3583
+ setPages((current) => Object.keys(current).length === 0 ? current : {});
3584
+ }, [key, applied]);
3585
+ useEffect4(() => {
3586
+ if (titleFields === null) return;
3587
+ let cancelled = false;
3588
+ const wanted = key ? key.split(",") : [];
3589
+ setLoading(true);
3590
+ void Promise.all(
3591
+ wanted.map(
3592
+ (collection) => fetchChoices(
3593
+ collection,
3594
+ applied,
3595
+ pages[collection] ?? 1,
3596
+ titleFields[collection] ?? {
3597
+ titleField: void 0,
3598
+ localized: void 0
3599
+ }
3600
+ )
3601
+ )
3602
+ ).then((results) => {
3603
+ if (cancelled) return;
3604
+ setLoading(false);
3605
+ setFailed(wanted.filter((_, index) => results[index] === null));
3606
+ const loaded = results.filter(
3607
+ (result) => result !== null
3608
+ );
3609
+ setHasMore(loaded.some((result) => result.hasMore));
3610
+ const rows = loaded.flatMap((result) => result.rows);
3611
+ const first = wanted.every((collection) => (pages[collection] ?? 1) === 1);
3612
+ setChoices((current) => mergeChoices(first ? [] : current ?? [], rows));
3613
+ });
3614
+ return () => {
3615
+ cancelled = true;
3616
+ };
3617
+ }, [key, applied, pages, titleFields]);
3618
+ return {
3619
+ choices,
3620
+ setChoices,
3621
+ failed,
3622
+ hasMore,
3623
+ loading,
3624
+ /**
3625
+ * Advances only the collections that have more to give, and RETRIES the
3626
+ * ones whose last page failed rather than stepping over it.
3627
+ *
3628
+ * Guarded on the in-flight flag: two clicks before the first resolves
3629
+ * either batch into a double increment or cancel the pending effect, and a
3630
+ * skipped page is then absent from a list that reports having loaded it.
3631
+ */
3632
+ loadMore: () => {
3633
+ if (loading) return;
3634
+ setPages((current) => {
3635
+ const next = { ...current };
3636
+ for (const collection of key ? key.split(",") : []) {
3637
+ if (failed.includes(collection)) continue;
3638
+ next[collection] = (current[collection] ?? 1) + 1;
3639
+ }
3640
+ return next;
3641
+ });
3642
+ },
3643
+ /** Ask the failed collections again, without advancing anyone. */
3644
+ retryFailed: () => {
3645
+ if (!loading) setPages((current) => ({ ...current }));
3646
+ }
3647
+ };
3648
+ }
3649
+ function useSelectedChoice(selected, choices, setChoices, titleFields) {
3650
+ const [unreadable, setUnreadable] = useState8(false);
3651
+ useEffect4(() => {
3652
+ if (!selected || choices === null) return;
3653
+ const separator = selected.indexOf(":");
3654
+ const collection = selected.slice(0, separator);
3655
+ const id = selected.slice(separator + 1);
3656
+ if (!id || choices.some(
3657
+ (choice) => choice.id === id && choice.collection === collection
3658
+ )) {
3659
+ setUnreadable(false);
3660
+ return;
3661
+ }
3662
+ let cancelled = false;
3663
+ void (async () => {
3664
+ try {
3665
+ const response = await fetch(
3666
+ `/admin/api/collections/${collection}/entries/${id}?depth=0&status=all`,
3667
+ { credentials: "include" }
3668
+ );
3669
+ if (!response.ok) {
3670
+ if (!cancelled) setUnreadable(true);
3671
+ return;
3672
+ }
3673
+ const row = await response.json();
3674
+ if (cancelled) return;
3675
+ if (!row || typeof row.id !== "string") {
3676
+ setUnreadable(true);
3677
+ return;
3678
+ }
3679
+ setUnreadable(false);
3680
+ setChoices((current) => [
3681
+ {
3682
+ collection,
3683
+ id: row.id,
3684
+ // The same fields the listing labels by. Labelling the recovered
3685
+ // document differently would show one document under two names
3686
+ // depending on whether the listing happened to reach it.
3687
+ label: documentLabel(
3688
+ row,
3689
+ labelFieldsFor(titleFields?.[collection]?.titleField)
3690
+ ),
3691
+ reachability: documentReachability(
3692
+ row,
3693
+ titleFields?.[collection]?.localized
3694
+ )
3695
+ },
3696
+ ...current ?? []
3697
+ ]);
3698
+ } catch {
3699
+ if (!cancelled) setUnreadable(true);
3700
+ }
3701
+ })();
3702
+ return () => {
3703
+ cancelled = true;
3704
+ };
3705
+ }, [selected, choices, setChoices, titleFields]);
3706
+ return unreadable;
3707
+ }
3708
+ function groupChoices(choices, collections) {
3709
+ const present = [...new Set(choices.map((choice) => choice.collection))];
3710
+ const order = [
3711
+ ...collections.filter((name) => present.includes(name)),
3712
+ ...present.filter((name) => !collections.includes(name))
3713
+ ];
3714
+ return order.map((collection) => {
3715
+ const removed = !collections.includes(collection);
3716
+ return {
3717
+ collection,
3718
+ // Named as unavailable rather than hidden: the value is still stored and
3719
+ // the server refuses a save into a collection with no pattern, so an
3720
+ // author needs to see what is set in order to change it.
3721
+ label: removed ? `${collection} \u2014 no longer configured` : collection,
3722
+ removed,
3723
+ choices: choices.filter((choice) => choice.collection === collection)
3724
+ };
3725
+ });
3726
+ }
3727
+ function Options({
3728
+ choices,
3729
+ collections
3730
+ }) {
3731
+ const groups = groupChoices(choices, collections);
3732
+ if (groups.length <= 1 && !groups[0]?.removed) {
3733
+ return /* @__PURE__ */ jsx9(Fragment4, { children: choices.map(renderChoice) });
3734
+ }
3735
+ return /* @__PURE__ */ jsx9(Fragment4, { children: groups.map((group) => /* @__PURE__ */ jsxs8(SelectGroup, { children: [
3736
+ /* @__PURE__ */ jsx9(SelectLabel, { children: group.label }),
3737
+ group.choices.map(renderChoice)
3738
+ ] }, group.collection)) });
3739
+ }
3740
+ function Empty({
3741
+ applied,
3742
+ collections
3743
+ }) {
3744
+ return /* @__PURE__ */ jsx9("p", { className: "text-[12px] text-muted-foreground", children: applied ? `No documents match \u201C${applied}\u201D.` : collections.length === 0 ? "No collection is configured as a redirect target." : `No documents to redirect to yet. Create one in ${collections.join(" or ")} first.` });
3745
+ }
3746
+ function PickerNotices({
3747
+ failed,
3748
+ loading,
3749
+ onRetry,
3750
+ unidentifiable,
3751
+ unreadableSelection
3752
+ }) {
3753
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
3754
+ failed.length > 0 && /* @__PURE__ */ jsxs8("p", { className: "text-[12px] text-destructive", children: [
3755
+ "Could not read ",
3756
+ failed.join(" or "),
3757
+ ". You may not have permission to list ",
3758
+ failed.length > 1 ? "them" : "it",
3759
+ ", or the request failed.",
3760
+ " ",
3761
+ /* @__PURE__ */ jsx9(
3762
+ "button",
3423
3763
  {
3424
- initial: initialNotification,
3425
- isEditing: !!sheetState.editing,
3426
- providers,
3427
- templates,
3428
- fields: fieldList,
3429
- defaults,
3430
- onSave: handleSave,
3431
- onClose: closeSheet
3764
+ type: "button",
3765
+ onClick: onRetry,
3766
+ disabled: loading,
3767
+ className: "underline underline-offset-2 disabled:no-underline",
3768
+ children: "Try again"
3769
+ }
3770
+ )
3771
+ ] }),
3772
+ unidentifiable && /* @__PURE__ */ jsx9("p", { className: "text-[12px] text-muted-foreground", children: "A page is saved on this form, but it cannot be identified without a configured redirect collection. Choosing a different confirmation replaces it." }),
3773
+ unreadableSelection && /* @__PURE__ */ jsx9("p", { className: "text-[12px] text-destructive", children: "This form has a page selected that could not be read. Leave it as it is unless you mean to change it \u2014 saving a new choice replaces the stored one." })
3774
+ ] });
3775
+ }
3776
+ function RedirectPagePicker({
3777
+ collections,
3778
+ value,
3779
+ onChange
3780
+ }) {
3781
+ const [query, setQuery] = useState8("");
3782
+ const [applied, setApplied] = useState8("");
3783
+ useEffect4(() => {
3784
+ const timer = setTimeout(() => setApplied(query), 250);
3785
+ return () => clearTimeout(timer);
3786
+ }, [query]);
3787
+ const titleFields = useTitleFields(metadataCollections(collections, value));
3788
+ const {
3789
+ choices,
3790
+ setChoices,
3791
+ failed,
3792
+ hasMore,
3793
+ loading,
3794
+ loadMore,
3795
+ retryFailed
3796
+ } = useChoices(collections.join(","), applied, titleFields);
3797
+ const selected = selectionKey(value, collections);
3798
+ const unidentifiable = value != null && selected === void 0;
3799
+ const selectionUnreadable = useSelectedChoice(
3800
+ selected,
3801
+ choices,
3802
+ setChoices,
3803
+ titleFields
3804
+ );
3805
+ if (choices === null) {
3806
+ return /* @__PURE__ */ jsx9("p", { className: "text-[12px] text-muted-foreground", children: "Loading pages\u2026" });
3807
+ }
3808
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-2", children: [
3809
+ collections.length > 0 && /* @__PURE__ */ jsx9(
3810
+ Input4,
3811
+ {
3812
+ "aria-label": "Search pages",
3813
+ value: query,
3814
+ onChange: (event) => setQuery(event.target.value),
3815
+ placeholder: "Search\u2026"
3816
+ }
3817
+ ),
3818
+ /* @__PURE__ */ jsx9(
3819
+ PickerNotices,
3820
+ {
3821
+ failed,
3822
+ loading,
3823
+ onRetry: retryFailed,
3824
+ unidentifiable,
3825
+ unreadableSelection: selectionUnreadable
3826
+ }
3827
+ ),
3828
+ choices.length === 0 ? failed.length < collections.length && /* @__PURE__ */ jsx9(
3829
+ Empty,
3830
+ {
3831
+ applied,
3832
+ collections: collections.filter((name) => !failed.includes(name))
3833
+ }
3834
+ ) : /* @__PURE__ */ jsxs8(
3835
+ Select6,
3836
+ {
3837
+ value: selected ?? "",
3838
+ onValueChange: (next) => {
3839
+ const separator = next.indexOf(":");
3840
+ if (separator < 0) return onChange(void 0);
3841
+ onChange({
3842
+ relationTo: next.slice(0, separator),
3843
+ value: next.slice(separator + 1)
3844
+ });
3845
+ },
3846
+ children: [
3847
+ /* @__PURE__ */ jsx9(SelectTrigger6, { "aria-label": "Redirect page", className: "w-full", children: /* @__PURE__ */ jsx9(SelectValue6, { placeholder: "Choose a page" }) }),
3848
+ /* @__PURE__ */ jsx9(SelectContent6, { children: /* @__PURE__ */ jsx9(Options, { choices, collections }) })
3849
+ ]
3850
+ }
3851
+ ),
3852
+ hasMore && /* @__PURE__ */ jsx9(
3853
+ "button",
3854
+ {
3855
+ type: "button",
3856
+ onClick: loadMore,
3857
+ disabled: loading,
3858
+ className: "text-[12px] text-muted-foreground underline underline-offset-2 hover:text-foreground disabled:no-underline disabled:opacity-60",
3859
+ children: loading ? "Loading\u2026" : "Load more"
3860
+ }
3861
+ )
3862
+ ] });
3863
+ }
3864
+
3865
+ // src/admin/components/builder/FormSettingsTab.tsx
3866
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3867
+ function SettingRow({
3868
+ label,
3869
+ description,
3870
+ htmlFor,
3871
+ children
3872
+ }) {
3873
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-6", children: [
3874
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-1", children: [
3875
+ /* @__PURE__ */ jsx10(
3876
+ Label3,
3877
+ {
3878
+ htmlFor,
3879
+ className: "text-[13px] font-semibold text-foreground tracking-tight",
3880
+ children: label
3881
+ }
3882
+ ),
3883
+ description && /* @__PURE__ */ jsx10("p", { className: "text-[12px] text-muted-foreground leading-relaxed", children: description })
3884
+ ] }),
3885
+ /* @__PURE__ */ jsx10("div", { className: "shrink-0 flex items-center justify-end min-w-25", children })
3886
+ ] });
3887
+ }
3888
+ function InheritToggle({
3889
+ id,
3890
+ value,
3891
+ inherited,
3892
+ onChange
3893
+ }) {
3894
+ const inheritedLabel = inherited === void 0 ? "Inherit" : `Inherit (${inherited ? "on" : "off"})`;
3895
+ return /* @__PURE__ */ jsxs9(
3896
+ Select7,
3897
+ {
3898
+ value: value === void 0 ? "inherit" : value ? "on" : "off",
3899
+ onValueChange: (selected) => onChange(selected === "inherit" ? void 0 : selected === "on"),
3900
+ children: [
3901
+ /* @__PURE__ */ jsx10(
3902
+ SelectTrigger7,
3903
+ {
3904
+ id,
3905
+ className: "w-40 bg-transparent border-input dark:bg-muted/50",
3906
+ children: /* @__PURE__ */ jsx10(SelectValue7, {})
3907
+ }
3908
+ ),
3909
+ /* @__PURE__ */ jsxs9(SelectContent7, { children: [
3910
+ /* @__PURE__ */ jsx10(SelectItem7, { value: "inherit", children: inheritedLabel }),
3911
+ /* @__PURE__ */ jsx10(SelectItem7, { value: "on", children: "On" }),
3912
+ /* @__PURE__ */ jsx10(SelectItem7, { value: "off", children: "Off" })
3913
+ ] })
3914
+ ]
3915
+ }
3916
+ );
3917
+ }
3918
+ function hasStoredRedirectPage(settings) {
3919
+ const value = settings.redirectPage;
3920
+ if (typeof value === "string") return value.trim().length > 0;
3921
+ return parseRedirectReference(value, []) !== null;
3922
+ }
3923
+ function redirectOptionState(input) {
3924
+ const configured = input.collections !== null && input.collections.length > 0;
3925
+ if (configured) return "ready";
3926
+ if (input.hasStoredPage) return "stored-only";
3927
+ return input.configFailed ? "unknown" : "hidden";
3928
+ }
3929
+ function PageRedirectOption({
3930
+ redirectCollections,
3931
+ configFailed,
3932
+ settings,
3933
+ updateSettings
3934
+ }) {
3935
+ const state = redirectOptionState({
3936
+ hasStoredPage: hasStoredRedirectPage(settings),
3937
+ collections: redirectCollections,
3938
+ configFailed
3939
+ });
3940
+ if (state === "hidden") return null;
3941
+ return /* @__PURE__ */ jsxs9("div", { className: "flex items-start gap-3", children: [
3942
+ /* @__PURE__ */ jsx10(
3943
+ RadioGroupItem,
3944
+ {
3945
+ value: "relationship",
3946
+ id: "settings-confirm-page",
3947
+ className: "mt-0.5",
3948
+ disabled: state !== "ready"
3949
+ }
3950
+ ),
3951
+ /* @__PURE__ */ jsxs9("div", { className: "w-full space-y-2", children: [
3952
+ /* @__PURE__ */ jsx10(Label3, { htmlFor: "settings-confirm-page", children: "Redirect to a page" }),
3953
+ state !== "ready" && /* @__PURE__ */ jsx10("p", { className: "text-[12px] text-muted-foreground", children: configFailed ? "The redirect configuration could not be loaded, so the pages you can choose from are not listed. Reload to try again." : "No collection is configured as a redirect target right now. The page below is still saved, and stays until you choose a different confirmation." }),
3954
+ state === "stored-only" || state === "ready" ? settings.confirmationType === "relationship" && /* @__PURE__ */ jsx10(
3955
+ RedirectPagePicker,
3956
+ {
3957
+ collections: redirectCollections ?? [],
3958
+ value: settings.redirectPage,
3959
+ onChange: (next) => updateSettings({ redirectPage: next })
3960
+ }
3961
+ ) : null
3962
+ ] })
3963
+ ] });
3964
+ }
3965
+ function FormSettingsTab({
3966
+ spamDefaults,
3967
+ redirectCollections,
3968
+ redirectConfigFailed
3969
+ }) {
3970
+ const { settings, updateSettings } = useFormBuilder();
3971
+ return (
3972
+ // The measure belongs to the page's shell, not to this tab.
3973
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-10", children: [
3974
+ /* @__PURE__ */ jsxs9(FormSection, { label: "Submission", children: [
3975
+ /* @__PURE__ */ jsx10(
3976
+ SettingRow,
3977
+ {
3978
+ label: "Submit button text",
3979
+ description: "Label on the form's primary action button",
3980
+ htmlFor: "settings-submit-text",
3981
+ children: /* @__PURE__ */ jsx10(
3982
+ Input5,
3983
+ {
3984
+ id: "settings-submit-text",
3985
+ type: "text",
3986
+ className: "w-56",
3987
+ value: settings.submitButtonText ?? "",
3988
+ onChange: (e) => updateSettings({ submitButtonText: e.target.value })
3989
+ }
3990
+ )
3991
+ }
3992
+ ),
3993
+ /* @__PURE__ */ jsx10(
3994
+ SettingRow,
3995
+ {
3996
+ label: "Allow multiple submissions",
3997
+ description: "When off, the same visitor (by IP) can submit this form only once. Best-effort: IP-based, so shared networks count as one visitor.",
3998
+ htmlFor: "settings-multiple",
3999
+ children: /* @__PURE__ */ jsx10(
4000
+ Switch3,
4001
+ {
4002
+ id: "settings-multiple",
4003
+ checked: settings.allowMultipleSubmissions ?? true,
4004
+ onCheckedChange: (checked) => updateSettings({ allowMultipleSubmissions: checked })
4005
+ }
4006
+ )
4007
+ }
4008
+ )
4009
+ ] }),
4010
+ /* @__PURE__ */ jsx10(FormSection, { label: "After submission", children: /* @__PURE__ */ jsxs9(
4011
+ RadioGroup,
4012
+ {
4013
+ value: settings.confirmationType ?? "message",
4014
+ onValueChange: (value) => updateSettings({
4015
+ confirmationType: value
4016
+ }),
4017
+ className: "flex flex-col gap-3",
4018
+ children: [
4019
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-start gap-3", children: [
4020
+ /* @__PURE__ */ jsx10(
4021
+ RadioGroupItem,
4022
+ {
4023
+ value: "message",
4024
+ id: "settings-confirm-message",
4025
+ className: "mt-0.5"
4026
+ }
4027
+ ),
4028
+ /* @__PURE__ */ jsxs9("div", { className: "w-full space-y-2", children: [
4029
+ /* @__PURE__ */ jsx10(Label3, { htmlFor: "settings-confirm-message", children: "Show a message" }),
4030
+ (settings.confirmationType ?? "message") === "message" && /* @__PURE__ */ jsx10(
4031
+ Textarea2,
4032
+ {
4033
+ "aria-label": "Success message",
4034
+ value: settings.successMessage ?? "",
4035
+ onChange: (e) => updateSettings({ successMessage: e.target.value }),
4036
+ rows: 3,
4037
+ placeholder: "Thank you for your submission!"
4038
+ }
4039
+ )
4040
+ ] })
4041
+ ] }),
4042
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-start gap-3", children: [
4043
+ /* @__PURE__ */ jsx10(
4044
+ RadioGroupItem,
4045
+ {
4046
+ value: "redirect",
4047
+ id: "settings-confirm-redirect",
4048
+ className: "mt-0.5"
4049
+ }
4050
+ ),
4051
+ /* @__PURE__ */ jsxs9("div", { className: "w-full space-y-2", children: [
4052
+ /* @__PURE__ */ jsx10(Label3, { htmlFor: "settings-confirm-redirect", children: "Redirect to a URL" }),
4053
+ settings.confirmationType === "redirect" && /* @__PURE__ */ jsx10(
4054
+ Input5,
4055
+ {
4056
+ "aria-label": "Redirect URL",
4057
+ type: "url",
4058
+ value: settings.redirectUrl ?? "",
4059
+ onChange: (e) => updateSettings({
4060
+ redirectUrl: e.target.value || void 0
4061
+ }),
4062
+ placeholder: "https://example.com/thanks"
4063
+ }
4064
+ )
4065
+ ] })
4066
+ ] }),
4067
+ /* @__PURE__ */ jsx10(
4068
+ PageRedirectOption,
4069
+ {
4070
+ redirectCollections,
4071
+ configFailed: redirectConfigFailed,
4072
+ settings,
4073
+ updateSettings
4074
+ }
4075
+ )
4076
+ ]
3432
4077
  }
3433
- )
4078
+ ) }),
4079
+ /* @__PURE__ */ jsxs9(FormSection, { label: "Spam protection", children: [
4080
+ /* @__PURE__ */ jsx10(
4081
+ SettingRow,
4082
+ {
4083
+ label: "Honeypot",
4084
+ description: "Invisible trap field for bots. Inherits the plugin default unless overridden here.",
4085
+ htmlFor: "settings-honeypot",
4086
+ children: /* @__PURE__ */ jsx10(
4087
+ InheritToggle,
4088
+ {
4089
+ id: "settings-honeypot",
4090
+ value: settings.honeypotEnabled,
4091
+ inherited: spamDefaults?.honeypot,
4092
+ onChange: (honeypotEnabled) => updateSettings({ honeypotEnabled })
4093
+ }
4094
+ )
4095
+ }
4096
+ ),
4097
+ /* @__PURE__ */ jsx10(
4098
+ SettingRow,
4099
+ {
4100
+ label: "reCAPTCHA",
4101
+ description: "Challenge-based bot check. Inherits the plugin default unless overridden here.",
4102
+ htmlFor: "settings-captcha",
4103
+ children: /* @__PURE__ */ jsx10(
4104
+ InheritToggle,
4105
+ {
4106
+ id: "settings-captcha",
4107
+ value: settings.captchaEnabled,
4108
+ inherited: spamDefaults?.recaptchaEnabled,
4109
+ onChange: (captchaEnabled) => updateSettings({ captchaEnabled })
4110
+ }
4111
+ )
4112
+ }
4113
+ ),
4114
+ settings.captchaEnabled === true && /* @__PURE__ */ jsx10(
4115
+ SettingRow,
4116
+ {
4117
+ label: "reCAPTCHA site key",
4118
+ description: "The client-facing site key for this form",
4119
+ htmlFor: "settings-captcha-key",
4120
+ children: /* @__PURE__ */ jsx10(
4121
+ Input5,
4122
+ {
4123
+ id: "settings-captcha-key",
4124
+ type: "text",
4125
+ className: "w-full font-mono sm:w-72",
4126
+ value: settings.captchaSiteKey ?? "",
4127
+ onChange: (e) => updateSettings({
4128
+ captchaSiteKey: e.target.value || void 0
4129
+ })
4130
+ }
4131
+ )
4132
+ }
4133
+ )
4134
+ ] })
3434
4135
  ] })
3435
4136
  );
3436
4137
  }
3437
4138
 
3438
4139
  // src/admin/components/builder/FormPreview.tsx
3439
- import {
3440
- Badge as Badge4,
3441
- Button as Button7,
3442
- Checkbox as Checkbox3,
3443
- Input as Input4,
3444
- Label as Label3,
3445
- RadioGroup,
3446
- RadioGroupItem,
3447
- Select as Select6,
3448
- SelectContent as SelectContent6,
3449
- SelectItem as SelectItem6,
3450
- SelectTrigger as SelectTrigger6,
3451
- SelectValue as SelectValue6,
3452
- Tabs as Tabs3,
3453
- TabsList as TabsList3,
3454
- TabsTrigger as TabsTrigger3,
3455
- Textarea as Textarea2
3456
- } from "@nextlyhq/ui";
3457
- import { Eye, Monitor, Paperclip, RotateCcw, Smartphone } from "lucide-react";
3458
- import { useMemo as useMemo7, useState as useState8 } from "react";
3459
- import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
4140
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3460
4141
  function PreviewFieldInput({
3461
4142
  field,
3462
4143
  value,
@@ -3465,8 +4146,8 @@ function PreviewFieldInput({
3465
4146
  const id = `preview-${field.name}`;
3466
4147
  switch (field.type) {
3467
4148
  case "textarea":
3468
- return /* @__PURE__ */ jsx9(
3469
- Textarea2,
4149
+ return /* @__PURE__ */ jsx11(
4150
+ Textarea3,
3470
4151
  {
3471
4152
  id,
3472
4153
  value: typeof value === "string" ? value : "",
@@ -3476,7 +4157,7 @@ function PreviewFieldInput({
3476
4157
  }
3477
4158
  );
3478
4159
  case "checkbox":
3479
- return /* @__PURE__ */ jsx9(
4160
+ return /* @__PURE__ */ jsx11(
3480
4161
  Checkbox3,
3481
4162
  {
3482
4163
  id,
@@ -3486,49 +4167,49 @@ function PreviewFieldInput({
3486
4167
  );
3487
4168
  case "select": {
3488
4169
  const options = "options" in field ? field.options ?? [] : [];
3489
- return /* @__PURE__ */ jsxs8(
3490
- Select6,
4170
+ return /* @__PURE__ */ jsxs10(
4171
+ Select8,
3491
4172
  {
3492
4173
  value: typeof value === "string" ? value : "",
3493
4174
  onValueChange: onChange,
3494
4175
  children: [
3495
- /* @__PURE__ */ jsx9(
3496
- SelectTrigger6,
4176
+ /* @__PURE__ */ jsx11(
4177
+ SelectTrigger8,
3497
4178
  {
3498
4179
  id,
3499
4180
  className: "w-full bg-transparent border-input dark:bg-muted/50",
3500
- children: /* @__PURE__ */ jsx9(SelectValue6, { placeholder: field.placeholder ?? "Select\u2026" })
4181
+ children: /* @__PURE__ */ jsx11(SelectValue8, { placeholder: field.placeholder ?? "Select\u2026" })
3501
4182
  }
3502
4183
  ),
3503
- /* @__PURE__ */ jsx9(SelectContent6, { children: options.map((option) => /* @__PURE__ */ jsx9(SelectItem6, { value: option.value, children: option.label }, option.value)) })
4184
+ /* @__PURE__ */ jsx11(SelectContent8, { children: options.map((option) => /* @__PURE__ */ jsx11(SelectItem8, { value: option.value, children: option.label }, option.value)) })
3504
4185
  ]
3505
4186
  }
3506
4187
  );
3507
4188
  }
3508
4189
  case "radio": {
3509
4190
  const options = "options" in field ? field.options ?? [] : [];
3510
- return /* @__PURE__ */ jsx9(
3511
- RadioGroup,
4191
+ return /* @__PURE__ */ jsx11(
4192
+ RadioGroup2,
3512
4193
  {
3513
4194
  value: typeof value === "string" ? value : "",
3514
4195
  onValueChange: onChange,
3515
4196
  className: "flex flex-col gap-2",
3516
- children: options.map((option) => /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
3517
- /* @__PURE__ */ jsx9(
3518
- RadioGroupItem,
4197
+ children: options.map((option) => /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
4198
+ /* @__PURE__ */ jsx11(
4199
+ RadioGroupItem2,
3519
4200
  {
3520
4201
  value: option.value,
3521
4202
  id: `${id}-${option.value}`
3522
4203
  }
3523
4204
  ),
3524
- /* @__PURE__ */ jsx9(Label3, { htmlFor: `${id}-${option.value}`, className: "font-normal", children: option.label })
4205
+ /* @__PURE__ */ jsx11(Label4, { htmlFor: `${id}-${option.value}`, className: "font-normal", children: option.label })
3525
4206
  ] }, option.value))
3526
4207
  }
3527
4208
  );
3528
4209
  }
3529
4210
  case "number":
3530
- return /* @__PURE__ */ jsx9(
3531
- Input4,
4211
+ return /* @__PURE__ */ jsx11(
4212
+ Input6,
3532
4213
  {
3533
4214
  id,
3534
4215
  type: "number",
@@ -3539,8 +4220,8 @@ function PreviewFieldInput({
3539
4220
  );
3540
4221
  case "date":
3541
4222
  case "time":
3542
- return /* @__PURE__ */ jsx9(
3543
- Input4,
4223
+ return /* @__PURE__ */ jsx11(
4224
+ Input6,
3544
4225
  {
3545
4226
  id,
3546
4227
  type: field.type,
@@ -3549,13 +4230,13 @@ function PreviewFieldInput({
3549
4230
  }
3550
4231
  );
3551
4232
  case "file":
3552
- return /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground", children: [
3553
- /* @__PURE__ */ jsx9(Paperclip, { className: "h-4 w-4", "aria-hidden": "true" }),
4233
+ return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground", children: [
4234
+ /* @__PURE__ */ jsx11(Paperclip, { className: "h-4 w-4", "aria-hidden": "true" }),
3554
4235
  "File upload (disabled in preview)"
3555
4236
  ] });
3556
4237
  default:
3557
- return /* @__PURE__ */ jsx9(
3558
- Input4,
4238
+ return /* @__PURE__ */ jsx11(
4239
+ Input6,
3559
4240
  {
3560
4241
  id,
3561
4242
  type: field.type === "email" ? "email" : field.type === "url" ? "url" : field.type === "phone" ? "tel" : "text",
@@ -3567,25 +4248,43 @@ function PreviewFieldInput({
3567
4248
  }
3568
4249
  }
3569
4250
  function PluginFieldPreview({ field }) {
3570
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-1.5", children: [
3571
- /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
4251
+ return /* @__PURE__ */ jsxs10("div", { className: "space-y-1.5", children: [
4252
+ /* @__PURE__ */ jsxs10(Label4, { htmlFor: `preview-${field.name}`, children: [
3572
4253
  field.label || field.name,
3573
- field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
4254
+ field.required && /* @__PURE__ */ jsx11("span", { className: "ml-1 text-destructive", children: "*" })
3574
4255
  ] }),
3575
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground", children: [
4256
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground", children: [
3576
4257
  "Custom field (",
3577
4258
  field.type,
3578
4259
  ") \u2014 renders from its plugin component in the live form."
3579
4260
  ] }),
3580
- field.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: field.helpText })
4261
+ field.helpText && /* @__PURE__ */ jsx11("p", { className: "text-xs text-muted-foreground", children: field.helpText })
3581
4262
  ] });
3582
4263
  }
4264
+ function Confirmation({
4265
+ settings
4266
+ }) {
4267
+ const panel = "border border-border bg-muted/40 p-4 text-sm text-foreground";
4268
+ if (settings.confirmationType === "redirect") {
4269
+ return /* @__PURE__ */ jsxs10("p", { className: panel, children: [
4270
+ "The visitor would now be redirected to",
4271
+ " ",
4272
+ /* @__PURE__ */ jsx11("span", { className: "font-mono", children: settings.redirectUrl || "(no redirect URL set)" }),
4273
+ "."
4274
+ ] });
4275
+ }
4276
+ if (settings.confirmationType === "relationship") {
4277
+ const named = hasStoredRedirectPage(settings);
4278
+ return /* @__PURE__ */ jsx11("p", { className: panel, children: named ? "The visitor would now be redirected to the linked page. Its URL is built when the form is submitted, from the pattern the site configured for that collection." : "No page is selected yet, so this form has no destination. Choose one under Settings, or pick a different confirmation." });
4279
+ }
4280
+ return /* @__PURE__ */ jsx11("p", { className: panel, children: settings.successMessage || "Thank you for your submission!" });
4281
+ }
3583
4282
  function FormPreview({ fields, formData }) {
3584
4283
  const { settings } = useFormBuilder();
3585
- const [values, setValues] = useState8({});
3586
- const [device, setDevice] = useState8("desktop");
3587
- const [confirmed, setConfirmed] = useState8(false);
3588
- const [missingRequired, setMissingRequired] = useState8(
4284
+ const [values, setValues] = useState9({});
4285
+ const [device, setDevice] = useState9("desktop");
4286
+ const [confirmed, setConfirmed] = useState9(false);
4287
+ const [missingRequired, setMissingRequired] = useState9(
3589
4288
  /* @__PURE__ */ new Set()
3590
4289
  );
3591
4290
  const visibleFields = useMemo7(() => {
@@ -3621,362 +4320,121 @@ function FormPreview({ fields, formData }) {
3621
4320
  setMissingRequired(missing);
3622
4321
  if (missing.size === 0) setConfirmed(true);
3623
4322
  };
3624
- return /* @__PURE__ */ jsxs8("div", { className: "space-y-4", children: [
3625
- /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
3626
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
3627
- /* @__PURE__ */ jsx9(Eye, { className: "h-4 w-4", "aria-hidden": "true" }),
3628
- "Preview \u2014 a simulation of this form. Nothing here submits anywhere."
3629
- ] }),
3630
- /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
3631
- /* @__PURE__ */ jsx9(
3632
- Tabs3,
3633
- {
3634
- value: device,
3635
- onValueChange: (value) => setDevice(value),
3636
- children: /* @__PURE__ */ jsxs8(TabsList3, { children: [
3637
- /* @__PURE__ */ jsxs8(TabsTrigger3, { value: "desktop", className: "gap-1.5", children: [
3638
- /* @__PURE__ */ jsx9(Monitor, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3639
- "Desktop"
3640
- ] }),
3641
- /* @__PURE__ */ jsxs8(TabsTrigger3, { value: "mobile", className: "gap-1.5", children: [
3642
- /* @__PURE__ */ jsx9(Smartphone, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3643
- "Mobile"
3644
- ] })
3645
- ] })
3646
- }
3647
- ),
3648
- /* @__PURE__ */ jsxs8(Button7, { type: "button", variant: "outline", size: "sm", onClick: reset, children: [
3649
- /* @__PURE__ */ jsx9(RotateCcw, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
3650
- "Reset"
3651
- ] })
3652
- ] })
3653
- ] }),
3654
- /* @__PURE__ */ jsx9("div", { className: "flex justify-center border border-border bg-muted/30 p-6", children: /* @__PURE__ */ jsxs8(
3655
- "div",
3656
- {
3657
- className: `w-full space-y-5 border border-border bg-background p-6 ${device === "mobile" ? "max-w-95" : "max-w-2xl"}`,
3658
- children: [
3659
- /* @__PURE__ */ jsxs8("div", { children: [
3660
- /* @__PURE__ */ jsx9("h3", { className: "text-lg font-semibold text-foreground", children: formData?.name || "Untitled form" }),
3661
- formData?.description && /* @__PURE__ */ jsx9("p", { className: "mt-1 text-sm text-muted-foreground", children: formData.description })
3662
- ] }),
3663
- confirmed ? /* @__PURE__ */ jsxs8("div", { className: "space-y-4", children: [
3664
- settings.confirmationType === "redirect" ? /* @__PURE__ */ jsxs8("p", { className: "border border-border bg-muted/40 p-4 text-sm text-foreground", children: [
3665
- "The visitor would now be redirected to",
3666
- " ",
3667
- /* @__PURE__ */ jsx9("span", { className: "font-mono", children: settings.redirectUrl || "(no redirect URL set)" }),
3668
- "."
3669
- ] }) : /* @__PURE__ */ jsx9("p", { className: "border border-border bg-muted/40 p-4 text-sm text-foreground", children: settings.successMessage || "Thank you for your submission!" }),
3670
- /* @__PURE__ */ jsx9(Button7, { type: "button", variant: "outline", onClick: reset, children: "Fill again" })
3671
- ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
3672
- visibleFields.length === 0 && /* @__PURE__ */ jsx9("p", { className: "text-sm text-muted-foreground", children: "No visible fields \u2014 add fields in the Builder tab." }),
3673
- visibleFields.map(
3674
- (field) => !isKnownFormField(field) ? /* @__PURE__ */ jsx9(PluginFieldPreview, { field }, field.name) : /* @__PURE__ */ jsxs8("div", { className: "space-y-1.5", children: [
3675
- field.type !== "checkbox" && /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3676
- field.label || field.name,
3677
- field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3678
- ] }),
3679
- field.type === "checkbox" ? /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
3680
- /* @__PURE__ */ jsx9(
3681
- PreviewFieldInput,
3682
- {
3683
- field,
3684
- value: values[field.name],
3685
- onChange: (value) => setValues((prev) => ({
3686
- ...prev,
3687
- [field.name]: value
3688
- }))
3689
- }
3690
- ),
3691
- /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3692
- field.label || field.name,
3693
- field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3694
- ] })
3695
- ] }) : /* @__PURE__ */ jsx9(
3696
- PreviewFieldInput,
3697
- {
3698
- field,
3699
- value: values[field.name],
3700
- onChange: (value) => setValues((prev) => ({ ...prev, [field.name]: value }))
3701
- }
3702
- ),
3703
- field.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: field.helpText }),
3704
- missingRequired.has(field.name) && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: "This field is required." })
3705
- ] }, field.name)
3706
- ),
3707
- /* @__PURE__ */ jsx9(
3708
- Button7,
3709
- {
3710
- type: "button",
3711
- onClick: simulateSubmit,
3712
- disabled: visibleFields.length === 0,
3713
- children: settings.submitButtonText || "Submit"
3714
- }
3715
- )
3716
- ] }),
3717
- hiddenCount > 0 && !confirmed && /* @__PURE__ */ jsxs8("p", { className: "text-xs text-muted-foreground", children: [
3718
- /* @__PURE__ */ jsx9(
3719
- Badge4,
3720
- {
3721
- variant: "outline",
3722
- className: "mr-1.5 rounded-none border-border px-1 py-0 text-[10px]",
3723
- children: hiddenCount
3724
- }
3725
- ),
3726
- "hidden ",
3727
- hiddenCount === 1 ? "field" : "fields",
3728
- " will be submitted invisibly."
3729
- ] })
3730
- ]
3731
- }
3732
- ) })
3733
- ] });
3734
- }
3735
-
3736
- // src/admin/components/builder/FormSettingsTab.tsx
3737
- import {
3738
- Input as Input5,
3739
- Label as Label4,
3740
- RadioGroup as RadioGroup2,
3741
- RadioGroupItem as RadioGroupItem2,
3742
- Select as Select7,
3743
- SelectContent as SelectContent7,
3744
- SelectItem as SelectItem7,
3745
- SelectTrigger as SelectTrigger7,
3746
- SelectValue as SelectValue7,
3747
- Switch as Switch3,
3748
- Textarea as Textarea3
3749
- } from "@nextlyhq/ui";
3750
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3751
- function SettingRow({
3752
- label,
3753
- description,
3754
- htmlFor,
3755
- children
3756
- }) {
3757
- return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col sm:flex-row sm:items-center justify-between gap-6 py-2", children: [
3758
- /* @__PURE__ */ jsxs9("div", { className: "space-y-1", children: [
3759
- /* @__PURE__ */ jsx10(
3760
- Label4,
3761
- {
3762
- htmlFor,
3763
- className: "text-[13px] font-semibold text-foreground tracking-tight",
3764
- children: label
3765
- }
3766
- ),
3767
- description && /* @__PURE__ */ jsx10("p", { className: "text-[12px] text-muted-foreground leading-relaxed", children: description })
3768
- ] }),
3769
- /* @__PURE__ */ jsx10("div", { className: "shrink-0 flex items-center justify-end min-w-25", children })
3770
- ] });
3771
- }
3772
- function SectionHeading({ children }) {
3773
- return /* @__PURE__ */ jsx10("div", { className: "border-b border-border pb-3", children: /* @__PURE__ */ jsx10("h3", { className: "text-[16px] font-bold text-foreground tracking-tight", children }) });
3774
- }
3775
- function InheritToggle({
3776
- id,
3777
- value,
3778
- inherited,
3779
- onChange
3780
- }) {
3781
- const inheritedLabel = inherited === void 0 ? "Inherit" : `Inherit (${inherited ? "on" : "off"})`;
3782
- return /* @__PURE__ */ jsxs9(
3783
- Select7,
3784
- {
3785
- value: value === void 0 ? "inherit" : value ? "on" : "off",
3786
- onValueChange: (selected) => onChange(selected === "inherit" ? void 0 : selected === "on"),
3787
- children: [
3788
- /* @__PURE__ */ jsx10(
3789
- SelectTrigger7,
3790
- {
3791
- id,
3792
- className: "w-40 bg-transparent border-input dark:bg-muted/50",
3793
- children: /* @__PURE__ */ jsx10(SelectValue7, {})
3794
- }
3795
- ),
3796
- /* @__PURE__ */ jsxs9(SelectContent7, { children: [
3797
- /* @__PURE__ */ jsx10(SelectItem7, { value: "inherit", children: inheritedLabel }),
3798
- /* @__PURE__ */ jsx10(SelectItem7, { value: "on", children: "On" }),
3799
- /* @__PURE__ */ jsx10(SelectItem7, { value: "off", children: "Off" })
3800
- ] })
3801
- ]
3802
- }
3803
- );
3804
- }
3805
- function FormSettingsTab({ spamDefaults }) {
3806
- const { settings, updateSettings } = useFormBuilder();
3807
4323
  return (
3808
- // The measure belongs to `FormLayout` now, not to this tab.
3809
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-10", children: [
3810
- /* @__PURE__ */ jsxs9("section", { children: [
3811
- /* @__PURE__ */ jsx10(SectionHeading, { children: "Submission" }),
3812
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1 pt-2", children: [
3813
- /* @__PURE__ */ jsx10(
3814
- SettingRow,
4324
+ // One surface for the whole tab. The chrome and the stage are the same
4325
+ // panel seen twice, and leaving the chrome unbacked put it on the page's
4326
+ // grey while the stage below it had a frame — reading as a control strip
4327
+ // that had lost its box.
4328
+ /* @__PURE__ */ jsxs10(Card2, { className: "overflow-hidden p-0", children: [
4329
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-3 border-b border-border px-6 py-4 sm:flex-row sm:items-center sm:justify-between", children: [
4330
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
4331
+ /* @__PURE__ */ jsx11(Eye, { className: "h-4 w-4", "aria-hidden": "true" }),
4332
+ "Preview \u2014 a simulation of this form. Nothing here submits anywhere."
4333
+ ] }),
4334
+ /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [
4335
+ /* @__PURE__ */ jsx11(
4336
+ Tabs3,
3815
4337
  {
3816
- label: "Submit button text",
3817
- description: "Label on the form's primary action button",
3818
- htmlFor: "settings-submit-text",
3819
- children: /* @__PURE__ */ jsx10(
3820
- Input5,
3821
- {
3822
- id: "settings-submit-text",
3823
- type: "text",
3824
- className: "w-56",
3825
- value: settings.submitButtonText ?? "",
3826
- onChange: (e) => updateSettings({ submitButtonText: e.target.value })
3827
- }
3828
- )
4338
+ value: device,
4339
+ onValueChange: (value) => setDevice(value),
4340
+ children: /* @__PURE__ */ jsxs10(TabsList3, { children: [
4341
+ /* @__PURE__ */ jsxs10(TabsTrigger3, { value: "desktop", className: "gap-1.5", children: [
4342
+ /* @__PURE__ */ jsx11(Monitor, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
4343
+ "Desktop"
4344
+ ] }),
4345
+ /* @__PURE__ */ jsxs10(TabsTrigger3, { value: "mobile", className: "gap-1.5", children: [
4346
+ /* @__PURE__ */ jsx11(Smartphone, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
4347
+ "Mobile"
4348
+ ] })
4349
+ ] })
3829
4350
  }
3830
4351
  ),
3831
- /* @__PURE__ */ jsx10(
3832
- SettingRow,
3833
- {
3834
- label: "Allow multiple submissions",
3835
- description: "When off, the same visitor (by IP) can submit this form only once. Best-effort: IP-based, so shared networks count as one visitor.",
3836
- htmlFor: "settings-multiple",
3837
- children: /* @__PURE__ */ jsx10(
3838
- Switch3,
3839
- {
3840
- id: "settings-multiple",
3841
- checked: settings.allowMultipleSubmissions ?? true,
3842
- onCheckedChange: (checked) => updateSettings({ allowMultipleSubmissions: checked })
3843
- }
3844
- )
3845
- }
3846
- )
4352
+ /* @__PURE__ */ jsxs10(Button7, { type: "button", variant: "outline", size: "sm", onClick: reset, children: [
4353
+ /* @__PURE__ */ jsx11(RotateCcw, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
4354
+ "Reset"
4355
+ ] })
3847
4356
  ] })
3848
4357
  ] }),
3849
- /* @__PURE__ */ jsxs9("section", { children: [
3850
- /* @__PURE__ */ jsx10(SectionHeading, { children: "After submission" }),
3851
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-4 pt-4", children: [
3852
- settings.confirmationType === "relationship" && /* @__PURE__ */ jsx10("p", { className: "border border-border bg-muted/40 p-3 text-sm text-muted-foreground", children: "This form redirects to a linked page (configured via the API). Picking an option below replaces that behavior." }),
3853
- /* @__PURE__ */ jsxs9(
3854
- RadioGroup2,
3855
- {
3856
- value: settings.confirmationType ?? "message",
3857
- onValueChange: (value) => updateSettings({
3858
- confirmationType: value
3859
- }),
3860
- className: "flex flex-col gap-3",
3861
- children: [
3862
- /* @__PURE__ */ jsxs9("div", { className: "flex items-start gap-3", children: [
3863
- /* @__PURE__ */ jsx10(
3864
- RadioGroupItem2,
3865
- {
3866
- value: "message",
3867
- id: "settings-confirm-message",
3868
- className: "mt-0.5"
3869
- }
3870
- ),
3871
- /* @__PURE__ */ jsxs9("div", { className: "w-full space-y-2", children: [
3872
- /* @__PURE__ */ jsx10(Label4, { htmlFor: "settings-confirm-message", children: "Show a message" }),
3873
- (settings.confirmationType ?? "message") === "message" && /* @__PURE__ */ jsx10(
3874
- Textarea3,
4358
+ /* @__PURE__ */ jsx11("div", { className: "flex justify-center bg-muted/30 p-6", children: /* @__PURE__ */ jsxs10(
4359
+ "div",
4360
+ {
4361
+ className: `w-full space-y-5 border border-border bg-background p-6 ${device === "mobile" ? "max-w-95" : "max-w-2xl"}`,
4362
+ children: [
4363
+ /* @__PURE__ */ jsxs10("div", { children: [
4364
+ /* @__PURE__ */ jsx11("h3", { className: "text-lg font-semibold text-foreground", children: formData?.name || "Untitled form" }),
4365
+ formData?.description && /* @__PURE__ */ jsx11("p", { className: "mt-1 text-sm text-muted-foreground", children: formData.description })
4366
+ ] }),
4367
+ confirmed ? /* @__PURE__ */ jsxs10("div", { className: "space-y-4", children: [
4368
+ /* @__PURE__ */ jsx11(Confirmation, { settings }),
4369
+ /* @__PURE__ */ jsx11(Button7, { type: "button", variant: "outline", onClick: reset, children: "Fill again" })
4370
+ ] }) : /* @__PURE__ */ jsxs10(Fragment5, { children: [
4371
+ visibleFields.length === 0 && /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground", children: "No visible fields \u2014 add fields in the Builder tab." }),
4372
+ visibleFields.map(
4373
+ (field) => !isKnownFormField(field) ? /* @__PURE__ */ jsx11(PluginFieldPreview, { field }, field.name) : /* @__PURE__ */ jsxs10("div", { className: "space-y-1.5", children: [
4374
+ field.type !== "checkbox" && /* @__PURE__ */ jsxs10(Label4, { htmlFor: `preview-${field.name}`, children: [
4375
+ field.label || field.name,
4376
+ field.required && /* @__PURE__ */ jsx11("span", { className: "ml-1 text-destructive", children: "*" })
4377
+ ] }),
4378
+ field.type === "checkbox" ? /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
4379
+ /* @__PURE__ */ jsx11(
4380
+ PreviewFieldInput,
3875
4381
  {
3876
- "aria-label": "Success message",
3877
- value: settings.successMessage ?? "",
3878
- onChange: (e) => updateSettings({ successMessage: e.target.value }),
3879
- rows: 3,
3880
- placeholder: "Thank you for your submission!"
4382
+ field,
4383
+ value: values[field.name],
4384
+ onChange: (value) => setValues((prev) => ({
4385
+ ...prev,
4386
+ [field.name]: value
4387
+ }))
3881
4388
  }
3882
- )
3883
- ] })
3884
- ] }),
3885
- /* @__PURE__ */ jsxs9("div", { className: "flex items-start gap-3", children: [
3886
- /* @__PURE__ */ jsx10(
3887
- RadioGroupItem2,
4389
+ ),
4390
+ /* @__PURE__ */ jsxs10(Label4, { htmlFor: `preview-${field.name}`, children: [
4391
+ field.label || field.name,
4392
+ field.required && /* @__PURE__ */ jsx11("span", { className: "ml-1 text-destructive", children: "*" })
4393
+ ] })
4394
+ ] }) : /* @__PURE__ */ jsx11(
4395
+ PreviewFieldInput,
3888
4396
  {
3889
- value: "redirect",
3890
- id: "settings-confirm-redirect",
3891
- className: "mt-0.5"
4397
+ field,
4398
+ value: values[field.name],
4399
+ onChange: (value) => setValues((prev) => ({ ...prev, [field.name]: value }))
3892
4400
  }
3893
4401
  ),
3894
- /* @__PURE__ */ jsxs9("div", { className: "w-full space-y-2", children: [
3895
- /* @__PURE__ */ jsx10(Label4, { htmlFor: "settings-confirm-redirect", children: "Redirect to a URL" }),
3896
- settings.confirmationType === "redirect" && /* @__PURE__ */ jsx10(
3897
- Input5,
3898
- {
3899
- "aria-label": "Redirect URL",
3900
- type: "url",
3901
- value: settings.redirectUrl ?? "",
3902
- onChange: (e) => updateSettings({
3903
- redirectUrl: e.target.value || void 0
3904
- }),
3905
- placeholder: "https://example.com/thanks"
3906
- }
3907
- )
3908
- ] })
3909
- ] })
3910
- ]
3911
- }
3912
- )
3913
- ] })
3914
- ] }),
3915
- /* @__PURE__ */ jsxs9("section", { children: [
3916
- /* @__PURE__ */ jsx10(SectionHeading, { children: "Spam protection" }),
3917
- /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1 pt-2", children: [
3918
- /* @__PURE__ */ jsx10(
3919
- SettingRow,
3920
- {
3921
- label: "Honeypot",
3922
- description: "Invisible trap field for bots. Inherits the plugin default unless overridden here.",
3923
- htmlFor: "settings-honeypot",
3924
- children: /* @__PURE__ */ jsx10(
3925
- InheritToggle,
3926
- {
3927
- id: "settings-honeypot",
3928
- value: settings.honeypotEnabled,
3929
- inherited: spamDefaults?.honeypot,
3930
- onChange: (honeypotEnabled) => updateSettings({ honeypotEnabled })
3931
- }
3932
- )
3933
- }
3934
- ),
3935
- /* @__PURE__ */ jsx10(
3936
- SettingRow,
3937
- {
3938
- label: "reCAPTCHA",
3939
- description: "Challenge-based bot check. Inherits the plugin default unless overridden here.",
3940
- htmlFor: "settings-captcha",
3941
- children: /* @__PURE__ */ jsx10(
3942
- InheritToggle,
4402
+ field.helpText && /* @__PURE__ */ jsx11("p", { className: "text-xs text-muted-foreground", children: field.helpText }),
4403
+ missingRequired.has(field.name) && /* @__PURE__ */ jsx11("p", { className: "text-xs text-destructive", children: "This field is required." })
4404
+ ] }, field.name)
4405
+ ),
4406
+ /* @__PURE__ */ jsx11(
4407
+ Button7,
3943
4408
  {
3944
- id: "settings-captcha",
3945
- value: settings.captchaEnabled,
3946
- inherited: spamDefaults?.recaptchaEnabled,
3947
- onChange: (captchaEnabled) => updateSettings({ captchaEnabled })
4409
+ type: "button",
4410
+ onClick: simulateSubmit,
4411
+ disabled: visibleFields.length === 0,
4412
+ children: settings.submitButtonText || "Submit"
3948
4413
  }
3949
4414
  )
3950
- }
3951
- ),
3952
- settings.captchaEnabled === true && /* @__PURE__ */ jsx10(
3953
- SettingRow,
3954
- {
3955
- label: "reCAPTCHA site key",
3956
- description: "The client-facing site key for this form",
3957
- htmlFor: "settings-captcha-key",
3958
- children: /* @__PURE__ */ jsx10(
3959
- Input5,
4415
+ ] }),
4416
+ hiddenCount > 0 && !confirmed && /* @__PURE__ */ jsxs10("p", { className: "text-xs text-muted-foreground", children: [
4417
+ /* @__PURE__ */ jsx11(
4418
+ Badge4,
3960
4419
  {
3961
- id: "settings-captcha-key",
3962
- type: "text",
3963
- className: "w-72 font-mono",
3964
- value: settings.captchaSiteKey ?? "",
3965
- onChange: (e) => updateSettings({
3966
- captchaSiteKey: e.target.value || void 0
3967
- })
4420
+ variant: "outline",
4421
+ className: "mr-1.5 rounded-none border-border px-1 py-0 text-[10px]",
4422
+ children: hiddenCount
3968
4423
  }
3969
- )
3970
- }
3971
- )
3972
- ] })
3973
- ] })
4424
+ ),
4425
+ "hidden ",
4426
+ hiddenCount === 1 ? "field" : "fields",
4427
+ " will be submitted invisibly."
4428
+ ] })
4429
+ ]
4430
+ }
4431
+ ) })
3974
4432
  ] })
3975
4433
  );
3976
4434
  }
3977
4435
 
3978
4436
  // src/admin/FormBuilderView.tsx
3979
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
4437
+ import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
3980
4438
  function slugify(text) {
3981
4439
  return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
3982
4440
  }
@@ -3986,7 +4444,8 @@ function FormBuilderViewInner({
3986
4444
  entryId,
3987
4445
  onSave,
3988
4446
  onSuccess,
3989
- onCancel
4447
+ onCancel,
4448
+ documentLock
3990
4449
  }) {
3991
4450
  const {
3992
4451
  fields,
@@ -4003,15 +4462,17 @@ function FormBuilderViewInner({
4003
4462
  markAsSaved
4004
4463
  } = useFormBuilder();
4005
4464
  const queryClient = useQueryClient();
4006
- const [isSaving, setIsSaving] = useState9(false);
4007
- const [saveError, setSaveError] = useState9(null);
4008
- const [enabledTypes, setEnabledTypes] = useState9(null);
4009
- const [disabledFieldTypes, setDisabledFieldTypes] = useState9(
4465
+ const [isSaving, setIsSaving] = useState10(false);
4466
+ const [saveError, setSaveError] = useState10(null);
4467
+ const [enabledTypes, setEnabledTypes] = useState10(null);
4468
+ const [disabledFieldTypes, setDisabledFieldTypes] = useState10(
4010
4469
  () => /* @__PURE__ */ new Set()
4011
4470
  );
4012
- const [notificationDefaults, setNotificationDefaults] = useState9(null);
4013
- const [spamDefaults, setSpamDefaults] = useState9(null);
4014
- useEffect4(() => {
4471
+ const [notificationDefaults, setNotificationDefaults] = useState10(null);
4472
+ const [spamDefaults, setSpamDefaults] = useState10(null);
4473
+ const [redirectCollections, setRedirectCollections] = useState10(null);
4474
+ const [redirectConfigFailed, setRedirectConfigFailed] = useState10(false);
4475
+ useEffect5(() => {
4015
4476
  let cancelled = false;
4016
4477
  const allTypes = FORM_FIELD_TYPE_CATALOG3.map((entry) => entry.type);
4017
4478
  void fetch(
@@ -4030,19 +4491,23 @@ function FormBuilderViewInner({
4030
4491
  );
4031
4492
  setNotificationDefaults(config?.notifications ?? {});
4032
4493
  setSpamDefaults(config?.spamProtection ?? {});
4494
+ setRedirectConfigFailed(config === null);
4495
+ setRedirectCollections(config?.redirectCollections ?? []);
4033
4496
  }
4034
4497
  ).catch(() => {
4035
4498
  if (cancelled) return;
4036
4499
  setEnabledTypes(allTypes);
4037
4500
  setNotificationDefaults({});
4038
4501
  setSpamDefaults({});
4502
+ setRedirectConfigFailed(true);
4503
+ setRedirectCollections([]);
4039
4504
  });
4040
4505
  return () => {
4041
4506
  cancelled = true;
4042
4507
  };
4043
4508
  }, []);
4044
4509
  const seededRef = useRef2(false);
4045
- useEffect4(() => {
4510
+ useEffect5(() => {
4046
4511
  if (!isCreating || notificationDefaults === null || seededRef.current) {
4047
4512
  return;
4048
4513
  }
@@ -4067,10 +4532,16 @@ function FormBuilderViewInner({
4067
4532
  [fields, addField, selectField]
4068
4533
  );
4069
4534
  const handleSave = useCallback7(async () => {
4535
+ if (documentLock?.actionsDisabled === true) return;
4070
4536
  if (fields.length === 0) {
4071
4537
  toast.error("Please add at least one field to the form");
4072
4538
  return;
4073
4539
  }
4540
+ const blocked = notificationsBlockingSave(notifications);
4541
+ if (blocked) {
4542
+ toast.error(blocked);
4543
+ return;
4544
+ }
4074
4545
  const { id: _id, ...formDataWithoutId } = formData;
4075
4546
  const saveData = {
4076
4547
  ...formDataWithoutId,
@@ -4093,9 +4564,11 @@ function FormBuilderViewInner({
4093
4564
  body: JSON.stringify(saveData)
4094
4565
  });
4095
4566
  if (!response.ok) {
4096
- const err = await response.json().catch(() => ({}));
4097
4567
  throw new Error(
4098
- err.error?.message ?? `Failed to save form: ${response.statusText}`
4568
+ saveErrorMessage(
4569
+ await response.json().catch(() => ({})),
4570
+ response.statusText
4571
+ )
4099
4572
  );
4100
4573
  }
4101
4574
  const result = await response.json();
@@ -4132,7 +4605,11 @@ function FormBuilderViewInner({
4132
4605
  collectionSlug,
4133
4606
  entryId,
4134
4607
  queryClient,
4135
- markAsSaved
4608
+ markAsSaved,
4609
+ // Read inside, so it belongs here. Without it the callback closes over the
4610
+ // claim as it stood when the form mounted, and a colleague taking the
4611
+ // document over mid-session would leave this still committing.
4612
+ documentLock?.actionsDisabled
4136
4613
  ]);
4137
4614
  const handleCancel = useCallback7(() => {
4138
4615
  if (onCancel) onCancel();
@@ -4165,103 +4642,119 @@ function FormBuilderViewInner({
4165
4642
  count: notifications.length > 0 ? notifications.length : null
4166
4643
  }
4167
4644
  ];
4168
- return /* @__PURE__ */ jsxs10(FormLayout, { children: [
4169
- /* @__PURE__ */ jsxs10("div", { className: "mb-6", children: [
4170
- /* @__PURE__ */ jsx11("h1", { className: "text-2xl font-semibold text-foreground tracking-tight", children: isCreating ? "Create Form" : "Edit Form" }),
4171
- /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground mt-1", children: isCreating ? "Design a new form with drag-and-drop fields." : "Modify your form fields and settings." })
4645
+ return /* @__PURE__ */ jsxs11(Fragment6, { children: [
4646
+ /* @__PURE__ */ jsxs11("div", { className: "mb-6", children: [
4647
+ /* @__PURE__ */ jsx12("h1", { className: "text-2xl font-semibold text-foreground tracking-tight", children: isCreating ? "Create Form" : "Edit Form" }),
4648
+ /* @__PURE__ */ jsx12("p", { className: "text-sm text-muted-foreground mt-1", children: isCreating ? "Design a new form with drag-and-drop fields." : "Modify your form fields and settings." })
4172
4649
  ] }),
4173
- /* @__PURE__ */ jsxs10("div", { className: "border-b border-border space-y-4 pb-4 mb-6", children: [
4174
- /* @__PURE__ */ jsxs10("div", { className: "flex flex-wrap gap-4", children: [
4175
- /* @__PURE__ */ jsx11(
4650
+ /* @__PURE__ */ jsxs11(Card3, { className: "mb-6 px-6 pt-5 pb-6", children: [
4651
+ /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap gap-4", children: [
4652
+ /* @__PURE__ */ jsx12(
4176
4653
  FieldShell2,
4177
4654
  {
4178
4655
  label: "Form Name",
4179
4656
  htmlFor: "form-name",
4180
4657
  className: "flex-1 min-w-[200px]",
4181
- children: /* @__PURE__ */ jsx11(
4182
- Input6,
4658
+ children: /* @__PURE__ */ jsx12(
4659
+ Input7,
4183
4660
  {
4184
4661
  type: "text",
4185
4662
  value: formData.name || "",
4186
4663
  onChange: (e) => handleNameChange(e.target.value),
4187
- placeholder: "e.g., Contact Form",
4188
- className: "bg-transparent"
4664
+ placeholder: "e.g., Contact Form"
4189
4665
  }
4190
4666
  )
4191
4667
  }
4192
4668
  ),
4193
- /* @__PURE__ */ jsx11(
4669
+ /* @__PURE__ */ jsx12(
4194
4670
  FieldShell2,
4195
4671
  {
4196
4672
  label: "Slug",
4197
4673
  htmlFor: "form-slug",
4198
4674
  className: "flex-1 min-w-[160px]",
4199
- children: /* @__PURE__ */ jsx11(
4200
- Input6,
4675
+ children: /* @__PURE__ */ jsx12(
4676
+ Input7,
4201
4677
  {
4202
4678
  type: "text",
4203
4679
  value: formData.slug || "",
4204
4680
  onChange: (e) => updateFormData({ slug: e.target.value }),
4205
4681
  placeholder: "e.g., contact-form",
4206
- className: "bg-transparent placeholder:text-muted-foreground"
4682
+ className: "placeholder:text-muted-foreground"
4207
4683
  }
4208
4684
  )
4209
4685
  }
4210
4686
  ),
4211
- /* @__PURE__ */ jsx11("div", { className: "w-36", children: /* @__PURE__ */ jsx11(FieldShell2, { label: "Status", htmlFor: "form-status", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs10(
4212
- Select8,
4687
+ /* @__PURE__ */ jsx12("div", { className: "w-36", children: /* @__PURE__ */ jsx12(FieldShell2, { label: "Status", htmlFor: "form-status", children: ({ id, describedBy, invalid }) => /* @__PURE__ */ jsxs11(
4688
+ Select9,
4213
4689
  {
4214
4690
  value: formData.status || "draft",
4215
4691
  onValueChange: (value) => updateFormData({
4216
4692
  status: value
4217
4693
  }),
4218
4694
  children: [
4219
- /* @__PURE__ */ jsx11(
4220
- SelectTrigger8,
4695
+ /* @__PURE__ */ jsx12(
4696
+ SelectTrigger9,
4221
4697
  {
4222
4698
  id,
4223
4699
  "aria-describedby": describedBy,
4224
4700
  "aria-invalid": invalid,
4225
- className: "bg-transparent",
4226
- children: /* @__PURE__ */ jsx11(SelectValue8, { placeholder: "Status" })
4701
+ children: /* @__PURE__ */ jsx12(SelectValue9, { placeholder: "Status" })
4227
4702
  }
4228
4703
  ),
4229
- /* @__PURE__ */ jsxs10(SelectContent8, { children: [
4230
- /* @__PURE__ */ jsx11(SelectItem8, { value: "draft", children: "Draft" }),
4231
- /* @__PURE__ */ jsx11(SelectItem8, { value: "published", children: "Published" }),
4232
- /* @__PURE__ */ jsx11(SelectItem8, { value: "closed", children: "Closed" })
4704
+ /* @__PURE__ */ jsxs11(SelectContent9, { children: [
4705
+ /* @__PURE__ */ jsx12(SelectItem9, { value: "draft", children: "Draft" }),
4706
+ /* @__PURE__ */ jsx12(SelectItem9, { value: "published", children: "Published" }),
4707
+ /* @__PURE__ */ jsx12(SelectItem9, { value: "closed", children: "Closed" })
4233
4708
  ] })
4234
4709
  ]
4235
4710
  }
4236
4711
  ) }) })
4237
4712
  ] }),
4238
- /* @__PURE__ */ jsx11(
4239
- Tabs4,
4713
+ formData.status === "closed" && /* @__PURE__ */ jsx12("div", { className: "mt-4", children: /* @__PURE__ */ jsx12(
4714
+ FieldShell2,
4240
4715
  {
4241
- value: activeTab,
4242
- onValueChange: (v) => setActiveTab(v),
4243
- children: /* @__PURE__ */ jsx11(TabsList4, { className: "bg-transparent justify-start gap-0 max-w-full overflow-x-auto", children: mainTabs.map((tab) => /* @__PURE__ */ jsxs10(
4244
- TabsTrigger4,
4716
+ label: "Message for visitors",
4717
+ htmlFor: "form-closed-message",
4718
+ width: "full",
4719
+ description: "Shown to anyone who reaches this form now that it is closed. Left empty, they are told only that it is not accepting submissions.",
4720
+ children: /* @__PURE__ */ jsx12(
4721
+ Textarea4,
4245
4722
  {
4246
- value: tab.value,
4247
- className: "shrink-0 whitespace-nowrap",
4248
- children: [
4249
- tab.label,
4250
- tab.count !== null && /* @__PURE__ */ jsx11(
4251
- "span",
4252
- {
4253
- className: `inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-semibold rounded-sm ml-2 transition-colors ${activeTab === tab.value ? "bg-primary/5 text-primary" : "bg-primary/5 text-muted-foreground"}`,
4254
- children: tab.count
4255
- }
4256
- )
4257
- ]
4258
- },
4259
- tab.value
4260
- )) })
4723
+ value: formData.closedMessage ?? "",
4724
+ onChange: (e) => updateFormData({ closedMessage: e.target.value }),
4725
+ placeholder: "e.g., Applications closed on 31 March. Thank you to everyone who applied.",
4726
+ rows: 3
4727
+ }
4728
+ )
4261
4729
  }
4262
- )
4730
+ ) })
4263
4731
  ] }),
4264
- activeTab === "builder" && /* @__PURE__ */ jsx11(
4732
+ /* @__PURE__ */ jsx12("div", { className: "border-b border-border mb-6", children: /* @__PURE__ */ jsx12(
4733
+ Tabs4,
4734
+ {
4735
+ value: activeTab,
4736
+ onValueChange: (v) => setActiveTab(v),
4737
+ children: /* @__PURE__ */ jsx12(TabsList4, { scrollable: true, className: "bg-transparent justify-start gap-0", children: mainTabs.map((tab) => /* @__PURE__ */ jsxs11(
4738
+ TabsTrigger4,
4739
+ {
4740
+ value: tab.value,
4741
+ className: "shrink-0 whitespace-nowrap",
4742
+ children: [
4743
+ tab.label,
4744
+ tab.count !== null && /* @__PURE__ */ jsx12(
4745
+ "span",
4746
+ {
4747
+ className: `inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-semibold rounded-sm ml-2 transition-colors ${activeTab === tab.value ? "bg-primary/5 text-primary" : "bg-primary/5 text-muted-foreground"}`,
4748
+ children: tab.count
4749
+ }
4750
+ )
4751
+ ]
4752
+ },
4753
+ tab.value
4754
+ )) })
4755
+ }
4756
+ ) }),
4757
+ activeTab === "builder" && /* @__PURE__ */ jsx12(
4265
4758
  FieldCards,
4266
4759
  {
4267
4760
  enabledTypes,
@@ -4269,13 +4762,20 @@ function FormBuilderViewInner({
4269
4762
  onAddField: handleAddField
4270
4763
  }
4271
4764
  ),
4272
- activeTab === "preview" && /* @__PURE__ */ jsx11(FormPreview, { fields, formData }),
4273
- activeTab === "settings" && /* @__PURE__ */ jsx11(FormSettingsTab, { spamDefaults }),
4274
- activeTab === "notifications" && /* @__PURE__ */ jsx11(FormNotificationsTab, { defaults: notificationDefaults }),
4765
+ activeTab === "preview" && /* @__PURE__ */ jsx12(FormPreview, { fields, formData }),
4766
+ activeTab === "settings" && /* @__PURE__ */ jsx12(
4767
+ FormSettingsTab,
4768
+ {
4769
+ spamDefaults,
4770
+ redirectCollections,
4771
+ redirectConfigFailed
4772
+ }
4773
+ ),
4774
+ activeTab === "notifications" && /* @__PURE__ */ jsx12(FormNotificationsTab, { defaults: notificationDefaults }),
4275
4775
  saveError && // Full-strength destructive border so the error box boundary is perceivable over its tinted fill.
4276
- /* @__PURE__ */ jsx11("div", { className: "mt-6 p-3 text-sm text-destructive bg-destructive/10 border border-destructive rounded-none", children: saveError }),
4277
- /* @__PURE__ */ jsxs10(FormActions, { dirty: isDirty, children: [
4278
- /* @__PURE__ */ jsx11(
4776
+ /* @__PURE__ */ jsx12("div", { className: "mt-6 p-3 text-sm text-destructive bg-destructive/10 border border-destructive rounded-none", children: saveError }),
4777
+ /* @__PURE__ */ jsxs11(FormActions, { dirty: isDirty, children: [
4778
+ /* @__PURE__ */ jsx12(
4279
4779
  Button8,
4280
4780
  {
4281
4781
  type: "button",
@@ -4286,7 +4786,7 @@ function FormBuilderViewInner({
4286
4786
  children: "Cancel"
4287
4787
  }
4288
4788
  ),
4289
- /* @__PURE__ */ jsx11(
4789
+ /* @__PURE__ */ jsx12(
4290
4790
  Button8,
4291
4791
  {
4292
4792
  type: "button",
@@ -4294,17 +4794,17 @@ function FormBuilderViewInner({
4294
4794
  onClick: () => {
4295
4795
  void handleSave();
4296
4796
  },
4297
- disabled: isSaving,
4797
+ disabled: isSaving || documentLock?.actionsDisabled === true,
4298
4798
  className: "flex items-center gap-1.5",
4299
- children: isSaving ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
4300
- /* @__PURE__ */ jsxs10(
4799
+ children: isSaving ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
4800
+ /* @__PURE__ */ jsxs11(
4301
4801
  "svg",
4302
4802
  {
4303
4803
  className: "animate-spin h-3.5 w-3.5",
4304
4804
  viewBox: "0 0 24 24",
4305
4805
  fill: "none",
4306
4806
  children: [
4307
- /* @__PURE__ */ jsx11(
4807
+ /* @__PURE__ */ jsx12(
4308
4808
  "circle",
4309
4809
  {
4310
4810
  className: "opacity-25",
@@ -4315,7 +4815,7 @@ function FormBuilderViewInner({
4315
4815
  strokeWidth: "4"
4316
4816
  }
4317
4817
  ),
4318
- /* @__PURE__ */ jsx11(
4818
+ /* @__PURE__ */ jsx12(
4319
4819
  "path",
4320
4820
  {
4321
4821
  className: "opacity-75",
@@ -4333,6 +4833,11 @@ function FormBuilderViewInner({
4333
4833
  ] })
4334
4834
  ] });
4335
4835
  }
4836
+ function saveErrorMessage(body, statusText) {
4837
+ const error = body?.error;
4838
+ const fieldIssues = (error?.data?.errors ?? []).map((issue) => issue.message).filter((message) => Boolean(message));
4839
+ return fieldIssues.join(" ") || error?.message || `Failed to save form: ${statusText}`;
4840
+ }
4336
4841
  function FormBuilderView({
4337
4842
  id,
4338
4843
  entryId,
@@ -4342,26 +4847,29 @@ function FormBuilderView({
4342
4847
  initialData,
4343
4848
  onSave,
4344
4849
  onSuccess,
4345
- onCancel
4850
+ onCancel,
4851
+ documentLock
4346
4852
  }) {
4347
4853
  const resolvedEntryId = entryId || id || initialData?.id;
4348
4854
  const resolvedCollectionSlug = collectionSlug || collection || "forms";
4349
4855
  const resolvedIsCreating = isCreating ?? !resolvedEntryId;
4350
4856
  const providerKey = resolvedEntryId || "new";
4351
- return /* @__PURE__ */ jsx11(
4857
+ return /* @__PURE__ */ jsx12(
4352
4858
  FormBuilderProvider,
4353
4859
  {
4860
+ readOnly: documentLock?.readOnly === true,
4354
4861
  initialData: {
4355
4862
  id: resolvedEntryId,
4356
4863
  name: initialData?.name,
4357
4864
  slug: initialData?.slug,
4358
4865
  description: initialData?.description,
4359
4866
  status: initialData?.status,
4867
+ closedMessage: initialData?.closedMessage,
4360
4868
  fields: initialData?.fields,
4361
4869
  settings: initialData?.settings,
4362
4870
  notifications: initialData?.notifications
4363
4871
  },
4364
- children: /* @__PURE__ */ jsx11(
4872
+ children: /* @__PURE__ */ jsx12(
4365
4873
  FormBuilderViewInner,
4366
4874
  {
4367
4875
  isCreating: resolvedIsCreating,
@@ -4369,7 +4877,8 @@ function FormBuilderView({
4369
4877
  entryId: resolvedEntryId,
4370
4878
  onSave,
4371
4879
  onSuccess,
4372
- onCancel
4880
+ onCancel,
4881
+ documentLock
4373
4882
  }
4374
4883
  )
4375
4884
  },