@burdenoff/fluidgrids-fe-sdk 2026.1111.24 → 2026.1112.2

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.
Files changed (55) hide show
  1. package/dist/{AiStudioPage-NfhraV2M.js → AiStudioPage-C-jio5Bz.js} +4 -4
  2. package/dist/{DataSinkDashboardPage-BfdKumMH.js → DataSinkDashboardPage-CRBsNjoS.js} +1 -1
  3. package/dist/{ExecutionDetailPage-Dtpn7Xnl.js → ExecutionDetailPage-Cw4zyaIG.js} +5 -5
  4. package/dist/{ExecutionsListPage-DeqJnEO7.js → ExecutionsListPage-BgeqwVr2.js} +4 -4
  5. package/dist/{FluidGridsProvider-BuZERrTA.js → FluidGridsProvider-BFfTxl9m.js} +2 -6
  6. package/dist/{HistoryPage-BC7ld-oQ.js → HistoryPage-BdijEOHp.js} +2 -2
  7. package/dist/{InsightsPage-C8X1g_Ya.js → InsightsPage-2jnYlozA.js} +1 -1
  8. package/dist/{MissingNodesInstallModal-DUmwDs4e.js → MissingNodesInstallModal-C0wPwR0Q.js} +1 -1
  9. package/dist/{QuotaExhaustedDialog-RctNPfz_.js → QuotaExhaustedDialog-BKZ9moP8.js} +1 -1
  10. package/dist/{SettingsPage-BbE4S63W.js → SettingsPage-BaM98QU_.js} +3 -3
  11. package/dist/{TemplateCard-D800haiE.js → TemplateCard-DYDVNYo6.js} +2 -2
  12. package/dist/{TemplateConfigurationModal-DYs1YxYy.js → TemplateConfigurationModal-BzdkGWim.js} +1 -1
  13. package/dist/{TemplateDetailPanel-CKhCQlyo.js → TemplateDetailPanel-s4EqQhra.js} +1 -1
  14. package/dist/{TemplatePreview-DbF97Jwi.js → TemplatePreview-B_-zS_Mm.js} +3 -3
  15. package/dist/{TemplatesPage-BR6iOnC0.js → TemplatesPage-OIMd6cIl.js} +4 -4
  16. package/dist/{VersionDiffDialog-B00qeGiE.js → VersionDiffDialog-Cp2doV3f.js} +1 -1
  17. package/dist/{WorkflowBuilder-BWqiMpdk.js → WorkflowBuilder-B2tIOPi0.js} +4 -4
  18. package/dist/{WorkflowBuilder-CWsUITzN.js → WorkflowBuilder-DD4UIAjC.js} +10 -9
  19. package/dist/{WorkflowSearchRedirect-7Kc7NniX.js → WorkflowSearchRedirect-BhXzWDj7.js} +1 -1
  20. package/dist/{WorkflowsList-Bnov6LCT.js → WorkflowsList-BV-2J_gG.js} +138 -10
  21. package/dist/{agentTools-yi8CDKzs.js → agentTools-EVdEThES.js} +4 -4
  22. package/dist/{canvasNoteTools-kRtdgYMQ.js → canvasNoteTools-C5Vf0pxu.js} +3 -3
  23. package/dist/{code-editor-field-Dg2U8mk1.js → code-editor-field-o7qp9eTq.js} +2 -2
  24. package/dist/{definition-canvas--3MPHoP-.js → definition-canvas-F8DGty5m.js} +1 -1
  25. package/dist/executions.js +2 -2
  26. package/dist/{flow-builder-CEYMv_sG.js → flow-builder-Dn4PCWuf.js} +193 -18
  27. package/dist/history.js +1 -1
  28. package/dist/{index-T8fJI6Ys.d.ts → index-DbTgk7dK.d.ts} +1 -1
  29. package/dist/{index-DjiFgEQl.d.ts → index-f_Nl7Ons.d.ts} +1 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +25 -25
  32. package/dist/{node-detail-view-Ft8huQ5T.js → node-detail-view-kWiwJEKr.js} +6 -6
  33. package/dist/{node-editor-tab-body-CjwxJdlF.js → node-editor-tab-body-C71Y0fRJ.js} +3 -3
  34. package/dist/{node-operation-selection--oy-yEq_.js → node-operation-selection-icAhaNza.js} +1 -0
  35. package/dist/observability.d.ts +1 -1
  36. package/dist/observability.js +2 -2
  37. package/dist/{popover-B6Rjrhar.js → popover-v7g8TWmm.js} +1 -1
  38. package/dist/{quick-config-panel-D95dc_2F.js → quick-config-panel-C84vsDYD.js} +4 -4
  39. package/dist/{schedule-panel-C-N92gxl.js → schedule-panel-BrNKy9DY.js} +662 -24
  40. package/dist/settings.js +1 -1
  41. package/dist/{sidebar-node-editor-BHBicLCn.js → sidebar-node-editor-D-2lPHwq.js} +4 -4
  42. package/dist/templates.js +2 -2
  43. package/dist/{useDurableParity-DEceXnEa.js → useDurableParity-fFmfa97r.js} +1 -1
  44. package/dist/{useExecutions-D3S4aqIQ.js → useExecutions-DNGW4yxG.js} +1 -1
  45. package/dist/{useObservability-B9cOl5mw.js → useObservability-u-MZCWUV.js} +1 -1
  46. package/dist/{useRunHistory-uTJepemz.js → useRunHistory-x9bVUB9T.js} +1 -1
  47. package/dist/{useWorkflowCodeExport-L-5NIRiH.js → useWorkflowCodeExport-CV3SZeKm.js} +82 -3
  48. package/dist/{useWorkflows-D9BlhpWH.js → useWorkflows-CVb5zXKt.js} +1 -1
  49. package/dist/workflow-builder.d.ts +1 -1
  50. package/dist/workflow-builder.js +5 -5
  51. package/dist/workflow-pages.d.ts +2 -2
  52. package/dist/workflow-pages.js +13 -13
  53. package/dist/workflow-preview.js +2 -2
  54. package/dist/workflows-list.js +1 -1
  55. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { _ as throwIfResultError, m as describeOperationError, n as getFluidGridsClient, r as getFluidGridsClientSafe, x as getFluidGridsConfig } from "./client-C9XSzSXr.js";
3
- import { H as useFlowStore } from "./node-operation-selection--oy-yEq_.js";
4
- import { Bn as useBuilderReadOnly, G as asRecord, K as coerceNumber, U as useRunGuard, X as optionText, Y as nodeTitle, Z as paramsFromOperation, _n as openRunQuotaUpgradePrompt, at as resolveLoadOptionsSpec, ct as casesFromRules, dn as parseParameterValue, fn as structuredParameterType, ft as boundFieldsOf, gn as useFluidGridsAnalytics, hn as isParamVisible, it as optionListValues, nt as isStringOptionList, ot as ConditionTreeBuilder, pt as missingRequiredParameters, rt as optionListText, st as SwitchRulesEditor, t as FluidGridsContext, un as formatParameterValue, ut as parseMappingValue } from "./FluidGridsProvider-BuZERrTA.js";
3
+ import { H as useFlowStore } from "./node-operation-selection-icAhaNza.js";
4
+ import { G as asRecord, Hn as useBuilderReadOnly, K as coerceNumber, U as useRunGuard, X as optionText, Y as nodeTitle, Z as paramsFromOperation, _n as isParamVisible, at as resolveLoadOptionsSpec, ct as casesFromRules, fn as formatParameterValue, ft as boundFieldsOf, it as optionListValues, mn as structuredParameterType, nt as isStringOptionList, ot as ConditionTreeBuilder, pn as parseParameterValue, pt as missingRequiredParameters, rt as optionListText, st as SwitchRulesEditor, t as FluidGridsContext, ut as parseMappingValue, vn as useFluidGridsAnalytics, yn as openRunQuotaUpgradePrompt } from "./FluidGridsProvider-BFfTxl9m.js";
5
5
  import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
6
- import { y as useGlobalStoreClient } from "./TemplateCard-D800haiE.js";
6
+ import { y as useGlobalStoreClient } from "./TemplateCard-DYDVNYo6.js";
7
7
  import { Bn as StopWebhookTestListenerDocument, Tr as WorkflowCallInputsDocument, Tt as ListenForWebhookTestEventDocument, Ut as PreviewScheduleDocument, hr as WebhookTestListenerDocument } from "./graphql-operation-types-BdU65FNm.js";
8
8
  import { t as useAgentDefinitionList, vt as useRunFromNodeEligibilityQuery } from "./useAgentDefinitions-DSU8Eek4.js";
9
9
  import { a as collectExtensions } from "./quota-Cdt8yz7N.js";
@@ -12,8 +12,8 @@ import { C as rowsFromRules, S as readAssertRulesSetting, _ as ASSERT_OPERATORS,
12
12
  import { r as describeRule, s as isAgentNodeType } from "./agent-form-Bfvm32j1.js";
13
13
  import { i as NativeSelect, o as StatusPill, t as FieldError, u as useResource } from "./shared-CMfn30xT.js";
14
14
  import { L as triggerSchedule, T as pauseSchedule, t as backfillSchedule, u as describeSchedule, z as unpauseSchedule } from "./api-DwaHAJtA.js";
15
- import { a as ConnectionSelector, i as RunOnSection, n as PopoverContent$1, r as PopoverTrigger$1, t as Popover$1 } from "./popover-B6Rjrhar.js";
16
- import { r as normalizeCodeLanguage, t as CodeEditorField } from "./code-editor-field-Dg2U8mk1.js";
15
+ import { a as ConnectionSelector, i as RunOnSection, n as PopoverContent$1, r as PopoverTrigger$1, t as Popover$1 } from "./popover-v7g8TWmm.js";
16
+ import { r as normalizeCodeLanguage, t as CodeEditorField } from "./code-editor-field-o7qp9eTq.js";
17
17
  import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
18
18
  import { Icon } from "@iconify/react";
19
19
  import { cn } from "@burdenoff/fe-libs/utils";
@@ -22,7 +22,8 @@ import { toast } from "sonner";
22
22
  import { gql } from "@apollo/client";
23
23
  import { Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, ResponsiveDialog, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea } from "@burdenoff/fe-libs/ui";
24
24
  import { Button as Button$1 } from "@burdenoff/fe-libs/button";
25
- import { Plus, X } from "lucide-react";
25
+ import { v4 } from "uuid";
26
+ import { ChevronDown, ChevronUp, Plus, X } from "lucide-react";
26
27
  import { createSharePrincipalLister } from "@burdenoff/fe-libs/connections";
27
28
  import cronstrue from "cronstrue";
28
29
  import { digest } from "ohash";
@@ -2111,7 +2112,7 @@ function defaultScheduleRule() {
2111
2112
  function isScheduleRuleField(value) {
2112
2113
  return typeof value === "string" && SCHEDULE_RULE_FIELDS.some((field) => field === value);
2113
2114
  }
2114
- function isRecord$3(value) {
2115
+ function isRecord$4(value) {
2115
2116
  return typeof value === "object" && value !== null && !Array.isArray(value);
2116
2117
  }
2117
2118
  function toNumber(value) {
@@ -2210,10 +2211,10 @@ function rulesFromLegacyState(state) {
2210
2211
  * array and the legacy fields, and always yields at least one rule.
2211
2212
  */
2212
2213
  function normalizeScheduleState(state) {
2213
- const source = isRecord$3(state) ? state : {};
2214
+ const source = isRecord$4(state) ? state : {};
2214
2215
  let rules;
2215
2216
  if (Array.isArray(source.rules)) {
2216
- rules = source.rules.filter(isRecord$3).map(normalizeScheduleRule);
2217
+ rules = source.rules.filter(isRecord$4).map(normalizeScheduleRule);
2217
2218
  if (rules.length === 0) rules = [defaultScheduleRule()];
2218
2219
  } else rules = rulesFromLegacyState(source);
2219
2220
  const timezone = toTrimmedString(source.timezone) || "UTC";
@@ -2228,7 +2229,7 @@ function normalizeScheduleState(state) {
2228
2229
  }
2229
2230
  /** Whether the saved state predates `rules[]` (so the editor is showing a conversion). */
2230
2231
  function isLegacyScheduleState(state) {
2231
- if (!isRecord$3(state) || Array.isArray(state.rules)) return false;
2232
+ if (!isRecord$4(state) || Array.isArray(state.rules)) return false;
2232
2233
  return state.mode !== void 0 || toTrimmedString(state.cronExpression) !== "" || toNumber(state.interval) !== void 0;
2233
2234
  }
2234
2235
  function inRange(value, min, max) {
@@ -2432,8 +2433,8 @@ function CronExpressionField({ value, onChange, className, testId, ariaLabel })
2432
2433
  }
2433
2434
 
2434
2435
  // src/builder/components/ndv/schedule-rules-editor.tsx
2435
- const rowInput$2 = "rounded-md border border-input bg-background px-2 py-1 text-xs outline-none focus:border-primary";
2436
- const selectCx = "scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary";
2436
+ const rowInput$3 = "rounded-md border border-input bg-background px-2 py-1 text-xs outline-none focus:border-primary";
2437
+ const selectCx$1 = "scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary";
2437
2438
  function isNumericOption(value) {
2438
2439
  return /^-?\d+$/.test(value);
2439
2440
  }
@@ -2580,7 +2581,7 @@ function ScheduleRulesEditor({ rules, ruleFields, onChange, testIdPrefix = "ndv"
2580
2581
  isCron ? /* @__PURE__ */ jsx(CronExpressionField, {
2581
2582
  value: stringValue(row[spec.name]),
2582
2583
  onChange: (value) => patchRow(index, spec.name, value),
2583
- className: cn(rowInput$2, "w-full"),
2584
+ className: cn(rowInput$3, "w-full"),
2584
2585
  testId,
2585
2586
  ariaLabel: label
2586
2587
  }) : spec.type === "multiOptions" && spec.options ? /* @__PURE__ */ jsx("div", {
@@ -2613,7 +2614,7 @@ function ScheduleRulesEditor({ rules, ruleFields, onChange, testIdPrefix = "ndv"
2613
2614
  onChange: (event) => patchRow(index, spec.name, optionValue(spec, event.target.value)),
2614
2615
  "aria-label": label,
2615
2616
  "data-testid": testId,
2616
- className: cn(rowInput$2, selectCx),
2617
+ className: cn(rowInput$3, selectCx$1),
2617
2618
  children: spec.options.map((option) => /* @__PURE__ */ jsx("option", {
2618
2619
  value: option,
2619
2620
  children: spec.optionLabels?.[option] ?? option
@@ -2626,14 +2627,14 @@ function ScheduleRulesEditor({ rules, ruleFields, onChange, testIdPrefix = "ndv"
2626
2627
  onChange: (event) => patchRow(index, spec.name, coerceNumber(spec.type, event.target.value)),
2627
2628
  "aria-label": label,
2628
2629
  "data-testid": testId,
2629
- className: rowInput$2
2630
+ className: rowInput$3
2630
2631
  }) : /* @__PURE__ */ jsx("input", {
2631
2632
  type: "text",
2632
2633
  value: stringValue(row[spec.name]),
2633
2634
  onChange: (event) => patchRow(index, spec.name, event.target.value),
2634
2635
  "aria-label": label,
2635
2636
  "data-testid": testId,
2636
- className: rowInput$2
2637
+ className: rowInput$3
2637
2638
  }),
2638
2639
  spec.description && /* @__PURE__ */ jsx("span", {
2639
2640
  className: "text-[10px] leading-snug text-muted-foreground",
@@ -2678,7 +2679,7 @@ function ScheduleRulesEditor({ rules, ruleFields, onChange, testIdPrefix = "ndv"
2678
2679
  * switched to at any time. Problems are shown under the rows; the worker
2679
2680
  * checks the rules again when the node runs.
2680
2681
  */
2681
- const rowInput$1 = "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground outline-none focus:border-primary";
2682
+ const rowInput$2 = "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground outline-none focus:border-primary";
2682
2683
  /** The JSON text of rows, as stored. */
2683
2684
  function serialiseRows(rows, previous) {
2684
2685
  const rules = rows.map(ruleFromRow);
@@ -2765,7 +2766,7 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2765
2766
  setJsonText(event.target.value);
2766
2767
  write(Array.isArray(value) ? readAssertRulesSetting(event.target.value) ?? value : event.target.value);
2767
2768
  },
2768
- className: cn(rowInput$1, "w-full font-mono"),
2769
+ className: cn(rowInput$2, "w-full font-mono"),
2769
2770
  "data-testid": `${testIdPrefix}-assert-rules-json`
2770
2771
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2771
2772
  rows.length === 0 && /* @__PURE__ */ jsx("p", {
@@ -2791,13 +2792,13 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2791
2792
  placeholder: tr("fluidgrids.builder.assertRules.pathPlaceholder", "path, e.g. body.items.0.id (empty = whole input)"),
2792
2793
  "aria-label": tr("fluidgrids.builder.assertRules.path", "Path"),
2793
2794
  onChange: (event) => setRow(index, { path: event.target.value }),
2794
- className: cn(rowInput$1, "min-w-0 flex-1 font-mono")
2795
+ className: cn(rowInput$2, "min-w-0 flex-1 font-mono")
2795
2796
  }),
2796
2797
  /* @__PURE__ */ jsx("select", {
2797
2798
  value: row.operator,
2798
2799
  "aria-label": tr("fluidgrids.builder.assertRules.operator", "Operator"),
2799
2800
  onChange: (event) => setRow(index, { operator: event.target.value }),
2800
- className: rowInput$1,
2801
+ className: rowInput$2,
2801
2802
  children: ASSERT_OPERATORS.map((spec) => /* @__PURE__ */ jsx("option", {
2802
2803
  value: spec.wire,
2803
2804
  children: tr(`fluidgrids.builder.assertRules.op.${spec.wire}`, spec.label)
@@ -2818,7 +2819,7 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2818
2819
  placeholder: row.operator === "matches" ? tr("fluidgrids.builder.assertRules.regexPlaceholder", "regular expression, e.g. ^ord_") : tr("fluidgrids.builder.assertRules.expectedPlaceholder", "expected value as JSON, e.g. 42, \"ok\" or {\"a\": 1}"),
2819
2820
  "aria-label": tr("fluidgrids.builder.assertRules.expected", "Expected"),
2820
2821
  onChange: (event) => setRow(index, { expected: event.target.value }),
2821
- className: cn(rowInput$1, "mt-1.5 w-full font-mono")
2822
+ className: cn(rowInput$2, "mt-1.5 w-full font-mono")
2822
2823
  }),
2823
2824
  operand === "range" && /* @__PURE__ */ jsxs("div", {
2824
2825
  className: "mt-1.5 flex gap-1.5",
@@ -2829,7 +2830,7 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2829
2830
  placeholder: tr("fluidgrids.builder.assertRules.min", "min"),
2830
2831
  "aria-label": tr("fluidgrids.builder.assertRules.min", "min"),
2831
2832
  onChange: (event) => setRow(index, { min: event.target.value }),
2832
- className: cn(rowInput$1, "w-full")
2833
+ className: cn(rowInput$2, "w-full")
2833
2834
  }), /* @__PURE__ */ jsx("input", {
2834
2835
  type: "text",
2835
2836
  inputMode: "decimal",
@@ -2837,7 +2838,7 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2837
2838
  placeholder: tr("fluidgrids.builder.assertRules.max", "max"),
2838
2839
  "aria-label": tr("fluidgrids.builder.assertRules.max", "max"),
2839
2840
  onChange: (event) => setRow(index, { max: event.target.value }),
2840
- className: cn(rowInput$1, "w-full")
2841
+ className: cn(rowInput$2, "w-full")
2841
2842
  })]
2842
2843
  }),
2843
2844
  /* @__PURE__ */ jsx("input", {
@@ -2846,7 +2847,7 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2846
2847
  placeholder: tr("fluidgrids.builder.assertRules.messagePlaceholder", "message when it does not hold (optional)"),
2847
2848
  "aria-label": tr("fluidgrids.builder.assertRules.message", "Message"),
2848
2849
  onChange: (event) => setRow(index, { message: event.target.value }),
2849
- className: cn(rowInput$1, "mt-1.5 w-full")
2850
+ className: cn(rowInput$2, "mt-1.5 w-full")
2850
2851
  })
2851
2852
  ]
2852
2853
  }, index);
@@ -2869,6 +2870,625 @@ function AssertRulesEditor({ value, onChange, testIdPrefix = "ndv" }) {
2869
2870
  });
2870
2871
  }
2871
2872
 
2873
+ // src/builder/lib/form-trigger-fields.ts
2874
+ /**
2875
+ * The `fields` setting of the Form Trigger (`internal:form`, BOFF-11146), as
2876
+ * the structured field editor reads and writes it.
2877
+ *
2878
+ * The shape is the one wspace-workflow-svc parses (`parseFields` in
2879
+ * `src/modules/triggers/rest/form-handler.ts`) and renders (`renderFieldControl`
2880
+ * in `src/modules/triggers/utils/form-fields.ts`): a JSON array, saved either as
2881
+ * JSON text (what the manifest's `string` parameter holds) or as an array, of
2882
+ *
2883
+ * { name, label?, type?, required?, placeholder?, options?: string[],
2884
+ * defaultValue? | default? }
2885
+ *
2886
+ * The service reads `defaultValue ?? default`, lower-cases `type` and treats a
2887
+ * missing type as `text`. The editor keeps the representation it was given
2888
+ * (text stays text, an array stays an array) and keeps every key it does not
2889
+ * edit, so saved workflows load without a migration.
2890
+ */
2891
+ const FORM_TRIGGER_NODE_TYPE = "internal:form";
2892
+ const FORM_FIELDS_PARAMETER = "fields";
2893
+ /**
2894
+ * The field types the service renders, in the order the manifest's `fields`
2895
+ * description lists them. Used when that description cannot be read.
2896
+ */
2897
+ const FORM_FIELD_TYPES = [
2898
+ "text",
2899
+ "textarea",
2900
+ "email",
2901
+ "number",
2902
+ "date",
2903
+ "password",
2904
+ "tel",
2905
+ "url",
2906
+ "dropdown",
2907
+ "multiselect",
2908
+ "radio",
2909
+ "checkbox",
2910
+ "checkboxes",
2911
+ "hidden"
2912
+ ];
2913
+ /** Types that render their `options`; `select` is the service's alias of `dropdown`. */
2914
+ const CHOICE_TYPES = /* @__PURE__ */ new Set([
2915
+ "dropdown",
2916
+ "select",
2917
+ "multiselect",
2918
+ "radio",
2919
+ "checkboxes"
2920
+ ]);
2921
+ /** Choice types whose value (and default) is a list. */
2922
+ const MULTI_CHOICE_TYPES = /* @__PURE__ */ new Set(["multiselect", "checkboxes"]);
2923
+ /** Types the service renders without a placeholder. */
2924
+ const NO_PLACEHOLDER_TYPES = /* @__PURE__ */ new Set(["checkbox", "hidden"]);
2925
+ function isRecord$3(value) {
2926
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2927
+ }
2928
+ /**
2929
+ * The field types the manifest advertises, read from the `Types: a, b, c.`
2930
+ * sentence of the `fields` description. Falls back to {@link FORM_FIELD_TYPES}
2931
+ * when the description has no such list.
2932
+ */
2933
+ function advertisedFormFieldTypes(description) {
2934
+ const types = (description?.match(/\bTypes:\s*([^.]+)\./)?.[1] ?? "").split(",").map((type) => type.trim().toLowerCase()).filter((type) => /^[a-z][a-z0-9_-]*$/.test(type));
2935
+ return types.length > 0 ? [...new Set(types)] : [...FORM_FIELD_TYPES];
2936
+ }
2937
+ /** A field's type the way the service reads it: lower case, `text` when unset. */
2938
+ function formFieldTypeOf(field) {
2939
+ return typeof field.type === "string" && field.type.trim() ? field.type.trim().toLowerCase() : "text";
2940
+ }
2941
+ function isChoiceFieldType(type) {
2942
+ return CHOICE_TYPES.has(type);
2943
+ }
2944
+ function isMultiChoiceFieldType(type) {
2945
+ return MULTI_CHOICE_TYPES.has(type);
2946
+ }
2947
+ function fieldTypeTakesPlaceholder(type) {
2948
+ return !CHOICE_TYPES.has(type) && !NO_PLACEHOLDER_TYPES.has(type);
2949
+ }
2950
+ /**
2951
+ * The fields of a saved setting, or null when it is not a JSON array of
2952
+ * objects (the editor then opens the JSON view so nothing is lost). An unset or
2953
+ * blank setting is an empty list.
2954
+ */
2955
+ function readFormFieldsSetting(value) {
2956
+ let parsed = value;
2957
+ if (value === void 0 || value === null) return [];
2958
+ if (typeof value === "string") {
2959
+ if (!value.trim()) return [];
2960
+ try {
2961
+ parsed = JSON.parse(value);
2962
+ } catch {
2963
+ return null;
2964
+ }
2965
+ }
2966
+ if (!Array.isArray(parsed) || !parsed.every(isRecord$3)) return null;
2967
+ return parsed.map((field) => ({ ...field }));
2968
+ }
2969
+ /** The text the JSON view shows for a setting. */
2970
+ function formFieldsText(value) {
2971
+ if (typeof value === "string") return value;
2972
+ if (value === void 0 || value === null) return "";
2973
+ return JSON.stringify(value, null, 2);
2974
+ }
2975
+ function toFormFieldRows(fields) {
2976
+ return fields.map((field) => ({
2977
+ key: v4(),
2978
+ field
2979
+ }));
2980
+ }
2981
+ /** A field ready to save: name trimmed, blank options dropped. */
2982
+ function savedField(field) {
2983
+ const next = { ...field };
2984
+ if (typeof next.name === "string") next.name = next.name.trim();
2985
+ if (Array.isArray(next.options)) next.options = next.options.filter((option) => typeof option !== "string" || option.trim() !== "");
2986
+ return next;
2987
+ }
2988
+ /**
2989
+ * The value to save for `fields`, in the representation of the `previous`
2990
+ * setting: an array stays an array, anything else is saved as JSON text.
2991
+ */
2992
+ function serialiseFormFields(fields, previous) {
2993
+ const saved = fields.map(savedField);
2994
+ return Array.isArray(previous) ? saved : JSON.stringify(saved, null, 2);
2995
+ }
2996
+ /**
2997
+ * What stops the fields from being saved: a field without a name, a name used
2998
+ * twice, a choice field without options. `index` is the field's position.
2999
+ */
3000
+ function formFieldProblems(fields) {
3001
+ const problems = [];
3002
+ const seen = /* @__PURE__ */ new Set();
3003
+ fields.forEach((field, index) => {
3004
+ const name = typeof field.name === "string" ? field.name.trim() : "";
3005
+ if (!name) problems.push({
3006
+ code: "nameMissing",
3007
+ index
3008
+ });
3009
+ else if (seen.has(name)) problems.push({
3010
+ code: "duplicateName",
3011
+ index,
3012
+ name
3013
+ });
3014
+ else seen.add(name);
3015
+ if (isChoiceFieldType(formFieldTypeOf(field)) && formFieldOptions(field).every((option) => !option.trim())) problems.push({
3016
+ code: "optionsMissing",
3017
+ index
3018
+ });
3019
+ });
3020
+ return problems;
3021
+ }
3022
+ /** A new text field with a name no other field uses. */
3023
+ function newFormField(fields) {
3024
+ const names = new Set(fields.map((field) => field.name));
3025
+ let number = fields.length + 1;
3026
+ while (names.has(`field_${number}`)) number += 1;
3027
+ return {
3028
+ name: `field_${number}`,
3029
+ type: "text"
3030
+ };
3031
+ }
3032
+ /**
3033
+ * `field` with `key` set, or removed when `value` is empty (`undefined`, an
3034
+ * empty string or `false`), so cleared settings do not linger in the JSON.
3035
+ */
3036
+ function setFormFieldKey(field, key, value) {
3037
+ const next = { ...field };
3038
+ if (value === void 0 || value === "" || value === false) delete next[key];
3039
+ else next[key] = value;
3040
+ return next;
3041
+ }
3042
+ /** The option labels of a field; anything that is not text is shown as text. */
3043
+ function formFieldOptions(field) {
3044
+ if (!Array.isArray(field.options)) return [];
3045
+ return field.options.map((option) => {
3046
+ if (typeof option === "string") return option;
3047
+ if (isRecord$3(option)) return String(option.value ?? option.label ?? "");
3048
+ return option === void 0 || option === null ? "" : String(option);
3049
+ });
3050
+ }
3051
+ /** The default the service applies: `defaultValue`, else `default`. */
3052
+ function rawDefault(field) {
3053
+ return field.defaultValue ?? field.default;
3054
+ }
3055
+ /** A single-value default (text-like, textarea, hidden, dropdown, radio). */
3056
+ function formFieldTextDefault(field) {
3057
+ const raw = rawDefault(field);
3058
+ const first = Array.isArray(raw) ? raw[0] : raw;
3059
+ return first === void 0 || first === null || typeof first === "boolean" ? "" : String(first);
3060
+ }
3061
+ /** The defaults of a multiselect or checkbox group. */
3062
+ function formFieldListDefault(field) {
3063
+ const raw = rawDefault(field);
3064
+ if (raw === void 0 || raw === null || typeof raw === "boolean") return [];
3065
+ return (Array.isArray(raw) ? raw : [raw]).map(String);
3066
+ }
3067
+ /** Whether a single checkbox starts checked. */
3068
+ function formFieldCheckedDefault(field) {
3069
+ const raw = rawDefault(field);
3070
+ return raw === true || raw === "true";
3071
+ }
3072
+ /**
3073
+ * `field` with a new default. It goes where the field keeps it (`default` when
3074
+ * only that key is set, else `defaultValue`); clearing it removes both keys, so
3075
+ * an older `default` does not take over.
3076
+ */
3077
+ function withFormFieldDefault(field, value) {
3078
+ const next = { ...field };
3079
+ if (value === void 0 || value === "" || value === false || Array.isArray(value) && value.length === 0) {
3080
+ delete next.defaultValue;
3081
+ delete next.default;
3082
+ return next;
3083
+ }
3084
+ const key = "default" in field && !("defaultValue" in field) ? "default" : "defaultValue";
3085
+ next[key] = value;
3086
+ return next;
3087
+ }
3088
+ /** `list` with the item at `index` moved one place up (-1) or down (1). */
3089
+ function moveFormFieldRow(list, index, direction) {
3090
+ const next = list.slice();
3091
+ const target = index + direction;
3092
+ if (index < 0 || index >= list.length || target < 0 || target >= list.length) return next;
3093
+ [next[index], next[target]] = [list[target], list[index]];
3094
+ return next;
3095
+ }
3096
+
3097
+ // src/builder/components/ndv/form-fields-messages.ts
3098
+ /** The name of a field type in the type picker; unknown types show as saved. */
3099
+ function formFieldTypeLabel(tr, type) {
3100
+ switch (type) {
3101
+ case "text": return tr("fluidgrids.builder.formFields.typeText", "Text");
3102
+ case "textarea": return tr("fluidgrids.builder.formFields.typeTextarea", "Long text");
3103
+ case "email": return tr("fluidgrids.builder.formFields.typeEmail", "Email");
3104
+ case "number": return tr("fluidgrids.builder.formFields.typeNumber", "Number");
3105
+ case "date": return tr("fluidgrids.builder.formFields.typeDate", "Date");
3106
+ case "password": return tr("fluidgrids.builder.formFields.typePassword", "Password");
3107
+ case "tel": return tr("fluidgrids.builder.formFields.typeTel", "Phone number");
3108
+ case "url": return tr("fluidgrids.builder.formFields.typeUrl", "URL");
3109
+ case "dropdown": return tr("fluidgrids.builder.formFields.typeDropdown", "Dropdown");
3110
+ case "multiselect": return tr("fluidgrids.builder.formFields.typeMultiselect", "Multi-select");
3111
+ case "radio": return tr("fluidgrids.builder.formFields.typeRadio", "Radio buttons");
3112
+ case "checkbox": return tr("fluidgrids.builder.formFields.typeCheckbox", "Checkbox");
3113
+ case "checkboxes": return tr("fluidgrids.builder.formFields.typeCheckboxes", "Checkboxes");
3114
+ case "hidden": return tr("fluidgrids.builder.formFields.typeHidden", "Hidden");
3115
+ default: return type;
3116
+ }
3117
+ }
3118
+ /** The message for a problem that keeps the fields from being saved. */
3119
+ function formFieldProblemMessage(tr, problem) {
3120
+ const number = problem.index + 1;
3121
+ switch (problem.code) {
3122
+ case "nameMissing": return tr("fluidgrids.builder.formFields.nameMissing", "Field {{number}} needs a name.", { number });
3123
+ case "duplicateName": return tr("fluidgrids.builder.formFields.duplicateName", "Field {{number}} reuses the name \"{{name}}\". Each field needs its own name.", {
3124
+ number,
3125
+ name: problem.name
3126
+ });
3127
+ case "optionsMissing": return tr("fluidgrids.builder.formFields.optionsMissing", "Field {{number}} needs at least one option.", { number });
3128
+ }
3129
+ }
3130
+
3131
+ // src/builder/components/ndv/form-fields-editor.tsx
3132
+ /**
3133
+ * The field list of the Form Trigger (`internal:form`, BOFF-11146).
3134
+ *
3135
+ * The manifest declares `fields` as JSON text. This editor shows it as one
3136
+ * card per field (label, name, type, required, placeholder, default and, for
3137
+ * choice types, options) that can be added, removed and reordered, and writes
3138
+ * the JSON back in the representation the setting already uses. Fields that do
3139
+ * not parse open in the JSON view, so nothing a person typed is thrown away.
3140
+ *
3141
+ * A list with a problem (a field without a name, a name used twice, a choice
3142
+ * field without options) is not saved: the editor keeps the edit on screen,
3143
+ * says what to fix, and writes once the list is valid again.
3144
+ */
3145
+ const rowInput$1 = "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground outline-none focus:border-primary aria-invalid:border-status-error-border";
3146
+ const selectCx = "scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary";
3147
+ const iconButton = "rounded p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30";
3148
+ const removeButton = "rounded p-0.5 text-muted-foreground hover:text-[var(--color-status-error-text)]";
3149
+ const addButton = "flex items-center gap-1 self-start text-xs text-primary hover:underline";
3150
+ const smallLabel = "text-[10px] font-semibold text-muted-foreground";
3151
+ function FormFieldsEditor({ value, onChange, description, testIdPrefix = "ndv" }) {
3152
+ const tr = useTr();
3153
+ const types = advertisedFormFieldTypes(description);
3154
+ const [rows, setRows] = useState(() => toFormFieldRows(readFormFieldsSetting(value) ?? []));
3155
+ const [jsonView, setJsonView] = useState(() => readFormFieldsSetting(value) === null);
3156
+ const [jsonText, setJsonText] = useState(() => formFieldsText(value));
3157
+ const [synced, setSynced] = useState(value);
3158
+ if (!Object.is(value, synced)) {
3159
+ setSynced(value);
3160
+ setJsonText(formFieldsText(value));
3161
+ const parsed = readFormFieldsSetting(value);
3162
+ if (parsed === null) setJsonView(true);
3163
+ else setRows(toFormFieldRows(parsed));
3164
+ }
3165
+ const write = (next) => {
3166
+ setSynced(next);
3167
+ onChange(next);
3168
+ };
3169
+ const setRowList = (next) => {
3170
+ setRows(next);
3171
+ const fields = next.map((row) => row.field);
3172
+ const serialised = serialiseFormFields(fields, value);
3173
+ setJsonText(formFieldsText(serialised));
3174
+ if (formFieldProblems(fields).length === 0) write(serialised);
3175
+ };
3176
+ const parsedJson = jsonView ? readFormFieldsSetting(jsonText) : null;
3177
+ const problems = formFieldProblems(jsonView ? parsedJson ?? [] : rows.map((row) => row.field));
3178
+ const invalidJson = jsonView && parsedJson === null;
3179
+ const toggleView = () => {
3180
+ if (jsonView) {
3181
+ if (parsedJson === null) return;
3182
+ setRows(toFormFieldRows(parsedJson));
3183
+ setJsonView(false);
3184
+ } else {
3185
+ setJsonText(formFieldsText(serialiseFormFields(rows.map((row) => row.field), value)));
3186
+ setJsonView(true);
3187
+ }
3188
+ };
3189
+ return /* @__PURE__ */ jsxs("div", {
3190
+ className: "flex flex-col gap-2",
3191
+ "data-testid": `${testIdPrefix}-form-fields`,
3192
+ children: [
3193
+ /* @__PURE__ */ jsx("div", {
3194
+ className: "flex items-center justify-end",
3195
+ children: /* @__PURE__ */ jsx("button", {
3196
+ type: "button",
3197
+ onClick: toggleView,
3198
+ disabled: invalidJson,
3199
+ className: "text-[11px] text-primary hover:underline disabled:opacity-50",
3200
+ "data-testid": `${testIdPrefix}-form-fields-view`,
3201
+ children: jsonView ? tr("fluidgrids.builder.formFields.showFields", "Edit as fields") : tr("fluidgrids.builder.formFields.showJson", "Edit as JSON")
3202
+ })
3203
+ }),
3204
+ jsonView ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("textarea", {
3205
+ value: jsonText,
3206
+ rows: 10,
3207
+ spellCheck: false,
3208
+ "aria-label": tr("fluidgrids.builder.formFields.label", "Form fields"),
3209
+ onChange: (event) => {
3210
+ const text = event.target.value;
3211
+ setJsonText(text);
3212
+ const parsed = readFormFieldsSetting(text);
3213
+ if (parsed !== null && formFieldProblems(parsed).length === 0) write(Array.isArray(value) ? parsed : text);
3214
+ },
3215
+ className: cn(rowInput$1, "w-full font-mono"),
3216
+ "data-testid": `${testIdPrefix}-form-fields-json`
3217
+ }), description && /* @__PURE__ */ jsx("span", {
3218
+ className: "text-[11px] leading-snug text-muted-foreground",
3219
+ children: description
3220
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3221
+ rows.length === 0 && /* @__PURE__ */ jsx("p", {
3222
+ className: "text-xs text-muted-foreground",
3223
+ children: tr("fluidgrids.builder.formFields.empty", "No fields yet. Add one to build the form.")
3224
+ }),
3225
+ rows.map(({ key, field }, index) => /* @__PURE__ */ jsx(FormFieldCard, {
3226
+ field,
3227
+ index,
3228
+ count: rows.length,
3229
+ types,
3230
+ problems: problems.filter((problem) => problem.index === index),
3231
+ onChange: (next) => setRowList(rows.map((row, i) => i === index ? {
3232
+ ...row,
3233
+ field: next
3234
+ } : row)),
3235
+ onMove: (direction) => setRowList(moveFormFieldRow(rows, index, direction)),
3236
+ onRemove: () => setRowList(rows.filter((_, i) => i !== index)),
3237
+ testIdPrefix
3238
+ }, key)),
3239
+ /* @__PURE__ */ jsxs("button", {
3240
+ type: "button",
3241
+ onClick: () => setRowList([...rows, ...toFormFieldRows([newFormField(rows.map((row) => row.field))])]),
3242
+ className: addButton,
3243
+ "data-testid": `${testIdPrefix}-form-field-add`,
3244
+ children: [/* @__PURE__ */ jsx(Plus, { className: "size-3.5" }), tr("fluidgrids.builder.formFields.add", "Add field")]
3245
+ })
3246
+ ] }),
3247
+ (invalidJson || problems.length > 0) && /* @__PURE__ */ jsx("div", {
3248
+ className: "text-[11px] text-[var(--color-status-error-text)]",
3249
+ role: "alert",
3250
+ "data-testid": `${testIdPrefix}-form-fields-problems`,
3251
+ children: invalidJson ? /* @__PURE__ */ jsx("p", { children: tr("fluidgrids.builder.formFields.invalidJson", "The fields must be a JSON array of objects.") }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: tr("fluidgrids.builder.formFields.notSaved", "These changes are not saved until this is fixed:") }), /* @__PURE__ */ jsx("ul", {
3252
+ className: "list-inside list-disc",
3253
+ children: problems.map((problem) => /* @__PURE__ */ jsx("li", { children: formFieldProblemMessage(tr, problem) }, `${problem.code}-${problem.index}`))
3254
+ })] })
3255
+ })
3256
+ ]
3257
+ });
3258
+ }
3259
+ /** One field: label, type, order, name, required, placeholder, options, default. */
3260
+ function FormFieldCard({ field, index, count, types, problems, onChange, onMove, onRemove, testIdPrefix }) {
3261
+ const tr = useTr();
3262
+ const type = formFieldTypeOf(field);
3263
+ const typeChoices = types.includes(type) ? types : [...types, type];
3264
+ const options = formFieldOptions(field);
3265
+ const name = typeof field.name === "string" ? field.name : "";
3266
+ const nameInvalid = problems.some((problem) => problem.code === "nameMissing" || problem.code === "duplicateName");
3267
+ const labelText = tr("fluidgrids.builder.formFields.fieldLabel", "Label");
3268
+ const nameText = tr("fluidgrids.builder.formFields.name", "Name");
3269
+ const placeholderText = tr("fluidgrids.builder.formFields.placeholder", "Placeholder");
3270
+ return /* @__PURE__ */ jsxs("div", {
3271
+ className: "rounded-md border border-border p-2",
3272
+ "data-testid": `${testIdPrefix}-form-field-${index}`,
3273
+ children: [
3274
+ /* @__PURE__ */ jsxs("div", {
3275
+ className: "flex items-center gap-1.5",
3276
+ children: [
3277
+ /* @__PURE__ */ jsxs("span", {
3278
+ className: smallLabel,
3279
+ children: [index + 1, "."]
3280
+ }),
3281
+ /* @__PURE__ */ jsx("input", {
3282
+ type: "text",
3283
+ value: typeof field.label === "string" ? field.label : "",
3284
+ placeholder: name || labelText,
3285
+ "aria-label": labelText,
3286
+ onChange: (event) => onChange(setFormFieldKey(field, "label", event.target.value)),
3287
+ className: cn(rowInput$1, "min-w-0 flex-1 font-medium")
3288
+ }),
3289
+ /* @__PURE__ */ jsx("select", {
3290
+ value: type,
3291
+ "aria-label": tr("fluidgrids.builder.formFields.type", "Type"),
3292
+ onChange: (event) => onChange(setFormFieldKey(field, "type", event.target.value)),
3293
+ className: cn(rowInput$1, selectCx),
3294
+ children: typeChoices.map((choice) => /* @__PURE__ */ jsx("option", {
3295
+ value: choice,
3296
+ children: formFieldTypeLabel(tr, choice)
3297
+ }, choice))
3298
+ }),
3299
+ /* @__PURE__ */ jsx("button", {
3300
+ type: "button",
3301
+ onClick: () => onMove(-1),
3302
+ disabled: index === 0,
3303
+ title: tr("fluidgrids.builder.switchRulesEditor.moveUp", "Move up"),
3304
+ className: iconButton,
3305
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4" })
3306
+ }),
3307
+ /* @__PURE__ */ jsx("button", {
3308
+ type: "button",
3309
+ onClick: () => onMove(1),
3310
+ disabled: index === count - 1,
3311
+ title: tr("fluidgrids.builder.switchRulesEditor.moveDown", "Move down"),
3312
+ className: iconButton,
3313
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4" })
3314
+ }),
3315
+ /* @__PURE__ */ jsx("button", {
3316
+ type: "button",
3317
+ onClick: onRemove,
3318
+ title: tr("fluidgrids.builder.formFields.remove", "Remove field"),
3319
+ className: removeButton,
3320
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
3321
+ })
3322
+ ]
3323
+ }),
3324
+ /* @__PURE__ */ jsxs("div", {
3325
+ className: "mt-1.5 flex items-center gap-1.5",
3326
+ children: [/* @__PURE__ */ jsx("input", {
3327
+ type: "text",
3328
+ value: name,
3329
+ placeholder: nameText,
3330
+ "aria-label": nameText,
3331
+ "aria-invalid": nameInvalid || void 0,
3332
+ onChange: (event) => onChange({
3333
+ ...field,
3334
+ name: event.target.value
3335
+ }),
3336
+ className: cn(rowInput$1, "min-w-0 flex-1 font-mono")
3337
+ }), type !== "hidden" && /* @__PURE__ */ jsxs("label", {
3338
+ className: "flex shrink-0 cursor-pointer items-center gap-1 text-xs text-muted-foreground",
3339
+ children: [/* @__PURE__ */ jsx("input", {
3340
+ type: "checkbox",
3341
+ checked: field.required === true,
3342
+ onChange: (event) => onChange(setFormFieldKey(field, "required", event.target.checked)),
3343
+ className: "size-3.5"
3344
+ }), tr("fluidgrids.builder.formFields.required", "Required")]
3345
+ })]
3346
+ }),
3347
+ fieldTypeTakesPlaceholder(type) && /* @__PURE__ */ jsx("input", {
3348
+ type: "text",
3349
+ value: typeof field.placeholder === "string" ? field.placeholder : "",
3350
+ placeholder: placeholderText,
3351
+ "aria-label": placeholderText,
3352
+ onChange: (event) => onChange(setFormFieldKey(field, "placeholder", event.target.value)),
3353
+ className: cn(rowInput$1, "mt-1.5 w-full")
3354
+ }),
3355
+ isChoiceFieldType(type) && /* @__PURE__ */ jsx(FieldOptions, {
3356
+ options,
3357
+ invalid: problems.some((problem) => problem.code === "optionsMissing"),
3358
+ onChange: (next) => onChange({
3359
+ ...field,
3360
+ options: next
3361
+ }),
3362
+ testId: `${testIdPrefix}-form-field-${index}-add-option`
3363
+ }),
3364
+ /* @__PURE__ */ jsx(FieldDefault, {
3365
+ field,
3366
+ type,
3367
+ options,
3368
+ onChange: (next) => onChange(withFormFieldDefault(field, next))
3369
+ })
3370
+ ]
3371
+ });
3372
+ }
3373
+ /** The options of a choice field; blank ones are dropped when saved. */
3374
+ function FieldOptions({ options, invalid, onChange, testId }) {
3375
+ const tr = useTr();
3376
+ const label = tr("fluidgrids.builder.formFields.options", "Options");
3377
+ return /* @__PURE__ */ jsxs("div", {
3378
+ role: "group",
3379
+ "aria-label": label,
3380
+ className: "mt-1.5 flex flex-col gap-1",
3381
+ children: [
3382
+ /* @__PURE__ */ jsx("span", {
3383
+ className: smallLabel,
3384
+ children: label
3385
+ }),
3386
+ options.map((option, optionIndex) => {
3387
+ const optionLabel = tr("fluidgrids.builder.formFields.option", "Option {{number}}", { number: optionIndex + 1 });
3388
+ return /* @__PURE__ */ jsxs("div", {
3389
+ className: "flex items-center gap-1.5",
3390
+ children: [/* @__PURE__ */ jsx("input", {
3391
+ type: "text",
3392
+ value: option,
3393
+ placeholder: optionLabel,
3394
+ "aria-label": optionLabel,
3395
+ "aria-invalid": invalid || void 0,
3396
+ onChange: (event) => onChange(options.map((o, i) => i === optionIndex ? event.target.value : o)),
3397
+ className: cn(rowInput$1, "min-w-0 flex-1")
3398
+ }), /* @__PURE__ */ jsx("button", {
3399
+ type: "button",
3400
+ onClick: () => onChange(options.filter((_, i) => i !== optionIndex)),
3401
+ title: tr("fluidgrids.builder.formFields.removeOption", "Remove option"),
3402
+ className: removeButton,
3403
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
3404
+ })]
3405
+ }, optionIndex);
3406
+ }),
3407
+ /* @__PURE__ */ jsxs("button", {
3408
+ type: "button",
3409
+ onClick: () => onChange([...options, ""]),
3410
+ className: addButton,
3411
+ "data-testid": testId,
3412
+ children: [/* @__PURE__ */ jsx(Plus, { className: "size-3.5" }), tr("fluidgrids.builder.formFields.addOption", "Add option")]
3413
+ })
3414
+ ]
3415
+ });
3416
+ }
3417
+ /** The default-value control a field type takes. */
3418
+ function FieldDefault({ field, type, options, onChange }) {
3419
+ const tr = useTr();
3420
+ const label = tr("fluidgrids.builder.formFields.defaultValue", "Default value");
3421
+ const choices = [...new Set(options.filter((option) => option.trim() !== ""))];
3422
+ if (type === "checkbox") return /* @__PURE__ */ jsxs("label", {
3423
+ className: "mt-1.5 flex w-fit cursor-pointer items-center gap-1 text-xs text-muted-foreground",
3424
+ children: [/* @__PURE__ */ jsx("input", {
3425
+ type: "checkbox",
3426
+ checked: formFieldCheckedDefault(field),
3427
+ onChange: (event) => onChange(event.target.checked),
3428
+ className: "size-3.5"
3429
+ }), tr("fluidgrids.builder.formFields.checkedByDefault", "Checked by default")]
3430
+ });
3431
+ if (isMultiChoiceFieldType(type)) {
3432
+ if (choices.length === 0) return null;
3433
+ const selected = formFieldListDefault(field);
3434
+ const selectedSet = new Set(selected);
3435
+ return /* @__PURE__ */ jsxs("div", {
3436
+ role: "group",
3437
+ "aria-label": label,
3438
+ className: "mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1",
3439
+ children: [/* @__PURE__ */ jsx("span", {
3440
+ className: smallLabel,
3441
+ children: label
3442
+ }), choices.map((option) => /* @__PURE__ */ jsxs("label", {
3443
+ className: "flex cursor-pointer items-center gap-1 text-xs",
3444
+ children: [/* @__PURE__ */ jsx("input", {
3445
+ type: "checkbox",
3446
+ checked: selectedSet.has(option),
3447
+ onChange: (event) => {
3448
+ const next = new Set(selectedSet);
3449
+ if (event.target.checked) next.add(option);
3450
+ else next.delete(option);
3451
+ onChange(choices.filter((choice) => next.has(choice)));
3452
+ },
3453
+ className: "size-3.5"
3454
+ }), option]
3455
+ }, option))]
3456
+ });
3457
+ }
3458
+ if (isChoiceFieldType(type)) {
3459
+ if (choices.length === 0) return null;
3460
+ const current = formFieldTextDefault(field);
3461
+ return /* @__PURE__ */ jsxs("select", {
3462
+ value: current,
3463
+ "aria-label": label,
3464
+ onChange: (event) => onChange(event.target.value),
3465
+ className: cn(rowInput$1, selectCx, "mt-1.5 w-full"),
3466
+ children: [
3467
+ /* @__PURE__ */ jsx("option", {
3468
+ value: "",
3469
+ children: tr("fluidgrids.builder.formFields.noDefault", "No default")
3470
+ }),
3471
+ current && !choices.includes(current) && /* @__PURE__ */ jsx("option", {
3472
+ value: current,
3473
+ children: current
3474
+ }),
3475
+ choices.map((option) => /* @__PURE__ */ jsx("option", {
3476
+ value: option,
3477
+ children: option
3478
+ }, option))
3479
+ ]
3480
+ });
3481
+ }
3482
+ return /* @__PURE__ */ jsx("input", {
3483
+ type: "text",
3484
+ value: formFieldTextDefault(field),
3485
+ placeholder: label,
3486
+ "aria-label": label,
3487
+ onChange: (event) => onChange(event.target.value),
3488
+ className: cn(rowInput$1, "mt-1.5 w-full")
3489
+ });
3490
+ }
3491
+
2872
3492
  // src/builder/lib/tool-approval-rules.ts
2873
3493
  const TOOL_APPROVAL_RULES = [
2874
3494
  "always",
@@ -6412,6 +7032,24 @@ function NodeParamsForm({ config, nodeType, testIdPrefix = "ndv" }) {
6412
7032
  testIdPrefix
6413
7033
  })]
6414
7034
  }, p.name);
7035
+ if (nodeType === "internal:form" && p.name === "fields" && mapping[p.name] === void 0) return /* @__PURE__ */ jsxs("div", {
7036
+ className: "flex flex-col gap-1.5",
7037
+ children: [/* @__PURE__ */ jsxs("div", {
7038
+ className: "flex items-center gap-1",
7039
+ children: [/* @__PURE__ */ jsx("label", {
7040
+ className: "text-xs font-medium text-foreground",
7041
+ children: tr("fluidgrids.builder.formFields.label", "Form fields")
7042
+ }), p.required && /* @__PURE__ */ jsx("span", {
7043
+ className: "text-destructive",
7044
+ children: "*"
7045
+ })]
7046
+ }), /* @__PURE__ */ jsx(FormFieldsEditor, {
7047
+ value: argsState[p.name],
7048
+ onChange: (next) => writeFixed(p.name, next),
7049
+ description: p.description,
7050
+ testIdPrefix
7051
+ }, config.node?.id)]
7052
+ }, p.name);
6415
7053
  if (nodeType === "store/ai-agent" && p.name === "toolApprovalRules") return /* @__PURE__ */ jsxs("div", {
6416
7054
  className: "flex flex-col gap-1.5",
6417
7055
  children: [