@burdenoff/fluidgrids-fe-sdk 2026.1101.1 → 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.
- package/dist/{ExecutionDetailPage-4LPCNWM3.js → ExecutionDetailPage-Q77U6YJO.js} +11 -8
- package/dist/{ExecutionsListPage-HNPUKYD7.js → ExecutionsListPage-X4H4FVLN.js} +6 -5
- package/dist/{WorkflowBuilder-QFSCYCUC.js → WorkflowBuilder-RJPRUMQR.js} +11 -8
- package/dist/{WorkflowsList-RV4BRJO7.js → WorkflowsList-R2JJK7BL.js} +7 -6
- package/dist/agentTools-MISTH7JI.js +349 -0
- package/dist/{chunk-77HIU3CX.js → chunk-26HGYPBC.js} +1 -1
- package/dist/{chunk-76UUEMTN.js → chunk-37VUN74D.js} +4 -4
- package/dist/{chunk-QPZCJKII.js → chunk-3ERT6FQK.js} +3 -3
- package/dist/chunk-472HJ3S3.js +3929 -0
- package/dist/{chunk-WNZV4SLU.js → chunk-4QHCXTHW.js} +4 -4
- package/dist/chunk-AASHQI77.js +126 -0
- package/dist/chunk-CGXQVZGY.js +260 -0
- package/dist/chunk-CRDG2SQQ.js +185 -0
- package/dist/chunk-DQ2ZCXYX.js +21875 -0
- package/dist/chunk-F7DD7BOB.js +1164 -0
- package/dist/chunk-GTEBQQ7J.js +24 -0
- package/dist/{chunk-ALFMJSD6.js → chunk-H7XPXONT.js} +1 -1
- package/dist/{chunk-UO5DUKYN.js → chunk-HKKSXOFO.js} +8 -7
- package/dist/chunk-IA2ZCHNQ.js +1336 -0
- package/dist/{chunk-3M36XWAH.js → chunk-IGRIN7NL.js} +8 -7
- package/dist/chunk-JDLQ6QIB.js +283 -0
- package/dist/{chunk-AD7BICOT.js → chunk-JKBCLBHY.js} +1 -1
- package/dist/{chunk-6JX5UPDW.js → chunk-JNZCXFLD.js} +1 -1
- package/dist/{chunk-SEFOT2LD.js → chunk-LWLHPVIA.js} +1 -1
- package/dist/{chunk-JWROEOG5.js → chunk-LYTR6IQO.js} +1 -38
- package/dist/chunk-MR2B67NW.js +338 -0
- package/dist/{chunk-RIHYPPMQ.js → chunk-NLYQ3XDE.js} +1 -1
- package/dist/{chunk-6ILIBJFH.js → chunk-NZJWKVER.js} +7904 -7943
- package/dist/chunk-OCJTTHTS.js +46 -0
- package/dist/chunk-OSQ3OCR3.js +1193 -0
- package/dist/{chunk-VJL3DFJT.js → chunk-REO4S5MY.js} +2 -2
- package/dist/{chunk-XKQMCPSB.js → chunk-T2UUYYVJ.js} +4 -4
- package/dist/{chunk-BXR6BWXM.js → chunk-W7MTSATB.js} +5 -5
- package/dist/{chunk-A47PSD7A.js → chunk-WF6GMBR4.js} +7 -4
- package/dist/chunk-WYCB6OQL.js +19709 -0
- package/dist/{chunk-YOLPHVOE.js → chunk-YKFR7BZJ.js} +1 -1
- package/dist/{chunk-26NENGXA.js → chunk-YSXNJGCY.js} +4 -4
- package/dist/code-editor-field-JKCUR6BI.js +3 -0
- package/dist/executions.js +12 -9
- package/dist/history.js +4 -3
- package/dist/index.js +41 -32
- package/dist/node-detail-view-PCYFQX62.js +2034 -0
- package/dist/observability.js +5 -4
- package/dist/quick-config-panel-BRZZHOSI.js +402 -0
- package/dist/settings.js +6 -5
- package/dist/templates.js +10 -7
- package/dist/workflow-builder.js +12 -9
- package/dist/workflows-list.js +7 -6
- package/package.json +4 -1
- package/dist/chunk-NMEI7TQW.js +0 -2628
- 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 };
|