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

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