@burdenoff/fluidgrids-fe-sdk 2026.1031.7 → 2026.1101.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 (51) hide show
  1. package/dist/{ExecutionDetailPage-4LPCNWM3.js → ExecutionDetailPage-Q77U6YJO.js} +11 -8
  2. package/dist/{ExecutionsListPage-HNPUKYD7.js → ExecutionsListPage-X4H4FVLN.js} +6 -5
  3. package/dist/{WorkflowBuilder-QFSCYCUC.js → WorkflowBuilder-RJPRUMQR.js} +11 -8
  4. package/dist/{WorkflowsList-RV4BRJO7.js → WorkflowsList-R2JJK7BL.js} +7 -6
  5. package/dist/agentTools-MISTH7JI.js +349 -0
  6. package/dist/{chunk-77HIU3CX.js → chunk-26HGYPBC.js} +1 -1
  7. package/dist/{chunk-76UUEMTN.js → chunk-37VUN74D.js} +4 -4
  8. package/dist/{chunk-QPZCJKII.js → chunk-3ERT6FQK.js} +3 -3
  9. package/dist/chunk-472HJ3S3.js +3929 -0
  10. package/dist/{chunk-WNZV4SLU.js → chunk-4QHCXTHW.js} +4 -4
  11. package/dist/chunk-AASHQI77.js +126 -0
  12. package/dist/chunk-CGXQVZGY.js +260 -0
  13. package/dist/chunk-CRDG2SQQ.js +185 -0
  14. package/dist/chunk-DQ2ZCXYX.js +21875 -0
  15. package/dist/chunk-F7DD7BOB.js +1164 -0
  16. package/dist/chunk-GTEBQQ7J.js +24 -0
  17. package/dist/{chunk-ALFMJSD6.js → chunk-H7XPXONT.js} +1 -1
  18. package/dist/{chunk-UO5DUKYN.js → chunk-HKKSXOFO.js} +8 -7
  19. package/dist/chunk-IA2ZCHNQ.js +1336 -0
  20. package/dist/{chunk-3M36XWAH.js → chunk-IGRIN7NL.js} +8 -7
  21. package/dist/chunk-JDLQ6QIB.js +283 -0
  22. package/dist/{chunk-AD7BICOT.js → chunk-JKBCLBHY.js} +1 -1
  23. package/dist/{chunk-6JX5UPDW.js → chunk-JNZCXFLD.js} +1 -1
  24. package/dist/{chunk-SEFOT2LD.js → chunk-LWLHPVIA.js} +1 -1
  25. package/dist/{chunk-JWROEOG5.js → chunk-LYTR6IQO.js} +1 -38
  26. package/dist/chunk-MR2B67NW.js +338 -0
  27. package/dist/{chunk-RIHYPPMQ.js → chunk-NLYQ3XDE.js} +1 -1
  28. package/dist/{chunk-6ILIBJFH.js → chunk-NZJWKVER.js} +7904 -7943
  29. package/dist/chunk-OCJTTHTS.js +46 -0
  30. package/dist/chunk-OSQ3OCR3.js +1193 -0
  31. package/dist/{chunk-VJL3DFJT.js → chunk-REO4S5MY.js} +2 -2
  32. package/dist/{chunk-XKQMCPSB.js → chunk-T2UUYYVJ.js} +4 -4
  33. package/dist/{chunk-BXR6BWXM.js → chunk-W7MTSATB.js} +5 -5
  34. package/dist/{chunk-A47PSD7A.js → chunk-WF6GMBR4.js} +7 -4
  35. package/dist/chunk-WYCB6OQL.js +19709 -0
  36. package/dist/{chunk-YOLPHVOE.js → chunk-YKFR7BZJ.js} +1 -1
  37. package/dist/{chunk-26NENGXA.js → chunk-YSXNJGCY.js} +4 -4
  38. package/dist/code-editor-field-JKCUR6BI.js +3 -0
  39. package/dist/executions.js +12 -9
  40. package/dist/history.js +4 -3
  41. package/dist/index.js +41 -32
  42. package/dist/node-detail-view-PCYFQX62.js +2034 -0
  43. package/dist/observability.js +5 -4
  44. package/dist/quick-config-panel-BRZZHOSI.js +402 -0
  45. package/dist/settings.js +6 -5
  46. package/dist/templates.js +10 -7
  47. package/dist/workflow-builder.js +12 -9
  48. package/dist/workflows-list.js +7 -6
  49. package/package.json +4 -1
  50. package/dist/chunk-NMEI7TQW.js +0 -2628
  51. package/dist/chunk-S7XV65DB.js +0 -50350
@@ -0,0 +1,3929 @@
1
+ import { CodeEditorField, normalizeCodeLanguage } from './chunk-AASHQI77.js';
2
+ import { asRecord, ConditionTreeBuilder, SwitchRulesEditor, casesFromRules, isStringOptionList, optionText, coerceNumber, paramsFromOperation, resolveLoadOptionsSpec, optionListValues, parseMappingValue, optionListText } from './chunk-IA2ZCHNQ.js';
3
+ import { Popover, PopoverTrigger, PopoverContent } from './chunk-GTEBQQ7J.js';
4
+ import { isAgentNodeType, AgentDefinitionSelectField } from './chunk-YKFR7BZJ.js';
5
+ import { readAssertRulesSetting, rowsFromRules, normaliseAssertRules, ruleFromRow, describeAssertRuleProblem, operatorSpec, ASSERT_OPERATORS, emptyAssertRuleRow } from './chunk-JDLQ6QIB.js';
6
+ import { useFlowStore, isParamVisible, ConnectionSelector, structuredParameterType, FluidGridsContext, ExpressionEditor, formatParameterValue, parseParameterValue, useFluidGridsAnalytics, throwIfResultError, openRunQuotaUpgradePrompt, describeOperationError } from './chunk-NZJWKVER.js';
7
+ import { useBuilderReadOnly } from './chunk-OCJTTHTS.js';
8
+ import { useRunFromNodeEligibilityQuery, PreviewScheduleDocument, WebhookTestListenerDocument, StopWebhookTestListenerDocument, ListenForWebhookTestEventDocument } from './chunk-IHEV5HTD.js';
9
+ import { useTr } from './chunk-UULVIT25.js';
10
+ import { getFluidGridsClientSafe, getFluidGridsClient } from './chunk-NTWG3CJX.js';
11
+ import { useId, useState, useMemo, useRef, useEffect, useContext, useCallback } from 'react';
12
+ import { Icon } from '@iconify/react';
13
+ import { cn } from '@burdenoff/fe-libs/utils';
14
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
15
+ import { gql } from '@apollo/client';
16
+ import cronstrue from 'cronstrue';
17
+ import { digest } from 'ohash';
18
+ import { X, Plus } from 'lucide-react';
19
+
20
+ // src/builder/components/ndv/layering.ts
21
+ var NDV_Z_INDEX = 1e3;
22
+ var NDV_OVERLAY_Z_INDEX = NDV_Z_INDEX + 1;
23
+
24
+ // src/builder/lib/loop-return-status.ts
25
+ function loopReturnStatus(nodeId, edges, loopHandle = "loop") {
26
+ const enabled = edges.filter(
27
+ (edge) => edge.disabled !== true && edge.data?.disabled !== true
28
+ );
29
+ const queue = enabled.filter(
30
+ (edge) => (edge.source ?? edge.from) === nodeId && (edge.sourceHandle ?? edge.fromHandle ?? "loop") === loopHandle
31
+ ).map((edge) => edge.target ?? edge.to).filter((target) => typeof target === "string");
32
+ const visited = /* @__PURE__ */ new Set();
33
+ let returns = false;
34
+ let exits = false;
35
+ for (let index = 0; index < queue.length; index++) {
36
+ const current = queue[index];
37
+ if (current === nodeId) {
38
+ returns = true;
39
+ continue;
40
+ }
41
+ if (!current || visited.has(current)) continue;
42
+ visited.add(current);
43
+ const outgoing = enabled.filter(
44
+ (edge) => (edge.source ?? edge.from) === current
45
+ );
46
+ if (outgoing.length === 0) exits = true;
47
+ for (const edge of outgoing) {
48
+ const target = edge.target ?? edge.to;
49
+ if (target) queue.push(target);
50
+ }
51
+ }
52
+ if (!returns) return "missing-return";
53
+ return exits ? "conditional-exit" : "connected";
54
+ }
55
+ var SECTION_LABEL = "text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground";
56
+ var RESUME_URL_EXPRESSION = "${RunContext.resumeWebhookUrl}";
57
+ function WaitResumeUrlField() {
58
+ const tr = useTr();
59
+ const [copied, setCopied] = useState(false);
60
+ const handleCopy = async () => {
61
+ try {
62
+ await navigator.clipboard.writeText(RESUME_URL_EXPRESSION);
63
+ setCopied(true);
64
+ setTimeout(() => setCopied(false), 1500);
65
+ } catch {
66
+ }
67
+ };
68
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "ndv-wait-resume-url", children: [
69
+ /* @__PURE__ */ jsx("div", { className: SECTION_LABEL, children: tr("fluidgrids.builder.waitResumeUrl.label", "Resume URL") }),
70
+ /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2", children: [
71
+ /* @__PURE__ */ jsx(
72
+ "input",
73
+ {
74
+ readOnly: true,
75
+ value: RESUME_URL_EXPRESSION,
76
+ onFocus: (e) => e.currentTarget.select(),
77
+ className: "min-w-0 flex-1 rounded-md border border-border bg-muted/40 px-3 py-2 font-mono text-xs text-foreground outline-none focus:border-primary"
78
+ }
79
+ ),
80
+ /* @__PURE__ */ jsxs(
81
+ "button",
82
+ {
83
+ type: "button",
84
+ onClick: handleCopy,
85
+ className: cn(
86
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-3 py-2 text-xs font-medium transition-colors",
87
+ copied ? "border-primary bg-primary/10 text-primary" : "bg-background text-foreground hover:bg-muted"
88
+ ),
89
+ "aria-label": tr(
90
+ "fluidgrids.builder.waitResumeUrl.copyAriaLabel",
91
+ "Copy resume URL expression"
92
+ ),
93
+ children: [
94
+ /* @__PURE__ */ jsx(
95
+ Icon,
96
+ {
97
+ icon: copied ? "mdi:check" : "mdi:content-copy",
98
+ className: "h-4 w-4"
99
+ }
100
+ ),
101
+ copied ? tr("fluidgrids.builder.waitResumeUrl.copied", "Copied") : tr("fluidgrids.builder.waitResumeUrl.copy", "Copy")
102
+ ]
103
+ }
104
+ )
105
+ ] }),
106
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tr(
107
+ "fluidgrids.builder.waitResumeUrl.help",
108
+ "Each run gets its own signed, single-use resume URL. Use this expression in an earlier node (an email or message) to send the link; the run continues when it is called. The URL for a finished or waiting run appears on this node's output as _resumeUrl."
109
+ ) })
110
+ ] });
111
+ }
112
+ var POLL_INTERVAL_MS = 2e3;
113
+ function useWebhookTestListener(target, onReceived) {
114
+ const [listener, setListener] = useState(
115
+ null
116
+ );
117
+ const [busy, setBusy] = useState(false);
118
+ const [error, setError] = useState(null);
119
+ const tr = useTr();
120
+ const { bus, workspaceId } = useFluidGridsAnalytics();
121
+ const pollTimer = useRef(null);
122
+ const openId = useRef(null);
123
+ const generation = useRef(0);
124
+ const onReceivedRef = useRef(onReceived);
125
+ onReceivedRef.current = onReceived;
126
+ const workflowKey = target?.workflowKey;
127
+ const version = target?.version;
128
+ const triggerNodeId = target?.triggerNodeId;
129
+ const clearPoll = useCallback(() => {
130
+ if (pollTimer.current) {
131
+ clearTimeout(pollTimer.current);
132
+ pollTimer.current = null;
133
+ }
134
+ }, []);
135
+ const poll = useCallback(
136
+ (id) => {
137
+ clearPoll();
138
+ pollTimer.current = setTimeout(async () => {
139
+ const client = getFluidGridsClientSafe();
140
+ if (!client || openId.current !== id) return;
141
+ try {
142
+ const result = await client.query({
143
+ query: WebhookTestListenerDocument,
144
+ variables: { id },
145
+ fetchPolicy: "network-only"
146
+ });
147
+ const latest = result.data?.webhookTestListener;
148
+ if (openId.current !== id) return;
149
+ if (!latest) {
150
+ openId.current = null;
151
+ setListener(null);
152
+ return;
153
+ }
154
+ const status = latest.status;
155
+ setListener(
156
+ (current) => current && current.id === id ? {
157
+ ...current,
158
+ status,
159
+ runId: latest.runId ?? null,
160
+ error: latest.error ?? null
161
+ } : current
162
+ );
163
+ if (status === "LISTENING") {
164
+ poll(id);
165
+ return;
166
+ }
167
+ openId.current = null;
168
+ if (status === "RECEIVED" && latest.runId) {
169
+ onReceivedRef.current(latest.runId);
170
+ }
171
+ } catch {
172
+ if (openId.current === id) poll(id);
173
+ }
174
+ }, POLL_INTERVAL_MS);
175
+ },
176
+ [clearPoll]
177
+ );
178
+ const stopAbandoned = useCallback((id) => {
179
+ void getFluidGridsClientSafe()?.mutate({ mutation: StopWebhookTestListenerDocument, variables: { id } }).catch(() => void 0);
180
+ }, []);
181
+ const stopOpenListener = useCallback(() => {
182
+ const id = openId.current;
183
+ openId.current = null;
184
+ clearPoll();
185
+ if (id) stopAbandoned(id);
186
+ }, [clearPoll, stopAbandoned]);
187
+ const listen = useCallback(async () => {
188
+ const client = getFluidGridsClientSafe();
189
+ if (!client || !workflowKey || !version || !triggerNodeId) return;
190
+ stopOpenListener();
191
+ const armedFor = generation.current;
192
+ setBusy(true);
193
+ setError(null);
194
+ try {
195
+ const result = throwIfResultError(
196
+ await client.mutate({
197
+ mutation: ListenForWebhookTestEventDocument,
198
+ variables: { input: { workflowKey, version, triggerNodeId } }
199
+ })
200
+ );
201
+ const armed = result.data?.listenForWebhookTestEvent;
202
+ if (!armed) throw new Error("The test listener could not be started");
203
+ if (generation.current !== armedFor) {
204
+ stopAbandoned(armed.id);
205
+ return;
206
+ }
207
+ openId.current = armed.id;
208
+ setListener({
209
+ id: armed.id,
210
+ status: armed.status,
211
+ testUrl: armed.testUrl ?? null,
212
+ expiresAt: armed.expiresAt,
213
+ runId: armed.runId ?? null,
214
+ error: armed.error ?? null
215
+ });
216
+ poll(armed.id);
217
+ } catch (err) {
218
+ if (generation.current === armedFor) {
219
+ openRunQuotaUpgradePrompt(err, tr, {
220
+ bus,
221
+ workspaceId,
222
+ surface: "webhook-test"
223
+ });
224
+ setError(describeOperationError(err));
225
+ }
226
+ } finally {
227
+ if (generation.current === armedFor) setBusy(false);
228
+ }
229
+ }, [
230
+ workflowKey,
231
+ version,
232
+ triggerNodeId,
233
+ poll,
234
+ stopOpenListener,
235
+ stopAbandoned,
236
+ tr,
237
+ bus,
238
+ workspaceId
239
+ ]);
240
+ const stop = useCallback(async () => {
241
+ const id = openId.current;
242
+ const client = getFluidGridsClientSafe();
243
+ if (!id || !client) return;
244
+ clearPoll();
245
+ const stoppedFor = generation.current;
246
+ setBusy(true);
247
+ try {
248
+ const result = await client.mutate({ mutation: StopWebhookTestListenerDocument, variables: { id } });
249
+ const stopped = result.data?.stopWebhookTestListener;
250
+ if (generation.current !== stoppedFor) return;
251
+ if (!stopped) throw new Error("The test listener could not be stopped");
252
+ const closedHere = openId.current === id;
253
+ if (closedHere) openId.current = null;
254
+ const status = stopped.status;
255
+ setListener(
256
+ (current) => current && current.id === id ? {
257
+ ...current,
258
+ status,
259
+ runId: stopped.runId ?? null,
260
+ error: stopped.error ?? null
261
+ } : current
262
+ );
263
+ if (closedHere && status === "RECEIVED" && stopped.runId) {
264
+ onReceivedRef.current(stopped.runId);
265
+ }
266
+ } catch (err) {
267
+ if (generation.current !== stoppedFor) return;
268
+ setError(describeOperationError(err));
269
+ if (openId.current === id) poll(id);
270
+ } finally {
271
+ if (generation.current === stoppedFor) setBusy(false);
272
+ }
273
+ }, [clearPoll, poll]);
274
+ useEffect(() => {
275
+ setListener(null);
276
+ setError(null);
277
+ setBusy(false);
278
+ return () => {
279
+ generation.current += 1;
280
+ stopOpenListener();
281
+ };
282
+ }, [workflowKey, version, triggerNodeId, stopOpenListener]);
283
+ return { listener, busy, error, listen, stop };
284
+ }
285
+ var SECTION_LABEL2 = "text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground";
286
+ function isSafeHttpUrl(value) {
287
+ try {
288
+ const parsed = new URL(value);
289
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+ function CopyableUrl({
295
+ url,
296
+ copyAriaLabel,
297
+ testId,
298
+ openLabel
299
+ }) {
300
+ const tr = useTr();
301
+ const [copied, setCopied] = useState(false);
302
+ const handleCopy = async () => {
303
+ try {
304
+ await navigator.clipboard.writeText(url);
305
+ setCopied(true);
306
+ setTimeout(() => setCopied(false), 1500);
307
+ } catch {
308
+ }
309
+ };
310
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2", children: [
311
+ /* @__PURE__ */ jsx(
312
+ "input",
313
+ {
314
+ readOnly: true,
315
+ value: url,
316
+ "data-testid": testId,
317
+ onFocus: (e) => e.currentTarget.select(),
318
+ className: "min-w-0 flex-1 rounded-md border border-border bg-muted/40 px-3 py-2 font-mono text-xs text-foreground outline-none focus:border-primary"
319
+ }
320
+ ),
321
+ /* @__PURE__ */ jsxs(
322
+ "button",
323
+ {
324
+ type: "button",
325
+ onClick: handleCopy,
326
+ className: cn(
327
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border px-3 py-2 text-xs font-medium transition-colors",
328
+ copied ? "border-primary bg-primary/10 text-primary" : "bg-background text-foreground hover:bg-muted"
329
+ ),
330
+ "aria-label": copyAriaLabel,
331
+ children: [
332
+ /* @__PURE__ */ jsx(
333
+ Icon,
334
+ {
335
+ icon: copied ? "mdi:check" : "mdi:content-copy",
336
+ className: "h-4 w-4"
337
+ }
338
+ ),
339
+ copied ? tr("fluidgrids.builder.webhookUrlField.copied", "Copied") : tr("fluidgrids.builder.webhookUrlField.copy", "Copy")
340
+ ]
341
+ }
342
+ ),
343
+ openLabel && /* @__PURE__ */ jsxs(
344
+ "a",
345
+ {
346
+ href: url,
347
+ target: "_blank",
348
+ rel: "noopener noreferrer",
349
+ "data-testid": `${testId}-open`,
350
+ className: "inline-flex shrink-0 items-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted",
351
+ children: [
352
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:open-in-new", className: "h-4 w-4" }),
353
+ openLabel
354
+ ]
355
+ }
356
+ )
357
+ ] });
358
+ }
359
+ function WebhookUrlField({ webhookUrl, loading }) {
360
+ const tr = useTr();
361
+ const isActiveVersion = useFlowStore((s) => s.workflow.status === "ACTIVE");
362
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "ndv-webhook-url", children: [
363
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
364
+ /* @__PURE__ */ jsx("div", { className: SECTION_LABEL2, children: tr(
365
+ "fluidgrids.builder.webhookUrlField.productionLabel",
366
+ "Production URL"
367
+ ) }),
368
+ webhookUrl && isActiveVersion && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary", children: tr("fluidgrids.builder.webhookUrlField.live", "Live") })
369
+ ] }),
370
+ webhookUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
371
+ /* @__PURE__ */ jsx(
372
+ CopyableUrl,
373
+ {
374
+ url: webhookUrl,
375
+ testId: "ndv-webhook-production-url",
376
+ copyAriaLabel: tr(
377
+ "fluidgrids.builder.webhookUrlField.copyAriaLabel",
378
+ "Copy webhook URL"
379
+ )
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: isActiveVersion ? tr(
383
+ "fluidgrids.builder.webhookUrlField.activeHint",
384
+ "Requests to this URL run this version of the workflow."
385
+ ) : tr(
386
+ "fluidgrids.builder.webhookUrlField.inactiveHint",
387
+ "Requests to this URL run the workflow's active version only. Activate this version to make it live, or use a test URL to try it."
388
+ ) })
389
+ ] }) : /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: loading ? tr(
390
+ "fluidgrids.builder.webhookUrlField.loading",
391
+ "Loading webhook URL\u2026"
392
+ ) : tr(
393
+ "fluidgrids.builder.webhookUrlField.unsaved",
394
+ "Save the workflow to generate its webhook URL."
395
+ ) })
396
+ ] });
397
+ }
398
+ function FormUrlField({ formUrl, loading, error }) {
399
+ const tr = useTr();
400
+ const safeUrl = formUrl && isSafeHttpUrl(formUrl) ? formUrl : null;
401
+ let message = null;
402
+ if (!safeUrl || error) {
403
+ if (error || formUrl) {
404
+ message = tr(
405
+ "fluidgrids.builder.formUrlField.error",
406
+ "Unable to load a safe trigger URL."
407
+ );
408
+ } else if (loading) {
409
+ message = tr(
410
+ "fluidgrids.builder.formUrlField.loading",
411
+ "Loading form URL\u2026"
412
+ );
413
+ } else {
414
+ message = tr(
415
+ "fluidgrids.builder.formUrlField.unsaved",
416
+ "Save the workflow to generate its form URL."
417
+ );
418
+ }
419
+ }
420
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "ndv-form-url", children: [
421
+ /* @__PURE__ */ jsx("div", { className: SECTION_LABEL2, children: tr("fluidgrids.builder.formUrlField.label", "Form URL") }),
422
+ safeUrl && !error ? /* @__PURE__ */ jsxs(Fragment, { children: [
423
+ /* @__PURE__ */ jsx(
424
+ CopyableUrl,
425
+ {
426
+ url: safeUrl,
427
+ testId: "ndv-form-url-value",
428
+ copyAriaLabel: tr(
429
+ "fluidgrids.builder.formUrlField.copyAriaLabel",
430
+ "Copy form URL"
431
+ ),
432
+ openLabel: tr("fluidgrids.builder.formUrlField.open", "Open form")
433
+ }
434
+ ),
435
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: tr(
436
+ "fluidgrids.builder.formUrlField.hint",
437
+ "Share this link to collect submissions; each submission starts the workflow's active version."
438
+ ) })
439
+ ] }) : /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: message })
440
+ ] });
441
+ }
442
+ function useSecondsLeft(expiresAt) {
443
+ const [now, setNow] = useState(() => Date.now());
444
+ useEffect(() => {
445
+ if (!expiresAt) return;
446
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
447
+ return () => clearInterval(timer);
448
+ }, [expiresAt]);
449
+ if (!expiresAt) return 0;
450
+ return Math.max(0, Math.ceil((Date.parse(expiresAt) - now) / 1e3));
451
+ }
452
+ function WebhookTestListenerField({
453
+ nodeId
454
+ }) {
455
+ const readOnly = useBuilderReadOnly();
456
+ return readOnly ? null : /* @__PURE__ */ jsx(WebhookTestListenerControls, { nodeId });
457
+ }
458
+ function WebhookTestListenerControls({
459
+ nodeId
460
+ }) {
461
+ const tr = useTr();
462
+ const workflowKey = useFlowStore((s) => s.workflow.workflowKey);
463
+ const version = useFlowStore((s) => s.workflow.version);
464
+ const requestFollowRun = useFlowStore(
465
+ (s) => s.actions.execution.requestFollowRun
466
+ );
467
+ const saved = !!workflowKey && !!version && !!nodeId;
468
+ const target = saved ? { workflowKey, version, triggerNodeId: nodeId } : null;
469
+ const { listener, busy, error, listen, stop } = useWebhookTestListener(
470
+ target,
471
+ (runId) => {
472
+ if (nodeId) requestFollowRun(runId, nodeId);
473
+ }
474
+ );
475
+ const listening = listener?.status === "LISTENING";
476
+ const secondsLeft = useSecondsLeft(listening ? listener.expiresAt : null);
477
+ const outcome = (() => {
478
+ switch (listener?.status) {
479
+ case "RECEIVED":
480
+ return tr(
481
+ "fluidgrids.builder.webhookTestListener.received",
482
+ "Test event received \u2014 the run is shown on the canvas."
483
+ );
484
+ case "EXPIRED":
485
+ return tr(
486
+ "fluidgrids.builder.webhookTestListener.expired",
487
+ "Stopped listening: no test event arrived in time."
488
+ );
489
+ case "STOPPED":
490
+ return tr(
491
+ "fluidgrids.builder.webhookTestListener.stopped",
492
+ "Stopped listening."
493
+ );
494
+ case "FAILED":
495
+ return listener.error ?? tr(
496
+ "fluidgrids.builder.webhookTestListener.failed",
497
+ "The test event arrived but the run could not be started."
498
+ );
499
+ default:
500
+ return null;
501
+ }
502
+ })();
503
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": "ndv-webhook-test", children: [
504
+ /* @__PURE__ */ jsx("div", { className: SECTION_LABEL2, children: tr("fluidgrids.builder.webhookTestListener.label", "Test URL") }),
505
+ !saved ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: tr(
506
+ "fluidgrids.builder.webhookTestListener.unsaved",
507
+ "Save the workflow to test this webhook."
508
+ ) }) : listening && listener.testUrl ? /* @__PURE__ */ jsxs(Fragment, { children: [
509
+ /* @__PURE__ */ jsx(
510
+ CopyableUrl,
511
+ {
512
+ url: listener.testUrl,
513
+ testId: "ndv-webhook-test-url",
514
+ copyAriaLabel: tr(
515
+ "fluidgrids.builder.webhookTestListener.copyAriaLabel",
516
+ "Copy test URL"
517
+ )
518
+ }
519
+ ),
520
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
521
+ /* @__PURE__ */ jsxs(
522
+ "p",
523
+ {
524
+ className: "flex items-center gap-1.5 text-[11px] text-muted-foreground",
525
+ "data-testid": "ndv-webhook-test-status",
526
+ children: [
527
+ /* @__PURE__ */ jsx(
528
+ Icon,
529
+ {
530
+ icon: "mdi:access-point",
531
+ className: "h-3.5 w-3.5 text-primary"
532
+ }
533
+ ),
534
+ tr(
535
+ "fluidgrids.builder.webhookTestListener.listening",
536
+ "Listening for one test event to version {{version}} \u2014 {{seconds}}s left",
537
+ { version, seconds: secondsLeft }
538
+ )
539
+ ]
540
+ }
541
+ ),
542
+ /* @__PURE__ */ jsx(
543
+ "button",
544
+ {
545
+ type: "button",
546
+ onClick: () => void stop(),
547
+ disabled: busy,
548
+ "data-testid": "ndv-webhook-test-stop",
549
+ className: "shrink-0 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground hover:bg-muted disabled:opacity-50",
550
+ children: tr("fluidgrids.builder.webhookTestListener.stop", "Stop")
551
+ }
552
+ )
553
+ ] })
554
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
555
+ /* @__PURE__ */ jsxs(
556
+ "button",
557
+ {
558
+ type: "button",
559
+ onClick: () => void listen(),
560
+ disabled: busy,
561
+ "data-testid": "ndv-webhook-test-listen",
562
+ className: "inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-2 text-xs font-medium text-foreground hover:bg-muted disabled:opacity-50",
563
+ children: [
564
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:access-point", className: "h-4 w-4" }),
565
+ tr(
566
+ "fluidgrids.builder.webhookTestListener.listen",
567
+ "Listen for test event"
568
+ )
569
+ ]
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsx(
573
+ "p",
574
+ {
575
+ className: "text-[11px] text-muted-foreground",
576
+ "data-testid": "ndv-webhook-test-status",
577
+ children: outcome ?? tr(
578
+ "fluidgrids.builder.webhookTestListener.hint",
579
+ "Creates a one-time URL for the saved version {{version}}. Unsaved changes are not included.",
580
+ { version }
581
+ )
582
+ }
583
+ )
584
+ ] }),
585
+ error && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive", role: "alert", children: error })
586
+ ] });
587
+ }
588
+ var LIST_WORKFLOWS_QUERY = gql`
589
+ query ListWorkflowDefinitionsForPicker($limit: Int, $offset: Int) {
590
+ workflowDefinitions(input: { limit: $limit, offset: $offset }) {
591
+ items {
592
+ id
593
+ workflowKey
594
+ name
595
+ version
596
+ status
597
+ }
598
+ }
599
+ }
600
+ `;
601
+ function formatVersion(version) {
602
+ return /^v/i.test(version) ? version : `v${version}`;
603
+ }
604
+ function WorkflowPickerField({
605
+ value,
606
+ onChange,
607
+ testId
608
+ }) {
609
+ const tr = useTr();
610
+ const [workflows, setWorkflows] = useState([]);
611
+ const [loading, setLoading] = useState(false);
612
+ const [failed, setFailed] = useState(false);
613
+ const [search, setSearch] = useState("");
614
+ const [open, setOpen] = useState(false);
615
+ const load = useCallback(async () => {
616
+ const client = getFluidGridsClient();
617
+ if (!client) {
618
+ setFailed(true);
619
+ return;
620
+ }
621
+ setLoading(true);
622
+ try {
623
+ const result = await client.query({
624
+ query: LIST_WORKFLOWS_QUERY,
625
+ variables: { limit: 200, offset: 0 },
626
+ fetchPolicy: "cache-first"
627
+ });
628
+ const data = result.data;
629
+ setWorkflows(
630
+ (data?.workflowDefinitions?.items ?? []).map((w) => ({
631
+ workflowKey: w.workflowKey,
632
+ name: w.name || w.workflowKey,
633
+ version: w.version,
634
+ status: w.status
635
+ }))
636
+ );
637
+ setFailed(false);
638
+ } catch {
639
+ setFailed(true);
640
+ } finally {
641
+ setLoading(false);
642
+ }
643
+ }, []);
644
+ useEffect(() => {
645
+ void load();
646
+ }, [load]);
647
+ const byKey = /* @__PURE__ */ new Map();
648
+ for (const workflow of workflows) {
649
+ const existing = byKey.get(workflow.workflowKey);
650
+ if (!existing || workflow.status === "ACTIVE" && existing.status !== "ACTIVE") {
651
+ byKey.set(workflow.workflowKey, workflow);
652
+ }
653
+ }
654
+ const options = [...byKey.values()].filter((w) => {
655
+ const needle = search.trim().toLowerCase();
656
+ if (!needle) return true;
657
+ return w.name.toLowerCase().includes(needle) || w.workflowKey.toLowerCase().includes(needle);
658
+ });
659
+ const selected = byKey.get(value);
660
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", "data-testid": testId, children: [
661
+ /* @__PURE__ */ jsxs(
662
+ "button",
663
+ {
664
+ type: "button",
665
+ onClick: () => setOpen((o) => !o),
666
+ className: "flex items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 py-1.5 text-left text-sm outline-none transition-colors hover:border-primary",
667
+ children: [
668
+ /* @__PURE__ */ jsx("span", { className: cn("truncate", !value && "text-muted-foreground"), children: selected ? `${selected.name} (${selected.workflowKey.slice(0, 8)}\u2026)` : value || tr(
669
+ "fluidgrids.builder.workflowPicker.placeholder",
670
+ "Select a workflow to call"
671
+ ) }),
672
+ /* @__PURE__ */ jsx(
673
+ Icon,
674
+ {
675
+ icon: open ? "mdi:chevron-up" : "mdi:chevron-down",
676
+ className: "h-4 w-4 shrink-0 text-muted-foreground"
677
+ }
678
+ )
679
+ ]
680
+ }
681
+ ),
682
+ open && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-border bg-background p-2", children: [
683
+ /* @__PURE__ */ jsx(
684
+ "input",
685
+ {
686
+ autoFocus: true,
687
+ value: search,
688
+ onChange: (e) => setSearch(e.target.value),
689
+ placeholder: tr(
690
+ "fluidgrids.builder.workflowPicker.search",
691
+ "Search workflows"
692
+ ),
693
+ className: "mb-2 w-full rounded-md border border-input bg-background px-2 py-1 text-sm outline-none focus:border-primary"
694
+ }
695
+ ),
696
+ loading && /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: tr("fluidgrids.builder.workflowPicker.loading", "Loading\u2026") }),
697
+ !loading && options.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: failed ? tr(
698
+ "fluidgrids.builder.workflowPicker.failed",
699
+ "Could not load workflows \u2014 enter a workflow key below."
700
+ ) : tr(
701
+ "fluidgrids.builder.workflowPicker.empty",
702
+ "No workflows found"
703
+ ) }),
704
+ /* @__PURE__ */ jsx("div", { className: "max-h-48 overflow-y-auto", children: options.map((workflow) => /* @__PURE__ */ jsxs(
705
+ "button",
706
+ {
707
+ type: "button",
708
+ onClick: () => {
709
+ onChange(workflow.workflowKey);
710
+ setOpen(false);
711
+ setSearch("");
712
+ },
713
+ className: cn(
714
+ "flex w-full flex-col items-start gap-0.5 rounded px-2 py-1.5 text-left hover:bg-muted",
715
+ workflow.workflowKey === value && "bg-muted"
716
+ ),
717
+ children: [
718
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-foreground", children: workflow.name }),
719
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] text-muted-foreground", children: [
720
+ workflow.workflowKey,
721
+ " \xB7 ",
722
+ formatVersion(workflow.version),
723
+ " \xB7",
724
+ " ",
725
+ workflow.status
726
+ ] })
727
+ ]
728
+ },
729
+ workflow.workflowKey
730
+ )) })
731
+ ] }),
732
+ /* @__PURE__ */ jsx(
733
+ "input",
734
+ {
735
+ value,
736
+ onChange: (e) => onChange(e.target.value),
737
+ placeholder: tr(
738
+ "fluidgrids.builder.workflowPicker.manual",
739
+ "\u2026or paste a workflow key"
740
+ ),
741
+ className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-xs outline-none focus:border-primary"
742
+ }
743
+ )
744
+ ] });
745
+ }
746
+ var LOAD_NODE_OPTIONS = gql`
747
+ query LoadNodeOptions(
748
+ $nodeType: String!
749
+ $nodeVersion: String!
750
+ $operationName: String!
751
+ $resultPath: String
752
+ $labelField: String!
753
+ $valueField: String!
754
+ $connectionId: ID
755
+ $args: JSON
756
+ ) {
757
+ loadNodeOptions(
758
+ nodeType: $nodeType
759
+ nodeVersion: $nodeVersion
760
+ operationName: $operationName
761
+ resultPath: $resultPath
762
+ labelField: $labelField
763
+ valueField: $valueField
764
+ connectionId: $connectionId
765
+ args: $args
766
+ ) {
767
+ label
768
+ value
769
+ }
770
+ }
771
+ `;
772
+ var CACHE_TTL_MS = 3 * 60 * 1e3;
773
+ var clientCaches = /* @__PURE__ */ new WeakMap();
774
+ function requestLoadNodeOptions(variables, publish, refresh = false, getClient = getFluidGridsClient) {
775
+ let cancelled = false;
776
+ const cancel = () => {
777
+ cancelled = true;
778
+ };
779
+ if (!variables) {
780
+ publish({ options: [], loading: false, error: null });
781
+ return cancel;
782
+ }
783
+ publish({ options: [], loading: true, error: null });
784
+ void (async () => {
785
+ try {
786
+ const client = getClient();
787
+ let cache = clientCaches.get(client);
788
+ if (!cache) {
789
+ cache = /* @__PURE__ */ new Map();
790
+ clientCaches.set(client, cache);
791
+ }
792
+ const key = JSON.stringify(variables);
793
+ const cached = cache.get(key);
794
+ if (!refresh && cached?.options && cached.expiresAt > Date.now()) {
795
+ publish({ options: cached.options, loading: false, error: null });
796
+ return;
797
+ }
798
+ const entry = { expiresAt: 0 };
799
+ cache.set(key, entry);
800
+ const result = await client.query({
801
+ query: LOAD_NODE_OPTIONS,
802
+ variables,
803
+ fetchPolicy: "network-only"
804
+ });
805
+ if (cancelled) return;
806
+ const options = result.data?.loadNodeOptions ?? [];
807
+ if (cache.get(key) === entry) {
808
+ entry.options = options;
809
+ entry.expiresAt = Date.now() + CACHE_TTL_MS;
810
+ }
811
+ publish({ options, loading: false, error: null });
812
+ } catch (error) {
813
+ if (cancelled) return;
814
+ publish({
815
+ options: [],
816
+ loading: false,
817
+ error: error instanceof Error ? error : new Error(String(error))
818
+ });
819
+ }
820
+ })();
821
+ return cancel;
822
+ }
823
+
824
+ // src/hooks/useLoadNodeOptions.ts
825
+ function useLoadNodeOptions(nodeType, nodeVersion, spec, connectionId) {
826
+ const [published, setPublished] = useState();
827
+ const [nonce, setNonce] = useState(0);
828
+ const refreshRequested = useRef(false);
829
+ const specRef = useRef(spec);
830
+ const specKey = JSON.stringify(spec ?? null);
831
+ const context = useContext(FluidGridsContext);
832
+ const client = context?.workspaceClient ?? getFluidGridsClientSafe();
833
+ const requestKey = JSON.stringify([
834
+ nodeType,
835
+ nodeVersion,
836
+ specKey,
837
+ connectionId,
838
+ nonce
839
+ ]);
840
+ useEffect(() => {
841
+ specRef.current = spec;
842
+ }, [spec]);
843
+ useEffect(() => {
844
+ const currentSpec = specRef.current;
845
+ const refresh = refreshRequested.current;
846
+ refreshRequested.current = false;
847
+ const variables = nodeType && nodeVersion && currentSpec && connectionId ? {
848
+ nodeType,
849
+ nodeVersion,
850
+ operationName: currentSpec.operation,
851
+ resultPath: currentSpec.resultPath ?? null,
852
+ labelField: currentSpec.labelField,
853
+ valueField: currentSpec.valueField,
854
+ connectionId,
855
+ args: currentSpec.staticArgs ?? {}
856
+ } : void 0;
857
+ return requestLoadNodeOptions(
858
+ variables,
859
+ (result2) => setPublished({ requestKey, client, result: result2 }),
860
+ refresh,
861
+ client ? () => client : void 0
862
+ );
863
+ }, [nodeType, nodeVersion, specKey, connectionId, nonce, client, requestKey]);
864
+ const result = published?.requestKey === requestKey && published.client === client ? published.result : {
865
+ options: [],
866
+ loading: Boolean(nodeType && nodeVersion && spec && connectionId),
867
+ error: null
868
+ };
869
+ return {
870
+ ...result,
871
+ refetch: () => {
872
+ refreshRequested.current = true;
873
+ setNonce((value) => value + 1);
874
+ }
875
+ };
876
+ }
877
+ var EMPTY_VALUES = {};
878
+ var EMPTY_PARAMETERS = [];
879
+ function OptionListInput({
880
+ value,
881
+ onChange,
882
+ className,
883
+ testId,
884
+ fieldName,
885
+ label
886
+ }) {
887
+ const tr = useTr();
888
+ const [draft, setDraft] = useState(() => optionListText(value));
889
+ const [invalid, setInvalid] = useState(
890
+ () => optionListValues(value) === void 0
891
+ );
892
+ const [edited, setEdited] = useState(false);
893
+ useEffect(() => {
894
+ setDraft(optionListText(value));
895
+ setInvalid(optionListValues(value) === void 0);
896
+ setEdited(false);
897
+ }, [value]);
898
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
899
+ /* @__PURE__ */ jsx(
900
+ "textarea",
901
+ {
902
+ value: draft,
903
+ onChange: (event) => {
904
+ setDraft(event.target.value);
905
+ setEdited(true);
906
+ setInvalid(optionListValues(event.target.value) === void 0);
907
+ },
908
+ onBlur: () => {
909
+ const values = optionListValues(draft);
910
+ if (edited && values !== void 0) onChange(values);
911
+ },
912
+ placeholder: tr(
913
+ "fluidgrids.builder.nodeParamsForm.loadOptionsListPlaceholder",
914
+ "Comma-separated IDs or a JSON array of IDs"
915
+ ),
916
+ "aria-label": label ?? fieldName,
917
+ "aria-invalid": invalid,
918
+ "data-testid": testId,
919
+ className,
920
+ rows: 2
921
+ }
922
+ ),
923
+ invalid && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-[11px] text-destructive", children: tr(
924
+ "fluidgrids.builder.nodeParamsForm.loadOptionsInvalidList",
925
+ "Enter comma-separated IDs or a JSON array of strings. The saved value has not changed."
926
+ ) })
927
+ ] });
928
+ }
929
+ function NodeLoadOptionsSelect({
930
+ nodeType,
931
+ nodeVersion,
932
+ spec,
933
+ connectionId,
934
+ value,
935
+ onChange,
936
+ className,
937
+ testId,
938
+ parameterValues = EMPTY_VALUES,
939
+ parameterDefaults = EMPTY_VALUES,
940
+ unavailableParameters = EMPTY_PARAMETERS,
941
+ multiple = false,
942
+ fieldName = "Options",
943
+ label
944
+ }) {
945
+ const tr = useTr();
946
+ const [manual, setManual] = useState(false);
947
+ const resolved = useMemo(
948
+ () => resolveLoadOptionsSpec(
949
+ spec,
950
+ parameterValues,
951
+ parameterDefaults,
952
+ unavailableParameters
953
+ ),
954
+ [spec, parameterValues, parameterDefaults, unavailableParameters]
955
+ );
956
+ const { options, loading, error } = useLoadNodeOptions(
957
+ nodeType,
958
+ nodeVersion,
959
+ resolved.spec,
960
+ connectionId
961
+ );
962
+ const listValues = multiple ? optionListValues(value) : [];
963
+ const selectedValues = multiple ? listValues ?? [] : typeof value === "string" && value ? [value] : [];
964
+ const waiting = resolved.missingParameters.length > 0;
965
+ const useManual = manual || !connectionId || waiting || !!error || multiple && listValues === void 0;
966
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
967
+ useManual ? multiple ? /* @__PURE__ */ jsx(
968
+ OptionListInput,
969
+ {
970
+ value,
971
+ onChange,
972
+ className,
973
+ testId,
974
+ fieldName,
975
+ label
976
+ }
977
+ ) : /* @__PURE__ */ jsx(
978
+ "input",
979
+ {
980
+ type: "text",
981
+ value: typeof value === "string" ? value : "",
982
+ onChange: (event) => onChange(event.target.value),
983
+ placeholder: tr(
984
+ "fluidgrids.builder.nodeParamsForm.loadOptionsErrorPlaceholder",
985
+ "Enter a value"
986
+ ),
987
+ "aria-label": label ?? fieldName,
988
+ "data-testid": testId,
989
+ className
990
+ }
991
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
992
+ /* @__PURE__ */ jsxs(
993
+ "select",
994
+ {
995
+ multiple,
996
+ value: multiple ? selectedValues : selectedValues[0] ?? "",
997
+ onChange: (event) => onChange(
998
+ multiple ? Array.from(
999
+ event.target.selectedOptions,
1000
+ (option) => option.value
1001
+ ) : event.target.value
1002
+ ),
1003
+ disabled: loading,
1004
+ "aria-label": label ?? fieldName,
1005
+ "data-testid": testId,
1006
+ className: cn(className, loading && "opacity-60"),
1007
+ children: [
1008
+ !multiple && /* @__PURE__ */ jsx("option", { value: "", children: loading ? tr(
1009
+ "fluidgrids.builder.nodeParamsForm.loadOptionsLoading",
1010
+ "Loading\u2026"
1011
+ ) : tr(
1012
+ "fluidgrids.builder.nodeParamsForm.selectPlaceholder",
1013
+ "Select\u2026"
1014
+ ) }),
1015
+ selectedValues.filter(
1016
+ (selected) => !options.some((option) => option.value === selected)
1017
+ ).map((selected) => /* @__PURE__ */ jsx("option", { value: selected, children: selected }, selected)),
1018
+ options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
1019
+ ]
1020
+ }
1021
+ ),
1022
+ multiple && /* @__PURE__ */ jsxs(Fragment, { children: [
1023
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: loading ? tr(
1024
+ "fluidgrids.builder.nodeParamsForm.loadOptionsLoading",
1025
+ "Loading\u2026"
1026
+ ) : tr(
1027
+ "fluidgrids.builder.nodeParamsForm.loadOptionsMultipleHelp",
1028
+ "Choose multiple values with Ctrl or Command. Saved IDs remain available."
1029
+ ) }),
1030
+ /* @__PURE__ */ jsx(
1031
+ "button",
1032
+ {
1033
+ type: "button",
1034
+ disabled: loading,
1035
+ onClick: () => onChange([]),
1036
+ className: "w-fit text-xs underline",
1037
+ children: tr(
1038
+ "fluidgrids.builder.nodeParamsForm.loadOptionsClear",
1039
+ "Clear selection"
1040
+ )
1041
+ }
1042
+ )
1043
+ ] })
1044
+ ] }),
1045
+ !connectionId ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: tr(
1046
+ "fluidgrids.builder.nodeParamsForm.loadOptionsNoConnection",
1047
+ "Connect an account above to load options, or type a value"
1048
+ ) }) : waiting ? /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-muted-foreground", children: [
1049
+ tr(
1050
+ "fluidgrids.builder.nodeParamsForm.loadOptionsMissingParameters",
1051
+ "Set fixed values for these parameters to load options:"
1052
+ ),
1053
+ " ",
1054
+ resolved.missingParameters.join(", ")
1055
+ ] }) : error ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-destructive", children: tr(
1056
+ "fluidgrids.builder.nodeParamsForm.loadOptionsError",
1057
+ "Couldn't load options \u2014 enter a value manually."
1058
+ ) }) : /* @__PURE__ */ jsx(
1059
+ "button",
1060
+ {
1061
+ type: "button",
1062
+ onClick: () => setManual((current) => !current),
1063
+ className: "w-fit text-xs underline",
1064
+ children: manual ? tr(
1065
+ "fluidgrids.builder.nodeParamsForm.loadOptionsChoose",
1066
+ "Choose from list"
1067
+ ) : tr(
1068
+ "fluidgrids.builder.nodeParamsForm.loadOptionsManual",
1069
+ "Enter IDs manually"
1070
+ )
1071
+ }
1072
+ )
1073
+ ] });
1074
+ }
1075
+
1076
+ // src/builder/lib/boolean-parameter.ts
1077
+ function booleanParameterValue(value) {
1078
+ if (typeof value !== "string") return value;
1079
+ const normalized = value.trim().toLowerCase();
1080
+ if (normalized === "true") return true;
1081
+ if (normalized === "false") return false;
1082
+ return value;
1083
+ }
1084
+ function StructuredParameterInput({
1085
+ name,
1086
+ label,
1087
+ schema,
1088
+ value,
1089
+ onChange,
1090
+ className,
1091
+ testId
1092
+ }) {
1093
+ const tr = useTr();
1094
+ const descriptionId = useId();
1095
+ const [draft, setDraft] = useState(() => ({
1096
+ value,
1097
+ text: formatParameterValue(value)
1098
+ }));
1099
+ const text = Object.is(draft.value, value) ? draft.text : formatParameterValue(value);
1100
+ const result = parseParameterValue(schema, text);
1101
+ const type = structuredParameterType(schema);
1102
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1103
+ /* @__PURE__ */ jsx(
1104
+ "textarea",
1105
+ {
1106
+ "aria-label": label ?? name,
1107
+ "aria-describedby": descriptionId,
1108
+ "aria-invalid": !result.valid,
1109
+ value: text,
1110
+ rows: 4,
1111
+ spellCheck: false,
1112
+ className,
1113
+ "data-testid": testId,
1114
+ placeholder: type === "array" ? "[]" : "{}",
1115
+ onChange: (event) => {
1116
+ const nextText = event.target.value;
1117
+ const nextResult = parseParameterValue(schema, nextText);
1118
+ const nextValue = nextResult.valid ? nextResult.value : nextText;
1119
+ setDraft({ value: nextValue, text: nextText });
1120
+ onChange(nextValue);
1121
+ }
1122
+ }
1123
+ ),
1124
+ /* @__PURE__ */ jsx(
1125
+ "span",
1126
+ {
1127
+ id: descriptionId,
1128
+ role: result.valid ? void 0 : "alert",
1129
+ className: result.valid ? "text-[11px] text-muted-foreground" : "text-[11px] text-destructive",
1130
+ children: result.valid ? type === "array" ? tr(
1131
+ "fluidgrids.builder.nodeParamsForm.arrayHint",
1132
+ "Enter a JSON array. String lists also accept comma-separated values."
1133
+ ) : tr(
1134
+ "fluidgrids.builder.nodeParamsForm.objectHint",
1135
+ "Enter a JSON object."
1136
+ ) : tr(
1137
+ "fluidgrids.builder.nodeParamsForm.structuredInputInvalid",
1138
+ result.error
1139
+ )
1140
+ }
1141
+ )
1142
+ ] });
1143
+ }
1144
+ function JsonMappingInput({
1145
+ name,
1146
+ label,
1147
+ value,
1148
+ onChange
1149
+ }) {
1150
+ const tr = useTr();
1151
+ const descriptionId = useId();
1152
+ const serialized = JSON.stringify(value, null, 2);
1153
+ const [text, setText] = useState(serialized);
1154
+ const result = parseMappingValue(text);
1155
+ const changed = text !== serialized;
1156
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
1157
+ /* @__PURE__ */ jsx(
1158
+ "textarea",
1159
+ {
1160
+ "aria-label": label ?? name,
1161
+ "aria-describedby": descriptionId,
1162
+ "aria-invalid": !result.valid,
1163
+ className: "w-full rounded-md border border-input bg-background px-2.5 py-1.5 font-mono text-sm outline-none focus:border-primary",
1164
+ rows: 4,
1165
+ spellCheck: false,
1166
+ value: text,
1167
+ onChange: (event) => setText(event.target.value)
1168
+ }
1169
+ ),
1170
+ /* @__PURE__ */ jsx(
1171
+ "span",
1172
+ {
1173
+ id: descriptionId,
1174
+ role: result.valid ? void 0 : "alert",
1175
+ className: result.valid ? "text-[11px] text-muted-foreground" : "text-[11px] text-destructive",
1176
+ children: result.valid ? tr(
1177
+ "fluidgrids.builder.nodeParamsForm.jsonMappingHint",
1178
+ "JSON mapping. Apply changes before saving the workflow. Nested expressions are preserved."
1179
+ ) : tr(
1180
+ "fluidgrids.builder.nodeParamsForm.jsonMappingInvalid",
1181
+ result.error
1182
+ )
1183
+ }
1184
+ ),
1185
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1186
+ /* @__PURE__ */ jsx(
1187
+ "button",
1188
+ {
1189
+ type: "button",
1190
+ className: "rounded-md border border-input px-2 py-1 text-xs disabled:opacity-50",
1191
+ disabled: !changed || !result.valid,
1192
+ onClick: () => {
1193
+ if (!result.valid) return;
1194
+ onChange(result.value);
1195
+ setText(JSON.stringify(result.value, null, 2));
1196
+ },
1197
+ children: tr(
1198
+ "fluidgrids.builder.nodeParamsForm.applyJsonMapping",
1199
+ "Apply mapping"
1200
+ )
1201
+ }
1202
+ ),
1203
+ /* @__PURE__ */ jsx(
1204
+ "button",
1205
+ {
1206
+ type: "button",
1207
+ className: "rounded-md border border-input px-2 py-1 text-xs disabled:opacity-50",
1208
+ disabled: !changed,
1209
+ onClick: () => setText(serialized),
1210
+ children: tr("fluidgrids.builder.nodeParamsForm.resetJsonMapping", "Reset")
1211
+ }
1212
+ )
1213
+ ] })
1214
+ ] });
1215
+ }
1216
+ function MappedParameterInput({
1217
+ name,
1218
+ label,
1219
+ value,
1220
+ onChange,
1221
+ nodeId,
1222
+ placeholder
1223
+ }) {
1224
+ if (value !== void 0 && typeof value !== "string") {
1225
+ return /* @__PURE__ */ jsx(
1226
+ JsonMappingInput,
1227
+ {
1228
+ name,
1229
+ label,
1230
+ value,
1231
+ onChange
1232
+ },
1233
+ JSON.stringify(value)
1234
+ );
1235
+ }
1236
+ return /* @__PURE__ */ jsx(
1237
+ ExpressionEditor,
1238
+ {
1239
+ value: value ?? "",
1240
+ onChange,
1241
+ rows: 2,
1242
+ nodeId,
1243
+ placeholder,
1244
+ ariaLabel: label ?? name
1245
+ }
1246
+ );
1247
+ }
1248
+ var SCHEDULE_RULE_FIELDS = [
1249
+ "seconds",
1250
+ "minutes",
1251
+ "hours",
1252
+ "days",
1253
+ "weeks",
1254
+ "months",
1255
+ "cronExpression"
1256
+ ];
1257
+ var LEGACY_INTERVAL_UNITS = [
1258
+ "minutes",
1259
+ "hours",
1260
+ "days",
1261
+ "weeks"
1262
+ ];
1263
+ var LEGACY_DEFAULT_UNIT = "hours";
1264
+ var DEFAULT_TIMEZONE = "UTC";
1265
+ var RRULE_WEEKDAYS = [
1266
+ "SU",
1267
+ "MO",
1268
+ "TU",
1269
+ "WE",
1270
+ "TH",
1271
+ "FR",
1272
+ "SA"
1273
+ ];
1274
+ var INTERVAL_KEY = {
1275
+ seconds: "secondsInterval",
1276
+ minutes: "minutesInterval",
1277
+ hours: "hoursInterval",
1278
+ days: "daysInterval",
1279
+ weeks: "weeksInterval",
1280
+ months: "monthsInterval"
1281
+ };
1282
+ var DEFAULT_INTERVAL = {
1283
+ seconds: 30,
1284
+ minutes: 5,
1285
+ hours: 1,
1286
+ days: 1,
1287
+ weeks: 1,
1288
+ months: 1
1289
+ };
1290
+ function defaultScheduleRule() {
1291
+ return {
1292
+ field: "days",
1293
+ daysInterval: 1,
1294
+ triggerAtHour: 0,
1295
+ triggerAtMinute: 0
1296
+ };
1297
+ }
1298
+ function isScheduleRuleField(value) {
1299
+ return typeof value === "string" && SCHEDULE_RULE_FIELDS.some((field) => field === value);
1300
+ }
1301
+ function isRecord(value) {
1302
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1303
+ }
1304
+ function toNumber(value) {
1305
+ if (typeof value === "number")
1306
+ return Number.isFinite(value) ? value : void 0;
1307
+ if (typeof value === "string") {
1308
+ const trimmed = value.trim();
1309
+ if (trimmed === "") return void 0;
1310
+ const parsed = Number(trimmed);
1311
+ return Number.isFinite(parsed) ? parsed : void 0;
1312
+ }
1313
+ return void 0;
1314
+ }
1315
+ function toWeekdayList(value) {
1316
+ if (Array.isArray(value)) {
1317
+ return value.map(toNumber).filter((day) => day !== void 0);
1318
+ }
1319
+ const single = toNumber(value);
1320
+ return single === void 0 ? void 0 : [single];
1321
+ }
1322
+ function toTrimmedString(value) {
1323
+ return typeof value === "string" ? value.trim() : "";
1324
+ }
1325
+ function isPositiveInteger(value) {
1326
+ return value !== void 0 && Number.isInteger(value) && value >= 1;
1327
+ }
1328
+ function intervalOf(rule) {
1329
+ if (rule.field === "cronExpression") return 1;
1330
+ const own = rule[INTERVAL_KEY[rule.field]];
1331
+ return typeof own === "number" ? own : DEFAULT_INTERVAL[rule.field];
1332
+ }
1333
+ function normalizeScheduleRule(raw) {
1334
+ const field = isScheduleRuleField(raw.field) ? raw.field : "days";
1335
+ switch (field) {
1336
+ case "seconds":
1337
+ return {
1338
+ field,
1339
+ secondsInterval: toNumber(raw.secondsInterval) ?? DEFAULT_INTERVAL.seconds
1340
+ };
1341
+ case "minutes":
1342
+ return {
1343
+ field,
1344
+ minutesInterval: toNumber(raw.minutesInterval) ?? DEFAULT_INTERVAL.minutes
1345
+ };
1346
+ case "hours":
1347
+ return {
1348
+ field,
1349
+ hoursInterval: toNumber(raw.hoursInterval) ?? DEFAULT_INTERVAL.hours,
1350
+ triggerAtMinute: toNumber(raw.triggerAtMinute) ?? 0
1351
+ };
1352
+ case "days":
1353
+ return {
1354
+ field,
1355
+ daysInterval: toNumber(raw.daysInterval) ?? DEFAULT_INTERVAL.days,
1356
+ triggerAtHour: toNumber(raw.triggerAtHour) ?? 0,
1357
+ triggerAtMinute: toNumber(raw.triggerAtMinute) ?? 0
1358
+ };
1359
+ case "weeks":
1360
+ return {
1361
+ field,
1362
+ weeksInterval: toNumber(raw.weeksInterval) ?? DEFAULT_INTERVAL.weeks,
1363
+ triggerAtDay: toWeekdayList(raw.triggerAtDay) ?? [0],
1364
+ triggerAtHour: toNumber(raw.triggerAtHour) ?? 0,
1365
+ triggerAtMinute: toNumber(raw.triggerAtMinute) ?? 0
1366
+ };
1367
+ case "months":
1368
+ return {
1369
+ field,
1370
+ monthsInterval: toNumber(raw.monthsInterval) ?? DEFAULT_INTERVAL.months,
1371
+ triggerAtDayOfMonth: toNumber(raw.triggerAtDayOfMonth) ?? 1,
1372
+ triggerAtHour: toNumber(raw.triggerAtHour) ?? 0,
1373
+ triggerAtMinute: toNumber(raw.triggerAtMinute) ?? 0
1374
+ };
1375
+ case "cronExpression":
1376
+ return { field, expression: toTrimmedString(raw.expression) };
1377
+ }
1378
+ }
1379
+ function rulesFromLegacyState(state) {
1380
+ const cronExpression = toTrimmedString(state.cronExpression);
1381
+ const interval = toNumber(state.interval);
1382
+ const intervalMode = state.mode === "interval" || cronExpression === "" && interval !== void 0 && interval > 0;
1383
+ if (intervalMode) {
1384
+ const unit = isScheduleRuleField(state.unit) && LEGACY_INTERVAL_UNITS.includes(state.unit) ? state.unit : LEGACY_DEFAULT_UNIT;
1385
+ const raw = { field: unit };
1386
+ if (unit !== "cronExpression" && interval !== void 0 && interval > 0) {
1387
+ raw[INTERVAL_KEY[unit]] = interval;
1388
+ }
1389
+ return [normalizeScheduleRule(raw)];
1390
+ }
1391
+ if (cronExpression !== "") {
1392
+ return [{ field: "cronExpression", expression: cronExpression }];
1393
+ }
1394
+ return [defaultScheduleRule()];
1395
+ }
1396
+ function normalizeScheduleState(state) {
1397
+ const source = isRecord(state) ? state : {};
1398
+ let rules;
1399
+ if (Array.isArray(source.rules)) {
1400
+ rules = source.rules.filter(isRecord).map(normalizeScheduleRule);
1401
+ if (rules.length === 0) rules = [defaultScheduleRule()];
1402
+ } else {
1403
+ rules = rulesFromLegacyState(source);
1404
+ }
1405
+ const timezone = toTrimmedString(source.timezone) || DEFAULT_TIMEZONE;
1406
+ const misfirePolicy = typeof source.misfirePolicy === "string" && source.misfirePolicy.trim().toLowerCase() === "coalesce" ? "coalesce" : "skip";
1407
+ const grace = toNumber(source.misfireGraceSeconds);
1408
+ const misfireGraceSeconds = grace !== void 0 && Number.isInteger(grace) && grace >= 0 ? grace : 0;
1409
+ return { rules, timezone, misfirePolicy, misfireGraceSeconds };
1410
+ }
1411
+ function isLegacyScheduleState(state) {
1412
+ if (!isRecord(state) || Array.isArray(state.rules)) return false;
1413
+ return state.mode !== void 0 || toTrimmedString(state.cronExpression) !== "" || toNumber(state.interval) !== void 0;
1414
+ }
1415
+ function inRange(value, min, max) {
1416
+ return value !== void 0 && Number.isInteger(value) && value >= min && value <= max;
1417
+ }
1418
+ function describeCron(expression) {
1419
+ const text = expression.trim();
1420
+ if (text === "") return null;
1421
+ const fieldCount = text.split(/\s+/).length;
1422
+ if (fieldCount !== 5 && fieldCount !== 6) return null;
1423
+ try {
1424
+ return cronstrue.toString(text, { throwExceptionOnParseError: true });
1425
+ } catch {
1426
+ return null;
1427
+ }
1428
+ }
1429
+ function validateRule(rule) {
1430
+ const issues = [];
1431
+ switch (rule.field) {
1432
+ case "seconds":
1433
+ if (!inRange(rule.secondsInterval, 1, 59)) issues.push("secondsRange");
1434
+ break;
1435
+ case "minutes":
1436
+ if (!inRange(rule.minutesInterval, 1, 59)) issues.push("minutesRange");
1437
+ break;
1438
+ case "hours":
1439
+ if (!inRange(rule.hoursInterval, 1, 23)) issues.push("hoursRange");
1440
+ break;
1441
+ case "days":
1442
+ if (!inRange(rule.daysInterval, 1, 31)) issues.push("daysRange");
1443
+ break;
1444
+ case "weeks": {
1445
+ if (!isPositiveInteger(rule.weeksInterval)) issues.push("weeksMin");
1446
+ const days = rule.triggerAtDay;
1447
+ if (days === void 0 || days.length === 0)
1448
+ issues.push("weekdaysRequired");
1449
+ else {
1450
+ if (!days.every((day) => inRange(day, 0, 6)))
1451
+ issues.push("weekdaysRange");
1452
+ if (new Set(days).size !== days.length) issues.push("weekdaysRepeat");
1453
+ }
1454
+ break;
1455
+ }
1456
+ case "months":
1457
+ if (!isPositiveInteger(rule.monthsInterval)) issues.push("monthsMin");
1458
+ if (rule.triggerAtDayOfMonth !== void 0 && !inRange(rule.triggerAtDayOfMonth, 1, 31)) {
1459
+ issues.push("dayOfMonthRange");
1460
+ }
1461
+ break;
1462
+ case "cronExpression": {
1463
+ const text = (rule.expression ?? "").trim();
1464
+ if (text === "") return ["cronRequired"];
1465
+ const fieldCount = text.split(/\s+/).length;
1466
+ if (fieldCount !== 5 && fieldCount !== 6) return ["cronFieldCount"];
1467
+ return describeCron(text) === null ? ["cronInvalid"] : [];
1468
+ }
1469
+ }
1470
+ if (rule.triggerAtHour !== void 0 && !inRange(rule.triggerAtHour, 0, 23)) {
1471
+ issues.push("hourRange");
1472
+ }
1473
+ if (rule.triggerAtMinute !== void 0 && !inRange(rule.triggerAtMinute, 0, 59)) {
1474
+ issues.push("minuteRange");
1475
+ }
1476
+ return issues;
1477
+ }
1478
+ function stableInt(seed, range) {
1479
+ const base64 = digest(seed).replace(/-/g, "+").replace(/_/g, "/");
1480
+ const bytes = atob(base64 + "=".repeat((4 - base64.length % 4) % 4));
1481
+ const value = (bytes.charCodeAt(0) << 24 >>> 0) + (bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(2) << 8) + bytes.charCodeAt(3);
1482
+ return value % range;
1483
+ }
1484
+ function fillerFromKey(nodeKey) {
1485
+ return {
1486
+ second: stableInt(`${nodeKey}:second`, 60),
1487
+ minute: stableInt(`${nodeKey}:minute`, 60),
1488
+ hour: stableInt(`${nodeKey}:hour`, 24),
1489
+ dayOfMonth: 1 + stableInt(`${nodeKey}:dayOfMonth`, 28)
1490
+ };
1491
+ }
1492
+ function isCronExact(rule) {
1493
+ const interval = intervalOf(rule);
1494
+ switch (rule.field) {
1495
+ case "seconds":
1496
+ case "cronExpression":
1497
+ return true;
1498
+ case "minutes":
1499
+ return isPositiveInteger(interval) && 60 % interval === 0;
1500
+ case "hours":
1501
+ return isPositiveInteger(interval) && 24 % interval === 0;
1502
+ case "days":
1503
+ case "weeks":
1504
+ return interval === 1;
1505
+ case "months":
1506
+ return isPositiveInteger(interval) && 12 % interval === 0;
1507
+ }
1508
+ }
1509
+ function resolveAnchors(rule, filler) {
1510
+ return {
1511
+ second: filler.second,
1512
+ minute: rule.triggerAtMinute ?? filler.minute,
1513
+ hour: rule.triggerAtHour ?? filler.hour,
1514
+ dayOfMonth: rule.triggerAtDayOfMonth ?? filler.dayOfMonth
1515
+ };
1516
+ }
1517
+ function sortedWeekdays(rule) {
1518
+ return [...rule.triggerAtDay ?? []].sort((a, b) => a - b);
1519
+ }
1520
+ function toCronExpression(rule, filler) {
1521
+ if (!isCronExact(rule)) {
1522
+ throw new Error(
1523
+ `Schedule rule "every ${intervalOf(rule)} ${rule.field}" has no exact cron expression; use an RRULE`
1524
+ );
1525
+ }
1526
+ const interval = intervalOf(rule);
1527
+ const { second, minute, hour, dayOfMonth } = resolveAnchors(rule, filler);
1528
+ switch (rule.field) {
1529
+ case "seconds":
1530
+ return `*/${interval} * * * * *`;
1531
+ case "minutes":
1532
+ return `${second} */${interval} * * * *`;
1533
+ case "hours":
1534
+ return `${second} ${minute} */${interval} * * *`;
1535
+ case "days":
1536
+ return `${second} ${minute} ${hour} * * *`;
1537
+ case "weeks": {
1538
+ const days = sortedWeekdays(rule);
1539
+ return `${second} ${minute} ${hour} * * ${days.length === 0 ? "*" : days.join(",")}`;
1540
+ }
1541
+ case "months":
1542
+ return `${second} ${minute} ${hour} ${dayOfMonth} */${interval} *`;
1543
+ case "cronExpression":
1544
+ return (rule.expression ?? "").trim();
1545
+ }
1546
+ }
1547
+ function toRRule(rule, filler) {
1548
+ const interval = intervalOf(rule);
1549
+ const { second, minute, hour, dayOfMonth } = resolveAnchors(rule, filler);
1550
+ switch (rule.field) {
1551
+ case "minutes":
1552
+ return `FREQ=MINUTELY;INTERVAL=${interval};BYSECOND=${second}`;
1553
+ case "hours":
1554
+ return `FREQ=HOURLY;INTERVAL=${interval};BYMINUTE=${minute};BYSECOND=${second}`;
1555
+ case "days":
1556
+ return `FREQ=DAILY;INTERVAL=${interval};BYHOUR=${hour};BYMINUTE=${minute};BYSECOND=${second}`;
1557
+ case "weeks": {
1558
+ const byDay = sortedWeekdays(rule).map((day) => RRULE_WEEKDAYS[day]).filter((code) => code !== void 0);
1559
+ const byDayPart = byDay.length === 0 ? "" : `;BYDAY=${byDay.join(",")}`;
1560
+ return `FREQ=WEEKLY;INTERVAL=${interval}${byDayPart};BYHOUR=${hour};BYMINUTE=${minute};BYSECOND=${second}`;
1561
+ }
1562
+ case "months":
1563
+ return `FREQ=MONTHLY;INTERVAL=${interval};BYMONTHDAY=${dayOfMonth};BYHOUR=${hour};BYMINUTE=${minute};BYSECOND=${second}`;
1564
+ case "seconds":
1565
+ case "cronExpression":
1566
+ throw new Error(
1567
+ `Schedule rule field "${rule.field}" is always expressed as cron`
1568
+ );
1569
+ }
1570
+ }
1571
+ function ruleToScheduleSpec(rule, nodeKey) {
1572
+ const filler = fillerFromKey(nodeKey);
1573
+ if (isCronExact(rule)) {
1574
+ return {
1575
+ scheduleType: "CRON",
1576
+ cronExpression: toCronExpression(rule, filler)
1577
+ };
1578
+ }
1579
+ return { scheduleType: "RRULE", rrule: toRRule(rule, filler) };
1580
+ }
1581
+ function scheduleNodeKey(workflowKey, nodeId, ruleIndex) {
1582
+ return `${workflowKey || "draft"}:${nodeId}:${ruleIndex}`;
1583
+ }
1584
+ function CronExpressionField({
1585
+ value,
1586
+ onChange,
1587
+ className,
1588
+ testId,
1589
+ ariaLabel
1590
+ }) {
1591
+ const tr = useTr();
1592
+ const text = value.trim();
1593
+ const description = text === "" ? null : describeCron(text);
1594
+ const invalid = text !== "" && description === null;
1595
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1596
+ /* @__PURE__ */ jsx(
1597
+ "input",
1598
+ {
1599
+ type: "text",
1600
+ value,
1601
+ onChange: (event) => onChange(event.target.value),
1602
+ placeholder: tr(
1603
+ "fluidgrids.builder.scheduleRules.cronPlaceholder",
1604
+ "eg. 0 15 * 1 sun"
1605
+ ),
1606
+ "aria-label": ariaLabel,
1607
+ "aria-invalid": invalid,
1608
+ spellCheck: false,
1609
+ "data-testid": testId,
1610
+ className: cn(className, "font-mono", invalid && "border-destructive")
1611
+ }
1612
+ ),
1613
+ description && /* @__PURE__ */ jsx(
1614
+ "span",
1615
+ {
1616
+ className: "text-[11px] leading-snug text-muted-foreground",
1617
+ "data-testid": testId ? `${testId}-description` : void 0,
1618
+ children: description
1619
+ }
1620
+ ),
1621
+ invalid && /* @__PURE__ */ jsx(
1622
+ "span",
1623
+ {
1624
+ className: "text-[11px] leading-snug text-destructive",
1625
+ "data-testid": testId ? `${testId}-error` : void 0,
1626
+ children: tr(
1627
+ "fluidgrids.builder.scheduleRules.cronInvalid",
1628
+ "Invalid cron expression"
1629
+ )
1630
+ }
1631
+ )
1632
+ ] });
1633
+ }
1634
+ var rowInput = "rounded-md border border-input bg-background px-2 py-1 text-xs outline-none focus:border-primary";
1635
+ var selectCx = "scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary";
1636
+ function isNumericOption(value) {
1637
+ return /^-?\d+$/.test(value);
1638
+ }
1639
+ function optionValue(spec, raw) {
1640
+ const numeric = (spec.options ?? []).every(isNumericOption);
1641
+ return numeric && isNumericOption(raw) ? Number(raw) : raw;
1642
+ }
1643
+ function stringValue(value) {
1644
+ return value === void 0 || value === null ? "" : String(value);
1645
+ }
1646
+ function pad(value) {
1647
+ return String(value ?? 0).padStart(2, "0");
1648
+ }
1649
+ function issueMessage(tr, issue) {
1650
+ switch (issue) {
1651
+ case "secondsRange":
1652
+ return tr(
1653
+ "fluidgrids.builder.scheduleRules.issue.secondsRange",
1654
+ "Seconds must be between 1 and 59"
1655
+ );
1656
+ case "minutesRange":
1657
+ return tr(
1658
+ "fluidgrids.builder.scheduleRules.issue.minutesRange",
1659
+ "Minutes must be between 1 and 59"
1660
+ );
1661
+ case "hoursRange":
1662
+ return tr(
1663
+ "fluidgrids.builder.scheduleRules.issue.hoursRange",
1664
+ "Hours must be between 1 and 23"
1665
+ );
1666
+ case "daysRange":
1667
+ return tr(
1668
+ "fluidgrids.builder.scheduleRules.issue.daysRange",
1669
+ "Days must be between 1 and 31"
1670
+ );
1671
+ case "weeksMin":
1672
+ return tr(
1673
+ "fluidgrids.builder.scheduleRules.issue.weeksMin",
1674
+ "Weeks must be at least 1"
1675
+ );
1676
+ case "monthsMin":
1677
+ return tr(
1678
+ "fluidgrids.builder.scheduleRules.issue.monthsMin",
1679
+ "Months must be at least 1"
1680
+ );
1681
+ case "dayOfMonthRange":
1682
+ return tr(
1683
+ "fluidgrids.builder.scheduleRules.issue.dayOfMonthRange",
1684
+ "Day of month must be between 1 and 31"
1685
+ );
1686
+ case "weekdaysRequired":
1687
+ return tr(
1688
+ "fluidgrids.builder.scheduleRules.issue.weekdaysRequired",
1689
+ "Select at least one weekday"
1690
+ );
1691
+ case "weekdaysRange":
1692
+ return tr(
1693
+ "fluidgrids.builder.scheduleRules.issue.weekdaysRange",
1694
+ "Weekdays must be Sunday to Saturday"
1695
+ );
1696
+ case "weekdaysRepeat":
1697
+ return tr(
1698
+ "fluidgrids.builder.scheduleRules.issue.weekdaysRepeat",
1699
+ "Weekdays must not repeat"
1700
+ );
1701
+ case "hourRange":
1702
+ return tr(
1703
+ "fluidgrids.builder.scheduleRules.issue.hourRange",
1704
+ "Hour must be between 0 and 23"
1705
+ );
1706
+ case "minuteRange":
1707
+ return tr(
1708
+ "fluidgrids.builder.scheduleRules.issue.minuteRange",
1709
+ "Minute must be between 0 and 59"
1710
+ );
1711
+ case "cronRequired":
1712
+ return tr(
1713
+ "fluidgrids.builder.scheduleRules.issue.cronRequired",
1714
+ "Enter a cron expression"
1715
+ );
1716
+ case "cronFieldCount":
1717
+ return tr(
1718
+ "fluidgrids.builder.scheduleRules.issue.cronFieldCount",
1719
+ "A cron expression has 5 or 6 fields"
1720
+ );
1721
+ case "cronInvalid":
1722
+ return tr(
1723
+ "fluidgrids.builder.scheduleRules.issue.cronInvalid",
1724
+ "Invalid cron expression"
1725
+ );
1726
+ }
1727
+ }
1728
+ function ruleSummary(tr, rule, weekdayLabels = {}) {
1729
+ const at = `${pad(rule.triggerAtHour)}:${pad(rule.triggerAtMinute)}`;
1730
+ switch (rule.field) {
1731
+ case "seconds":
1732
+ return tr(
1733
+ "fluidgrids.builder.scheduleRules.summary.seconds",
1734
+ "Every {{n}} seconds",
1735
+ { n: rule.secondsInterval ?? 30 }
1736
+ );
1737
+ case "minutes":
1738
+ return tr(
1739
+ "fluidgrids.builder.scheduleRules.summary.minutes",
1740
+ "Every {{n}} minutes",
1741
+ { n: rule.minutesInterval ?? 5 }
1742
+ );
1743
+ case "hours":
1744
+ return tr(
1745
+ "fluidgrids.builder.scheduleRules.summary.hours",
1746
+ "Every {{n}} hours at minute {{minute}}",
1747
+ { n: rule.hoursInterval ?? 1, minute: pad(rule.triggerAtMinute) }
1748
+ );
1749
+ case "days":
1750
+ return tr(
1751
+ "fluidgrids.builder.scheduleRules.summary.days",
1752
+ "Every {{n}} days at {{time}}",
1753
+ { n: rule.daysInterval ?? 1, time: at }
1754
+ );
1755
+ case "weeks": {
1756
+ const days = (rule.triggerAtDay ?? []).map((day) => weekdayLabels[String(day)] ?? String(day)).join(", ");
1757
+ return tr(
1758
+ "fluidgrids.builder.scheduleRules.summary.weeks",
1759
+ "Every {{n}} weeks on {{days}} at {{time}}",
1760
+ { n: rule.weeksInterval ?? 1, days, time: at }
1761
+ );
1762
+ }
1763
+ case "months":
1764
+ return tr(
1765
+ "fluidgrids.builder.scheduleRules.summary.months",
1766
+ "Every {{n}} months on day {{day}} at {{time}}",
1767
+ {
1768
+ n: rule.monthsInterval ?? 1,
1769
+ day: rule.triggerAtDayOfMonth ?? 1,
1770
+ time: at
1771
+ }
1772
+ );
1773
+ case "cronExpression":
1774
+ return tr(
1775
+ "fluidgrids.builder.scheduleRules.summary.cron",
1776
+ "Cron: {{expression}}",
1777
+ { expression: rule.expression ?? "" }
1778
+ );
1779
+ }
1780
+ }
1781
+ function ScheduleRulesEditor({
1782
+ rules,
1783
+ ruleFields,
1784
+ onChange,
1785
+ testIdPrefix = "ndv"
1786
+ }) {
1787
+ const tr = useTr();
1788
+ const specs = paramsFromOperation({ name: "rule", input_fields: ruleFields });
1789
+ const defaults = Object.fromEntries(
1790
+ specs.map((spec) => [spec.name, spec.defaultValue])
1791
+ );
1792
+ const weekdayLabels = specs.find((spec) => spec.name === "triggerAtDay")?.optionLabels ?? {};
1793
+ const setRow = (index, row) => {
1794
+ const next = rules.slice();
1795
+ next[index] = row;
1796
+ onChange(next);
1797
+ };
1798
+ const patchRow = (index, name, value) => {
1799
+ const row = { ...rules[index] ?? {} };
1800
+ if (value === void 0 || value === "") delete row[name];
1801
+ else row[name] = value;
1802
+ setRow(
1803
+ index,
1804
+ name === "field" ? { ...normalizeScheduleRule({ field: value }) } : row
1805
+ );
1806
+ };
1807
+ return /* @__PURE__ */ jsxs(
1808
+ "div",
1809
+ {
1810
+ className: "flex flex-col gap-2",
1811
+ "data-testid": `${testIdPrefix}-schedule-rules`,
1812
+ children: [
1813
+ rules.map((row, index) => {
1814
+ const rule = normalizeScheduleRule(asRecord(row) ?? {});
1815
+ const issues = validateRule(rule);
1816
+ return /* @__PURE__ */ jsxs(
1817
+ "div",
1818
+ {
1819
+ className: "rounded-md border border-border/70 bg-muted/10 p-2",
1820
+ "data-testid": `${testIdPrefix}-schedule-rule-${index}`,
1821
+ children: [
1822
+ /* @__PURE__ */ jsxs("div", { className: "mb-1.5 flex items-center gap-1.5", children: [
1823
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-semibold text-muted-foreground", children: [
1824
+ index + 1,
1825
+ "."
1826
+ ] }),
1827
+ /* @__PURE__ */ jsx(
1828
+ "span",
1829
+ {
1830
+ className: "flex-1 truncate text-[11px] text-muted-foreground",
1831
+ "data-testid": `${testIdPrefix}-schedule-rule-summary`,
1832
+ children: ruleSummary(tr, rule, weekdayLabels)
1833
+ }
1834
+ ),
1835
+ /* @__PURE__ */ jsx(
1836
+ "button",
1837
+ {
1838
+ type: "button",
1839
+ onClick: () => onChange(rules.filter((_, i) => i !== index)),
1840
+ disabled: rules.length === 1,
1841
+ title: tr(
1842
+ "fluidgrids.builder.scheduleRules.removeRule",
1843
+ "Remove rule"
1844
+ ),
1845
+ "aria-label": tr(
1846
+ "fluidgrids.builder.scheduleRules.removeRule",
1847
+ "Remove rule"
1848
+ ),
1849
+ className: "rounded p-0.5 text-muted-foreground hover:text-[var(--color-status-error-text)] disabled:opacity-30",
1850
+ "data-testid": `${testIdPrefix}-schedule-rule-remove`,
1851
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:trash", className: "size-3.5" })
1852
+ }
1853
+ )
1854
+ ] }),
1855
+ /* @__PURE__ */ jsx("div", { className: "grid gap-2 sm:grid-cols-2", children: specs.filter((spec) => isParamVisible(spec, row, defaults)).map((spec) => {
1856
+ const current = row[spec.name] ?? spec.defaultValue;
1857
+ const label = spec.label ?? spec.name;
1858
+ const testId = `${testIdPrefix}-schedule-rule-${index}-${spec.name}`;
1859
+ const isCron = spec.name === "expression" && rule.field === "cronExpression";
1860
+ return /* @__PURE__ */ jsxs(
1861
+ "label",
1862
+ {
1863
+ className: cn(
1864
+ "flex flex-col gap-1",
1865
+ (isCron || spec.type === "multiOptions") && "sm:col-span-2"
1866
+ ),
1867
+ children: [
1868
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-medium text-foreground", children: [
1869
+ label,
1870
+ spec.required && /* @__PURE__ */ jsx("span", { className: "ml-0.5 text-destructive", children: "*" })
1871
+ ] }),
1872
+ isCron ? /* @__PURE__ */ jsx(
1873
+ CronExpressionField,
1874
+ {
1875
+ value: stringValue(row[spec.name]),
1876
+ onChange: (value) => patchRow(index, spec.name, value),
1877
+ className: cn(rowInput, "w-full"),
1878
+ testId,
1879
+ ariaLabel: label
1880
+ }
1881
+ ) : spec.type === "multiOptions" && spec.options ? /* @__PURE__ */ jsx(
1882
+ "div",
1883
+ {
1884
+ className: "flex flex-wrap gap-x-3 gap-y-1",
1885
+ role: "group",
1886
+ "aria-label": label,
1887
+ "data-testid": testId,
1888
+ children: spec.options.map((option) => {
1889
+ const selected = Array.isArray(current) ? current.map(String) : [stringValue(current)];
1890
+ const checked = selected.includes(option);
1891
+ return /* @__PURE__ */ jsxs(
1892
+ "label",
1893
+ {
1894
+ className: "flex cursor-pointer items-center gap-1 text-xs",
1895
+ children: [
1896
+ /* @__PURE__ */ jsx(
1897
+ "input",
1898
+ {
1899
+ type: "checkbox",
1900
+ checked,
1901
+ onChange: (event) => {
1902
+ const nextSet = new Set(selected);
1903
+ if (event.target.checked)
1904
+ nextSet.add(option);
1905
+ else nextSet.delete(option);
1906
+ const ordered = spec.options.filter(
1907
+ (o) => nextSet.has(o)
1908
+ );
1909
+ patchRow(
1910
+ index,
1911
+ spec.name,
1912
+ ordered.map((o) => optionValue(spec, o))
1913
+ );
1914
+ },
1915
+ className: "size-3.5",
1916
+ "data-testid": `${testId}-${option}`
1917
+ }
1918
+ ),
1919
+ /* @__PURE__ */ jsx("span", { children: spec.optionLabels?.[option] ?? option })
1920
+ ]
1921
+ },
1922
+ option
1923
+ );
1924
+ })
1925
+ }
1926
+ ) : spec.options && spec.options.length > 0 ? /* @__PURE__ */ jsx(
1927
+ "select",
1928
+ {
1929
+ value: stringValue(current),
1930
+ onChange: (event) => patchRow(
1931
+ index,
1932
+ spec.name,
1933
+ optionValue(spec, event.target.value)
1934
+ ),
1935
+ "aria-label": label,
1936
+ "data-testid": testId,
1937
+ className: cn(rowInput, selectCx),
1938
+ children: spec.options.map((option) => /* @__PURE__ */ jsx("option", { value: option, children: spec.optionLabels?.[option] ?? option }, option))
1939
+ }
1940
+ ) : spec.type === "integer" || spec.type === "number" ? /* @__PURE__ */ jsx(
1941
+ "input",
1942
+ {
1943
+ type: "number",
1944
+ step: spec.type === "integer" ? "1" : "any",
1945
+ value: stringValue(row[spec.name]),
1946
+ placeholder: spec.defaultValue !== void 0 ? String(spec.defaultValue) : void 0,
1947
+ onChange: (event) => patchRow(
1948
+ index,
1949
+ spec.name,
1950
+ coerceNumber(spec.type, event.target.value)
1951
+ ),
1952
+ "aria-label": label,
1953
+ "data-testid": testId,
1954
+ className: rowInput
1955
+ }
1956
+ ) : /* @__PURE__ */ jsx(
1957
+ "input",
1958
+ {
1959
+ type: "text",
1960
+ value: stringValue(row[spec.name]),
1961
+ onChange: (event) => patchRow(index, spec.name, event.target.value),
1962
+ "aria-label": label,
1963
+ "data-testid": testId,
1964
+ className: rowInput
1965
+ }
1966
+ ),
1967
+ spec.description && /* @__PURE__ */ jsx("span", { className: "text-[10px] leading-snug text-muted-foreground", children: spec.description })
1968
+ ]
1969
+ },
1970
+ spec.name
1971
+ );
1972
+ }) }),
1973
+ issues.length > 0 && /* @__PURE__ */ jsx(
1974
+ "ul",
1975
+ {
1976
+ className: "mt-1.5 flex flex-col gap-0.5 text-[11px] leading-snug text-destructive",
1977
+ "data-testid": `${testIdPrefix}-schedule-rule-${index}-issues`,
1978
+ children: issues.map((issue) => /* @__PURE__ */ jsx("li", { children: issueMessage(tr, issue) }, issue))
1979
+ }
1980
+ )
1981
+ ]
1982
+ },
1983
+ index
1984
+ );
1985
+ }),
1986
+ /* @__PURE__ */ jsxs(
1987
+ "button",
1988
+ {
1989
+ type: "button",
1990
+ onClick: () => onChange([...rules, { ...defaultScheduleRule() }]),
1991
+ className: "flex w-fit items-center gap-1 text-[11px] font-medium text-primary hover:underline",
1992
+ "data-testid": `${testIdPrefix}-schedule-add-rule`,
1993
+ children: [
1994
+ /* @__PURE__ */ jsx(Icon, { icon: "basil:add-solid", className: "size-3.5" }),
1995
+ " ",
1996
+ tr("fluidgrids.builder.scheduleRules.addRule", "Add Rule")
1997
+ ]
1998
+ }
1999
+ )
2000
+ ]
2001
+ }
2002
+ );
2003
+ }
2004
+ var rowInput2 = "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground outline-none focus:border-primary";
2005
+ function serialiseRows(rows, previous) {
2006
+ const rules = rows.map(ruleFromRow);
2007
+ return Array.isArray(previous) ? rules : JSON.stringify(rules, null, 2);
2008
+ }
2009
+ function textOf(value) {
2010
+ if (typeof value === "string") return value;
2011
+ if (value === void 0 || value === null) return "";
2012
+ return JSON.stringify(value, null, 2);
2013
+ }
2014
+ function AssertRulesEditor({
2015
+ value,
2016
+ onChange,
2017
+ testIdPrefix = "ndv"
2018
+ }) {
2019
+ const tr = useTr();
2020
+ const initial = readAssertRulesSetting(value);
2021
+ const [rows, setRows] = useState(
2022
+ () => rowsFromRules(initial ?? [])
2023
+ );
2024
+ const [jsonView, setJsonView] = useState(initial === null);
2025
+ const [jsonText, setJsonText] = useState(() => textOf(value));
2026
+ const written = useRef(value);
2027
+ useEffect(() => {
2028
+ if (value === written.current) return;
2029
+ written.current = value;
2030
+ const parsed = readAssertRulesSetting(value);
2031
+ setJsonText(textOf(value));
2032
+ if (parsed === null) {
2033
+ setJsonView(true);
2034
+ } else {
2035
+ setRows(rowsFromRules(parsed));
2036
+ }
2037
+ }, [value]);
2038
+ const write = (next) => {
2039
+ written.current = next;
2040
+ onChange(next);
2041
+ };
2042
+ const setRowList = (next) => {
2043
+ setRows(next);
2044
+ const serialised = serialiseRows(next, value);
2045
+ setJsonText(textOf(serialised));
2046
+ write(serialised);
2047
+ };
2048
+ const setRow = (index, patch) => {
2049
+ const next = rows.slice();
2050
+ const current = next[index];
2051
+ if (!current) return;
2052
+ next[index] = { ...current, ...patch };
2053
+ setRowList(next);
2054
+ };
2055
+ const parsedJson = jsonView ? readAssertRulesSetting(jsonText) : null;
2056
+ const check = normaliseAssertRules(
2057
+ jsonView ? parsedJson ?? [] : rows.map(ruleFromRow)
2058
+ );
2059
+ const problem = jsonView && parsedJson === null ? {
2060
+ key: "fluidgrids.builder.assertRules.invalidJson",
2061
+ fallback: "The rules must be a JSON array."
2062
+ } : "problem" in check ? describeAssertRuleProblem(check.problem) : null;
2063
+ const toggleView = () => {
2064
+ if (jsonView) {
2065
+ const parsed = readAssertRulesSetting(jsonText);
2066
+ if (parsed === null) return;
2067
+ setRows(rowsFromRules(parsed));
2068
+ setJsonView(false);
2069
+ } else {
2070
+ setJsonText(textOf(serialiseRows(rows, value)));
2071
+ setJsonView(true);
2072
+ }
2073
+ };
2074
+ return /* @__PURE__ */ jsxs(
2075
+ "div",
2076
+ {
2077
+ className: "flex flex-col gap-2",
2078
+ "data-testid": `${testIdPrefix}-assert-rules`,
2079
+ children: [
2080
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children: /* @__PURE__ */ jsx(
2081
+ "button",
2082
+ {
2083
+ type: "button",
2084
+ onClick: toggleView,
2085
+ disabled: jsonView && parsedJson === null,
2086
+ className: "text-[11px] text-primary hover:underline disabled:opacity-50",
2087
+ "data-testid": `${testIdPrefix}-assert-rules-view`,
2088
+ children: jsonView ? tr("fluidgrids.builder.assertRules.showRows", "Edit as rows") : tr("fluidgrids.builder.assertRules.showJson", "Edit as JSON")
2089
+ }
2090
+ ) }),
2091
+ jsonView ? /* @__PURE__ */ jsx(
2092
+ "textarea",
2093
+ {
2094
+ value: jsonText,
2095
+ rows: 8,
2096
+ spellCheck: false,
2097
+ "aria-label": tr(
2098
+ "fluidgrids.builder.assertRules.jsonLabel",
2099
+ "Rules (JSON array)"
2100
+ ),
2101
+ onChange: (event) => {
2102
+ setJsonText(event.target.value);
2103
+ write(
2104
+ Array.isArray(value) ? readAssertRulesSetting(event.target.value) ?? value : event.target.value
2105
+ );
2106
+ },
2107
+ className: cn(rowInput2, "w-full font-mono"),
2108
+ "data-testid": `${testIdPrefix}-assert-rules-json`
2109
+ }
2110
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
2111
+ rows.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tr(
2112
+ "fluidgrids.builder.assertRules.empty",
2113
+ "Add at least one rule."
2114
+ ) }),
2115
+ rows.map((row, index) => {
2116
+ const operand = operatorSpec(row.operator)?.operand ?? "expected";
2117
+ return /* @__PURE__ */ jsxs(
2118
+ "div",
2119
+ {
2120
+ className: "rounded-md border border-border p-2",
2121
+ "data-testid": `${testIdPrefix}-assert-rule-${index}`,
2122
+ children: [
2123
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
2124
+ /* @__PURE__ */ jsxs("span", { className: "text-[10px] font-semibold text-muted-foreground", children: [
2125
+ index + 1,
2126
+ "."
2127
+ ] }),
2128
+ /* @__PURE__ */ jsx(
2129
+ "input",
2130
+ {
2131
+ type: "text",
2132
+ value: row.path,
2133
+ placeholder: tr(
2134
+ "fluidgrids.builder.assertRules.pathPlaceholder",
2135
+ "path, e.g. body.items.0.id (empty = whole input)"
2136
+ ),
2137
+ "aria-label": tr(
2138
+ "fluidgrids.builder.assertRules.path",
2139
+ "Path"
2140
+ ),
2141
+ onChange: (event) => setRow(index, { path: event.target.value }),
2142
+ className: cn(rowInput2, "min-w-0 flex-1 font-mono")
2143
+ }
2144
+ ),
2145
+ /* @__PURE__ */ jsx(
2146
+ "select",
2147
+ {
2148
+ value: row.operator,
2149
+ "aria-label": tr(
2150
+ "fluidgrids.builder.assertRules.operator",
2151
+ "Operator"
2152
+ ),
2153
+ onChange: (event) => setRow(index, { operator: event.target.value }),
2154
+ className: rowInput2,
2155
+ children: ASSERT_OPERATORS.map((spec) => /* @__PURE__ */ jsx("option", { value: spec.wire, children: tr(
2156
+ `fluidgrids.builder.assertRules.op.${spec.wire}`,
2157
+ spec.label
2158
+ ) }, spec.wire))
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx(
2162
+ "button",
2163
+ {
2164
+ type: "button",
2165
+ onClick: () => setRowList(rows.filter((_, i) => i !== index)),
2166
+ title: tr(
2167
+ "fluidgrids.builder.assertRules.remove",
2168
+ "Remove rule"
2169
+ ),
2170
+ className: "rounded p-0.5 text-muted-foreground hover:text-[var(--color-status-error-text)]",
2171
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
2172
+ }
2173
+ )
2174
+ ] }),
2175
+ operand === "expected" && /* @__PURE__ */ jsx(
2176
+ "input",
2177
+ {
2178
+ type: "text",
2179
+ value: row.expected,
2180
+ placeholder: row.operator === "matches" ? tr(
2181
+ "fluidgrids.builder.assertRules.regexPlaceholder",
2182
+ "regular expression, e.g. ^ord_"
2183
+ ) : tr(
2184
+ "fluidgrids.builder.assertRules.expectedPlaceholder",
2185
+ 'expected value as JSON, e.g. 42, "ok" or {"a": 1}'
2186
+ ),
2187
+ "aria-label": tr(
2188
+ "fluidgrids.builder.assertRules.expected",
2189
+ "Expected"
2190
+ ),
2191
+ onChange: (event) => setRow(index, { expected: event.target.value }),
2192
+ className: cn(rowInput2, "mt-1.5 w-full font-mono")
2193
+ }
2194
+ ),
2195
+ operand === "range" && /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex gap-1.5", children: [
2196
+ /* @__PURE__ */ jsx(
2197
+ "input",
2198
+ {
2199
+ type: "text",
2200
+ inputMode: "decimal",
2201
+ value: row.min,
2202
+ placeholder: tr(
2203
+ "fluidgrids.builder.assertRules.min",
2204
+ "min"
2205
+ ),
2206
+ "aria-label": tr(
2207
+ "fluidgrids.builder.assertRules.min",
2208
+ "min"
2209
+ ),
2210
+ onChange: (event) => setRow(index, { min: event.target.value }),
2211
+ className: cn(rowInput2, "w-full")
2212
+ }
2213
+ ),
2214
+ /* @__PURE__ */ jsx(
2215
+ "input",
2216
+ {
2217
+ type: "text",
2218
+ inputMode: "decimal",
2219
+ value: row.max,
2220
+ placeholder: tr(
2221
+ "fluidgrids.builder.assertRules.max",
2222
+ "max"
2223
+ ),
2224
+ "aria-label": tr(
2225
+ "fluidgrids.builder.assertRules.max",
2226
+ "max"
2227
+ ),
2228
+ onChange: (event) => setRow(index, { max: event.target.value }),
2229
+ className: cn(rowInput2, "w-full")
2230
+ }
2231
+ )
2232
+ ] }),
2233
+ /* @__PURE__ */ jsx(
2234
+ "input",
2235
+ {
2236
+ type: "text",
2237
+ value: row.message,
2238
+ placeholder: tr(
2239
+ "fluidgrids.builder.assertRules.messagePlaceholder",
2240
+ "message when it does not hold (optional)"
2241
+ ),
2242
+ "aria-label": tr(
2243
+ "fluidgrids.builder.assertRules.message",
2244
+ "Message"
2245
+ ),
2246
+ onChange: (event) => setRow(index, { message: event.target.value }),
2247
+ className: cn(rowInput2, "mt-1.5 w-full")
2248
+ }
2249
+ )
2250
+ ]
2251
+ },
2252
+ index
2253
+ );
2254
+ }),
2255
+ /* @__PURE__ */ jsxs(
2256
+ "button",
2257
+ {
2258
+ type: "button",
2259
+ onClick: () => setRowList([...rows, emptyAssertRuleRow()]),
2260
+ className: "flex items-center gap-1 self-start text-xs text-primary hover:underline",
2261
+ "data-testid": `${testIdPrefix}-assert-rule-add`,
2262
+ children: [
2263
+ /* @__PURE__ */ jsx(Plus, { className: "size-3.5" }),
2264
+ tr("fluidgrids.builder.assertRules.add", "Add rule")
2265
+ ]
2266
+ }
2267
+ )
2268
+ ] }),
2269
+ problem && /* @__PURE__ */ jsx(
2270
+ "p",
2271
+ {
2272
+ className: "text-[11px] text-[var(--color-status-error-text)]",
2273
+ role: "alert",
2274
+ "data-testid": `${testIdPrefix}-assert-rules-problem`,
2275
+ children: tr(problem.key, problem.fallback)
2276
+ }
2277
+ )
2278
+ ]
2279
+ }
2280
+ );
2281
+ }
2282
+ function formatRunInZone(date, timezone) {
2283
+ try {
2284
+ return new Intl.DateTimeFormat(void 0, {
2285
+ timeZone: timezone || "UTC",
2286
+ dateStyle: "medium",
2287
+ timeStyle: "medium"
2288
+ }).format(date);
2289
+ } catch {
2290
+ return date.toISOString();
2291
+ }
2292
+ }
2293
+ function ScheduleNextRuns({
2294
+ preview,
2295
+ timezone,
2296
+ testId = "ndv-schedule-next-runs"
2297
+ }) {
2298
+ const tr = useTr();
2299
+ if (!preview.previewed && !preview.loading) return null;
2300
+ return /* @__PURE__ */ jsxs(
2301
+ "div",
2302
+ {
2303
+ className: "rounded-md border border-border/70 bg-muted/10 p-2",
2304
+ "data-testid": testId,
2305
+ "aria-busy": preview.loading,
2306
+ children: [
2307
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground", children: [
2308
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:calendar-clock-outline", className: "size-3.5" }),
2309
+ tr("fluidgrids.builder.scheduleRules.nextRunsTitle", "Next runs"),
2310
+ preview.loading && /* @__PURE__ */ jsx(
2311
+ Icon,
2312
+ {
2313
+ icon: "mdi:loading",
2314
+ className: "ml-auto size-3.5 animate-spin",
2315
+ "aria-hidden": true
2316
+ }
2317
+ )
2318
+ ] }),
2319
+ preview.error ? /* @__PURE__ */ jsx(
2320
+ "p",
2321
+ {
2322
+ className: "text-[11px] leading-snug text-destructive",
2323
+ "data-testid": `${testId}-error`,
2324
+ children: tr(
2325
+ "fluidgrids.builder.scheduleRules.nextRunsError",
2326
+ "The scheduler could not preview this schedule: {{message}}",
2327
+ { message: preview.error.message }
2328
+ )
2329
+ }
2330
+ ) : preview.runs.length === 0 ? !preview.loading && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: tr(
2331
+ "fluidgrids.builder.scheduleRules.nextRunsEmpty",
2332
+ "No upcoming runs for these rules."
2333
+ ) }) : /* @__PURE__ */ jsx("ol", { className: "flex flex-col gap-0.5 text-xs text-foreground", children: preview.runs.map((run) => /* @__PURE__ */ jsxs(
2334
+ "li",
2335
+ {
2336
+ className: "flex items-center gap-2",
2337
+ "data-testid": `${testId}-item`,
2338
+ children: [
2339
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2022" }),
2340
+ /* @__PURE__ */ jsx("time", { dateTime: run.toISOString(), children: formatRunInZone(run, timezone) })
2341
+ ]
2342
+ },
2343
+ run.toISOString()
2344
+ )) })
2345
+ ]
2346
+ }
2347
+ );
2348
+ }
2349
+ var SCHEDULE_PREVIEW_COUNT = 5;
2350
+ var DEBOUNCE_MS = 400;
2351
+ var IDLE = {
2352
+ runs: [],
2353
+ loading: false,
2354
+ error: null,
2355
+ previewed: false
2356
+ };
2357
+ function schedulePreviewVariables(rules, timezone, workflowKey, nodeId, now = /* @__PURE__ */ new Date()) {
2358
+ const variables = [];
2359
+ rules.forEach((raw, index) => {
2360
+ const rule = normalizeScheduleRule(raw);
2361
+ if (validateRule(rule).length > 0) return;
2362
+ const spec = ruleToScheduleSpec(
2363
+ rule,
2364
+ scheduleNodeKey(workflowKey, nodeId, index)
2365
+ );
2366
+ variables.push({
2367
+ scheduleType: spec.scheduleType === "CRON" ? "CRON" /* Cron */ : "RRULE" /* Rrule */,
2368
+ ...spec.scheduleType === "CRON" ? { cronExpression: spec.cronExpression } : { rrule: spec.rrule, startDate: now.toISOString() },
2369
+ timezone: timezone || "UTC",
2370
+ count: SCHEDULE_PREVIEW_COUNT
2371
+ });
2372
+ });
2373
+ return variables;
2374
+ }
2375
+ function mergePreviewRuns(lists, limit = SCHEDULE_PREVIEW_COUNT) {
2376
+ const seen = /* @__PURE__ */ new Set();
2377
+ const runs = [];
2378
+ for (const iso of lists.flat()) {
2379
+ const date = new Date(iso);
2380
+ const time = date.getTime();
2381
+ if (Number.isNaN(time) || seen.has(time)) continue;
2382
+ seen.add(time);
2383
+ runs.push(date);
2384
+ }
2385
+ return runs.sort((a, b) => a.getTime() - b.getTime()).slice(0, limit);
2386
+ }
2387
+ async function requestSchedulePreview(client, variables) {
2388
+ const results = await Promise.all(
2389
+ variables.map(
2390
+ (vars) => client.query({
2391
+ query: PreviewScheduleDocument,
2392
+ variables: vars,
2393
+ fetchPolicy: "network-only"
2394
+ })
2395
+ )
2396
+ );
2397
+ return mergePreviewRuns(
2398
+ results.map((result) => result.data?.previewSchedule.nextExecutions ?? [])
2399
+ );
2400
+ }
2401
+ function useSchedulePreview(rules, timezone, workflowKey, nodeId) {
2402
+ const [state, setState] = useState(IDLE);
2403
+ const context = useContext(FluidGridsContext);
2404
+ const client = context?.workspaceClient ?? getFluidGridsClientSafe();
2405
+ const requestKey = JSON.stringify([rules, timezone, workflowKey, nodeId]);
2406
+ const latest = useRef(0);
2407
+ useEffect(() => {
2408
+ if (!nodeId || !client) {
2409
+ setState(IDLE);
2410
+ return;
2411
+ }
2412
+ const variables = schedulePreviewVariables(
2413
+ rules,
2414
+ timezone,
2415
+ workflowKey,
2416
+ nodeId
2417
+ );
2418
+ if (variables.length === 0) {
2419
+ setState(IDLE);
2420
+ return;
2421
+ }
2422
+ const ticket = ++latest.current;
2423
+ setState((prev) => ({ ...prev, loading: true, error: null }));
2424
+ const timer = setTimeout(() => {
2425
+ requestSchedulePreview(client, variables).then((runs) => {
2426
+ if (latest.current !== ticket) return;
2427
+ setState({ runs, loading: false, error: null, previewed: true });
2428
+ }).catch((error) => {
2429
+ if (latest.current !== ticket) return;
2430
+ setState({
2431
+ runs: [],
2432
+ loading: false,
2433
+ error: error instanceof Error ? error : new Error(String(error)),
2434
+ previewed: true
2435
+ });
2436
+ });
2437
+ }, DEBOUNCE_MS);
2438
+ return () => clearTimeout(timer);
2439
+ }, [requestKey, client]);
2440
+ return state;
2441
+ }
2442
+ var TIMEZONE_ALIASES = {
2443
+ "Africa/Asmera": "Africa/Asmara",
2444
+ "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
2445
+ "America/Catamarca": "America/Argentina/Catamarca",
2446
+ "America/Cordoba": "America/Argentina/Cordoba",
2447
+ "America/Godthab": "America/Nuuk",
2448
+ "America/Indianapolis": "America/Indiana/Indianapolis",
2449
+ "America/Jujuy": "America/Argentina/Jujuy",
2450
+ "America/Louisville": "America/Kentucky/Louisville",
2451
+ "America/Mendoza": "America/Argentina/Mendoza",
2452
+ "Asia/Ashkhabad": "Asia/Ashgabat",
2453
+ "Asia/Calcutta": "Asia/Kolkata",
2454
+ "Asia/Dacca": "Asia/Dhaka",
2455
+ "Asia/Katmandu": "Asia/Kathmandu",
2456
+ "Asia/Macao": "Asia/Macau",
2457
+ "Asia/Rangoon": "Asia/Yangon",
2458
+ "Asia/Saigon": "Asia/Ho_Chi_Minh",
2459
+ "Asia/Thimbu": "Asia/Thimphu",
2460
+ "Asia/Ujung_Pandang": "Asia/Makassar",
2461
+ "Asia/Ulan_Bator": "Asia/Ulaanbaatar",
2462
+ "Atlantic/Faeroe": "Atlantic/Faroe",
2463
+ "Europe/Kiev": "Europe/Kyiv",
2464
+ "Pacific/Enderbury": "Pacific/Kanton",
2465
+ "Pacific/Ponape": "Pacific/Pohnpei",
2466
+ "Pacific/Truk": "Pacific/Chuuk"
2467
+ };
2468
+ var LEGACY_IDS_BY_ZONE = (() => {
2469
+ const byZone = /* @__PURE__ */ new Map();
2470
+ for (const [legacy, current] of Object.entries(TIMEZONE_ALIASES)) {
2471
+ byZone.set(current, [...byZone.get(current) ?? [], legacy]);
2472
+ }
2473
+ return byZone;
2474
+ })();
2475
+ function isKnownTimezone(zone) {
2476
+ try {
2477
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
2478
+ return true;
2479
+ } catch {
2480
+ return false;
2481
+ }
2482
+ }
2483
+ function canonicalTimezone(zone) {
2484
+ const current = TIMEZONE_ALIASES[zone];
2485
+ if (current === void 0) return zone;
2486
+ return isKnownTimezone(current) ? current : zone;
2487
+ }
2488
+ function supportedTimezones() {
2489
+ let zones = [];
2490
+ try {
2491
+ zones = Intl.supportedValuesOf("timeZone");
2492
+ } catch {
2493
+ zones = [];
2494
+ }
2495
+ const current = new Set(zones.map(canonicalTimezone));
2496
+ current.delete("UTC");
2497
+ return ["UTC", ...[...current].sort()];
2498
+ }
2499
+ function timezoneOffsetLabel(zone, at = /* @__PURE__ */ new Date()) {
2500
+ try {
2501
+ const part = new Intl.DateTimeFormat("en-US", {
2502
+ timeZone: zone,
2503
+ timeZoneName: "longOffset"
2504
+ }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
2505
+ if (!part) return "";
2506
+ const offset = part === "GMT" ? "+00:00" : part.replace(/^GMT/, "");
2507
+ return `(UTC${offset})`;
2508
+ } catch {
2509
+ return "";
2510
+ }
2511
+ }
2512
+ function filterTimezones(zones, query) {
2513
+ const needle = query.trim().toLowerCase().replace(/\s+/g, "_");
2514
+ if (needle === "") return [...zones];
2515
+ const matches = (id) => id.toLowerCase().includes(needle);
2516
+ return zones.filter(
2517
+ (zone) => matches(zone) || (LEGACY_IDS_BY_ZONE.get(zone) ?? []).some(matches)
2518
+ );
2519
+ }
2520
+ function TimezoneSelect({
2521
+ value,
2522
+ onChange,
2523
+ className,
2524
+ testId
2525
+ }) {
2526
+ const tr = useTr();
2527
+ const [open, setOpen] = useState(false);
2528
+ const [query, setQuery] = useState("");
2529
+ const zones = useMemo(supportedTimezones, []);
2530
+ const matches = useMemo(() => filterTimezones(zones, query), [zones, query]);
2531
+ const current = value || "UTC";
2532
+ const offset = timezoneOffsetLabel(current);
2533
+ const select = (zone) => {
2534
+ onChange(zone);
2535
+ setOpen(false);
2536
+ setQuery("");
2537
+ };
2538
+ return /* @__PURE__ */ jsxs(
2539
+ Popover,
2540
+ {
2541
+ open,
2542
+ onOpenChange: (next) => {
2543
+ setOpen(next);
2544
+ if (!next) setQuery("");
2545
+ },
2546
+ children: [
2547
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2548
+ "button",
2549
+ {
2550
+ type: "button",
2551
+ role: "combobox",
2552
+ "aria-expanded": open,
2553
+ "aria-haspopup": "listbox",
2554
+ "aria-label": tr(
2555
+ "fluidgrids.builder.scheduleRules.timezoneLabel",
2556
+ "Timezone"
2557
+ ),
2558
+ "data-testid": testId,
2559
+ className: cn(
2560
+ className,
2561
+ "flex items-center justify-between gap-2 text-left"
2562
+ ),
2563
+ children: [
2564
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
2565
+ current,
2566
+ offset && /* @__PURE__ */ jsx("span", { className: "ml-1 text-muted-foreground", children: offset })
2567
+ ] }),
2568
+ /* @__PURE__ */ jsx(
2569
+ Icon,
2570
+ {
2571
+ icon: "mdi:unfold-more-horizontal",
2572
+ className: "size-4 shrink-0 text-muted-foreground"
2573
+ }
2574
+ )
2575
+ ]
2576
+ }
2577
+ ) }),
2578
+ /* @__PURE__ */ jsxs(
2579
+ PopoverContent,
2580
+ {
2581
+ align: "start",
2582
+ className: "w-[var(--radix-popover-trigger-width)] min-w-64 p-0",
2583
+ style: { zIndex: NDV_OVERLAY_Z_INDEX },
2584
+ children: [
2585
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3", children: [
2586
+ /* @__PURE__ */ jsx(
2587
+ Icon,
2588
+ {
2589
+ icon: "mdi:magnify",
2590
+ className: "size-4 shrink-0 text-muted-foreground"
2591
+ }
2592
+ ),
2593
+ /* @__PURE__ */ jsx(
2594
+ "input",
2595
+ {
2596
+ type: "text",
2597
+ value: query,
2598
+ onChange: (event) => setQuery(event.target.value),
2599
+ onKeyDown: (event) => {
2600
+ if (event.key === "Enter" && matches[0]) {
2601
+ event.preventDefault();
2602
+ select(matches[0]);
2603
+ }
2604
+ },
2605
+ placeholder: tr(
2606
+ "fluidgrids.builder.scheduleRules.timezoneSearch",
2607
+ "Search timezones\u2026"
2608
+ ),
2609
+ "aria-label": tr(
2610
+ "fluidgrids.builder.scheduleRules.timezoneSearch",
2611
+ "Search timezones\u2026"
2612
+ ),
2613
+ autoFocus: true,
2614
+ "data-testid": testId ? `${testId}-search` : void 0,
2615
+ className: "h-9 flex-1 border-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
2616
+ }
2617
+ )
2618
+ ] }),
2619
+ /* @__PURE__ */ jsxs(
2620
+ "ul",
2621
+ {
2622
+ role: "listbox",
2623
+ "aria-label": tr(
2624
+ "fluidgrids.builder.scheduleRules.timezoneLabel",
2625
+ "Timezone"
2626
+ ),
2627
+ className: "max-h-64 overflow-y-auto py-1",
2628
+ children: [
2629
+ matches.length === 0 && /* @__PURE__ */ jsx("li", { className: "py-4 text-center text-xs text-muted-foreground", children: tr(
2630
+ "fluidgrids.builder.scheduleRules.timezoneEmpty",
2631
+ "No timezone matches"
2632
+ ) }),
2633
+ matches.slice(0, 400).map((zone) => /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": zone === current, children: /* @__PURE__ */ jsxs(
2634
+ "button",
2635
+ {
2636
+ type: "button",
2637
+ onClick: () => select(zone),
2638
+ "data-testid": testId ? `${testId}-option` : void 0,
2639
+ "data-value": zone,
2640
+ className: cn(
2641
+ "flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted",
2642
+ zone === current && "bg-muted/60"
2643
+ ),
2644
+ children: [
2645
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: zone }),
2646
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: timezoneOffsetLabel(zone) }),
2647
+ zone === current && /* @__PURE__ */ jsx(Icon, { icon: "mdi:check", className: "size-3.5 shrink-0" })
2648
+ ]
2649
+ }
2650
+ ) }, zone))
2651
+ ]
2652
+ }
2653
+ )
2654
+ ]
2655
+ }
2656
+ )
2657
+ ]
2658
+ }
2659
+ );
2660
+ }
2661
+ var PLATFORM_EVENT_MODULE_FIELD = "platform-event-module";
2662
+ var PLATFORM_EVENT_RECORD_TYPE_FIELD = "platform-event-record-type";
2663
+ var PLATFORM_EVENT_SOURCES_QUERY = gql`
2664
+ query PlatformEventSources {
2665
+ platformEventSources {
2666
+ module
2667
+ recordTypes {
2668
+ name
2669
+ actions
2670
+ }
2671
+ }
2672
+ }
2673
+ `;
2674
+ var CACHE_TTL_MS2 = 3 * 60 * 1e3;
2675
+ var caches = /* @__PURE__ */ new WeakMap();
2676
+ function isRecord2(value) {
2677
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2678
+ }
2679
+ function readPlatformEventSources(data) {
2680
+ const list = isRecord2(data) ? data.platformEventSources : void 0;
2681
+ if (!Array.isArray(list)) return [];
2682
+ const sources = [];
2683
+ for (const item of list) {
2684
+ if (!isRecord2(item) || typeof item.module !== "string") continue;
2685
+ const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap(
2686
+ (type) => isRecord2(type) && typeof type.name === "string" ? [
2687
+ {
2688
+ name: type.name,
2689
+ actions: Array.isArray(type.actions) ? type.actions.filter(
2690
+ (action) => typeof action === "string"
2691
+ ) : []
2692
+ }
2693
+ ] : []
2694
+ ) : [];
2695
+ sources.push({ module: item.module, recordTypes });
2696
+ }
2697
+ return sources;
2698
+ }
2699
+ function fetchPlatformEventSources(getClient = getFluidGridsClient, now = Date.now) {
2700
+ const client = getClient();
2701
+ const cached = caches.get(client);
2702
+ if (cached && cached.expiresAt > now()) return cached.sources;
2703
+ const sources = client.query({ query: PLATFORM_EVENT_SOURCES_QUERY, fetchPolicy: "network-only" }).then((result) => readPlatformEventSources(result.data));
2704
+ const entry = { sources, expiresAt: now() + CACHE_TTL_MS2 };
2705
+ caches.set(client, entry);
2706
+ sources.catch(() => {
2707
+ if (caches.get(client) === entry) caches.delete(client);
2708
+ });
2709
+ return sources;
2710
+ }
2711
+ function recordTypesOf(sources, module) {
2712
+ const wanted = module.trim().toLowerCase();
2713
+ return sources.find((source) => source.module === wanted)?.recordTypes ?? [];
2714
+ }
2715
+ function filterNames(names, query) {
2716
+ const needle = query.trim().toLowerCase();
2717
+ if (!needle) return [...names];
2718
+ return names.filter((name) => name.toLowerCase().includes(needle));
2719
+ }
2720
+ function isTypedName(kind, text) {
2721
+ return kind === "module" ? /^[a-z][a-z0-9-]{0,127}$/.test(text.trim().toLowerCase()) : /^[A-Z]\w{0,127}$/.test(text.trim());
2722
+ }
2723
+ function usePlatformEventSources() {
2724
+ const [state, setState] = useState({ status: "loading" });
2725
+ useEffect(() => {
2726
+ let cancelled = false;
2727
+ fetchPlatformEventSources().then((sources) => {
2728
+ if (!cancelled) setState({ status: "ready", sources });
2729
+ }).catch(() => {
2730
+ if (!cancelled) setState({ status: "failed" });
2731
+ });
2732
+ return () => {
2733
+ cancelled = true;
2734
+ };
2735
+ }, []);
2736
+ return state;
2737
+ }
2738
+ function PlatformEventSourceSelect({
2739
+ kind,
2740
+ value,
2741
+ module = "",
2742
+ required = false,
2743
+ onChange,
2744
+ className,
2745
+ testId,
2746
+ label
2747
+ }) {
2748
+ const tr = useTr();
2749
+ const state = usePlatformEventSources();
2750
+ const [open, setOpen] = useState(false);
2751
+ const [query, setQuery] = useState("");
2752
+ const names = useMemo(() => {
2753
+ if (state.status !== "ready") return [];
2754
+ return kind === "module" ? state.sources.map((source) => source.module) : recordTypesOf(state.sources, module).map((type) => type.name);
2755
+ }, [state, kind, module]);
2756
+ const actionsByType = useMemo(() => {
2757
+ if (state.status !== "ready" || kind !== "recordType") return null;
2758
+ return new Map(
2759
+ recordTypesOf(state.sources, module).map((type) => [
2760
+ type.name,
2761
+ type.actions
2762
+ ])
2763
+ );
2764
+ }, [state, kind, module]);
2765
+ const matches = useMemo(() => filterNames(names, query), [names, query]);
2766
+ const typed = query.trim();
2767
+ const canUseTyped = typed !== "" && isTypedName(kind, typed) && !names.includes(kind === "module" ? typed.toLowerCase() : typed);
2768
+ const fieldLabel = label ?? (kind === "module" ? tr("fluidgrids.builder.platformEventSource.moduleLabel", "Module") : tr(
2769
+ "fluidgrids.builder.platformEventSource.recordTypeLabel",
2770
+ "Record Type"
2771
+ ));
2772
+ const emptyText = kind === "module" ? tr(
2773
+ "fluidgrids.builder.platformEventSource.modulePlaceholder",
2774
+ "Select a module"
2775
+ ) : required ? tr(
2776
+ "fluidgrids.builder.platformEventSource.recordTypePlaceholder",
2777
+ "Select a record type"
2778
+ ) : tr(
2779
+ "fluidgrids.builder.platformEventSource.anyRecordType",
2780
+ "Any record type"
2781
+ );
2782
+ const select = (next) => {
2783
+ onChange(next);
2784
+ setOpen(false);
2785
+ setQuery("");
2786
+ };
2787
+ const notice = state.status === "failed" ? tr(
2788
+ "fluidgrids.builder.platformEventSource.loadFailed",
2789
+ "The list could not be loaded. Type the name instead."
2790
+ ) : kind === "recordType" && !module.trim() ? tr(
2791
+ "fluidgrids.builder.platformEventSource.pickModuleFirst",
2792
+ "Pick a module first to see its record types."
2793
+ ) : null;
2794
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2795
+ /* @__PURE__ */ jsxs(
2796
+ Popover,
2797
+ {
2798
+ open,
2799
+ onOpenChange: (next) => {
2800
+ setOpen(next);
2801
+ if (!next) setQuery("");
2802
+ },
2803
+ children: [
2804
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2805
+ "button",
2806
+ {
2807
+ type: "button",
2808
+ role: "combobox",
2809
+ "aria-expanded": open,
2810
+ "aria-haspopup": "listbox",
2811
+ "aria-label": fieldLabel,
2812
+ "data-testid": testId,
2813
+ className: cn(
2814
+ className,
2815
+ "flex items-center justify-between gap-2 text-left"
2816
+ ),
2817
+ children: [
2818
+ /* @__PURE__ */ jsx("span", { className: cn("truncate", !value && "text-muted-foreground"), children: value || emptyText }),
2819
+ /* @__PURE__ */ jsx(
2820
+ Icon,
2821
+ {
2822
+ icon: "mdi:unfold-more-horizontal",
2823
+ className: "size-4 shrink-0 text-muted-foreground"
2824
+ }
2825
+ )
2826
+ ]
2827
+ }
2828
+ ) }),
2829
+ /* @__PURE__ */ jsxs(
2830
+ PopoverContent,
2831
+ {
2832
+ align: "start",
2833
+ className: "w-[var(--radix-popover-trigger-width)] min-w-64 p-0",
2834
+ style: { zIndex: NDV_OVERLAY_Z_INDEX },
2835
+ children: [
2836
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-3", children: [
2837
+ /* @__PURE__ */ jsx(
2838
+ Icon,
2839
+ {
2840
+ icon: "mdi:magnify",
2841
+ className: "size-4 shrink-0 text-muted-foreground"
2842
+ }
2843
+ ),
2844
+ /* @__PURE__ */ jsx(
2845
+ "input",
2846
+ {
2847
+ type: "text",
2848
+ value: query,
2849
+ onChange: (event) => setQuery(event.target.value),
2850
+ onKeyDown: (event) => {
2851
+ if (event.key !== "Enter") return;
2852
+ event.preventDefault();
2853
+ if (matches[0]) select(matches[0]);
2854
+ else if (canUseTyped)
2855
+ select(kind === "module" ? typed.toLowerCase() : typed);
2856
+ },
2857
+ placeholder: tr(
2858
+ "fluidgrids.builder.platformEventSource.search",
2859
+ "Search or type a name\u2026"
2860
+ ),
2861
+ "aria-label": tr(
2862
+ "fluidgrids.builder.platformEventSource.search",
2863
+ "Search or type a name\u2026"
2864
+ ),
2865
+ autoFocus: true,
2866
+ "data-testid": testId ? `${testId}-search` : void 0,
2867
+ className: "h-9 flex-1 border-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
2868
+ }
2869
+ )
2870
+ ] }),
2871
+ /* @__PURE__ */ jsxs(
2872
+ "ul",
2873
+ {
2874
+ role: "listbox",
2875
+ "aria-label": fieldLabel,
2876
+ className: "max-h-64 overflow-y-auto py-1",
2877
+ children: [
2878
+ kind === "recordType" && !required && /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": value === "", children: /* @__PURE__ */ jsx(
2879
+ "button",
2880
+ {
2881
+ type: "button",
2882
+ onClick: () => select(""),
2883
+ "data-testid": testId ? `${testId}-any` : void 0,
2884
+ className: cn(
2885
+ "flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-1.5 text-left text-sm italic text-muted-foreground hover:bg-muted",
2886
+ value === "" && "bg-muted/60"
2887
+ ),
2888
+ children: tr(
2889
+ "fluidgrids.builder.platformEventSource.anyRecordType",
2890
+ "Any record type"
2891
+ )
2892
+ }
2893
+ ) }),
2894
+ state.status === "loading" && /* @__PURE__ */ jsx("li", { className: "py-3 text-center text-xs text-muted-foreground", children: tr(
2895
+ "fluidgrids.builder.platformEventSource.loading",
2896
+ "Loading\u2026"
2897
+ ) }),
2898
+ state.status === "ready" && matches.length === 0 && !canUseTyped && /* @__PURE__ */ jsx("li", { className: "py-3 text-center text-xs text-muted-foreground", children: tr(
2899
+ "fluidgrids.builder.platformEventSource.empty",
2900
+ "Nothing matches. Type the full name to use it."
2901
+ ) }),
2902
+ matches.map((name) => {
2903
+ const actions = actionsByType?.get(name);
2904
+ return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": name === value, children: /* @__PURE__ */ jsxs(
2905
+ "button",
2906
+ {
2907
+ type: "button",
2908
+ onClick: () => select(name),
2909
+ "data-testid": testId ? `${testId}-option` : void 0,
2910
+ "data-value": name,
2911
+ className: cn(
2912
+ "flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted",
2913
+ name === value && "bg-muted/60"
2914
+ ),
2915
+ children: [
2916
+ /* @__PURE__ */ jsx("span", { className: "truncate font-mono text-xs", children: name }),
2917
+ actions && actions.length > 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: actions.join(", ") }),
2918
+ name === value && /* @__PURE__ */ jsx(Icon, { icon: "mdi:check", className: "size-3.5 shrink-0" })
2919
+ ]
2920
+ }
2921
+ ) }, name);
2922
+ }),
2923
+ canUseTyped && /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": false, children: /* @__PURE__ */ jsxs(
2924
+ "button",
2925
+ {
2926
+ type: "button",
2927
+ onClick: () => select(kind === "module" ? typed.toLowerCase() : typed),
2928
+ "data-testid": testId ? `${testId}-typed` : void 0,
2929
+ className: "flex w-full cursor-pointer items-center gap-2 px-3 py-1.5 text-left text-sm hover:bg-muted",
2930
+ children: [
2931
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:keyboard-outline", className: "size-3.5" }),
2932
+ /* @__PURE__ */ jsxs("span", { children: [
2933
+ tr(
2934
+ "fluidgrids.builder.platformEventSource.useTyped",
2935
+ "Use"
2936
+ ),
2937
+ " ",
2938
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: kind === "module" ? typed.toLowerCase() : typed })
2939
+ ] })
2940
+ ]
2941
+ }
2942
+ ) })
2943
+ ]
2944
+ }
2945
+ )
2946
+ ]
2947
+ }
2948
+ )
2949
+ ]
2950
+ }
2951
+ ),
2952
+ notice && /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: notice })
2953
+ ] });
2954
+ }
2955
+ var SECTION_LABEL3 = "text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground";
2956
+ function NodeParamsForm({
2957
+ config,
2958
+ nodeType,
2959
+ testIdPrefix = "ndv"
2960
+ }) {
2961
+ const formId = useId();
2962
+ const tr = useTr();
2963
+ const edges = useFlowStore((state) => state.workflow.edges);
2964
+ const loopStatus = (nodeType === "internal:loop" || nodeType === "loop") && config.node ? loopReturnStatus(
2965
+ config.node.id,
2966
+ edges,
2967
+ typeof config.argsState.loopHandle === "string" ? config.argsState.loopHandle : "loop"
2968
+ ) : void 0;
2969
+ const {
2970
+ params,
2971
+ argsState,
2972
+ mapping,
2973
+ pickerFields,
2974
+ connectionAdapters,
2975
+ selectedConnections,
2976
+ isWebhookTrigger,
2977
+ isFormTrigger,
2978
+ formUrl,
2979
+ triggerUrlError,
2980
+ isWebhookWait,
2981
+ webhookUrl,
2982
+ webhookUrlLoading,
2983
+ currentEdge,
2984
+ modeByField,
2985
+ dragField,
2986
+ setDragField,
2987
+ writeFixed,
2988
+ writeMapping,
2989
+ handleConnectionsChange,
2990
+ toggleMode,
2991
+ handleDropOnParam
2992
+ } = config;
2993
+ const loadOptionsConnectionId = (() => {
2994
+ const adapterNames = new Set(connectionAdapters.map((a) => a.name));
2995
+ return selectedConnections.find((c) => adapterNames.has(c.adapter))?.connectionId ?? selectedConnections[0]?.connectionId;
2996
+ })();
2997
+ const parameterDefaults = Object.fromEntries(
2998
+ params.map((parameter) => [parameter.name, parameter.defaultValue])
2999
+ );
3000
+ const visibleParams = params.filter(
3001
+ (p) => mapping[p.name] !== void 0 || isParamVisible(p, argsState, parameterDefaults)
3002
+ );
3003
+ const scheduleParam = params.find((p) => p.type === "schedule-rules");
3004
+ const scheduleIsLegacy = Boolean(scheduleParam) && isLegacyScheduleState(argsState);
3005
+ const scheduleRules = scheduleParam ? Array.isArray(argsState[scheduleParam.name]) ? argsState[scheduleParam.name] : normalizeScheduleState(argsState).rules.map((rule) => ({ ...rule })) : [];
3006
+ const scheduleTimezone = typeof argsState.timezone === "string" && argsState.timezone.trim() ? argsState.timezone : "UTC";
3007
+ const workflowKey = useFlowStore((s) => s.workflow.workflowKey);
3008
+ const schedulePreview = useSchedulePreview(
3009
+ scheduleRules,
3010
+ scheduleTimezone,
3011
+ workflowKey || void 0,
3012
+ scheduleParam ? config.node?.id : void 0
3013
+ );
3014
+ const nodeVersion = (() => {
3015
+ const data = asRecord(config.node?.data);
3016
+ const nodeData = asRecord(data?.nodeData);
3017
+ const fromManifest = nodeData?.version;
3018
+ const fromData = data?.version;
3019
+ if (typeof fromManifest === "string") return fromManifest;
3020
+ if (typeof fromData === "string") return fromData;
3021
+ return "1.0.0";
3022
+ })();
3023
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3024
+ loopStatus === "missing-return" && /* @__PURE__ */ jsx(
3025
+ "p",
3026
+ {
3027
+ role: "status",
3028
+ "data-testid": `${testIdPrefix}-loop-return-hint`,
3029
+ className: "text-xs text-muted-foreground",
3030
+ children: tr(
3031
+ "fluidgrids.builder.loopPanel.missingReturn",
3032
+ "No Loop Body return path is connected. Only the first batch will run unless you connect a body node back to this Loop. An intentional early exit is allowed; Done does not run on that exit."
3033
+ )
3034
+ }
3035
+ ),
3036
+ loopStatus === "conditional-exit" && /* @__PURE__ */ jsx(
3037
+ "p",
3038
+ {
3039
+ role: "status",
3040
+ "data-testid": `${testIdPrefix}-loop-return-hint`,
3041
+ className: "text-xs text-muted-foreground",
3042
+ children: tr(
3043
+ "fluidgrids.builder.loopPanel.conditionalExit",
3044
+ "A return path and a possible early exit are connected. Conditional exits are allowed; Done runs only when the Loop finishes or reaches its safety limit."
3045
+ )
3046
+ }
3047
+ ),
3048
+ isWebhookTrigger && /* @__PURE__ */ jsxs(Fragment, { children: [
3049
+ /* @__PURE__ */ jsx(
3050
+ WebhookUrlField,
3051
+ {
3052
+ webhookUrl,
3053
+ loading: webhookUrlLoading
3054
+ }
3055
+ ),
3056
+ /* @__PURE__ */ jsx(WebhookTestListenerField, { nodeId: config.node?.id })
3057
+ ] }),
3058
+ isFormTrigger && /* @__PURE__ */ jsx(
3059
+ FormUrlField,
3060
+ {
3061
+ formUrl,
3062
+ loading: webhookUrlLoading,
3063
+ error: triggerUrlError
3064
+ }
3065
+ ),
3066
+ isWebhookWait && /* @__PURE__ */ jsx(WaitResumeUrlField, {}),
3067
+ isAgentNodeType(nodeType) && /* @__PURE__ */ jsx(
3068
+ AgentDefinitionSelectField,
3069
+ {
3070
+ value: typeof argsState.agentDefinitionId === "string" ? argsState.agentDefinitionId : null,
3071
+ onChange: (next) => writeFixed("agentDefinitionId", next),
3072
+ sectionLabelClassName: SECTION_LABEL3,
3073
+ testIdPrefix
3074
+ }
3075
+ ),
3076
+ connectionAdapters.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-connections`, children: [
3077
+ /* @__PURE__ */ jsx("div", { className: SECTION_LABEL3, children: tr(
3078
+ "fluidgrids.builder.nodeParamsForm.connectionsSection",
3079
+ "Connections"
3080
+ ) }),
3081
+ /* @__PURE__ */ jsx(
3082
+ ConnectionSelector,
3083
+ {
3084
+ adapters: connectionAdapters,
3085
+ selectedConnections,
3086
+ onChange: handleConnectionsChange,
3087
+ compact: true
3088
+ }
3089
+ )
3090
+ ] }),
3091
+ !currentEdge && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: tr(
3092
+ "fluidgrids.builder.nodeParamsForm.noUpstreamNode",
3093
+ "No upstream node connected \u2014 set fixed values below, or connect a node to map fields with expressions."
3094
+ ) }),
3095
+ params.length === 0 ? /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-xs text-muted-foreground", children: tr(
3096
+ "fluidgrids.builder.nodeParamsForm.noParameters",
3097
+ "This operation has no input parameters."
3098
+ ) }) : visibleParams.map((p) => {
3099
+ if (p.type === "schedule-rules") {
3100
+ return /* @__PURE__ */ jsxs(
3101
+ "div",
3102
+ {
3103
+ className: "flex flex-col gap-2",
3104
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3105
+ children: [
3106
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: tr(
3107
+ "fluidgrids.builder.scheduleRules.notice",
3108
+ "This workflow will run on the schedule you define here once you activate it. For testing, you can also trigger it manually from the canvas."
3109
+ ) }),
3110
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: p.label ?? tr(
3111
+ "fluidgrids.builder.scheduleRules.rulesLabel",
3112
+ "Trigger Rules"
3113
+ ) }),
3114
+ scheduleIsLegacy && /* @__PURE__ */ jsx(
3115
+ "span",
3116
+ {
3117
+ className: "text-[11px] leading-snug text-muted-foreground",
3118
+ "data-testid": `${testIdPrefix}-schedule-legacy-note`,
3119
+ children: tr(
3120
+ "fluidgrids.builder.scheduleRules.legacyNote",
3121
+ "Converted from the previous schedule settings \u2014 save the workflow to keep the new rules."
3122
+ )
3123
+ }
3124
+ ),
3125
+ /* @__PURE__ */ jsx(
3126
+ ScheduleRulesEditor,
3127
+ {
3128
+ rules: scheduleRules,
3129
+ ruleFields: p.fields,
3130
+ onChange: (rules) => writeFixed(p.name, rules),
3131
+ testIdPrefix
3132
+ }
3133
+ ),
3134
+ /* @__PURE__ */ jsx(
3135
+ ScheduleNextRuns,
3136
+ {
3137
+ preview: schedulePreview,
3138
+ timezone: scheduleTimezone,
3139
+ testId: `${testIdPrefix}-schedule-next-runs`
3140
+ }
3141
+ ),
3142
+ p.description && /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: p.description })
3143
+ ]
3144
+ },
3145
+ p.name
3146
+ );
3147
+ }
3148
+ if (p.type === "condition") {
3149
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
3150
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: p.name === "conditionTree" ? tr(
3151
+ "fluidgrids.builder.nodeParamsForm.conditionsLabel",
3152
+ "Conditions"
3153
+ ) : p.label ?? p.name }),
3154
+ /* @__PURE__ */ jsx(
3155
+ ConditionTreeBuilder,
3156
+ {
3157
+ value: argsState[p.name],
3158
+ fields: pickerFields,
3159
+ onChange: (tree) => writeFixed(p.name, tree)
3160
+ }
3161
+ ),
3162
+ p.description && /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: p.description })
3163
+ ] }, p.name);
3164
+ }
3165
+ if (p.type === "switch-rules") {
3166
+ const rules = Array.isArray(argsState[p.name]) ? argsState[p.name] : [];
3167
+ const fallbackHandle = typeof argsState.fallbackHandle === "string" ? argsState.fallbackHandle : "default";
3168
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
3169
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: tr(
3170
+ "fluidgrids.builder.nodeParamsForm.routingRulesLabel",
3171
+ "Routing rules"
3172
+ ) }),
3173
+ /* @__PURE__ */ jsx(
3174
+ SwitchRulesEditor,
3175
+ {
3176
+ rules,
3177
+ fallbackHandle,
3178
+ fields: pickerFields,
3179
+ onChange: (next) => {
3180
+ writeFixed(p.name, next.rules);
3181
+ writeFixed("cases", casesFromRules(next.rules));
3182
+ writeFixed("fallbackHandle", next.fallbackHandle);
3183
+ }
3184
+ }
3185
+ )
3186
+ ] }, p.name);
3187
+ }
3188
+ if (nodeType === "internal:assert" && p.name === "rules") {
3189
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
3190
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3191
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: tr(
3192
+ "fluidgrids.builder.nodeParamsForm.assertRulesLabel",
3193
+ "Rules"
3194
+ ) }),
3195
+ p.required && /* @__PURE__ */ jsx("span", { className: "text-destructive", children: "*" })
3196
+ ] }),
3197
+ /* @__PURE__ */ jsx(
3198
+ AssertRulesEditor,
3199
+ {
3200
+ value: argsState[p.name],
3201
+ onChange: (next) => writeFixed(p.name, next),
3202
+ testIdPrefix
3203
+ }
3204
+ )
3205
+ ] }, p.name);
3206
+ }
3207
+ if (p.type === "workflow" || nodeType === "internal:subworkflow" && p.name === "workflowKey") {
3208
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
3209
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3210
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-foreground", children: tr(
3211
+ "fluidgrids.builder.nodeParamsForm.workflowLabel",
3212
+ "Workflow to call"
3213
+ ) }),
3214
+ p.required && /* @__PURE__ */ jsx("span", { className: "text-destructive", children: "*" })
3215
+ ] }),
3216
+ /* @__PURE__ */ jsx(
3217
+ WorkflowPickerField,
3218
+ {
3219
+ value: typeof argsState[p.name] === "string" ? argsState[p.name] : "",
3220
+ onChange: (workflowKey2) => writeFixed(p.name, workflowKey2),
3221
+ testId: `${testIdPrefix}-workflow-picker`
3222
+ }
3223
+ ),
3224
+ p.description && /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: p.description })
3225
+ ] }, p.name);
3226
+ }
3227
+ const mode = modeByField[p.name] ?? (mapping[p.name] !== void 0 ? "expression" : "fixed");
3228
+ const exprValue = mapping[p.name];
3229
+ const fixedValue = argsState[p.name];
3230
+ const structuredType = structuredParameterType(p.schema ?? p.type);
3231
+ const optionList = Boolean(p.loadOptions) && isStringOptionList(p.schema ?? p.type);
3232
+ const booleanChecked = p.type === "boolean" && booleanParameterValue(fixedValue) === true;
3233
+ const isCodeParam = p.type === "code" || nodeType === "internal:code" && p.name === "code";
3234
+ const highlight = dragField === p.name;
3235
+ const controlId = `${formId}-param-${p.name}`;
3236
+ const nativeControl = mode !== "expression" && !isCodeParam && !(structuredType && !optionList) && !p.loadOptions && p.type !== PLATFORM_EVENT_MODULE_FIELD && p.type !== PLATFORM_EVENT_RECORD_TYPE_FIELD;
3237
+ const inputCx = cn(
3238
+ "w-full rounded-md border bg-background px-2.5 py-1.5 text-sm outline-none transition-colors disabled:opacity-50",
3239
+ highlight ? "border-primary ring-1 ring-primary" : "border-input focus:border-primary"
3240
+ );
3241
+ return /* @__PURE__ */ jsxs(
3242
+ "div",
3243
+ {
3244
+ className: "flex flex-col gap-1.5",
3245
+ onDragOver: (e) => e.preventDefault(),
3246
+ onDragEnter: () => setDragField(p.name),
3247
+ onDragLeave: () => setDragField((f) => f === p.name ? null : f),
3248
+ onDrop: handleDropOnParam(p.name),
3249
+ children: [
3250
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3251
+ /* @__PURE__ */ jsx(
3252
+ "label",
3253
+ {
3254
+ htmlFor: nativeControl ? controlId : void 0,
3255
+ className: "text-xs font-medium text-foreground",
3256
+ title: p.label && p.label !== p.name ? p.name : void 0,
3257
+ children: p.label ?? p.name
3258
+ }
3259
+ ),
3260
+ p.required && /* @__PURE__ */ jsx("span", { className: "text-destructive", children: "*" }),
3261
+ /* @__PURE__ */ jsx("span", { className: "ml-auto text-[10px] uppercase tracking-wide text-muted-foreground", children: p.type }),
3262
+ /* @__PURE__ */ jsx(
3263
+ "button",
3264
+ {
3265
+ type: "button",
3266
+ onClick: () => toggleMode(p.name, mode),
3267
+ title: mode === "expression" ? tr(
3268
+ "fluidgrids.builder.nodeParamsForm.switchToFixed",
3269
+ "Switch to a fixed value"
3270
+ ) : tr(
3271
+ "fluidgrids.builder.nodeParamsForm.switchToExpression",
3272
+ "Switch to an expression"
3273
+ ),
3274
+ "data-testid": `${testIdPrefix}-mode-${p.name}`,
3275
+ className: cn(
3276
+ "ml-1 rounded p-0.5 hover:text-foreground",
3277
+ mode === "expression" ? "text-primary" : "text-muted-foreground"
3278
+ ),
3279
+ children: /* @__PURE__ */ jsx(
3280
+ Icon,
3281
+ {
3282
+ icon: mode === "expression" ? "mdi:variable" : "mdi:form-textbox",
3283
+ className: "size-3.5"
3284
+ }
3285
+ )
3286
+ }
3287
+ )
3288
+ ] }),
3289
+ mode === "expression" ? /* @__PURE__ */ jsx(
3290
+ "div",
3291
+ {
3292
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3293
+ className: cn(
3294
+ !currentEdge && "pointer-events-none opacity-50"
3295
+ ),
3296
+ children: /* @__PURE__ */ jsx(
3297
+ MappedParameterInput,
3298
+ {
3299
+ name: p.name,
3300
+ label: p.label,
3301
+ value: exprValue,
3302
+ onChange: (value) => writeMapping(p.name, value),
3303
+ nodeId: config.node?.id,
3304
+ placeholder: currentEdge ? tr(
3305
+ "fluidgrids.builder.nodeParamsForm.expressionPlaceholder",
3306
+ "Drop a field or type ${output.\u2026} or {{ $json.\u2026 }}"
3307
+ ) : tr(
3308
+ "fluidgrids.builder.nodeParamsForm.expressionDisabledPlaceholder",
3309
+ "Connect an upstream node to use expressions"
3310
+ )
3311
+ },
3312
+ `${config.node?.id}:${currentEdge?.id}:${p.name}`
3313
+ )
3314
+ }
3315
+ ) : isCodeParam ? /* @__PURE__ */ jsx(
3316
+ CodeEditorField,
3317
+ {
3318
+ value: typeof fixedValue === "string" ? fixedValue : "",
3319
+ language: normalizeCodeLanguage(
3320
+ argsState.runtime ?? params.find((f) => f.name === "runtime")?.defaultValue
3321
+ ),
3322
+ placeholder: typeof p.defaultValue === "string" ? p.defaultValue : p.placeholder ?? p.description,
3323
+ label: p.label ?? p.name,
3324
+ onChange: (v) => writeFixed(p.name, v),
3325
+ "data-testid": `${testIdPrefix}-param-${p.name}`
3326
+ }
3327
+ ) : structuredType && !optionList ? /* @__PURE__ */ jsx(
3328
+ StructuredParameterInput,
3329
+ {
3330
+ name: p.name,
3331
+ label: p.label,
3332
+ schema: p.schema ?? p.type,
3333
+ value: fixedValue,
3334
+ onChange: (value) => writeFixed(p.name, value),
3335
+ className: cn(inputCx, "font-mono"),
3336
+ testId: `${testIdPrefix}-param-${p.name}`
3337
+ },
3338
+ `${config.node?.id}:${config.currentOp?.name}:${p.name}`
3339
+ ) : p.loadOptions ? /* @__PURE__ */ jsx(
3340
+ NodeLoadOptionsSelect,
3341
+ {
3342
+ nodeType,
3343
+ nodeVersion,
3344
+ spec: p.loadOptions,
3345
+ parameterValues: Object.fromEntries(
3346
+ params.map((parameter) => [
3347
+ parameter.name,
3348
+ argsState[parameter.name]
3349
+ ])
3350
+ ),
3351
+ parameterDefaults: Object.fromEntries(
3352
+ params.map((parameter) => [
3353
+ parameter.name,
3354
+ parameter.defaultValue
3355
+ ])
3356
+ ),
3357
+ unavailableParameters: params.filter(
3358
+ (parameter) => (modeByField[parameter.name] ?? (mapping[parameter.name] !== void 0 ? "expression" : "fixed")) === "expression"
3359
+ ).map((parameter) => parameter.name),
3360
+ multiple: optionList,
3361
+ fieldName: p.name,
3362
+ label: p.label,
3363
+ connectionId: loadOptionsConnectionId,
3364
+ value: fixedValue,
3365
+ onChange: (v) => writeFixed(p.name, v),
3366
+ className: inputCx,
3367
+ testId: `${testIdPrefix}-param-${p.name}`
3368
+ }
3369
+ ) : p.type === "boolean" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3370
+ /* @__PURE__ */ jsxs("label", { className: "flex w-fit cursor-pointer items-center gap-2 text-sm", children: [
3371
+ /* @__PURE__ */ jsx(
3372
+ "input",
3373
+ {
3374
+ type: "checkbox",
3375
+ checked: booleanChecked,
3376
+ onChange: (event) => writeFixed(p.name, event.target.checked),
3377
+ id: controlId,
3378
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3379
+ className: "size-4"
3380
+ }
3381
+ ),
3382
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: fixedValue === void 0 ? tr(
3383
+ "fluidgrids.builder.nodeParamsForm.booleanNotSet",
3384
+ "Not set (default)"
3385
+ ) : booleanChecked ? "true" : "false" })
3386
+ ] }),
3387
+ !p.required && /* @__PURE__ */ jsx(
3388
+ "button",
3389
+ {
3390
+ type: "button",
3391
+ onClick: () => {
3392
+ if (fixedValue !== void 0)
3393
+ writeFixed(p.name, void 0);
3394
+ },
3395
+ "aria-disabled": fixedValue === void 0,
3396
+ "aria-label": tr(
3397
+ "fluidgrids.builder.nodeParamsForm.unsetBooleanLabel",
3398
+ "Not set: {{field}}",
3399
+ { field: p.label ?? p.name }
3400
+ ),
3401
+ "data-testid": `${testIdPrefix}-param-${p.name}-unset`,
3402
+ className: "rounded px-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary aria-disabled:opacity-50",
3403
+ children: tr(
3404
+ "fluidgrids.builder.nodeParamsForm.unsetBoolean",
3405
+ "Not set"
3406
+ )
3407
+ }
3408
+ )
3409
+ ] }) : p.type === PLATFORM_EVENT_MODULE_FIELD ? /* @__PURE__ */ jsx(
3410
+ PlatformEventSourceSelect,
3411
+ {
3412
+ kind: "module",
3413
+ value: typeof fixedValue === "string" ? fixedValue : "",
3414
+ required: p.required,
3415
+ label: p.label,
3416
+ onChange: (next) => {
3417
+ const previous = argsState[p.name];
3418
+ writeFixed(p.name, next || void 0);
3419
+ const typeField = params.find(
3420
+ (f) => f.type === PLATFORM_EVENT_RECORD_TYPE_FIELD
3421
+ );
3422
+ if (typeField && previous !== next)
3423
+ writeFixed(typeField.name, void 0);
3424
+ },
3425
+ className: inputCx,
3426
+ testId: `${testIdPrefix}-param-${p.name}`
3427
+ }
3428
+ ) : p.type === PLATFORM_EVENT_RECORD_TYPE_FIELD ? /* @__PURE__ */ jsx(
3429
+ PlatformEventSourceSelect,
3430
+ {
3431
+ kind: "recordType",
3432
+ value: typeof fixedValue === "string" ? fixedValue : "",
3433
+ module: (() => {
3434
+ const moduleField = params.find(
3435
+ (f) => f.type === PLATFORM_EVENT_MODULE_FIELD
3436
+ );
3437
+ const module = moduleField ? argsState[moduleField.name] : void 0;
3438
+ return typeof module === "string" ? module : "";
3439
+ })(),
3440
+ required: p.required,
3441
+ label: p.label,
3442
+ onChange: (next) => writeFixed(p.name, next || void 0),
3443
+ className: inputCx,
3444
+ testId: `${testIdPrefix}-param-${p.name}`
3445
+ }
3446
+ ) : p.type === "timezone" ? /* @__PURE__ */ jsx(
3447
+ TimezoneSelect,
3448
+ {
3449
+ value: typeof fixedValue === "string" ? fixedValue : "",
3450
+ onChange: (zone) => writeFixed(p.name, zone),
3451
+ className: inputCx,
3452
+ testId: `${testIdPrefix}-param-${p.name}`
3453
+ }
3454
+ ) : p.type === "multiOptions" && p.options && p.options.length > 0 ? /* @__PURE__ */ jsx(
3455
+ "div",
3456
+ {
3457
+ className: "flex flex-wrap gap-x-3 gap-y-1",
3458
+ role: "group",
3459
+ "aria-label": p.label ?? p.name,
3460
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3461
+ children: p.options.map((o) => {
3462
+ const selected = Array.isArray(fixedValue) ? fixedValue.map(String) : [];
3463
+ const checked = selected.includes(o);
3464
+ return /* @__PURE__ */ jsxs(
3465
+ "label",
3466
+ {
3467
+ className: "flex cursor-pointer items-center gap-1 text-sm",
3468
+ children: [
3469
+ /* @__PURE__ */ jsx(
3470
+ "input",
3471
+ {
3472
+ type: "checkbox",
3473
+ checked,
3474
+ onChange: (event) => {
3475
+ const next = new Set(selected);
3476
+ if (event.target.checked) next.add(o);
3477
+ else next.delete(o);
3478
+ const ordered = p.options.filter(
3479
+ (option) => next.has(option)
3480
+ );
3481
+ writeFixed(
3482
+ p.name,
3483
+ ordered.length > 0 ? ordered : void 0
3484
+ );
3485
+ },
3486
+ className: "size-4",
3487
+ "data-testid": `${testIdPrefix}-param-${p.name}-${o}`
3488
+ }
3489
+ ),
3490
+ /* @__PURE__ */ jsx("span", { children: optionText(p, o) })
3491
+ ]
3492
+ },
3493
+ o
3494
+ );
3495
+ })
3496
+ }
3497
+ ) : p.options && p.options.length > 0 ? /* @__PURE__ */ jsxs(
3498
+ "select",
3499
+ {
3500
+ value: typeof fixedValue === "string" ? fixedValue : "",
3501
+ onChange: (e) => writeFixed(p.name, e.target.value),
3502
+ "aria-label": p.label ?? p.name,
3503
+ id: controlId,
3504
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3505
+ className: cn(
3506
+ inputCx,
3507
+ "scheme-normal dark:scheme-dark [&_option]:bg-bg-surface [&_option]:text-text-primary"
3508
+ ),
3509
+ children: [
3510
+ /* @__PURE__ */ jsx("option", { value: "", children: p.defaultValue !== void 0 ? `Default (${optionText(p, String(p.defaultValue))})` : tr(
3511
+ "fluidgrids.builder.nodeParamsForm.selectPlaceholder",
3512
+ "Select\u2026"
3513
+ ) }),
3514
+ p.options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: optionText(p, o) }, o))
3515
+ ]
3516
+ }
3517
+ ) : p.type === "integer" || p.type === "number" ? /* @__PURE__ */ jsx(
3518
+ "input",
3519
+ {
3520
+ type: "number",
3521
+ step: p.type === "integer" ? "1" : "any",
3522
+ value: typeof fixedValue === "number" ? String(fixedValue) : "",
3523
+ placeholder: p.defaultValue !== void 0 ? `Default: ${String(p.defaultValue)}` : p.placeholder ?? tr(
3524
+ "fluidgrids.builder.nodeParamsForm.numberPlaceholder",
3525
+ "Enter a number"
3526
+ ),
3527
+ "aria-label": p.label ?? p.name,
3528
+ onChange: (e) => writeFixed(p.name, coerceNumber(p.type, e.target.value)),
3529
+ id: controlId,
3530
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3531
+ className: inputCx
3532
+ }
3533
+ ) : p.type === "string" && p.isMultiline && !p.secret ? /* @__PURE__ */ jsx(
3534
+ "textarea",
3535
+ {
3536
+ rows: 4,
3537
+ value: fixedValue === void 0 || fixedValue === null ? "" : String(fixedValue),
3538
+ placeholder: p.defaultValue !== void 0 ? `Default: ${String(p.defaultValue)}` : p.placeholder ?? tr(
3539
+ "fluidgrids.builder.nodeParamsForm.textPlaceholder",
3540
+ "Enter a value"
3541
+ ),
3542
+ onChange: (event) => writeFixed(p.name, event.target.value),
3543
+ "aria-label": p.label ?? p.name,
3544
+ id: controlId,
3545
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3546
+ className: cn(inputCx, "min-h-24 resize-y")
3547
+ }
3548
+ ) : /* @__PURE__ */ jsx(
3549
+ "input",
3550
+ {
3551
+ type: p.secret ? "password" : "text",
3552
+ autoComplete: p.secret ? "new-password" : void 0,
3553
+ ...p.secret ? { "data-agent-sensitive": "" } : {},
3554
+ value: fixedValue === void 0 || fixedValue === null ? "" : String(fixedValue),
3555
+ placeholder: p.defaultValue !== void 0 ? `Default: ${String(p.defaultValue)}` : p.placeholder ?? tr(
3556
+ "fluidgrids.builder.nodeParamsForm.textPlaceholder",
3557
+ "Enter a value"
3558
+ ),
3559
+ onChange: (e) => writeFixed(p.name, e.target.value),
3560
+ "aria-label": p.label ?? p.name,
3561
+ id: controlId,
3562
+ "data-testid": `${testIdPrefix}-param-${p.name}`,
3563
+ className: inputCx
3564
+ }
3565
+ ),
3566
+ p.description && /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: p.description })
3567
+ ]
3568
+ },
3569
+ p.name
3570
+ );
3571
+ })
3572
+ ] });
3573
+ }
3574
+ function ReadOnlyFieldset({
3575
+ className,
3576
+ children
3577
+ }) {
3578
+ const readOnly = useBuilderReadOnly();
3579
+ return /* @__PURE__ */ jsx(
3580
+ "fieldset",
3581
+ {
3582
+ disabled: readOnly,
3583
+ "data-readonly": readOnly ? "true" : void 0,
3584
+ className: cn("m-0 min-w-0 border-0 p-0", className),
3585
+ children
3586
+ }
3587
+ );
3588
+ }
3589
+
3590
+ // src/builder/lib/continuation.ts
3591
+ var BLOCKED = {
3592
+ eligible: false,
3593
+ action: null,
3594
+ blockers: [],
3595
+ blockerReason: null,
3596
+ reason: null
3597
+ };
3598
+ function blocked(blockerReason, reason, blockers = [], targetStatus = null) {
3599
+ return {
3600
+ ...BLOCKED,
3601
+ targetStatus,
3602
+ blockerReason,
3603
+ blockers,
3604
+ reason
3605
+ };
3606
+ }
3607
+ function sourceMetadata(input) {
3608
+ const source = input.sourceRun;
3609
+ return {
3610
+ runId: source?.runId ?? input.sourceRunId ?? null,
3611
+ workflowKey: source?.workflowKey ?? input.sourceRunWorkflowKey ?? null,
3612
+ version: source?.version ?? source?.workflowVersion ?? input.sourceRunVersion ?? null,
3613
+ sourceMode: source?.sourceMode ?? null
3614
+ };
3615
+ }
3616
+ function getContinuationEligibility(input) {
3617
+ const source = sourceMetadata(input);
3618
+ if (!source.runId) {
3619
+ return blocked(
3620
+ "missing-source-run",
3621
+ "Load a completed or failed run before continuing from this node."
3622
+ );
3623
+ }
3624
+ if (source.sourceMode === "PRODUCTION") {
3625
+ return blocked(
3626
+ "production-source-run",
3627
+ "Production run output cannot be reused for TEST continuation."
3628
+ );
3629
+ }
3630
+ if (!source.workflowKey || !source.version || source.workflowKey !== input.workflowKey || source.version !== input.workflowVersion) {
3631
+ return blocked(
3632
+ "incompatible-source-run",
3633
+ "The selected run belongs to a different workflow version."
3634
+ );
3635
+ }
3636
+ const nodeIds = new Set(input.nodes.map((node) => node.id));
3637
+ if (!nodeIds.has(input.targetNodeId)) {
3638
+ return blocked(
3639
+ "missing-target",
3640
+ "The selected node is not present in this workflow.",
3641
+ [input.targetNodeId]
3642
+ );
3643
+ }
3644
+ const logsByNode = /* @__PURE__ */ new Map();
3645
+ for (const log of input.logs) {
3646
+ if (log.runId === source.runId) logsByNode.set(log.nodeId, log);
3647
+ }
3648
+ const targetLog = logsByNode.get(input.targetNodeId);
3649
+ const targetStatus = targetLog?.status?.toLowerCase() ?? null;
3650
+ const action = targetStatus === "failed" ? "retry" : targetStatus === null || targetStatus === "completed" ? "continue" : null;
3651
+ if (!action) {
3652
+ return blocked(
3653
+ "target-not-terminal",
3654
+ "Continue is available only for completed or failed nodes.",
3655
+ [input.targetNodeId],
3656
+ targetStatus
3657
+ );
3658
+ }
3659
+ const predecessors = /* @__PURE__ */ new Map();
3660
+ for (const edge of input.edges) {
3661
+ const parents = predecessors.get(edge.target) ?? [];
3662
+ if (!parents.includes(edge.source)) parents.push(edge.source);
3663
+ predecessors.set(edge.target, parents);
3664
+ }
3665
+ const ancestors = /* @__PURE__ */ new Set();
3666
+ const cycleNodes = /* @__PURE__ */ new Set();
3667
+ const visiting = /* @__PURE__ */ new Set([input.targetNodeId]);
3668
+ const visited = /* @__PURE__ */ new Set();
3669
+ const visitAncestor = (nodeId) => {
3670
+ if (visiting.has(nodeId)) {
3671
+ cycleNodes.add(nodeId);
3672
+ return;
3673
+ }
3674
+ if (visited.has(nodeId)) return;
3675
+ ancestors.add(nodeId);
3676
+ visiting.add(nodeId);
3677
+ for (const parentId of predecessors.get(nodeId) ?? []) {
3678
+ visitAncestor(parentId);
3679
+ }
3680
+ visiting.delete(nodeId);
3681
+ visited.add(nodeId);
3682
+ };
3683
+ for (const parentId of predecessors.get(input.targetNodeId) ?? []) {
3684
+ visitAncestor(parentId);
3685
+ }
3686
+ if (cycleNodes.size > 0) {
3687
+ return blocked(
3688
+ "cycle",
3689
+ "This workflow has a cycle upstream of the selected node, so continuation is disabled.",
3690
+ [...cycleNodes].sort(),
3691
+ targetStatus
3692
+ );
3693
+ }
3694
+ for (const ancestorId of ancestors) {
3695
+ if (!nodeIds.has(ancestorId) || !logsByNode.has(ancestorId)) {
3696
+ return blocked(
3697
+ "missing-ancestor-log",
3698
+ "Every upstream node must have a result in the selected run.",
3699
+ [ancestorId],
3700
+ targetStatus
3701
+ );
3702
+ }
3703
+ const ancestor = logsByNode.get(ancestorId);
3704
+ if (ancestor?.status.toLowerCase() !== "completed") {
3705
+ return blocked(
3706
+ "ancestor-not-completed",
3707
+ "Every upstream node must be completed before continuing.",
3708
+ [ancestorId],
3709
+ targetStatus
3710
+ );
3711
+ }
3712
+ if (ancestor.output == null) {
3713
+ return blocked(
3714
+ "ancestor-missing-output",
3715
+ "Every completed upstream node must have output data before continuing.",
3716
+ [ancestorId],
3717
+ targetStatus
3718
+ );
3719
+ }
3720
+ }
3721
+ return {
3722
+ eligible: true,
3723
+ action,
3724
+ targetStatus,
3725
+ blockers: [],
3726
+ blockerReason: null,
3727
+ reason: null
3728
+ };
3729
+ }
3730
+ function NodeExecutionActions({
3731
+ targetNodeId,
3732
+ nodes,
3733
+ edges,
3734
+ logs,
3735
+ workflowKey,
3736
+ workflowVersion,
3737
+ sourceRun,
3738
+ onExecuteStep,
3739
+ onContinueFromNode,
3740
+ isExecuting = false,
3741
+ className,
3742
+ testIdPrefix = "node-execution"
3743
+ }) {
3744
+ const tr = useTr();
3745
+ const [pendingAction, setPendingAction] = useState(null);
3746
+ const localEligibility = useMemo(
3747
+ () => getContinuationEligibility({
3748
+ nodes,
3749
+ edges,
3750
+ logs,
3751
+ targetNodeId,
3752
+ workflowKey,
3753
+ workflowVersion,
3754
+ sourceRun
3755
+ }),
3756
+ [nodes, edges, logs, targetNodeId, workflowKey, workflowVersion, sourceRun]
3757
+ );
3758
+ const sourceRunId = sourceRun?.runId;
3759
+ const queryIdentity = `${sourceRunId ?? ""}:${targetNodeId}`;
3760
+ const sourceStateVersion = useMemo(
3761
+ () => logs.filter((log) => !sourceRun || log.runId === sourceRun.runId).map(
3762
+ (log) => `${log.nodeId}:${log.status}:${log.output == null ? "empty" : "ready"}`
3763
+ ).sort().join("|"),
3764
+ [logs, sourceRun]
3765
+ );
3766
+ const queryIdentityRef = useRef(queryIdentity);
3767
+ const {
3768
+ data: eligibilityData,
3769
+ loading: eligibilityLoading,
3770
+ error: eligibilityError,
3771
+ refetch: refetchEligibility
3772
+ } = useRunFromNodeEligibilityQuery({
3773
+ variables: {
3774
+ input: {
3775
+ sourceRunId: sourceRun?.runId ?? "",
3776
+ nodeId: targetNodeId
3777
+ }
3778
+ },
3779
+ skip: !sourceRun,
3780
+ fetchPolicy: "network-only",
3781
+ nextFetchPolicy: "network-only"
3782
+ });
3783
+ useEffect(() => {
3784
+ if (sourceRunId === void 0) {
3785
+ queryIdentityRef.current = queryIdentity;
3786
+ return;
3787
+ }
3788
+ if (queryIdentityRef.current !== queryIdentity) {
3789
+ queryIdentityRef.current = queryIdentity;
3790
+ return;
3791
+ }
3792
+ void refetchEligibility();
3793
+ }, [queryIdentity, refetchEligibility, sourceRunId, sourceStateVersion]);
3794
+ const serverEligibility = sourceRun && !eligibilityLoading && !eligibilityError && eligibilityData?.runFromNodeEligibility ? (() => {
3795
+ const result = eligibilityData.runFromNodeEligibility;
3796
+ const targetStatus = result.targetStatus?.toLowerCase() ?? null;
3797
+ return {
3798
+ eligible: result.eligible,
3799
+ action: targetStatus === "failed" ? "retry" : result.eligible ? "continue" : null,
3800
+ targetStatus,
3801
+ blockers: result.blockedNodeIds.length ? result.blockedNodeIds : result.blockers,
3802
+ blockerReason: null,
3803
+ reason: result.reason ?? null
3804
+ };
3805
+ })() : null;
3806
+ const eligibility = sourceRun ? serverEligibility ?? {
3807
+ eligible: false,
3808
+ action: null,
3809
+ reason: eligibilityError ? tr(
3810
+ "fluidgrids.builder.nodeExecutionActions.queryError",
3811
+ "Unable to verify continuation readiness. Try loading the run again."
3812
+ ) : tr(
3813
+ "fluidgrids.builder.nodeExecutionActions.checking",
3814
+ "Checking continuation readiness\u2026"
3815
+ )
3816
+ } : localEligibility;
3817
+ const runAction = async (action, callback) => {
3818
+ setPendingAction(action);
3819
+ try {
3820
+ await callback();
3821
+ } finally {
3822
+ setPendingAction(null);
3823
+ }
3824
+ };
3825
+ const executePending = pendingAction === "execute" || isExecuting;
3826
+ const continuePending = pendingAction === "continue";
3827
+ const continuationLabel = eligibility.action === "retry" ? tr(
3828
+ "fluidgrids.builder.nodeExecutionActions.retryFromHere",
3829
+ "Retry from here"
3830
+ ) : tr(
3831
+ "fluidgrids.builder.nodeExecutionActions.continueFromHere",
3832
+ "Continue from here"
3833
+ );
3834
+ const blockerLabel = eligibility.reason ? tr(
3835
+ "fluidgrids.builder.nodeExecutionActions.blocked",
3836
+ "Unavailable: {{reason}}",
3837
+ { reason: eligibility.reason }
3838
+ ) : tr(
3839
+ "fluidgrids.builder.nodeExecutionActions.noSourceRun",
3840
+ "Load a run to enable continuation."
3841
+ );
3842
+ return /* @__PURE__ */ jsxs(
3843
+ "section",
3844
+ {
3845
+ className: cn(
3846
+ "shrink-0 border-t border-border bg-muted/20 px-3 py-2",
3847
+ className
3848
+ ),
3849
+ "aria-label": tr(
3850
+ "fluidgrids.builder.nodeExecutionActions.ariaLabel",
3851
+ "Node execution actions"
3852
+ ),
3853
+ "data-testid": `${testIdPrefix}-execution-actions`,
3854
+ children: [
3855
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
3856
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground", children: tr("fluidgrids.builder.nodeExecutionActions.heading", "Execution") }),
3857
+ /* @__PURE__ */ jsxs(
3858
+ "button",
3859
+ {
3860
+ "data-agent": "confirm",
3861
+ type: "button",
3862
+ onClick: () => {
3863
+ void runAction("execute", () => onExecuteStep(targetNodeId));
3864
+ },
3865
+ disabled: executePending || continuePending,
3866
+ title: tr(
3867
+ "fluidgrids.builder.nodeExecutionActions.executeStepHint",
3868
+ "Run only this node as a fresh TEST execution."
3869
+ ),
3870
+ className: "inline-flex items-center gap-1 rounded-md border border-border bg-background px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50",
3871
+ "data-testid": `${testIdPrefix}-execute-step`,
3872
+ children: [
3873
+ /* @__PURE__ */ jsx(
3874
+ Icon,
3875
+ {
3876
+ icon: executePending ? "svg-spinners:ring-resize" : "mdi:play",
3877
+ className: "size-3.5"
3878
+ }
3879
+ ),
3880
+ tr(
3881
+ "fluidgrids.builder.nodeExecutionActions.executeStep",
3882
+ "Execute step"
3883
+ )
3884
+ ]
3885
+ }
3886
+ ),
3887
+ /* @__PURE__ */ jsxs(
3888
+ "button",
3889
+ {
3890
+ "data-agent": "confirm",
3891
+ type: "button",
3892
+ onClick: () => {
3893
+ if (!eligibility.eligible || !sourceRun) return;
3894
+ void runAction(
3895
+ "continue",
3896
+ () => onContinueFromNode(sourceRun.runId, targetNodeId)
3897
+ );
3898
+ },
3899
+ disabled: !eligibility.eligible || !sourceRun || executePending || continuePending,
3900
+ title: eligibility.eligible ? continuationLabel : blockerLabel,
3901
+ className: "inline-flex items-center gap-1 rounded-md border border-primary/30 bg-primary/5 px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary/10 disabled:cursor-not-allowed disabled:opacity-50",
3902
+ "data-testid": `${testIdPrefix}-continue-from-here`,
3903
+ children: [
3904
+ /* @__PURE__ */ jsx(
3905
+ Icon,
3906
+ {
3907
+ icon: continuePending ? "svg-spinners:ring-resize" : "mdi:skip-forward",
3908
+ className: "size-3.5"
3909
+ }
3910
+ ),
3911
+ continuePending ? tr("fluidgrids.builder.nodeExecutionActions.running", "Running\u2026") : continuationLabel
3912
+ ]
3913
+ }
3914
+ )
3915
+ ] }),
3916
+ !eligibility.eligible && /* @__PURE__ */ jsx(
3917
+ "p",
3918
+ {
3919
+ className: "mt-1 text-[10px] leading-snug text-muted-foreground",
3920
+ "data-testid": `${testIdPrefix}-continuation-blocker`,
3921
+ children: blockerLabel
3922
+ }
3923
+ )
3924
+ ]
3925
+ }
3926
+ );
3927
+ }
3928
+
3929
+ export { NDV_Z_INDEX, NodeExecutionActions, NodeParamsForm, ReadOnlyFieldset };