@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.
- package/dist/{AiStudioPage-NfhraV2M.js → AiStudioPage-C-jio5Bz.js} +4 -4
- package/dist/{DataSinkDashboardPage-BfdKumMH.js → DataSinkDashboardPage-CRBsNjoS.js} +1 -1
- package/dist/{ExecutionDetailPage-Dtpn7Xnl.js → ExecutionDetailPage-Cw4zyaIG.js} +5 -5
- package/dist/{ExecutionsListPage-DeqJnEO7.js → ExecutionsListPage-BgeqwVr2.js} +4 -4
- package/dist/{FluidGridsProvider-BuZERrTA.js → FluidGridsProvider-BFfTxl9m.js} +2 -6
- package/dist/{HistoryPage-BC7ld-oQ.js → HistoryPage-BdijEOHp.js} +2 -2
- package/dist/{InsightsPage-C8X1g_Ya.js → InsightsPage-2jnYlozA.js} +1 -1
- package/dist/{MissingNodesInstallModal-DUmwDs4e.js → MissingNodesInstallModal-C0wPwR0Q.js} +1 -1
- package/dist/{QuotaExhaustedDialog-RctNPfz_.js → QuotaExhaustedDialog-BKZ9moP8.js} +1 -1
- package/dist/{SettingsPage-BbE4S63W.js → SettingsPage-BaM98QU_.js} +3 -3
- package/dist/{TemplateCard-D800haiE.js → TemplateCard-DYDVNYo6.js} +2 -2
- package/dist/{TemplateConfigurationModal-DYs1YxYy.js → TemplateConfigurationModal-BzdkGWim.js} +1 -1
- package/dist/{TemplateDetailPanel-CKhCQlyo.js → TemplateDetailPanel-s4EqQhra.js} +1 -1
- package/dist/{TemplatePreview-DbF97Jwi.js → TemplatePreview-B_-zS_Mm.js} +3 -3
- package/dist/{TemplatesPage-BR6iOnC0.js → TemplatesPage-OIMd6cIl.js} +4 -4
- package/dist/{VersionDiffDialog-B00qeGiE.js → VersionDiffDialog-Cp2doV3f.js} +1 -1
- package/dist/{WorkflowBuilder-BWqiMpdk.js → WorkflowBuilder-B2tIOPi0.js} +4 -4
- package/dist/{WorkflowBuilder-CWsUITzN.js → WorkflowBuilder-DD4UIAjC.js} +10 -9
- package/dist/{WorkflowSearchRedirect-7Kc7NniX.js → WorkflowSearchRedirect-BhXzWDj7.js} +1 -1
- package/dist/{WorkflowsList-Bnov6LCT.js → WorkflowsList-BV-2J_gG.js} +138 -10
- package/dist/{agentTools-yi8CDKzs.js → agentTools-EVdEThES.js} +4 -4
- package/dist/{canvasNoteTools-kRtdgYMQ.js → canvasNoteTools-C5Vf0pxu.js} +3 -3
- package/dist/{code-editor-field-Dg2U8mk1.js → code-editor-field-o7qp9eTq.js} +2 -2
- package/dist/{definition-canvas--3MPHoP-.js → definition-canvas-F8DGty5m.js} +1 -1
- package/dist/executions.js +2 -2
- package/dist/{flow-builder-CEYMv_sG.js → flow-builder-Dn4PCWuf.js} +193 -18
- package/dist/history.js +1 -1
- package/dist/{index-T8fJI6Ys.d.ts → index-DbTgk7dK.d.ts} +1 -1
- package/dist/{index-DjiFgEQl.d.ts → index-f_Nl7Ons.d.ts} +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -25
- package/dist/{node-detail-view-Ft8huQ5T.js → node-detail-view-kWiwJEKr.js} +6 -6
- package/dist/{node-editor-tab-body-CjwxJdlF.js → node-editor-tab-body-C71Y0fRJ.js} +3 -3
- package/dist/{node-operation-selection--oy-yEq_.js → node-operation-selection-icAhaNza.js} +1 -0
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/{popover-B6Rjrhar.js → popover-v7g8TWmm.js} +1 -1
- package/dist/{quick-config-panel-D95dc_2F.js → quick-config-panel-C84vsDYD.js} +4 -4
- package/dist/{schedule-panel-C-N92gxl.js → schedule-panel-BrNKy9DY.js} +662 -24
- package/dist/settings.js +1 -1
- package/dist/{sidebar-node-editor-BHBicLCn.js → sidebar-node-editor-D-2lPHwq.js} +4 -4
- package/dist/templates.js +2 -2
- package/dist/{useDurableParity-DEceXnEa.js → useDurableParity-fFmfa97r.js} +1 -1
- package/dist/{useExecutions-D3S4aqIQ.js → useExecutions-DNGW4yxG.js} +1 -1
- package/dist/{useObservability-B9cOl5mw.js → useObservability-u-MZCWUV.js} +1 -1
- package/dist/{useRunHistory-uTJepemz.js → useRunHistory-x9bVUB9T.js} +1 -1
- package/dist/{useWorkflowCodeExport-L-5NIRiH.js → useWorkflowCodeExport-CV3SZeKm.js} +82 -3
- package/dist/{useWorkflows-D9BlhpWH.js → useWorkflows-CVb5zXKt.js} +1 -1
- package/dist/workflow-builder.d.ts +1 -1
- package/dist/workflow-builder.js +5 -5
- package/dist/workflow-pages.d.ts +2 -2
- package/dist/workflow-pages.js +13 -13
- package/dist/workflow-preview.js +2 -2
- package/dist/workflows-list.js +1 -1
- 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
|
|
4
|
-
import {
|
|
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-
|
|
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-
|
|
16
|
-
import { r as normalizeCodeLanguage, t as CodeEditorField } from "./code-editor-field-
|
|
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 {
|
|
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$
|
|
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$
|
|
2214
|
+
const source = isRecord$4(state) ? state : {};
|
|
2214
2215
|
let rules;
|
|
2215
2216
|
if (Array.isArray(source.rules)) {
|
|
2216
|
-
rules = source.rules.filter(isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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: [
|