@burdenoff/fluidgrids-fe-sdk 2026.1014.5 → 2026.1014.7

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.
@@ -16,7 +16,7 @@ import { useTr, FluidGridsTranslationProvider, ENGLISH_FALLBACK_TRANSLATOR } fro
16
16
  import { waitForFluidGridsClient, getFluidGridsClient, getFluidGridsClientSafe, createFluidGridsClient, setFluidGridsClient, resetFluidGridsClient } from './chunk-NTWG3CJX.js';
17
17
  import * as React2 from 'react';
18
18
  import React2__default, { forwardRef, useState, useImperativeHandle, useRef, useEffect, useMemo, memo, createContext, lazy, useCallback, useSyncExternalStore, useContext, useLayoutEffect, Suspense, useDeferredValue, useId } from 'react';
19
- import { Textarea, Input, Button, Card, Badge, CardContent as CardContent$1, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, TooltipProvider, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, Popover, PopoverTrigger, PopoverContent, Tooltip, TooltipTrigger, TooltipContent, Toaster, Tabs, TabsList, TabsTrigger, TabsContent, CardHeader as CardHeader$1, CardTitle as CardTitle$1, CardDescription as CardDescription$1, ScrollArea, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@burdenoff/fe-libs/ui';
19
+ import { Textarea, Input, Button, Card, Badge, CardContent as CardContent$1, Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, TooltipProvider, AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, Popover, PopoverTrigger, PopoverContent, Tooltip, TooltipTrigger, TooltipContent, Toaster, Tabs, TabsList, TabsTrigger, TabsContent, CardHeader as CardHeader$1, CardTitle as CardTitle$1, CardDescription as CardDescription$1, ScrollArea, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@burdenoff/fe-libs/ui';
20
20
  import { cn } from '@burdenoff/fe-libs/utils';
21
21
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
22
22
  import { Icon } from '@iconify/react';
@@ -981,8 +981,8 @@ function isAdaptiveCard(data) {
981
981
  return data.type === "AdaptiveCard";
982
982
  }
983
983
  function isCustomFormat(data) {
984
- const record = data;
985
- return record.type !== "AdaptiveCard" && "inputs" in record;
984
+ const record2 = data;
985
+ return record2.type !== "AdaptiveCard" && "inputs" in record2;
986
986
  }
987
987
  function expandTemplate(obj, data) {
988
988
  if (typeof obj === "string") {
@@ -1959,6 +1959,30 @@ function ConnectionFormDialog({
1959
1959
  }
1960
1960
  );
1961
1961
  }
1962
+
1963
+ // src/modules/connections/utils/adapter-selection.ts
1964
+ function matchesAdapterProvider(adapter, providerKey) {
1965
+ const normalizedKey = providerKey.trim().toLowerCase();
1966
+ return normalizedKey.length > 0 && (adapter.provider.trim().toLowerCase() === normalizedKey || adapter.name.trim().toLowerCase() === normalizedKey);
1967
+ }
1968
+ function connectionsForAdapter(connections, adapterName) {
1969
+ return connections.filter(
1970
+ (connection) => connection.adapter.toLowerCase() === adapterName.toLowerCase()
1971
+ );
1972
+ }
1973
+ function replaceAdapterConnections(connections, adapterName, connectionIds = []) {
1974
+ const otherConnections = connections.filter(
1975
+ (connection) => connection.adapter.toLowerCase() !== adapterName.toLowerCase()
1976
+ );
1977
+ const uniqueIds = [...new Set(connectionIds.filter(Boolean))];
1978
+ return [
1979
+ ...otherConnections,
1980
+ ...uniqueIds.map((connectionId) => ({
1981
+ connectionId,
1982
+ adapter: adapterName
1983
+ }))
1984
+ ];
1985
+ }
1962
1986
  function formatAge(dateString) {
1963
1987
  try {
1964
1988
  return formatDistanceToNow(new Date(dateString), { addSuffix: true });
@@ -2004,6 +2028,7 @@ function ConnectionChip({
2004
2028
  connectionData,
2005
2029
  onRemove
2006
2030
  }) {
2031
+ const tr = useTr();
2007
2032
  const name = connectionData?.name || connection.connectionId;
2008
2033
  const status = connectionData?.status;
2009
2034
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2 py-1 bg-accent/30 rounded-md text-sm group", children: [
@@ -2031,6 +2056,10 @@ function ConnectionChip({
2031
2056
  "button",
2032
2057
  {
2033
2058
  type: "button",
2059
+ "aria-label": tr(
2060
+ "fluidgrids.connections.connectionSelector.removeConnection",
2061
+ "Remove connection"
2062
+ ),
2034
2063
  onClick: (e) => {
2035
2064
  e.stopPropagation();
2036
2065
  onRemove();
@@ -2041,46 +2070,57 @@ function ConnectionChip({
2041
2070
  )
2042
2071
  ] });
2043
2072
  }
2044
- function ConnectionSelector({
2073
+ function ConnectionSelector(props) {
2074
+ const connectionList = useConnections();
2075
+ if (props.adapters.length <= 1) {
2076
+ return /* @__PURE__ */ jsx(AdapterConnectionSelector, { ...props, connectionList });
2077
+ }
2078
+ return /* @__PURE__ */ jsx("div", { className: props.compact ? "space-y-2" : "space-y-3", children: props.adapters.map((adapter) => /* @__PURE__ */ jsxs("fieldset", { className: "min-w-0 space-y-1.5", children: [
2079
+ /* @__PURE__ */ jsx("legend", { className: "text-xs font-medium text-text-secondary", children: adapter.name }),
2080
+ /* @__PURE__ */ jsx(
2081
+ AdapterConnectionSelector,
2082
+ {
2083
+ ...props,
2084
+ adapters: [adapter],
2085
+ selectedConnections: connectionsForAdapter(
2086
+ props.selectedConnections,
2087
+ adapter.name
2088
+ ),
2089
+ onChange: (connections) => props.onChange(
2090
+ replaceAdapterConnections(
2091
+ props.selectedConnections,
2092
+ adapter.name,
2093
+ connections.map((connection) => connection.connectionId)
2094
+ )
2095
+ ),
2096
+ showRequired: false,
2097
+ connectionList
2098
+ }
2099
+ )
2100
+ ] }, adapter.name)) });
2101
+ }
2102
+ function AdapterConnectionSelector({
2045
2103
  adapters,
2046
2104
  selectedConnections,
2047
2105
  onChange,
2048
2106
  multiple = false,
2049
2107
  showStatus = true,
2050
- compact = false
2108
+ compact = false,
2109
+ showRequired = true,
2110
+ connectionList
2051
2111
  }) {
2052
2112
  const tr = useTr();
2053
2113
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
2054
2114
  const [dialogOpen, setDialogOpen] = useState(false);
2055
2115
  const [dialogProviderKey, setDialogProviderKey] = useState();
2056
- const {
2057
- connections,
2058
- loading: connectionsLoading,
2059
- refetch
2060
- } = useConnections();
2116
+ const { connections, loading: connectionsLoading, refetch } = connectionList;
2061
2117
  const availableConnections = useMemo(() => {
2062
2118
  if (!adapters || adapters.length === 0) return [];
2063
- const adapterIdentifiers = /* @__PURE__ */ new Set();
2064
- adapters.forEach((a) => {
2065
- adapterIdentifiers.add(a.provider.toLowerCase());
2066
- adapterIdentifiers.add(a.name.toLowerCase());
2067
- });
2068
- return connections.filter((conn) => {
2069
- const providerKey = conn.provider?.key?.toLowerCase() || "";
2070
- const category = conn.provider?.category?.toLowerCase() || "";
2071
- if (adapterIdentifiers.has(providerKey)) {
2072
- return true;
2073
- }
2074
- for (const identifier of adapterIdentifiers) {
2075
- if (providerKey.includes(identifier) || identifier.includes(providerKey)) {
2076
- return true;
2077
- }
2078
- if (category.includes(identifier)) {
2079
- return true;
2080
- }
2081
- }
2082
- return false;
2083
- });
2119
+ return connections.filter(
2120
+ (connection) => adapters.some(
2121
+ (adapter) => matchesAdapterProvider(adapter, connection.provider?.key ?? "")
2122
+ )
2123
+ );
2084
2124
  }, [connections, adapters]);
2085
2125
  const mergedConnections = useMemo(() => {
2086
2126
  const allConnections = [...availableConnections];
@@ -2114,11 +2154,12 @@ function ConnectionSelector({
2114
2154
  (connection) => {
2115
2155
  const providerKey = connection.provider?.key || "";
2116
2156
  const adapter = adapters.find(
2117
- (a) => a.provider.toLowerCase() === providerKey.toLowerCase() || a.name.toLowerCase() === providerKey.toLowerCase()
2157
+ (candidate) => matchesAdapterProvider(candidate, providerKey)
2118
2158
  );
2159
+ if (!adapter) return;
2119
2160
  const newSelection = {
2120
2161
  connectionId: connection.id,
2121
- adapter: adapter?.name || providerKey
2162
+ adapter: adapter.name
2122
2163
  };
2123
2164
  if (multiple) {
2124
2165
  const isSelected = selectedConnections.some(
@@ -2155,7 +2196,7 @@ function ConnectionSelector({
2155
2196
  (conn) => {
2156
2197
  const providerKey = conn.provider?.key || "";
2157
2198
  const adapter = adapters.find(
2158
- (a) => a.provider.toLowerCase() === providerKey.toLowerCase() || a.name.toLowerCase() === providerKey.toLowerCase()
2199
+ (candidate) => matchesAdapterProvider(candidate, providerKey)
2159
2200
  );
2160
2201
  if (adapter) {
2161
2202
  const newSelection = { connectionId: conn.id, adapter: adapter.name };
@@ -2285,7 +2326,10 @@ function ConnectionSelector({
2285
2326
  return /* @__PURE__ */ jsxs(
2286
2327
  DropdownMenuItem,
2287
2328
  {
2288
- onClick: () => handleSelect(conn),
2329
+ onSelect: (event) => {
2330
+ if (multiple) event.preventDefault();
2331
+ handleSelect(conn);
2332
+ },
2289
2333
  className: `flex items-center justify-between cursor-pointer ${isSelected ? "bg-action-primary-bg/10" : ""} ${!isActive ? "opacity-60" : ""}`,
2290
2334
  children: [
2291
2335
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col min-w-0", children: [
@@ -2366,7 +2410,7 @@ function ConnectionSelector({
2366
2410
  }
2367
2411
  )
2368
2412
  ] }),
2369
- showStatus && selectedConnections.length === 0 && adapters.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-[var(--color-status-warning-text)]", children: [
2413
+ showStatus && showRequired && selectedConnections.length === 0 && adapters.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-[var(--color-status-warning-text)]", children: [
2370
2414
  /* @__PURE__ */ jsx(Icon, { icon: "mdi:alert-circle", className: "w-3.5 h-3.5" }),
2371
2415
  /* @__PURE__ */ jsx("span", { children: tr(
2372
2416
  "fluidgrids.connections.connectionSelector.connectionRequired",
@@ -2454,1404 +2498,1100 @@ function isNodeTrigger(node) {
2454
2498
  return false;
2455
2499
  }
2456
2500
 
2457
- // src/hooks/useNodeExecution.ts
2458
- var TRIGGER_WORKFLOW = TriggerWorkflowDocument;
2459
- var EXECUTE_STEP = ExecuteStepDocument;
2460
- var WORKFLOW_RUN = WorkflowRunDocument;
2461
- var RUN_FROM_NODE = RunFromNodeDocument;
2462
- var EXECUTION_DETAIL = ExecutionDetailDocument;
2463
- var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
2464
- "COMPLETED",
2465
- "FAILED",
2466
- "CANCELLED",
2467
- "TERMINATED"
2468
- ]);
2469
- var POLL_INTERVAL = 1500;
2470
- var MAX_RUN_CREATION_WAIT = 6e4;
2471
- function calculateItemCount(output) {
2472
- if (!output || Object.keys(output).length === 0) {
2473
- return 0;
2501
+ // src/builder/lib/field-schema.ts
2502
+ function isRecord3(value) {
2503
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2504
+ }
2505
+ function isIdentifier(key) {
2506
+ return /^[A-Za-z_$][\w$]*$/.test(key);
2507
+ }
2508
+ function joinPath(base, key) {
2509
+ if (isIdentifier(key)) return base ? `${base}.${key}` : key;
2510
+ const bracket = `[${JSON.stringify(key)}]`;
2511
+ return base ? `${base}${bracket}` : bracket;
2512
+ }
2513
+ function pathToExpression(path) {
2514
+ if (!path) return "${output}";
2515
+ const accessor = path.startsWith("[") ? `output${path}` : `output.${path}`;
2516
+ return "${" + accessor + "}";
2517
+ }
2518
+ function fieldFromSpec(key, path, spec) {
2519
+ if (typeof spec === "string") {
2520
+ return { key, path, type: spec };
2474
2521
  }
2475
- const values = Object.values(output);
2476
- for (const value of values) {
2477
- if (Array.isArray(value)) {
2478
- return value.length;
2522
+ if (isRecord3(spec)) {
2523
+ const declaredType2 = typeof spec.type === "string" ? spec.type : void 0;
2524
+ const description = typeof spec.description === "string" ? spec.description : void 0;
2525
+ const required = spec.required === true;
2526
+ if (isRecord3(spec.properties)) {
2527
+ const children = Object.entries(spec.properties).map(
2528
+ ([k, v]) => fieldFromSpec(k, joinPath(path, k), v)
2529
+ );
2530
+ return { key, path, type: "object", required, description, children };
2479
2531
  }
2480
- if (typeof value === "object" && value !== null) {
2481
- const nestedValues = Object.values(value);
2482
- for (const nestedValue of nestedValues) {
2483
- if (Array.isArray(nestedValue)) {
2484
- return nestedValue.length;
2485
- }
2486
- }
2532
+ if (spec.items !== void 0) {
2533
+ const element = fieldFromSpec("0", `${path}[0]`, spec.items);
2534
+ return {
2535
+ key,
2536
+ path,
2537
+ type: "array",
2538
+ required,
2539
+ description,
2540
+ children: [element]
2541
+ };
2487
2542
  }
2543
+ return { key, path, type: declaredType2 ?? "any", required, description };
2488
2544
  }
2489
- return 1;
2545
+ return { key, path, type: "any" };
2490
2546
  }
2491
- function useNodeExecution() {
2492
- const pollTimerRef = useRef(null);
2493
- const pollStartRef = useRef(0);
2494
- const activeRunIdRef = useRef(null);
2495
- const loadRequestRef = useRef(0);
2496
- const workflow = useFlowStore((s) => s.workflow);
2497
- const startExecution = useFlowStore(
2498
- (s) => s.actions.execution.startExecution
2499
- );
2500
- const setRunMetadata = useFlowStore(
2501
- (s) => s.actions.execution.setRunMetadata
2502
- );
2503
- const setStepPlan = useFlowStore((s) => s.actions.execution.setStepPlan);
2504
- const addLog = useFlowStore((s) => s.actions.execution.addLog);
2505
- const updateLog = useFlowStore((s) => s.actions.execution.updateLog);
2506
- const setNodeStatus = useFlowStore((s) => s.actions.execution.setNodeStatus);
2507
- const setNodeItemCount = useFlowStore(
2508
- (s) => s.actions.execution.setNodeItemCount
2547
+ function fieldsFromIO(io, root = "") {
2548
+ if (!isRecord3(io)) return [];
2549
+ return Object.entries(io).map(
2550
+ ([key, spec]) => fieldFromSpec(key, joinPath(root, key), spec)
2509
2551
  );
2510
- const completeExecution = useFlowStore(
2511
- (s) => s.actions.execution.completeExecution
2552
+ }
2553
+ function typeOfValue(value) {
2554
+ if (value === null || value === void 0) return "any";
2555
+ if (Array.isArray(value)) return "array";
2556
+ if (typeof value === "object") return "object";
2557
+ if (typeof value === "number") return "number";
2558
+ if (typeof value === "boolean") return "boolean";
2559
+ if (typeof value === "string") return "string";
2560
+ return "any";
2561
+ }
2562
+ function fieldFromValue(key, path, value) {
2563
+ if (Array.isArray(value)) {
2564
+ const children = value.length > 0 ? [fieldFromValue("0", `${path}[0]`, value[0])] : void 0;
2565
+ return { key, path, type: "array", children };
2566
+ }
2567
+ if (isRecord3(value)) {
2568
+ const children = Object.entries(value).map(
2569
+ ([k, v]) => fieldFromValue(k, joinPath(path, k), v)
2570
+ );
2571
+ return { key, path, type: "object", children };
2572
+ }
2573
+ return { key, path, type: typeOfValue(value) };
2574
+ }
2575
+ function fieldsFromValue(value, root = "") {
2576
+ if (!isRecord3(value)) return [];
2577
+ return Object.entries(value).map(
2578
+ ([key, v]) => fieldFromValue(key, joinPath(root, key), v)
2512
2579
  );
2513
- const clearLogs = useFlowStore((s) => s.actions.execution.clearLogs);
2514
- const loadDebugRun = useFlowStore((s) => s.actions.execution.loadDebugRun);
2515
- const cleanup = useCallback(() => {
2516
- loadRequestRef.current += 1;
2517
- if (pollTimerRef.current) {
2518
- clearTimeout(pollTimerRef.current);
2519
- pollTimerRef.current = null;
2520
- }
2521
- activeRunIdRef.current = null;
2522
- }, []);
2523
- const findTriggerNode = useCallback(() => {
2524
- const { nodes, edges } = workflow;
2525
- const explicitTrigger = nodes.find(isNodeTrigger);
2526
- if (explicitTrigger) {
2527
- return explicitTrigger;
2580
+ }
2581
+ function declaredOutputFields(nodeType, op) {
2582
+ const name = op?.name;
2583
+ if (!name || nodeType?.startsWith("internal:")) {
2584
+ return fieldsFromIO(op?.output_fields);
2585
+ }
2586
+ const children = fieldsFromIO(op?.output_fields, name);
2587
+ if (children.length === 0) return [];
2588
+ return [{ key: name, path: name, type: "object", children }];
2589
+ }
2590
+ function normalizeOperations(raw) {
2591
+ if (Array.isArray(raw)) return raw;
2592
+ if (isRecord3(raw) && Array.isArray(raw.operations)) {
2593
+ return raw.operations;
2594
+ }
2595
+ return [];
2596
+ }
2597
+ function resolveNodeOperation(data, fallbackOperations2) {
2598
+ const nodeData = isRecord3(data?.nodeData) ? data?.nodeData : void 0;
2599
+ let operations = normalizeOperations(nodeData?.operations);
2600
+ if (operations.length === 0) {
2601
+ operations = normalizeOperations(data?.operations);
2602
+ }
2603
+ if (operations.length === 0 && fallbackOperations2 !== void 0) {
2604
+ operations = normalizeOperations(fallbackOperations2);
2605
+ }
2606
+ const opName = typeof data?.operation_name === "string" ? data.operation_name : void 0;
2607
+ const selected = operations.find((op) => op.name === opName) ?? operations[0];
2608
+ return { operations, selected };
2609
+ }
2610
+
2611
+ // src/builder/lib/parameter-values.ts
2612
+ function isRecord4(value) {
2613
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2614
+ }
2615
+ function record(value) {
2616
+ return isRecord4(value) ? value : void 0;
2617
+ }
2618
+ function declaredType(schema) {
2619
+ const type = typeof schema === "string" ? schema : record(schema)?.type;
2620
+ return typeof type === "string" ? type.toLowerCase().trim() : "";
2621
+ }
2622
+ function structuredParameterType(schema) {
2623
+ const type = declaredType(schema);
2624
+ if (type === "array" || /^array<.+>$/.test(type)) return "array";
2625
+ if (type === "object") return "object";
2626
+ return void 0;
2627
+ }
2628
+ function itemSchema(schema) {
2629
+ return record(schema)?.items ?? declaredType(schema).match(/^array<(.+)>$/)?.[1];
2630
+ }
2631
+ function matchesSchema(value, schema) {
2632
+ const type = declaredType(schema);
2633
+ const structured = structuredParameterType(schema);
2634
+ if (structured === "array") {
2635
+ return Array.isArray(value) && value.every((item) => matchesSchema(item, itemSchema(schema)));
2636
+ }
2637
+ if (structured === "object") {
2638
+ const object = record(value);
2639
+ if (!object) return false;
2640
+ const definition = record(schema);
2641
+ const required = definition?.required;
2642
+ if (Array.isArray(required) && required.some(
2643
+ (key) => typeof key === "string" && !Object.hasOwn(object, key)
2644
+ )) {
2645
+ return false;
2528
2646
  }
2529
- const nodesWithIncomingEdges = new Set(edges.map((e) => e.target));
2530
- const firstNode = nodes.find(
2531
- (node) => !nodesWithIncomingEdges.has(node.id)
2647
+ return Object.entries(record(definition?.properties) ?? {}).every(
2648
+ ([key, property]) => Object.hasOwn(object, key) ? matchesSchema(object[key], property) : record(property)?.required !== true
2532
2649
  );
2533
- if (firstNode) {
2534
- return firstNode;
2650
+ }
2651
+ if (type === "string") return typeof value === "string";
2652
+ if (type === "number")
2653
+ return typeof value === "number" && Number.isFinite(value);
2654
+ if (type === "integer")
2655
+ return typeof value === "number" && Number.isInteger(value);
2656
+ if (type === "boolean") return typeof value === "boolean";
2657
+ if (type === "null") return value === null;
2658
+ return true;
2659
+ }
2660
+ function parseParameterValue(schema, value) {
2661
+ const type = structuredParameterType(schema);
2662
+ if (!type) return { valid: true, value };
2663
+ if (value === void 0 || value === null || typeof value === "string" && !value.trim()) {
2664
+ return { valid: true, value: void 0 };
2665
+ }
2666
+ let parsed = value;
2667
+ if (typeof value === "string") {
2668
+ const text = value.trim();
2669
+ const elementType = declaredType(itemSchema(schema));
2670
+ const canSplit = type === "array" && (!elementType || elementType === "string");
2671
+ if (canSplit && !["[", "{", '"', "]", "}"].includes(text[0])) {
2672
+ parsed = text.split(",").map((item) => item.trim()).filter(Boolean);
2673
+ } else {
2674
+ try {
2675
+ parsed = JSON.parse(text);
2676
+ } catch {
2677
+ return { valid: false, error: `Enter valid JSON for this ${type}.` };
2678
+ }
2535
2679
  }
2536
- if (nodes.length > 0) {
2537
- return nodes[0];
2680
+ }
2681
+ if (type === "array" ? !Array.isArray(parsed) : !record(parsed)) {
2682
+ return { valid: false, error: `Expected a JSON ${type}.` };
2683
+ }
2684
+ if (!matchesSchema(parsed, schema)) {
2685
+ return {
2686
+ valid: false,
2687
+ error: "The value does not match the declared field types."
2688
+ };
2689
+ }
2690
+ return { valid: true, value: parsed };
2691
+ }
2692
+ function formatParameterValue(value) {
2693
+ if (value === void 0 || value === null) return "";
2694
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
2695
+ }
2696
+ function normalizeNodeParameters(data, fallbackOperations2, mappedFields = /* @__PURE__ */ new Set()) {
2697
+ const operation = resolveNodeOperation(data, fallbackOperations2).selected;
2698
+ const fields = record(operation?.input_fields);
2699
+ const args = record(data.args);
2700
+ const state = record(args?.state) ?? {};
2701
+ if (!fields) return data;
2702
+ const structuredFields = Object.entries(fields).filter(
2703
+ ([name, schema]) => structuredParameterType(schema) && !mappedFields.has(name)
2704
+ );
2705
+ if (structuredFields.length === 0) return data;
2706
+ const normalized = new Map(Object.entries(state));
2707
+ for (const [name, schema] of structuredFields) {
2708
+ const result = parseParameterValue(
2709
+ schema,
2710
+ Object.hasOwn(state, name) ? state[name] : void 0
2711
+ );
2712
+ if (!result.valid) throw new Error(`${name}: ${result.error}`);
2713
+ if (result.value === void 0) normalized.delete(name);
2714
+ else normalized.set(name, result.value);
2715
+ }
2716
+ return { ...data, args: { ...args, state: Object.fromEntries(normalized) } };
2717
+ }
2718
+ function normalizeWorkflowParameters(nodes, edges, fallbackOperations2) {
2719
+ return nodes.map((node) => {
2720
+ if (node.data.disabled === true) return node;
2721
+ const mappedFields = new Set(
2722
+ edges.filter((edge) => edge.target === node.id).flatMap(
2723
+ (edge) => Object.entries(
2724
+ record(record(record(edge)?.mapping)?.definition) ?? {}
2725
+ ).filter(
2726
+ ([, definition]) => typeof definition === "string" ? definition.trim().length > 0 : definition !== void 0
2727
+ ).map(([name]) => name)
2728
+ )
2729
+ );
2730
+ try {
2731
+ return {
2732
+ ...node,
2733
+ data: normalizeNodeParameters(
2734
+ node.data,
2735
+ fallbackOperations2(node.type),
2736
+ mappedFields
2737
+ )
2738
+ };
2739
+ } catch (error) {
2740
+ throw new Error(
2741
+ `Node ${node.id}: ${error instanceof Error ? error.message : "Invalid parameter value"}`
2742
+ );
2538
2743
  }
2539
- console.error("[useNodeExecution] No nodes found in workflow");
2540
- return null;
2541
- }, [workflow]);
2542
- const getNodeName = useCallback(
2543
- (nodeId) => {
2544
- const node = workflow.nodes.find((n) => n.id === nodeId);
2545
- if (!node) return `Node ${nodeId}`;
2546
- const data = node.data;
2547
- return data?.operation_name || data?.name || node.type || `Node ${nodeId}`;
2744
+ });
2745
+ }
2746
+ function useDeleteNode() {
2747
+ const { getNode, getEdges, deleteElements } = useReactFlow();
2748
+ const deleteNodes = useFlowStore((state) => state.actions.nodes.deleteNodes);
2749
+ return useCallback(
2750
+ (id) => {
2751
+ const node = getNode(id);
2752
+ if (!node) return;
2753
+ const edges = getEdges();
2754
+ const connectedEdges = getConnectedEdges([node], edges);
2755
+ deleteElements({ nodes: [node], edges: connectedEdges }).then();
2756
+ deleteNodes([node]);
2548
2757
  },
2549
- [workflow.nodes]
2758
+ [deleteElements, getEdges, getNode, deleteNodes]
2550
2759
  );
2551
- const processNodeStates = useCallback(
2552
- (runId, nodeStates) => {
2553
- for (const nodeState of nodeStates) {
2554
- const logId = `${runId}-${nodeState.nodeId}`;
2555
- const rawStatus = nodeState.status.toLowerCase();
2556
- const status = rawStatus === "completed" ? "completed" : rawStatus === "failed" || rawStatus === "timeout" ? "failed" : rawStatus === "running" || rawStatus === "waiting" || rawStatus === "delayed" || rawStatus === "waiting_subworkflow" ? "running" : "pending";
2557
- if (status === "completed") {
2558
- setNodeStatus(nodeState.nodeId, "success");
2559
- if (nodeState.output) {
2560
- const itemCount = calculateItemCount(
2561
- nodeState.output
2562
- );
2563
- setNodeItemCount(nodeState.nodeId, itemCount);
2564
- }
2565
- } else if (status === "failed") {
2566
- setNodeStatus(nodeState.nodeId, "error");
2567
- } else if (status === "running") {
2568
- setNodeStatus(nodeState.nodeId, "running");
2569
- }
2570
- const existingLog = useFlowStore.getState().execution.logs.find((l) => l.id === logId);
2571
- if (!existingLog) {
2572
- if (status === "pending") continue;
2573
- const newLog = {
2574
- id: logId,
2575
- runId,
2576
- nodeId: nodeState.nodeId,
2577
- nodeType: nodeState.nodeType,
2578
- nodeName: getNodeName(nodeState.nodeId),
2579
- status,
2580
- inputArgs: nodeState.inputArgs ?? null,
2581
- output: nodeState.output ?? null,
2582
- error: nodeState.error ?? null,
2583
- duration: nodeState.duration ?? null,
2584
- startTime: nodeState.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
2585
- endTime: nodeState.endTime ?? null,
2586
- usedPinnedData: nodeState.usedPinnedData ?? false,
2587
- reusedFromRunId: nodeState.reusedFromRunId ?? null
2588
- };
2589
- addLog(newLog);
2590
- } else {
2591
- if (existingLog.status !== status || status === "completed" && !existingLog.output) {
2592
- updateLog(logId, {
2593
- status,
2594
- inputArgs: nodeState.inputArgs ?? existingLog.inputArgs,
2595
- output: nodeState.output ?? existingLog.output,
2596
- error: nodeState.error ?? existingLog.error,
2597
- duration: nodeState.duration ?? existingLog.duration,
2598
- endTime: nodeState.endTime ?? existingLog.endTime,
2599
- usedPinnedData: nodeState.usedPinnedData ?? existingLog.usedPinnedData,
2600
- reusedFromRunId: nodeState.reusedFromRunId ?? existingLog.reusedFromRunId
2601
- });
2602
- }
2603
- }
2604
- }
2760
+ }
2761
+
2762
+ // src/builder/contants/symbols.ts
2763
+ var NODE_TYPE_DRAG_DATA_FORMAT = "application/flow-builder.node-type";
2764
+ var NODE_OPERATION_DRAG_DATA_FORMAT = "application/flow-builder.node-operation";
2765
+ var STICKY_NOTE_TYPE = "sticky-note";
2766
+ var STICKY_COLORS = {
2767
+ yellow: "#F2C94C",
2768
+ orange: "#EB8934",
2769
+ blue: "#3B82F6",
2770
+ green: "#10B981",
2771
+ red: "#EF4444",
2772
+ purple: "#8B5CF6",
2773
+ gray: "#9CA3AF"
2774
+ };
2775
+ var DEFAULT_STICKY_COLOR = "yellow";
2776
+ var STICKY_DEFAULT_SIZE = { width: 260, height: 170 };
2777
+ var STICKY_MIN_SIZE = { width: 160, height: 90 };
2778
+ function ToolbarButton({
2779
+ title,
2780
+ onClick,
2781
+ danger,
2782
+ children
2783
+ }) {
2784
+ return /* @__PURE__ */ jsx(
2785
+ "button",
2786
+ {
2787
+ type: "button",
2788
+ title,
2789
+ onClick: (e) => {
2790
+ e.stopPropagation();
2791
+ onClick();
2792
+ },
2793
+ className: cn(
2794
+ "nodrag flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
2795
+ danger && "text-[var(--color-status-error-text)] hover:text-[var(--color-status-error-text)]"
2796
+ ),
2797
+ children
2798
+ }
2799
+ );
2800
+ }
2801
+ function StickyNoteNodeComponent({
2802
+ id,
2803
+ selected,
2804
+ data
2805
+ }) {
2806
+ const tr = useTr();
2807
+ const { setNodes, getNode } = useReactFlow();
2808
+ const zustandSetNodes = useFlowStore((s) => s.actions.nodes.setNodes);
2809
+ const deleteNode = useDeleteNode();
2810
+ const [editing, setEditing] = useState(false);
2811
+ const [showPalette, setShowPalette] = useState(false);
2812
+ const [draft, setDraft] = useState(String(data.content ?? ""));
2813
+ const textareaRef = useRef(null);
2814
+ const rootRef = useRef(null);
2815
+ useEffect(() => {
2816
+ const wrapper = rootRef.current?.closest(".react-flow__node");
2817
+ if (wrapper && wrapper.style.zIndex !== "-1") {
2818
+ wrapper.style.zIndex = "-1";
2819
+ }
2820
+ });
2821
+ const colorKey = typeof data.color === "string" && STICKY_COLORS[data.color] ? data.color : DEFAULT_STICKY_COLOR;
2822
+ const accent = STICKY_COLORS[colorKey];
2823
+ const patchData = useCallback(
2824
+ (patch) => {
2825
+ setNodes((nds) => {
2826
+ const updated = nds.map(
2827
+ (n) => n.id === id ? { ...n, data: { ...n.data, ...patch } } : n
2828
+ );
2829
+ zustandSetNodes(updated);
2830
+ return updated;
2831
+ });
2605
2832
  },
2606
- [setNodeStatus, setNodeItemCount, addLog, updateLog, getNodeName]
2833
+ [id, setNodes, zustandSetNodes]
2607
2834
  );
2608
- const pollRunUpdates = useCallback(
2609
- (runId) => {
2610
- const client = getFluidGridsClient();
2611
- pollStartRef.current = Date.now();
2612
- activeRunIdRef.current = runId;
2613
- const doPoll = async () => {
2614
- if (activeRunIdRef.current !== runId) {
2615
- return;
2616
- }
2617
- try {
2618
- const result = await client.query({
2619
- query: WORKFLOW_RUN,
2620
- variables: { id: runId },
2621
- fetchPolicy: "network-only"
2622
- });
2623
- if (activeRunIdRef.current !== runId) {
2624
- return;
2625
- }
2626
- const run = result.data?.workflowRun;
2627
- if (!run) {
2628
- console.warn(
2629
- "[useNodeExecution] Poll returned no run data for",
2630
- runId
2631
- );
2632
- if (Date.now() - pollStartRef.current < MAX_RUN_CREATION_WAIT) {
2633
- pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL);
2634
- } else {
2635
- console.warn(
2636
- "[useNodeExecution] Run data not found after timeout for",
2637
- runId
2638
- );
2639
- completeExecution();
2640
- cleanup();
2641
- }
2642
- return;
2643
- }
2644
- setRunMetadata({
2645
- runId,
2646
- workflowKey: run.workflowKey,
2647
- version: run.version,
2648
- sourceMode: run.mode
2649
- });
2650
- if (run.nodeStates?.length) {
2651
- processNodeStates(runId, run.nodeStates);
2652
- }
2653
- if (TERMINAL_STATUSES.has(run.status)) {
2654
- completeExecution();
2655
- cleanup();
2656
- return;
2657
- }
2658
- pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL);
2659
- } catch (error) {
2660
- console.error("[useNodeExecution] Poll error:", error);
2661
- if (activeRunIdRef.current !== runId) {
2662
- return;
2835
+ const startEditing = useCallback(() => {
2836
+ setDraft(String(getNode(id)?.data?.content ?? ""));
2837
+ setEditing(true);
2838
+ setShowPalette(false);
2839
+ }, [getNode, id]);
2840
+ const commitEdit = useCallback(() => {
2841
+ setEditing(false);
2842
+ patchData({ content: draft });
2843
+ }, [draft, patchData]);
2844
+ const cancelEdit = useCallback(() => {
2845
+ setEditing(false);
2846
+ setDraft(String(getNode(id)?.data?.content ?? ""));
2847
+ }, [getNode, id]);
2848
+ useEffect(() => {
2849
+ if (editing) {
2850
+ const el = textareaRef.current;
2851
+ el?.focus();
2852
+ el?.setSelectionRange(el.value.length, el.value.length);
2853
+ }
2854
+ }, [editing]);
2855
+ const duplicate = useCallback(() => {
2856
+ const source = getNode(id);
2857
+ if (!source) return;
2858
+ const clone = {
2859
+ ...source,
2860
+ id: nanoid(),
2861
+ position: {
2862
+ x: source.position.x + 24,
2863
+ y: source.position.y + 24
2864
+ },
2865
+ selected: true,
2866
+ data: { ...source.data }
2867
+ };
2868
+ setNodes((nds) => {
2869
+ const updated = [
2870
+ ...nds.map((n) => n.selected ? { ...n, selected: false } : n),
2871
+ clone
2872
+ ];
2873
+ zustandSetNodes(updated);
2874
+ return updated;
2875
+ });
2876
+ }, [getNode, id, setNodes, zustandSetNodes]);
2877
+ const content = String(data.content ?? "");
2878
+ return (
2879
+ // Outer wrapper is NOT overflow-hidden: the NodeResizer's edge lines and
2880
+ // corner handles sit on the bounds and would be clipped/covered otherwise.
2881
+ /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "group relative h-full w-full", children: [
2882
+ /* @__PURE__ */ jsx(
2883
+ NodeResizer,
2884
+ {
2885
+ isVisible: !!selected,
2886
+ minWidth: STICKY_MIN_SIZE.width,
2887
+ minHeight: STICKY_MIN_SIZE.height,
2888
+ lineStyle: { border: "none", zIndex: 20 },
2889
+ handleStyle: {
2890
+ width: 12,
2891
+ height: 12,
2892
+ backgroundColor: "transparent",
2893
+ border: "none",
2894
+ zIndex: 20
2663
2895
  }
2664
- pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL * 2);
2665
2896
  }
2666
- };
2667
- pollTimerRef.current = setTimeout(doPoll, 800);
2668
- },
2669
- [processNodeStates, completeExecution, cleanup, setRunMetadata]
2670
- );
2671
- const executeStep = useCallback(
2672
- async (nodeId) => {
2673
- cleanup();
2674
- clearLogs();
2675
- const workflowKey = workflow.workflowKey || workflow.id;
2676
- const version = workflow.version || "1.0.0";
2677
- if (!workflowKey) {
2678
- console.error(
2679
- "[useNodeExecution] Workflow must be saved before step execution. No workflowKey found."
2680
- );
2681
- return {
2682
- success: false,
2683
- error: "Workflow must be saved before execution"
2684
- };
2685
- }
2686
- setNodeStatus(nodeId, "running");
2687
- try {
2688
- const client = getFluidGridsClient();
2689
- const result = throwIfResultError(
2690
- await client.mutate({
2691
- mutation: EXECUTE_STEP,
2692
- variables: {
2693
- input: {
2694
- workflowKey,
2695
- version,
2696
- nodeId
2897
+ ),
2898
+ /* @__PURE__ */ jsx(
2899
+ "div",
2900
+ {
2901
+ className: cn(
2902
+ "absolute right-1 z-30 flex items-center gap-0.5 rounded-lg border border-border bg-card p-0.5 shadow-md transition-all duration-150",
2903
+ selected || showPalette ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
2904
+ ),
2905
+ style: { top: -15 },
2906
+ onDoubleClick: (e) => e.stopPropagation(),
2907
+ children: showPalette ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-1 py-0.5", children: Object.entries(STICKY_COLORS).map(([name, hex]) => /* @__PURE__ */ jsx(
2908
+ "button",
2909
+ {
2910
+ type: "button",
2911
+ title: name,
2912
+ onClick: (e) => {
2913
+ e.stopPropagation();
2914
+ patchData({ color: name });
2915
+ setShowPalette(false);
2916
+ },
2917
+ className: cn(
2918
+ "nodrag size-4 rounded-full border transition-transform hover:scale-125",
2919
+ name === colorKey ? "border-foreground" : "border-transparent"
2920
+ ),
2921
+ style: { backgroundColor: hex }
2922
+ },
2923
+ name
2924
+ )) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2925
+ /* @__PURE__ */ jsx(
2926
+ ToolbarButton,
2927
+ {
2928
+ title: tr(
2929
+ "fluidgrids.builder.stickyNote.editNoteTooltip",
2930
+ "Edit note"
2931
+ ),
2932
+ onClick: startEditing,
2933
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:edit", className: "size-3.5" })
2934
+ }
2935
+ ),
2936
+ /* @__PURE__ */ jsx(
2937
+ ToolbarButton,
2938
+ {
2939
+ title: tr(
2940
+ "fluidgrids.builder.stickyNote.changeColorTooltip",
2941
+ "Change color"
2942
+ ),
2943
+ onClick: () => setShowPalette(true),
2944
+ children: /* @__PURE__ */ jsx(
2945
+ "span",
2946
+ {
2947
+ className: "size-3.5 rounded-full",
2948
+ style: { backgroundColor: accent }
2949
+ }
2950
+ )
2951
+ }
2952
+ ),
2953
+ /* @__PURE__ */ jsx(
2954
+ ToolbarButton,
2955
+ {
2956
+ title: tr(
2957
+ "fluidgrids.builder.stickyNote.duplicateNoteTooltip",
2958
+ "Duplicate note"
2959
+ ),
2960
+ onClick: duplicate,
2961
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:copy", className: "size-3.5" })
2962
+ }
2963
+ ),
2964
+ /* @__PURE__ */ jsx(
2965
+ ToolbarButton,
2966
+ {
2967
+ title: tr(
2968
+ "fluidgrids.builder.stickyNote.deleteNoteTooltip",
2969
+ "Delete note"
2970
+ ),
2971
+ danger: true,
2972
+ onClick: () => deleteNode(id),
2973
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:trash", className: "size-3.5" })
2697
2974
  }
2975
+ )
2976
+ ] })
2977
+ }
2978
+ ),
2979
+ /* @__PURE__ */ jsx(
2980
+ "div",
2981
+ {
2982
+ className: "flex h-full w-full flex-col overflow-hidden rounded-lg border",
2983
+ style: {
2984
+ backgroundColor: `${accent}1F`,
2985
+ // Same border whether selected or not — selection shows only via the
2986
+ // floating toolbar (no extra border/outline on click).
2987
+ borderColor: `${accent}59`
2988
+ },
2989
+ onDoubleClick: (e) => {
2990
+ e.stopPropagation();
2991
+ startEditing();
2992
+ },
2993
+ children: editing ? /* @__PURE__ */ jsx(
2994
+ "textarea",
2995
+ {
2996
+ ref: textareaRef,
2997
+ value: draft,
2998
+ onChange: (e) => setDraft(e.target.value),
2999
+ onBlur: commitEdit,
3000
+ onKeyDown: (e) => {
3001
+ e.stopPropagation();
3002
+ if (e.key === "Escape") cancelEdit();
3003
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commitEdit();
3004
+ },
3005
+ placeholder: tr(
3006
+ "fluidgrids.builder.stickyNote.editorPlaceholder",
3007
+ "# Note\nSupports **markdown** \u2014 headings, lists, links\u2026"
3008
+ ),
3009
+ className: "nodrag nowheel h-full w-full resize-none bg-transparent p-3 font-mono text-xs leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
2698
3010
  }
2699
- })
2700
- );
2701
- const stepResult = result.data?.executeStep;
2702
- if (stepResult) {
2703
- const backendRunId = stepResult.runId;
2704
- startExecution(nodeId, backendRunId, {
2705
- workflowKey: stepResult.workflowKey,
2706
- version: stepResult.version,
2707
- sourceMode: "TEST"
2708
- });
2709
- setStepPlan({
2710
- runId: backendRunId,
2711
- ...stepResult.plan,
2712
- sourceRunId: stepResult.plan.sourceRunId ?? null
2713
- });
2714
- const initialLog = {
2715
- id: `${backendRunId}-${nodeId}`,
2716
- runId: backendRunId,
2717
- nodeId,
2718
- nodeType: workflow.nodes.find((node) => node.id === nodeId)?.type || "unknown",
2719
- nodeName: getNodeName(nodeId),
2720
- status: "running",
2721
- inputArgs: null,
2722
- output: null,
2723
- error: null,
2724
- duration: null,
2725
- startTime: (/* @__PURE__ */ new Date()).toISOString(),
2726
- endTime: null
2727
- };
2728
- addLog(initialLog);
2729
- pollRunUpdates(backendRunId);
2730
- return { success: true, runId: backendRunId, plan: stepResult.plan };
3011
+ ) : content.trim() ? /* @__PURE__ */ jsx(
3012
+ "div",
3013
+ {
3014
+ className: cn(
3015
+ "nowheel h-full w-full overflow-auto p-3 text-xs leading-relaxed text-foreground",
3016
+ // Compact markdown typography scoped to the note body.
3017
+ "[&_h1]:mb-1.5 [&_h1]:text-sm [&_h1]:font-bold",
3018
+ "[&_h2]:mb-1 [&_h2]:text-[13px] [&_h2]:font-bold",
3019
+ "[&_h3]:mb-1 [&_h3]:text-xs [&_h3]:font-semibold",
3020
+ "[&_p]:mb-1.5 [&_ul]:mb-1.5 [&_ul]:list-disc [&_ul]:pl-4",
3021
+ "[&_ol]:mb-1.5 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-0.5",
3022
+ "[&_a]:underline [&_code]:rounded [&_code]:bg-muted [&_code]:px-1"
3023
+ ),
3024
+ children: /* @__PURE__ */ jsx(ReactMarkdown, { children: content })
3025
+ }
3026
+ ) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center p-3 text-xs text-muted-foreground/70", children: tr(
3027
+ "fluidgrids.builder.stickyNote.emptyStateHint",
3028
+ "Double-click to edit"
3029
+ ) })
2731
3030
  }
2732
- console.error("[useNodeExecution] No result from executeStep");
2733
- setNodeStatus(nodeId, "error");
2734
- return { success: false, error: "No result from executeStep" };
2735
- } catch (error) {
2736
- console.error("[useNodeExecution] Error executing step:", error);
2737
- const errorMessage = describeOperationError(error);
2738
- const errorRunId = nanoid$1();
2739
- const errorLog = {
2740
- id: `${errorRunId}-${nodeId}`,
2741
- runId: errorRunId,
2742
- nodeId,
2743
- nodeType: workflow.nodes.find((node) => node.id === nodeId)?.type || "unknown",
2744
- nodeName: getNodeName(nodeId),
2745
- status: "failed",
2746
- inputArgs: null,
2747
- output: null,
2748
- error: errorMessage,
2749
- duration: null,
2750
- startTime: (/* @__PURE__ */ new Date()).toISOString(),
2751
- endTime: (/* @__PURE__ */ new Date()).toISOString()
2752
- };
2753
- addLog(errorLog);
2754
- setNodeStatus(nodeId, "error");
2755
- completeExecution();
2756
- return { success: false, error: errorMessage };
2757
- }
2758
- },
2759
- [
2760
- cleanup,
2761
- clearLogs,
2762
- startExecution,
2763
- setStepPlan,
2764
- workflow,
2765
- getNodeName,
2766
- addLog,
2767
- setNodeStatus,
2768
- pollRunUpdates,
2769
- completeExecution
2770
- ]
3031
+ )
3032
+ ] })
2771
3033
  );
2772
- const executeFullWorkflow = useCallback(
2773
- async (customTriggerData) => {
2774
- const triggerNode = findTriggerNode();
2775
- if (!triggerNode) {
2776
- console.error("[useNodeExecution] No trigger node found in workflow");
2777
- return { success: false, error: "No trigger node found" };
2778
- }
2779
- cleanup();
2780
- clearLogs();
2781
- const workflowKey = workflow.workflowKey || workflow.id;
2782
- const version = workflow.version || "1.0.0";
2783
- if (!workflowKey) {
2784
- console.error(
2785
- "[useNodeExecution] Workflow must be saved before execution. No workflowKey found."
2786
- );
2787
- return {
2788
- success: false,
2789
- error: "Workflow must be saved before execution"
2790
- };
2791
- }
2792
- setNodeStatus(triggerNode.id, "running");
2793
- try {
2794
- const client = getFluidGridsClient();
2795
- const result = throwIfResultError(
2796
- await client.mutate({
2797
- mutation: TRIGGER_WORKFLOW,
2798
- variables: {
2799
- input: {
2800
- workflowKey,
2801
- version,
2802
- triggerNodeId: triggerNode.id,
2803
- triggerData: customTriggerData || {},
2804
- context: {},
2805
- // Builder "Test workflow" is always a manual/debug run.
2806
- // Pinned node data only applies to TEST executions, never a
2807
- // production trigger (webhook, schedule, API).
2808
- mode: "TEST"
2809
- }
2810
- }
2811
- })
2812
- );
2813
- const triggerResult = result.data?.triggerWorkflow;
2814
- if (triggerResult) {
2815
- const backendRunId = triggerResult.runId;
2816
- startExecution(triggerNode.id, backendRunId, {
2817
- workflowKey: triggerResult.workflowKey,
2818
- version: triggerResult.version,
2819
- sourceMode: "TEST"
2820
- });
2821
- pollRunUpdates(backendRunId);
2822
- return { success: true, runId: backendRunId };
2823
- }
2824
- console.error("[useNodeExecution] No result from trigger");
2825
- setNodeStatus(triggerNode.id, "error");
2826
- return { success: false, error: "No result from trigger" };
2827
- } catch (error) {
2828
- console.error("[useNodeExecution] Error triggering workflow:", error);
2829
- const errorMessage = describeOperationError(error);
2830
- const errorRunId = nanoid$1();
2831
- const errorLog = {
2832
- id: `${errorRunId}-${triggerNode.id}`,
2833
- runId: errorRunId,
2834
- nodeId: triggerNode.id,
2835
- nodeType: triggerNode.type || "unknown",
2836
- nodeName: getNodeName(triggerNode.id),
2837
- status: "failed",
2838
- inputArgs: null,
2839
- output: null,
2840
- error: errorMessage,
2841
- duration: null,
2842
- startTime: (/* @__PURE__ */ new Date()).toISOString(),
2843
- endTime: (/* @__PURE__ */ new Date()).toISOString()
2844
- };
2845
- addLog(errorLog);
2846
- setNodeStatus(triggerNode.id, "error");
2847
- completeExecution();
2848
- return { success: false, error: errorMessage };
2849
- }
2850
- },
2851
- [
2852
- findTriggerNode,
2853
- cleanup,
2854
- clearLogs,
2855
- startExecution,
2856
- workflow,
2857
- getNodeName,
2858
- addLog,
2859
- setNodeStatus,
2860
- pollRunUpdates,
2861
- completeExecution
2862
- ]
2863
- );
2864
- const executeFromNode = useCallback(
2865
- async (sourceRunId, nodeId) => {
2866
- cleanup();
2867
- clearLogs();
2868
- setNodeStatus(nodeId, "running");
2869
- try {
2870
- const client = getFluidGridsClient();
2871
- const result = throwIfResultError(
2872
- await client.mutate({
2873
- mutation: RUN_FROM_NODE,
2874
- variables: {
2875
- input: {
2876
- sourceRunId,
2877
- nodeId,
2878
- // Run-from-node is a debug affordance — always TEST, same
2879
- // rule as the other builder debug execution paths.
2880
- mode: "TEST"
2881
- }
2882
- }
2883
- })
2884
- );
2885
- const runFromNodeResult = result.data?.runFromNode;
2886
- if (runFromNodeResult) {
2887
- const backendRunId = runFromNodeResult.runId;
2888
- startExecution(nodeId, backendRunId, {
2889
- workflowKey: runFromNodeResult.workflowKey,
2890
- version: runFromNodeResult.version,
2891
- sourceMode: "TEST"
2892
- });
2893
- pollRunUpdates(backendRunId);
2894
- return { success: true, runId: backendRunId };
2895
- }
2896
- console.error("[useNodeExecution] No result from runFromNode");
2897
- setNodeStatus(nodeId, "error");
2898
- return { success: false, error: "No result from runFromNode" };
2899
- } catch (error) {
2900
- console.error("[useNodeExecution] Error running from node:", error);
2901
- const errorMessage = describeOperationError(error);
2902
- setNodeStatus(nodeId, "error");
2903
- completeExecution();
2904
- return { success: false, error: errorMessage };
2905
- }
2906
- },
2907
- [
2908
- cleanup,
2909
- clearLogs,
2910
- setNodeStatus,
2911
- startExecution,
2912
- pollRunUpdates,
2913
- completeExecution
2914
- ]
2915
- );
2916
- const loadExecution = useCallback(
2917
- async (runId) => {
2918
- cleanup();
2919
- const requestId = ++loadRequestRef.current;
2920
- try {
2921
- const client = getFluidGridsClient();
2922
- const result = throwIfResultError(
2923
- await client.query({
2924
- query: EXECUTION_DETAIL,
2925
- variables: { id: runId },
2926
- fetchPolicy: "network-only"
2927
- })
2928
- );
2929
- const run = result.data?.workflowRun;
2930
- if (!run) {
2931
- return { success: false, error: "Execution not found" };
2932
- }
2933
- if (run.mode !== "TEST") {
2934
- return {
2935
- success: false,
2936
- error: "Only TEST runs can be loaded for continuation."
2937
- };
2938
- }
2939
- if (!TERMINAL_STATUSES.has(run.status)) {
2940
- return {
2941
- success: false,
2942
- error: "Only terminal TEST runs can be loaded for continuation."
2943
- };
2944
- }
2945
- if (requestId !== loadRequestRef.current) {
2946
- return { success: false, error: "Execution load superseded." };
2947
- }
2948
- const currentWorkflow = useFlowStore.getState().workflow;
2949
- const currentWorkflowKey = currentWorkflow.workflowKey || currentWorkflow.id;
2950
- if (run.workflowKey !== currentWorkflowKey || run.version !== currentWorkflow.version) {
2951
- return {
2952
- success: false,
2953
- error: "The selected run belongs to a different workflow version."
2954
- };
2955
- }
2956
- const logs = (run.nodeStates ?? []).map((nodeState) => {
2957
- const rawStatus = nodeState.status.toLowerCase();
2958
- const status = rawStatus === "completed" ? "completed" : rawStatus === "failed" || rawStatus === "timeout" ? "failed" : rawStatus === "running" || rawStatus === "waiting" || rawStatus === "delayed" || rawStatus === "waiting_subworkflow" ? "running" : "pending";
2959
- return {
2960
- id: `${runId}-${nodeState.nodeId}`,
2961
- runId,
2962
- nodeId: nodeState.nodeId,
2963
- nodeType: nodeState.nodeType,
2964
- nodeName: getNodeName(nodeState.nodeId),
2965
- status,
2966
- inputArgs: nodeState.inputArgs ?? null,
2967
- output: nodeState.output ?? null,
2968
- error: nodeState.error ?? null,
2969
- duration: nodeState.duration ?? null,
2970
- startTime: nodeState.startTime ?? run.startedAt ?? "",
2971
- endTime: nodeState.endTime ?? null,
2972
- usedPinnedData: nodeState.usedPinnedData ?? false,
2973
- reusedFromRunId: nodeState.reusedFromRunId ?? null
2974
- };
2975
- });
2976
- loadDebugRun(runId, logs, {
2977
- workflowKey: run.workflowKey,
2978
- version: run.version,
2979
- sourceMode: run.mode
2980
- });
2981
- return { success: true, runId };
2982
- } catch (error) {
2983
- console.error("[useNodeExecution] Error loading execution:", error);
2984
- return { success: false, error: describeOperationError(error) };
2985
- }
2986
- },
2987
- [cleanup, getNodeName, loadDebugRun]
2988
- );
2989
- useEffect(() => {
2990
- return () => {
2991
- if (pollTimerRef.current) {
2992
- completeExecution();
2993
- }
2994
- cleanup();
3034
+ }
3035
+ var StickyNoteNode = memo(StickyNoteNodeComponent);
3036
+ function buildStickyNode(position) {
3037
+ return {
3038
+ id: nanoid(),
3039
+ type: STICKY_NOTE_TYPE,
3040
+ position,
3041
+ width: STICKY_DEFAULT_SIZE.width,
3042
+ height: STICKY_DEFAULT_SIZE.height,
3043
+ // Render under regular nodes and edges — notes are a background layer.
3044
+ zIndex: -1,
3045
+ selected: true,
3046
+ data: { content: "", color: DEFAULT_STICKY_COLOR }
3047
+ };
3048
+ }
3049
+ function getNodeDetail(nodeType) {
3050
+ const node = NODES.find((node2) => node2.type === nodeType);
3051
+ if (!node) {
3052
+ return {
3053
+ icon: "mynaui:keyboard-solid",
3054
+ title: nodeType ?? "Unknown",
3055
+ description: "",
3056
+ gradientColor: "gray"
2995
3057
  };
2996
- }, [cleanup, completeExecution]);
3058
+ }
3059
+ return node.detail;
3060
+ }
3061
+ function createNodeData(type, data) {
2997
3062
  return {
2998
- executeStep,
2999
- executeFullWorkflow,
3000
- executeFromNode,
3001
- loadExecution,
3002
- cleanup,
3003
- findTriggerNode
3063
+ id: nanoid$1(),
3064
+ type,
3065
+ data
3004
3066
  };
3005
3067
  }
3006
- function useDeleteNode() {
3007
- const { getNode, getEdges, deleteElements } = useReactFlow();
3008
- const deleteNodes = useFlowStore((state) => state.actions.nodes.deleteNodes);
3009
- return useCallback(
3010
- (id) => {
3011
- const node = getNode(id);
3012
- if (!node) return;
3013
- const edges = getEdges();
3014
- const connectedEdges = getConnectedEdges([node], edges);
3015
- deleteElements({ nodes: [node], edges: connectedEdges }).then();
3016
- deleteNodes([node]);
3017
- },
3018
- [deleteElements, getEdges, getNode, deleteNodes]
3019
- );
3068
+ function createNodeWithDefaultData(type, data) {
3069
+ const defaultData = NODES.find((node) => node.type === type)?.defaultData ?? {};
3070
+ return Object.assign(createNodeData(type, defaultData), data);
3020
3071
  }
3021
-
3022
- // src/builder/contants/symbols.ts
3023
- var NODE_TYPE_DRAG_DATA_FORMAT = "application/flow-builder.node-type";
3024
- var NODE_OPERATION_DRAG_DATA_FORMAT = "application/flow-builder.node-operation";
3025
- var STICKY_NOTE_TYPE = "sticky-note";
3026
- var STICKY_COLORS = {
3027
- yellow: "#F2C94C",
3028
- orange: "#EB8934",
3029
- blue: "#3B82F6",
3030
- green: "#10B981",
3031
- red: "#EF4444",
3032
- purple: "#8B5CF6",
3033
- gray: "#9CA3AF"
3034
- };
3035
- var DEFAULT_STICKY_COLOR = "yellow";
3036
- var STICKY_DEFAULT_SIZE = { width: 260, height: 170 };
3037
- var STICKY_MIN_SIZE = { width: 160, height: 90 };
3038
- function ToolbarButton({
3039
- title,
3040
- onClick,
3041
- danger,
3042
- children
3072
+ var NODE_DOCS_URL = gql`
3073
+ query NodeDocsUrlLookup($nodeType: String!, $nodeVersion: String!) {
3074
+ nodeEndpointConfig(nodeType: $nodeType, nodeVersion: $nodeVersion) {
3075
+ docsUrl
3076
+ }
3077
+ }
3078
+ `;
3079
+ var cache = /* @__PURE__ */ new Map();
3080
+ var inFlight = /* @__PURE__ */ new Map();
3081
+ function fetchDocsUrl(nodeType, nodeVersion) {
3082
+ const key = `${nodeType}@${nodeVersion}`;
3083
+ if (cache.has(key)) return Promise.resolve(cache.get(key));
3084
+ const existing = inFlight.get(key);
3085
+ if (existing) return existing;
3086
+ const client = getFluidGridsClientSafe();
3087
+ if (!client) {
3088
+ cache.set(key, void 0);
3089
+ return Promise.resolve(void 0);
3090
+ }
3091
+ const promise = client.query({
3092
+ query: NODE_DOCS_URL,
3093
+ variables: { nodeType, nodeVersion },
3094
+ fetchPolicy: "cache-first"
3095
+ }).then((result) => result.data?.nodeEndpointConfig?.docsUrl ?? void 0).catch(() => void 0).finally(() => inFlight.delete(key));
3096
+ inFlight.set(key, promise);
3097
+ return promise.then((url) => {
3098
+ cache.set(key, url);
3099
+ return url;
3100
+ });
3101
+ }
3102
+ function useNodeDocsUrl(nodeType, nodeVersion) {
3103
+ const [docsUrl, setDocsUrl] = useState(
3104
+ () => nodeType && nodeVersion ? cache.get(`${nodeType}@${nodeVersion}`) : void 0
3105
+ );
3106
+ useEffect(() => {
3107
+ if (!nodeType || !nodeVersion) {
3108
+ setDocsUrl(void 0);
3109
+ return;
3110
+ }
3111
+ let cancelled = false;
3112
+ fetchDocsUrl(nodeType, nodeVersion).then((url) => {
3113
+ if (!cancelled) setDocsUrl(url);
3114
+ });
3115
+ return () => {
3116
+ cancelled = true;
3117
+ };
3118
+ }, [nodeType, nodeVersion]);
3119
+ return docsUrl;
3120
+ }
3121
+ function isImageIcon(icon) {
3122
+ return !!icon && (/^https?:\/\//.test(icon) || icon.startsWith("data:"));
3123
+ }
3124
+ function NodeTypeIcon({
3125
+ icon,
3126
+ alt,
3127
+ className,
3128
+ fallbackIcon = "mynaui:puzzle"
3129
+ }) {
3130
+ if (isImageIcon(icon)) {
3131
+ return /* @__PURE__ */ jsx(
3132
+ "img",
3133
+ {
3134
+ src: icon,
3135
+ alt: alt ?? "",
3136
+ className: cn("object-contain", className)
3137
+ }
3138
+ );
3139
+ }
3140
+ return /* @__PURE__ */ jsx(Icon, { icon: icon || fallbackIcon, className });
3141
+ }
3142
+ var BRAND_ACCENTS = [
3143
+ [/gmail|google/, "#ea4335"],
3144
+ [/slack/, "#611f69"],
3145
+ [/github/, "#334155"],
3146
+ [/gitlab/, "#fc6d26"],
3147
+ [/stripe/, "#635bff"],
3148
+ [/discord/, "#5865f2"],
3149
+ [/notion/, "#111827"],
3150
+ [/telegram/, "#2aabee"],
3151
+ [/whatsapp/, "#25d366"],
3152
+ [/openai/, "#10a37f"],
3153
+ [/anthropic|claude/, "#d97757"],
3154
+ [/twilio/, "#f22f46"],
3155
+ [/sheet|excel/, "#21a366"],
3156
+ [/http|api|webhook|rest/, "#6366f1"],
3157
+ [/db|sql|postgres|mysql|mongo/, "#0ea5e9"]
3158
+ ];
3159
+ var HASH_PALETTE = [
3160
+ "#6366f1",
3161
+ "#0ea5e9",
3162
+ "#10b981",
3163
+ "#f59e0b",
3164
+ "#ef4444",
3165
+ "#a855f7",
3166
+ "#ec4899",
3167
+ "#14b8a6",
3168
+ "#f97316",
3169
+ "#8b5cf6"
3170
+ ];
3171
+ function nodeAccent(key) {
3172
+ const k = (key || "").toLowerCase();
3173
+ for (const [re, hex] of BRAND_ACCENTS) if (re.test(k)) return hex;
3174
+ let h = 0;
3175
+ for (let i = 0; i < k.length; i++) h = h * 31 + k.charCodeAt(i) >>> 0;
3176
+ return HASH_PALETTE[h % HASH_PALETTE.length];
3177
+ }
3178
+ function tint(hex, pct = 14) {
3179
+ return `color-mix(in srgb, ${hex} ${pct}%, transparent)`;
3180
+ }
3181
+ function InlineEditableLabel({
3182
+ value,
3183
+ placeholder,
3184
+ editing,
3185
+ onStartEdit,
3186
+ onCommit,
3187
+ onCancel,
3188
+ className,
3189
+ inputClassName
3043
3190
  }) {
3191
+ const tr = useTr();
3192
+ const [draft, setDraft] = React2.useState(value);
3193
+ const inputRef = React2.useRef(null);
3194
+ React2.useEffect(() => {
3195
+ if (!editing) return;
3196
+ setDraft(value);
3197
+ const raf = requestAnimationFrame(() => {
3198
+ inputRef.current?.focus();
3199
+ inputRef.current?.select();
3200
+ });
3201
+ return () => cancelAnimationFrame(raf);
3202
+ }, [editing, value]);
3203
+ if (editing) {
3204
+ return /* @__PURE__ */ jsx(
3205
+ "input",
3206
+ {
3207
+ ref: inputRef,
3208
+ value: draft,
3209
+ onChange: (e) => setDraft(e.target.value),
3210
+ onClick: (e) => e.stopPropagation(),
3211
+ onDoubleClick: (e) => e.stopPropagation(),
3212
+ onMouseDown: (e) => e.stopPropagation(),
3213
+ onKeyDown: (e) => {
3214
+ e.stopPropagation();
3215
+ if (e.key === "Enter") {
3216
+ e.preventDefault();
3217
+ onCommit(draft.trim());
3218
+ } else if (e.key === "Escape") {
3219
+ e.preventDefault();
3220
+ onCancel();
3221
+ }
3222
+ },
3223
+ onBlur: () => onCommit(draft.trim()),
3224
+ className: cn(
3225
+ "min-w-0 flex-1 rounded border border-primary/50 bg-card px-1 text-inherit outline-none",
3226
+ inputClassName
3227
+ )
3228
+ }
3229
+ );
3230
+ }
3044
3231
  return /* @__PURE__ */ jsx(
3045
- "button",
3232
+ "span",
3046
3233
  {
3047
- type: "button",
3048
- title,
3049
- onClick: (e) => {
3234
+ className: cn("cursor-text truncate", className),
3235
+ title: tr(
3236
+ "fluidgrids.builder.nodeCard.renameTooltip",
3237
+ "Double-click or press F2 to rename"
3238
+ ),
3239
+ onDoubleClick: (e) => {
3050
3240
  e.stopPropagation();
3051
- onClick();
3241
+ onStartEdit();
3052
3242
  },
3053
- className: cn(
3054
- "nodrag flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
3055
- danger && "text-[var(--color-status-error-text)] hover:text-[var(--color-status-error-text)]"
3056
- ),
3057
- children
3243
+ children: value || placeholder
3058
3244
  }
3059
3245
  );
3060
3246
  }
3061
- function StickyNoteNodeComponent({
3062
- id,
3063
- selected,
3064
- data
3065
- }) {
3066
- const tr = useTr();
3067
- const { setNodes, getNode } = useReactFlow();
3068
- const zustandSetNodes = useFlowStore((s) => s.actions.nodes.setNodes);
3069
- const deleteNode = useDeleteNode();
3070
- const [editing, setEditing] = useState(false);
3071
- const [showPalette, setShowPalette] = useState(false);
3072
- const [draft, setDraft] = useState(String(data.content ?? ""));
3073
- const textareaRef = useRef(null);
3074
- const rootRef = useRef(null);
3075
- useEffect(() => {
3076
- const wrapper = rootRef.current?.closest(".react-flow__node");
3077
- if (wrapper && wrapper.style.zIndex !== "-1") {
3078
- wrapper.style.zIndex = "-1";
3079
- }
3080
- });
3081
- const colorKey = typeof data.color === "string" && STICKY_COLORS[data.color] ? data.color : DEFAULT_STICKY_COLOR;
3082
- const accent = STICKY_COLORS[colorKey];
3083
- const patchData = useCallback(
3084
- (patch) => {
3085
- setNodes((nds) => {
3086
- const updated = nds.map(
3087
- (n) => n.id === id ? { ...n, data: { ...n.data, ...patch } } : n
3088
- );
3089
- zustandSetNodes(updated);
3090
- return updated;
3091
- });
3092
- },
3093
- [id, setNodes, zustandSetNodes]
3094
- );
3095
- const startEditing = useCallback(() => {
3096
- setDraft(String(getNode(id)?.data?.content ?? ""));
3097
- setEditing(true);
3098
- setShowPalette(false);
3099
- }, [getNode, id]);
3100
- const commitEdit = useCallback(() => {
3101
- setEditing(false);
3102
- patchData({ content: draft });
3103
- }, [draft, patchData]);
3104
- const cancelEdit = useCallback(() => {
3105
- setEditing(false);
3106
- setDraft(String(getNode(id)?.data?.content ?? ""));
3107
- }, [getNode, id]);
3108
- useEffect(() => {
3109
- if (editing) {
3110
- const el = textareaRef.current;
3111
- el?.focus();
3112
- el?.setSelectionRange(el.value.length, el.value.length);
3113
- }
3114
- }, [editing]);
3115
- const duplicate = useCallback(() => {
3116
- const source = getNode(id);
3117
- if (!source) return;
3118
- const clone = {
3119
- ...source,
3120
- id: nanoid(),
3121
- position: {
3122
- x: source.position.x + 24,
3123
- y: source.position.y + 24
3124
- },
3125
- selected: true,
3126
- data: { ...source.data }
3127
- };
3128
- setNodes((nds) => {
3129
- const updated = [
3130
- ...nds.map((n) => n.selected ? { ...n, selected: false } : n),
3131
- clone
3132
- ];
3133
- zustandSetNodes(updated);
3134
- return updated;
3135
- });
3136
- }, [getNode, id, setNodes, zustandSetNodes]);
3137
- const content = String(data.content ?? "");
3138
- return (
3139
- // Outer wrapper is NOT overflow-hidden: the NodeResizer's edge lines and
3140
- // corner handles sit on the bounds and would be clipped/covered otherwise.
3141
- /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "group relative h-full w-full", children: [
3142
- /* @__PURE__ */ jsx(
3143
- NodeResizer,
3144
- {
3145
- isVisible: !!selected,
3146
- minWidth: STICKY_MIN_SIZE.width,
3147
- minHeight: STICKY_MIN_SIZE.height,
3148
- lineStyle: { border: "none", zIndex: 20 },
3149
- handleStyle: {
3150
- width: 12,
3151
- height: 12,
3152
- backgroundColor: "transparent",
3153
- border: "none",
3154
- zIndex: 20
3155
- }
3156
- }
3157
- ),
3158
- /* @__PURE__ */ jsx(
3159
- "div",
3160
- {
3161
- className: cn(
3162
- "absolute right-1 z-30 flex items-center gap-0.5 rounded-lg border border-border bg-card p-0.5 shadow-md transition-all duration-150",
3163
- selected || showPalette ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
3164
- ),
3165
- style: { top: -15 },
3166
- onDoubleClick: (e) => e.stopPropagation(),
3167
- children: showPalette ? /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-1 py-0.5", children: Object.entries(STICKY_COLORS).map(([name, hex]) => /* @__PURE__ */ jsx(
3168
- "button",
3247
+ var NodeCard = React2.forwardRef(
3248
+ ({
3249
+ className,
3250
+ selected = false,
3251
+ executionStatus = "idle",
3252
+ itemCount,
3253
+ isTrigger = false,
3254
+ accent = "#3b82f6",
3255
+ onSettings,
3256
+ onExecute,
3257
+ onDelete,
3258
+ isExecuting = false,
3259
+ infoButton,
3260
+ disabled = false,
3261
+ onToggleDisabled,
3262
+ children,
3263
+ ...props
3264
+ }, ref) => {
3265
+ const tr = useTr();
3266
+ const [hovered, setHovered] = React2.useState(false);
3267
+ const statusRing = executionStatus === "running" ? "ring-2 ring-[var(--color-status-success-border)] animate-pulse-border" : executionStatus === "success" ? "ring-2 ring-[var(--color-status-success-border)]" : executionStatus === "error" ? "ring-2 ring-[var(--color-status-error-border)]" : "";
3268
+ return /* @__PURE__ */ jsxs(
3269
+ "div",
3270
+ {
3271
+ className: "group/node relative",
3272
+ onMouseEnter: () => setHovered(true),
3273
+ onMouseLeave: () => setHovered(false),
3274
+ children: [
3275
+ itemCount !== void 0 && itemCount > 0 && /* @__PURE__ */ jsx("div", { className: "absolute -top-7 left-1/2 -translate-x-1/2 z-10", children: /* @__PURE__ */ jsxs("div", { className: "rounded-full border border-border bg-card px-2 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm", children: [
3276
+ itemCount,
3277
+ " ",
3278
+ itemCount !== 1 ? tr("fluidgrids.builder.nodeCard.itemCountOther", "items") : tr("fluidgrids.builder.nodeCard.itemCountOne", "item")
3279
+ ] }) }),
3280
+ (onSettings || onExecute || onDelete || onToggleDisabled || infoButton) && /* @__PURE__ */ jsxs(
3281
+ "div",
3169
3282
  {
3170
- type: "button",
3171
- title: name,
3172
- onClick: (e) => {
3173
- e.stopPropagation();
3174
- patchData({ color: name });
3175
- setShowPalette(false);
3176
- },
3177
3283
  className: cn(
3178
- "nodrag size-4 rounded-full border transition-transform hover:scale-125",
3179
- name === colorKey ? "border-foreground" : "border-transparent"
3284
+ "absolute right-1 z-20 flex items-center gap-0.5 rounded-lg border border-border bg-card p-0.5 shadow-md transition-all duration-150",
3285
+ hovered ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
3180
3286
  ),
3181
- style: { backgroundColor: hex }
3182
- },
3183
- name
3184
- )) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3185
- /* @__PURE__ */ jsx(
3186
- ToolbarButton,
3187
- {
3188
- title: tr(
3189
- "fluidgrids.builder.stickyNote.editNoteTooltip",
3190
- "Edit note"
3191
- ),
3192
- onClick: startEditing,
3193
- children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:edit", className: "size-3.5" })
3194
- }
3195
- ),
3196
- /* @__PURE__ */ jsx(
3197
- ToolbarButton,
3198
- {
3199
- title: tr(
3200
- "fluidgrids.builder.stickyNote.changeColorTooltip",
3201
- "Change color"
3287
+ style: {
3288
+ // Inline offset (not a `-top-*` class) so it survives the host
3289
+ // app's Tailwind scan of fe-sdk source, which drops some spacing
3290
+ // utilities. The toolbar is ~30px tall; top:-15px centers it on
3291
+ // the card's top edge (half outside, half over the card).
3292
+ top: -15,
3293
+ transform: hovered ? "translateY(0)" : "translateY(4px)"
3294
+ },
3295
+ onClick: (e) => e.stopPropagation(),
3296
+ onDoubleClick: (e) => e.stopPropagation(),
3297
+ children: [
3298
+ onSettings && /* @__PURE__ */ jsx(
3299
+ ToolbarButton2,
3300
+ {
3301
+ title: tr(
3302
+ "fluidgrids.builder.nodeCard.configureTooltip",
3303
+ "Configure"
3304
+ ),
3305
+ onClick: onSettings,
3306
+ children: /* @__PURE__ */ jsx(Icon, { icon: "lucide:sliders-horizontal", className: "size-4" })
3307
+ }
3202
3308
  ),
3203
- onClick: () => setShowPalette(true),
3204
- children: /* @__PURE__ */ jsx(
3205
- "span",
3309
+ onExecute && /* @__PURE__ */ jsx(
3310
+ ToolbarButton2,
3206
3311
  {
3207
- className: "size-3.5 rounded-full",
3208
- style: { backgroundColor: accent }
3312
+ title: tr(
3313
+ "fluidgrids.builder.nodeCard.executeTooltip",
3314
+ "Execute step"
3315
+ ),
3316
+ onClick: onExecute,
3317
+ disabled: isExecuting,
3318
+ children: isExecuting ? /* @__PURE__ */ jsx(
3319
+ Icon,
3320
+ {
3321
+ icon: "svg-spinners:ring-resize",
3322
+ className: "size-4 text-[var(--color-status-success-text)]"
3323
+ }
3324
+ ) : /* @__PURE__ */ jsx(
3325
+ Icon,
3326
+ {
3327
+ icon: "mdi:play",
3328
+ className: "size-4 text-[var(--color-status-success-text)]"
3329
+ }
3330
+ )
3209
3331
  }
3210
- )
3211
- }
3212
- ),
3213
- /* @__PURE__ */ jsx(
3214
- ToolbarButton,
3215
- {
3216
- title: tr(
3217
- "fluidgrids.builder.stickyNote.duplicateNoteTooltip",
3218
- "Duplicate note"
3219
3332
  ),
3220
- onClick: duplicate,
3221
- children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:copy", className: "size-3.5" })
3222
- }
3223
- ),
3224
- /* @__PURE__ */ jsx(
3225
- ToolbarButton,
3226
- {
3227
- title: tr(
3228
- "fluidgrids.builder.stickyNote.deleteNoteTooltip",
3229
- "Delete note"
3333
+ infoButton,
3334
+ onToggleDisabled && /* @__PURE__ */ jsx(
3335
+ ToolbarButton2,
3336
+ {
3337
+ title: disabled ? tr(
3338
+ "fluidgrids.builder.nodeCard.enableTooltip",
3339
+ "Enable node"
3340
+ ) : tr(
3341
+ "fluidgrids.builder.nodeCard.disableTooltip",
3342
+ "Disable node (skip on run)"
3343
+ ),
3344
+ onClick: onToggleDisabled,
3345
+ children: /* @__PURE__ */ jsx(
3346
+ Icon,
3347
+ {
3348
+ icon: disabled ? "mdi:power" : "mdi:power-off",
3349
+ className: cn(
3350
+ "size-4",
3351
+ disabled ? "text-[var(--color-status-warning-text)]" : "text-muted-foreground"
3352
+ )
3353
+ }
3354
+ )
3355
+ }
3230
3356
  ),
3231
- danger: true,
3232
- onClick: () => deleteNode(id),
3233
- children: /* @__PURE__ */ jsx(Icon, { icon: "mynaui:trash", className: "size-3.5" })
3234
- }
3235
- )
3236
- ] })
3237
- }
3238
- ),
3239
- /* @__PURE__ */ jsx(
3240
- "div",
3241
- {
3242
- className: "flex h-full w-full flex-col overflow-hidden rounded-lg border",
3243
- style: {
3244
- backgroundColor: `${accent}1F`,
3245
- // Same border whether selected or not — selection shows only via the
3246
- // floating toolbar (no extra border/outline on click).
3247
- borderColor: `${accent}59`
3248
- },
3249
- onDoubleClick: (e) => {
3250
- e.stopPropagation();
3251
- startEditing();
3252
- },
3253
- children: editing ? /* @__PURE__ */ jsx(
3254
- "textarea",
3255
- {
3256
- ref: textareaRef,
3257
- value: draft,
3258
- onChange: (e) => setDraft(e.target.value),
3259
- onBlur: commitEdit,
3260
- onKeyDown: (e) => {
3261
- e.stopPropagation();
3262
- if (e.key === "Escape") cancelEdit();
3263
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) commitEdit();
3264
- },
3265
- placeholder: tr(
3266
- "fluidgrids.builder.stickyNote.editorPlaceholder",
3267
- "# Note\nSupports **markdown** \u2014 headings, lists, links\u2026"
3268
- ),
3269
- className: "nodrag nowheel h-full w-full resize-none bg-transparent p-3 font-mono text-xs leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
3357
+ onDelete && /* @__PURE__ */ jsx(
3358
+ ToolbarButton2,
3359
+ {
3360
+ title: tr(
3361
+ "fluidgrids.builder.nodeCard.deleteTooltip",
3362
+ "Delete"
3363
+ ),
3364
+ onClick: onDelete,
3365
+ children: /* @__PURE__ */ jsx(
3366
+ Icon,
3367
+ {
3368
+ icon: "mynaui:trash",
3369
+ className: "size-4 text-[var(--color-status-error-text)]"
3370
+ }
3371
+ )
3372
+ }
3373
+ )
3374
+ ]
3270
3375
  }
3271
- ) : content.trim() ? /* @__PURE__ */ jsx(
3272
- "div",
3273
- {
3274
- className: cn(
3275
- "nowheel h-full w-full overflow-auto p-3 text-xs leading-relaxed text-foreground",
3276
- // Compact markdown typography scoped to the note body.
3277
- "[&_h1]:mb-1.5 [&_h1]:text-sm [&_h1]:font-bold",
3278
- "[&_h2]:mb-1 [&_h2]:text-[13px] [&_h2]:font-bold",
3279
- "[&_h3]:mb-1 [&_h3]:text-xs [&_h3]:font-semibold",
3280
- "[&_p]:mb-1.5 [&_ul]:mb-1.5 [&_ul]:list-disc [&_ul]:pl-4",
3281
- "[&_ol]:mb-1.5 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-0.5",
3282
- "[&_a]:underline [&_code]:rounded [&_code]:bg-muted [&_code]:px-1"
3376
+ ),
3377
+ disabled && /* @__PURE__ */ jsx("div", { className: "absolute -top-2 right-2 z-10", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full border border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-[var(--color-status-warning-text)] shadow-sm", children: [
3378
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:power-off", className: "size-2.5" }),
3379
+ tr("fluidgrids.builder.nodeCard.disabledBadge", "Disabled")
3380
+ ] }) }),
3381
+ /* @__PURE__ */ jsxs(
3382
+ "div",
3383
+ {
3384
+ ref,
3385
+ "data-selected": selected,
3386
+ className: cn(
3387
+ "relative w-72 rounded-xl border border-border bg-card shadow-sm will-change-transform",
3388
+ statusRing,
3389
+ executionStatus === "success" && "animate-success-glow",
3390
+ disabled && "opacity-50 grayscale-[0.6]",
3391
+ className
3283
3392
  ),
3284
- children: /* @__PURE__ */ jsx(ReactMarkdown, { children: content })
3393
+ style: {
3394
+ borderTop: `3px solid ${accent}`,
3395
+ transition: "transform 0.15s ease, box-shadow 0.15s ease",
3396
+ transform: hovered ? "translateY(-2px)" : "none",
3397
+ boxShadow: hovered ? "0 2px 4px rgba(16,24,40,0.06), 0 16px 34px -12px rgba(16,24,40,0.24)" : void 0
3398
+ },
3399
+ ...props,
3400
+ children: [
3401
+ /* @__PURE__ */ jsx(
3402
+ "div",
3403
+ {
3404
+ className: "pointer-events-none absolute -inset-0.5 transition-opacity duration-150",
3405
+ style: {
3406
+ borderRadius: 14,
3407
+ opacity: selected || hovered ? 1 : 0,
3408
+ boxShadow: selected ? `0 0 0 2px ${accent}, 0 0 0 5px ${tint(accent, 18)}` : `0 0 0 2px ${accent}`
3409
+ }
3410
+ }
3411
+ ),
3412
+ isTrigger && /* @__PURE__ */ jsx(
3413
+ "div",
3414
+ {
3415
+ className: "absolute -top-2 -left-2 z-10 flex size-5 items-center justify-center rounded-full shadow-md ring-2 ring-card",
3416
+ style: { backgroundColor: "var(--color-status-success-text)" },
3417
+ children: /* @__PURE__ */ jsx(
3418
+ Icon,
3419
+ {
3420
+ icon: "mdi:lightning-bolt",
3421
+ className: "size-3",
3422
+ style: { color: "var(--color-status-success-text-inverse)" }
3423
+ }
3424
+ )
3425
+ }
3426
+ ),
3427
+ children
3428
+ ]
3285
3429
  }
3286
- ) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center p-3 text-xs text-muted-foreground/70", children: tr(
3287
- "fluidgrids.builder.stickyNote.emptyStateHint",
3288
- "Double-click to edit"
3289
- ) })
3290
- }
3291
- )
3292
- ] })
3293
- );
3294
- }
3295
- var StickyNoteNode = memo(StickyNoteNodeComponent);
3296
- function buildStickyNode(position) {
3297
- return {
3298
- id: nanoid(),
3299
- type: STICKY_NOTE_TYPE,
3300
- position,
3301
- width: STICKY_DEFAULT_SIZE.width,
3302
- height: STICKY_DEFAULT_SIZE.height,
3303
- // Render under regular nodes and edges — notes are a background layer.
3304
- zIndex: -1,
3305
- selected: true,
3306
- data: { content: "", color: DEFAULT_STICKY_COLOR }
3307
- };
3308
- }
3309
- function getNodeDetail(nodeType) {
3310
- const node = NODES.find((node2) => node2.type === nodeType);
3311
- if (!node) {
3312
- return {
3313
- icon: "mynaui:keyboard-solid",
3314
- title: nodeType ?? "Unknown",
3315
- description: "",
3316
- gradientColor: "gray"
3317
- };
3430
+ ),
3431
+ executionStatus === "success" && /* @__PURE__ */ jsx(StatusCorner, { icon: "mdi:check", tone: "success" }),
3432
+ executionStatus === "error" && /* @__PURE__ */ jsx(StatusCorner, { icon: "mdi:close", tone: "error" }),
3433
+ executionStatus === "running" && /* @__PURE__ */ jsx(StatusCorner, { icon: "svg-spinners:ring-resize", tone: "success" })
3434
+ ]
3435
+ }
3436
+ );
3318
3437
  }
3319
- return node.detail;
3320
- }
3321
- function createNodeData(type, data) {
3322
- return {
3323
- id: nanoid$1(),
3324
- type,
3325
- data
3326
- };
3327
- }
3328
- function createNodeWithDefaultData(type, data) {
3329
- const defaultData = NODES.find((node) => node.type === type)?.defaultData ?? {};
3330
- return Object.assign(createNodeData(type, defaultData), data);
3331
- }
3332
- var NODE_DOCS_URL = gql`
3333
- query NodeDocsUrlLookup($nodeType: String!, $nodeVersion: String!) {
3334
- nodeEndpointConfig(nodeType: $nodeType, nodeVersion: $nodeVersion) {
3335
- docsUrl
3438
+ );
3439
+ NodeCard.displayName = "NodeCard";
3440
+ function ToolbarButton2({
3441
+ title,
3442
+ onClick,
3443
+ disabled,
3444
+ children
3445
+ }) {
3446
+ return /* @__PURE__ */ jsx(
3447
+ "button",
3448
+ {
3449
+ type: "button",
3450
+ title,
3451
+ disabled,
3452
+ onClick: (e) => {
3453
+ e.stopPropagation();
3454
+ onClick?.();
3455
+ },
3456
+ className: "flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50",
3457
+ children
3336
3458
  }
3337
- }
3338
- `;
3339
- var cache = /* @__PURE__ */ new Map();
3340
- var inFlight = /* @__PURE__ */ new Map();
3341
- function fetchDocsUrl(nodeType, nodeVersion) {
3342
- const key = `${nodeType}@${nodeVersion}`;
3343
- if (cache.has(key)) return Promise.resolve(cache.get(key));
3344
- const existing = inFlight.get(key);
3345
- if (existing) return existing;
3346
- const client = getFluidGridsClientSafe();
3347
- if (!client) {
3348
- cache.set(key, void 0);
3349
- return Promise.resolve(void 0);
3350
- }
3351
- const promise = client.query({
3352
- query: NODE_DOCS_URL,
3353
- variables: { nodeType, nodeVersion },
3354
- fetchPolicy: "cache-first"
3355
- }).then((result) => result.data?.nodeEndpointConfig?.docsUrl ?? void 0).catch(() => void 0).finally(() => inFlight.delete(key));
3356
- inFlight.set(key, promise);
3357
- return promise.then((url) => {
3358
- cache.set(key, url);
3359
- return url;
3360
- });
3361
- }
3362
- function useNodeDocsUrl(nodeType, nodeVersion) {
3363
- const [docsUrl, setDocsUrl] = useState(
3364
- () => nodeType && nodeVersion ? cache.get(`${nodeType}@${nodeVersion}`) : void 0
3365
3459
  );
3366
- useEffect(() => {
3367
- if (!nodeType || !nodeVersion) {
3368
- setDocsUrl(void 0);
3369
- return;
3370
- }
3371
- let cancelled = false;
3372
- fetchDocsUrl(nodeType, nodeVersion).then((url) => {
3373
- if (!cancelled) setDocsUrl(url);
3374
- });
3375
- return () => {
3376
- cancelled = true;
3377
- };
3378
- }, [nodeType, nodeVersion]);
3379
- return docsUrl;
3380
- }
3381
- function isImageIcon(icon) {
3382
- return !!icon && (/^https?:\/\//.test(icon) || icon.startsWith("data:"));
3383
3460
  }
3384
- function NodeTypeIcon({
3461
+ function StatusCorner({
3385
3462
  icon,
3386
- alt,
3387
- className,
3388
- fallbackIcon = "mynaui:puzzle"
3463
+ tone
3389
3464
  }) {
3390
- if (isImageIcon(icon)) {
3391
- return /* @__PURE__ */ jsx(
3392
- "img",
3393
- {
3394
- src: icon,
3395
- alt: alt ?? "",
3396
- className: cn("object-contain", className)
3397
- }
3398
- );
3399
- }
3400
- return /* @__PURE__ */ jsx(Icon, { icon: icon || fallbackIcon, className });
3401
- }
3402
- var BRAND_ACCENTS = [
3403
- [/gmail|google/, "#ea4335"],
3404
- [/slack/, "#611f69"],
3405
- [/github/, "#334155"],
3406
- [/gitlab/, "#fc6d26"],
3407
- [/stripe/, "#635bff"],
3408
- [/discord/, "#5865f2"],
3409
- [/notion/, "#111827"],
3410
- [/telegram/, "#2aabee"],
3411
- [/whatsapp/, "#25d366"],
3412
- [/openai/, "#10a37f"],
3413
- [/anthropic|claude/, "#d97757"],
3414
- [/twilio/, "#f22f46"],
3415
- [/sheet|excel/, "#21a366"],
3416
- [/http|api|webhook|rest/, "#6366f1"],
3417
- [/db|sql|postgres|mysql|mongo/, "#0ea5e9"]
3418
- ];
3419
- var HASH_PALETTE = [
3420
- "#6366f1",
3421
- "#0ea5e9",
3422
- "#10b981",
3423
- "#f59e0b",
3424
- "#ef4444",
3425
- "#a855f7",
3426
- "#ec4899",
3427
- "#14b8a6",
3428
- "#f97316",
3429
- "#8b5cf6"
3430
- ];
3431
- function nodeAccent(key) {
3432
- const k = (key || "").toLowerCase();
3433
- for (const [re, hex] of BRAND_ACCENTS) if (re.test(k)) return hex;
3434
- let h = 0;
3435
- for (let i = 0; i < k.length; i++) h = h * 31 + k.charCodeAt(i) >>> 0;
3436
- return HASH_PALETTE[h % HASH_PALETTE.length];
3437
- }
3438
- function tint(hex, pct = 14) {
3439
- return `color-mix(in srgb, ${hex} ${pct}%, transparent)`;
3465
+ const bg = tone === "success" ? "var(--color-status-success-bg)" : "var(--color-status-error-bg)";
3466
+ const fg = tone === "success" ? "var(--color-status-success-text-inverse)" : "var(--color-status-error-text-inverse)";
3467
+ return /* @__PURE__ */ jsx("div", { className: "absolute -bottom-1.5 -right-1.5 z-10", children: /* @__PURE__ */ jsx(
3468
+ "div",
3469
+ {
3470
+ className: "rounded-full p-0.5 shadow-md animate-in zoom-in duration-200",
3471
+ style: { backgroundColor: bg },
3472
+ children: /* @__PURE__ */ jsx(Icon, { icon, className: "size-3", style: { color: fg } })
3473
+ }
3474
+ ) });
3440
3475
  }
3441
- function InlineEditableLabel({
3442
- value,
3443
- placeholder,
3444
- editing,
3445
- onStartEdit,
3446
- onCommit,
3447
- onCancel,
3448
- className,
3449
- inputClassName
3450
- }) {
3451
- const tr = useTr();
3452
- const [draft, setDraft] = React2.useState(value);
3453
- const inputRef = React2.useRef(null);
3454
- React2.useEffect(() => {
3455
- if (!editing) return;
3456
- setDraft(value);
3457
- const raf = requestAnimationFrame(() => {
3458
- inputRef.current?.focus();
3459
- inputRef.current?.select();
3460
- });
3461
- return () => cancelAnimationFrame(raf);
3462
- }, [editing, value]);
3463
- if (editing) {
3464
- return /* @__PURE__ */ jsx(
3465
- "input",
3476
+ var NodeCardHeader = React2.forwardRef(
3477
+ ({
3478
+ icon,
3479
+ appName,
3480
+ accentKey,
3481
+ missingCredentials = false,
3482
+ label,
3483
+ isRenaming = false,
3484
+ onStartRename,
3485
+ onCommitRename,
3486
+ onCancelRename
3487
+ }, ref) => {
3488
+ const tr = useTr();
3489
+ const accent = nodeAccent(accentKey);
3490
+ return /* @__PURE__ */ jsxs("div", { ref, className: "flex items-center gap-2.5 px-3 pb-3 pt-4", children: [
3491
+ /* @__PURE__ */ jsx(
3492
+ "div",
3493
+ {
3494
+ className: "flex size-8 shrink-0 items-center justify-center rounded-lg",
3495
+ style: {
3496
+ backgroundColor: tint(accent, 13),
3497
+ color: accent,
3498
+ border: `1px solid ${tint(accent, 26)}`
3499
+ },
3500
+ children: /* @__PURE__ */ jsx(NodeTypeIcon, { icon, alt: appName, className: "size-[18px]" })
3501
+ }
3502
+ ),
3503
+ onStartRename && onCommitRename && onCancelRename ? /* @__PURE__ */ jsx(
3504
+ InlineEditableLabel,
3505
+ {
3506
+ value: label ?? "",
3507
+ placeholder: appName,
3508
+ editing: isRenaming,
3509
+ onStartEdit: onStartRename,
3510
+ onCommit: onCommitRename,
3511
+ onCancel: onCancelRename,
3512
+ className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground",
3513
+ inputClassName: "text-[11px] font-semibold uppercase tracking-wide"
3514
+ }
3515
+ ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: label || appName }),
3516
+ missingCredentials && /* @__PURE__ */ jsx(
3517
+ "span",
3518
+ {
3519
+ title: tr(
3520
+ "fluidgrids.builder.nodeCard.missingCredentialsTooltip",
3521
+ "Credentials not configured"
3522
+ ),
3523
+ className: "flex items-center text-[var(--color-status-warning-text)]",
3524
+ "aria-label": tr(
3525
+ "fluidgrids.builder.nodeCard.missingCredentialsAriaLabel",
3526
+ "Credentials not configured"
3527
+ ),
3528
+ children: /* @__PURE__ */ jsx(Icon, { icon: "lucide:alert-triangle", className: "size-4" })
3529
+ }
3530
+ )
3531
+ ] });
3532
+ }
3533
+ );
3534
+ NodeCardHeader.displayName = "NodeCardHeader";
3535
+ var NodeCardBody = React2.forwardRef(
3536
+ ({ operationName, description }, ref) => {
3537
+ const tr = useTr();
3538
+ return /* @__PURE__ */ jsxs(
3539
+ "div",
3466
3540
  {
3467
- ref: inputRef,
3468
- value: draft,
3469
- onChange: (e) => setDraft(e.target.value),
3470
- onClick: (e) => e.stopPropagation(),
3471
- onDoubleClick: (e) => e.stopPropagation(),
3472
- onMouseDown: (e) => e.stopPropagation(),
3473
- onKeyDown: (e) => {
3474
- e.stopPropagation();
3475
- if (e.key === "Enter") {
3476
- e.preventDefault();
3477
- onCommit(draft.trim());
3478
- } else if (e.key === "Escape") {
3479
- e.preventDefault();
3480
- onCancel();
3481
- }
3482
- },
3483
- onBlur: () => onCommit(draft.trim()),
3484
- className: cn(
3485
- "min-w-0 flex-1 rounded border border-primary/50 bg-card px-1 text-inherit outline-none",
3486
- inputClassName
3487
- )
3541
+ ref,
3542
+ className: "rounded-b-xl border-t border-border/60 px-3 pb-3 pt-2.5",
3543
+ children: [
3544
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: tr("fluidgrids.builder.nodeCard.operationLabel", "Operation") }),
3545
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 truncate font-mono text-[13px] font-bold text-card-foreground", children: operationName }),
3546
+ description ? /* @__PURE__ */ jsx("p", { className: "mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground", children: description }) : null
3547
+ ]
3488
3548
  }
3489
3549
  );
3490
3550
  }
3491
- return /* @__PURE__ */ jsx(
3492
- "span",
3551
+ );
3552
+ NodeCardBody.displayName = "NodeCardBody";
3553
+ function CustomHandle({
3554
+ className,
3555
+ isConnectable,
3556
+ accent,
3557
+ ...props
3558
+ }) {
3559
+ const { nodeLookup, edges } = useStore(({ nodeLookup: nodeLookup2, edges: edges2 }) => ({
3560
+ nodeLookup: nodeLookup2,
3561
+ edges: edges2
3562
+ }));
3563
+ const nodeId = useNodeId();
3564
+ const isHandleConnectable = useMemo(() => {
3565
+ if (!nodeId) return false;
3566
+ const node = nodeLookup.get(nodeId);
3567
+ if (!node) return false;
3568
+ const connectedEdges = getConnectedEdges([node], edges);
3569
+ if (typeof isConnectable === "function")
3570
+ return isConnectable({ node, connectedEdges });
3571
+ if (typeof isConnectable === "number")
3572
+ return connectedEdges.length < isConnectable;
3573
+ return isConnectable;
3574
+ }, [edges, isConnectable, nodeId, nodeLookup]);
3575
+ return /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(
3576
+ Handle,
3493
3577
  {
3494
- className: cn("cursor-text truncate", className),
3495
- title: tr(
3496
- "fluidgrids.builder.nodeCard.renameTooltip",
3497
- "Double-click or press F2 to rename"
3578
+ className: cn(
3579
+ "cursor-crosshair transition-transform hover:scale-110",
3580
+ !accent && "size-3 border-2 border-card-foreground/40 hover:border-primary/50",
3581
+ className
3498
3582
  ),
3499
- onDoubleClick: (e) => {
3500
- e.stopPropagation();
3501
- onStartEdit();
3502
- },
3503
- children: value || placeholder
3583
+ isConnectable: isHandleConnectable,
3584
+ style: accent ? {
3585
+ width: 14,
3586
+ height: 14,
3587
+ borderRadius: "50%",
3588
+ background: "var(--card, #fff)",
3589
+ border: `2.5px solid ${accent}`,
3590
+ boxShadow: `0 0 0 3px color-mix(in srgb, ${accent} 12%, transparent)`
3591
+ } : void 0,
3592
+ ...props
3504
3593
  }
3505
- );
3506
- }
3507
- var NodeCard = React2.forwardRef(
3508
- ({
3509
- className,
3510
- selected = false,
3511
- executionStatus = "idle",
3512
- itemCount,
3513
- isTrigger = false,
3514
- accent = "#3b82f6",
3515
- onSettings,
3516
- onExecute,
3517
- onDelete,
3518
- isExecuting = false,
3519
- infoButton,
3520
- disabled = false,
3521
- onToggleDisabled,
3522
- children,
3523
- ...props
3524
- }, ref) => {
3525
- const tr = useTr();
3526
- const [hovered, setHovered] = React2.useState(false);
3527
- const statusRing = executionStatus === "running" ? "ring-2 ring-[var(--color-status-success-border)] animate-pulse-border" : executionStatus === "success" ? "ring-2 ring-[var(--color-status-success-border)]" : executionStatus === "error" ? "ring-2 ring-[var(--color-status-error-border)]" : "";
3528
- return /* @__PURE__ */ jsxs(
3529
- "div",
3530
- {
3531
- className: "group/node relative",
3532
- onMouseEnter: () => setHovered(true),
3533
- onMouseLeave: () => setHovered(false),
3534
- children: [
3535
- itemCount !== void 0 && itemCount > 0 && /* @__PURE__ */ jsx("div", { className: "absolute -top-7 left-1/2 -translate-x-1/2 z-10", children: /* @__PURE__ */ jsxs("div", { className: "rounded-full border border-border bg-card px-2 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm", children: [
3536
- itemCount,
3537
- " ",
3538
- itemCount !== 1 ? tr("fluidgrids.builder.nodeCard.itemCountOther", "items") : tr("fluidgrids.builder.nodeCard.itemCountOne", "item")
3539
- ] }) }),
3540
- (onSettings || onExecute || onDelete || onToggleDisabled || infoButton) && /* @__PURE__ */ jsxs(
3541
- "div",
3542
- {
3543
- className: cn(
3544
- "absolute right-1 z-20 flex items-center gap-0.5 rounded-lg border border-border bg-card p-0.5 shadow-md transition-all duration-150",
3545
- hovered ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
3546
- ),
3547
- style: {
3548
- // Inline offset (not a `-top-*` class) so it survives the host
3549
- // app's Tailwind scan of fe-sdk source, which drops some spacing
3550
- // utilities. The toolbar is ~30px tall; top:-15px centers it on
3551
- // the card's top edge (half outside, half over the card).
3552
- top: -15,
3553
- transform: hovered ? "translateY(0)" : "translateY(4px)"
3554
- },
3555
- onClick: (e) => e.stopPropagation(),
3556
- onDoubleClick: (e) => e.stopPropagation(),
3557
- children: [
3558
- onSettings && /* @__PURE__ */ jsx(
3559
- ToolbarButton2,
3560
- {
3561
- title: tr(
3562
- "fluidgrids.builder.nodeCard.configureTooltip",
3563
- "Configure"
3564
- ),
3565
- onClick: onSettings,
3566
- children: /* @__PURE__ */ jsx(Icon, { icon: "lucide:sliders-horizontal", className: "size-4" })
3567
- }
3568
- ),
3569
- onExecute && /* @__PURE__ */ jsx(
3570
- ToolbarButton2,
3571
- {
3572
- title: tr(
3573
- "fluidgrids.builder.nodeCard.executeTooltip",
3574
- "Execute step"
3575
- ),
3576
- onClick: onExecute,
3577
- disabled: isExecuting,
3578
- children: isExecuting ? /* @__PURE__ */ jsx(
3579
- Icon,
3580
- {
3581
- icon: "svg-spinners:ring-resize",
3582
- className: "size-4 text-[var(--color-status-success-text)]"
3583
- }
3584
- ) : /* @__PURE__ */ jsx(
3585
- Icon,
3586
- {
3587
- icon: "mdi:play",
3588
- className: "size-4 text-[var(--color-status-success-text)]"
3589
- }
3590
- )
3591
- }
3592
- ),
3593
- infoButton,
3594
- onToggleDisabled && /* @__PURE__ */ jsx(
3595
- ToolbarButton2,
3596
- {
3597
- title: disabled ? tr(
3598
- "fluidgrids.builder.nodeCard.enableTooltip",
3599
- "Enable node"
3600
- ) : tr(
3601
- "fluidgrids.builder.nodeCard.disableTooltip",
3602
- "Disable node (skip on run)"
3603
- ),
3604
- onClick: onToggleDisabled,
3605
- children: /* @__PURE__ */ jsx(
3606
- Icon,
3607
- {
3608
- icon: disabled ? "mdi:power" : "mdi:power-off",
3609
- className: cn(
3610
- "size-4",
3611
- disabled ? "text-[var(--color-status-warning-text)]" : "text-muted-foreground"
3612
- )
3613
- }
3614
- )
3615
- }
3616
- ),
3617
- onDelete && /* @__PURE__ */ jsx(
3618
- ToolbarButton2,
3619
- {
3620
- title: tr(
3621
- "fluidgrids.builder.nodeCard.deleteTooltip",
3622
- "Delete"
3623
- ),
3624
- onClick: onDelete,
3625
- children: /* @__PURE__ */ jsx(
3626
- Icon,
3627
- {
3628
- icon: "mynaui:trash",
3629
- className: "size-4 text-[var(--color-status-error-text)]"
3630
- }
3631
- )
3632
- }
3633
- )
3634
- ]
3635
- }
3636
- ),
3637
- disabled && /* @__PURE__ */ jsx("div", { className: "absolute -top-2 right-2 z-10", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full border border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg)] px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-[var(--color-status-warning-text)] shadow-sm", children: [
3638
- /* @__PURE__ */ jsx(Icon, { icon: "mdi:power-off", className: "size-2.5" }),
3639
- tr("fluidgrids.builder.nodeCard.disabledBadge", "Disabled")
3640
- ] }) }),
3641
- /* @__PURE__ */ jsxs(
3642
- "div",
3643
- {
3644
- ref,
3645
- "data-selected": selected,
3646
- className: cn(
3647
- "relative w-72 rounded-xl border border-border bg-card shadow-sm will-change-transform",
3648
- statusRing,
3649
- executionStatus === "success" && "animate-success-glow",
3650
- disabled && "opacity-50 grayscale-[0.6]",
3651
- className
3652
- ),
3653
- style: {
3654
- borderTop: `3px solid ${accent}`,
3655
- transition: "transform 0.15s ease, box-shadow 0.15s ease",
3656
- transform: hovered ? "translateY(-2px)" : "none",
3657
- boxShadow: hovered ? "0 2px 4px rgba(16,24,40,0.06), 0 16px 34px -12px rgba(16,24,40,0.24)" : void 0
3658
- },
3659
- ...props,
3660
- children: [
3661
- /* @__PURE__ */ jsx(
3662
- "div",
3663
- {
3664
- className: "pointer-events-none absolute -inset-0.5 transition-opacity duration-150",
3665
- style: {
3666
- borderRadius: 14,
3667
- opacity: selected || hovered ? 1 : 0,
3668
- boxShadow: selected ? `0 0 0 2px ${accent}, 0 0 0 5px ${tint(accent, 18)}` : `0 0 0 2px ${accent}`
3669
- }
3670
- }
3671
- ),
3672
- isTrigger && /* @__PURE__ */ jsx(
3673
- "div",
3674
- {
3675
- className: "absolute -top-2 -left-2 z-10 flex size-5 items-center justify-center rounded-full shadow-md ring-2 ring-card",
3676
- style: { backgroundColor: "var(--color-status-success-text)" },
3677
- children: /* @__PURE__ */ jsx(
3678
- Icon,
3679
- {
3680
- icon: "mdi:lightning-bolt",
3681
- className: "size-3",
3682
- style: { color: "var(--color-status-success-text-inverse)" }
3683
- }
3684
- )
3685
- }
3686
- ),
3687
- children
3688
- ]
3689
- }
3690
- ),
3691
- executionStatus === "success" && /* @__PURE__ */ jsx(StatusCorner, { icon: "mdi:check", tone: "success" }),
3692
- executionStatus === "error" && /* @__PURE__ */ jsx(StatusCorner, { icon: "mdi:close", tone: "error" }),
3693
- executionStatus === "running" && /* @__PURE__ */ jsx(StatusCorner, { icon: "svg-spinners:ring-resize", tone: "success" })
3694
- ]
3695
- }
3696
- );
3697
- }
3698
- );
3699
- NodeCard.displayName = "NodeCard";
3700
- function ToolbarButton2({
3701
- title,
3702
- onClick,
3703
- disabled,
3704
- children
3705
- }) {
3706
- return /* @__PURE__ */ jsx(
3707
- "button",
3708
- {
3709
- type: "button",
3710
- title,
3711
- disabled,
3712
- onClick: (e) => {
3713
- e.stopPropagation();
3714
- onClick?.();
3715
- },
3716
- className: "flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50",
3717
- children
3718
- }
3719
- );
3720
- }
3721
- function StatusCorner({
3722
- icon,
3723
- tone
3724
- }) {
3725
- const bg = tone === "success" ? "var(--color-status-success-bg)" : "var(--color-status-error-bg)";
3726
- const fg = tone === "success" ? "var(--color-status-success-text-inverse)" : "var(--color-status-error-text-inverse)";
3727
- return /* @__PURE__ */ jsx("div", { className: "absolute -bottom-1.5 -right-1.5 z-10", children: /* @__PURE__ */ jsx(
3728
- "div",
3729
- {
3730
- className: "rounded-full p-0.5 shadow-md animate-in zoom-in duration-200",
3731
- style: { backgroundColor: bg },
3732
- children: /* @__PURE__ */ jsx(Icon, { icon, className: "size-3", style: { color: fg } })
3733
- }
3734
- ) });
3735
- }
3736
- var NodeCardHeader = React2.forwardRef(
3737
- ({
3738
- icon,
3739
- appName,
3740
- accentKey,
3741
- missingCredentials = false,
3742
- label,
3743
- isRenaming = false,
3744
- onStartRename,
3745
- onCommitRename,
3746
- onCancelRename
3747
- }, ref) => {
3748
- const tr = useTr();
3749
- const accent = nodeAccent(accentKey);
3750
- return /* @__PURE__ */ jsxs("div", { ref, className: "flex items-center gap-2.5 px-3 pb-3 pt-4", children: [
3751
- /* @__PURE__ */ jsx(
3752
- "div",
3753
- {
3754
- className: "flex size-8 shrink-0 items-center justify-center rounded-lg",
3755
- style: {
3756
- backgroundColor: tint(accent, 13),
3757
- color: accent,
3758
- border: `1px solid ${tint(accent, 26)}`
3759
- },
3760
- children: /* @__PURE__ */ jsx(NodeTypeIcon, { icon, alt: appName, className: "size-[18px]" })
3761
- }
3762
- ),
3763
- onStartRename && onCommitRename && onCancelRename ? /* @__PURE__ */ jsx(
3764
- InlineEditableLabel,
3765
- {
3766
- value: label ?? "",
3767
- placeholder: appName,
3768
- editing: isRenaming,
3769
- onStartEdit: onStartRename,
3770
- onCommit: onCommitRename,
3771
- onCancel: onCancelRename,
3772
- className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground",
3773
- inputClassName: "text-[11px] font-semibold uppercase tracking-wide"
3774
- }
3775
- ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: label || appName }),
3776
- missingCredentials && /* @__PURE__ */ jsx(
3777
- "span",
3778
- {
3779
- title: tr(
3780
- "fluidgrids.builder.nodeCard.missingCredentialsTooltip",
3781
- "Credentials not configured"
3782
- ),
3783
- className: "flex items-center text-[var(--color-status-warning-text)]",
3784
- "aria-label": tr(
3785
- "fluidgrids.builder.nodeCard.missingCredentialsAriaLabel",
3786
- "Credentials not configured"
3787
- ),
3788
- children: /* @__PURE__ */ jsx(Icon, { icon: "lucide:alert-triangle", className: "size-4" })
3789
- }
3790
- )
3791
- ] });
3792
- }
3793
- );
3794
- NodeCardHeader.displayName = "NodeCardHeader";
3795
- var NodeCardBody = React2.forwardRef(
3796
- ({ operationName, description }, ref) => {
3797
- const tr = useTr();
3798
- return /* @__PURE__ */ jsxs(
3799
- "div",
3800
- {
3801
- ref,
3802
- className: "rounded-b-xl border-t border-border/60 px-3 pb-3 pt-2.5",
3803
- children: [
3804
- /* @__PURE__ */ jsx("div", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: tr("fluidgrids.builder.nodeCard.operationLabel", "Operation") }),
3805
- /* @__PURE__ */ jsx("div", { className: "mt-0.5 truncate font-mono text-[13px] font-bold text-card-foreground", children: operationName }),
3806
- description ? /* @__PURE__ */ jsx("p", { className: "mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground", children: description }) : null
3807
- ]
3808
- }
3809
- );
3810
- }
3811
- );
3812
- NodeCardBody.displayName = "NodeCardBody";
3813
- function CustomHandle({
3814
- className,
3815
- isConnectable,
3816
- accent,
3817
- ...props
3818
- }) {
3819
- const { nodeLookup, edges } = useStore(({ nodeLookup: nodeLookup2, edges: edges2 }) => ({
3820
- nodeLookup: nodeLookup2,
3821
- edges: edges2
3822
- }));
3823
- const nodeId = useNodeId();
3824
- const isHandleConnectable = useMemo(() => {
3825
- if (!nodeId) return false;
3826
- const node = nodeLookup.get(nodeId);
3827
- if (!node) return false;
3828
- const connectedEdges = getConnectedEdges([node], edges);
3829
- if (typeof isConnectable === "function")
3830
- return isConnectable({ node, connectedEdges });
3831
- if (typeof isConnectable === "number")
3832
- return connectedEdges.length < isConnectable;
3833
- return isConnectable;
3834
- }, [edges, isConnectable, nodeId, nodeLookup]);
3835
- return /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(
3836
- Handle,
3837
- {
3838
- className: cn(
3839
- "cursor-crosshair transition-transform hover:scale-110",
3840
- !accent && "size-3 border-2 border-card-foreground/40 hover:border-primary/50",
3841
- className
3842
- ),
3843
- isConnectable: isHandleConnectable,
3844
- style: accent ? {
3845
- width: 14,
3846
- height: 14,
3847
- borderRadius: "50%",
3848
- background: "var(--card, #fff)",
3849
- border: `2.5px solid ${accent}`,
3850
- boxShadow: `0 0 0 3px color-mix(in srgb, ${accent} 12%, transparent)`
3851
- } : void 0,
3852
- ...props
3853
- }
3854
- ) });
3594
+ ) });
3855
3595
  }
3856
3596
 
3857
3597
  // src/builder/components/flow-builder/components/blocks/nodes/node/handle-spec.ts
@@ -3865,8 +3605,8 @@ function resolveOutputHandles(data) {
3865
3605
  if (!spec) return DEFAULT_HANDLES;
3866
3606
  if (isDynamicSpec(spec)) {
3867
3607
  const args = data?.args;
3868
- const record = args?.state?.[spec.fromStateRecord] ?? data?.[spec.fromStateRecord] ?? {};
3869
- const dynamic = Object.entries(record).filter(([, handleId]) => typeof handleId === "string" && handleId).map(([label, handleId]) => ({ id: handleId, label }));
3608
+ const record2 = args?.state?.[spec.fromStateRecord] ?? data?.[spec.fromStateRecord] ?? {};
3609
+ const dynamic = Object.entries(record2).filter(([, handleId]) => typeof handleId === "string" && handleId).map(([label, handleId]) => ({ id: handleId, label }));
3870
3610
  return [...dynamic, ...spec.extra ?? []];
3871
3611
  }
3872
3612
  if (spec.length === 0) return DEFAULT_HANDLES;
@@ -4283,7 +4023,11 @@ function useSaveWorkflow() {
4283
4023
  const partitioned = partitionStickyNotes(workflow.nodes ?? []);
4284
4024
  const preparedWorkflow = {
4285
4025
  ...workflow,
4286
- nodes: partitioned.nodes.map((n) => {
4026
+ nodes: normalizeWorkflowParameters(
4027
+ partitioned.nodes,
4028
+ workflow.edges ?? [],
4029
+ (type) => NODE_CONFIGS.find((config2) => config2.type === type)?.operations
4030
+ ).map((n) => {
4287
4031
  const slimData = slimNodeDataToCurrentOperation(
4288
4032
  n.data ?? {}
4289
4033
  );
@@ -5155,11 +4899,11 @@ function canRenderOperationPanel(selectedOperation, supportedOperations) {
5155
4899
  const operationName = resolveOperationName(selectedOperation, "");
5156
4900
  return operationName === "" || supportedOperations.includes(operationName);
5157
4901
  }
5158
- function isRecord3(value) {
4902
+ function isRecord5(value) {
5159
4903
  return typeof value === "object" && value !== null && !Array.isArray(value);
5160
4904
  }
5161
4905
  function readRecord2(value) {
5162
- return isRecord3(value) ? value : void 0;
4906
+ return isRecord5(value) ? value : void 0;
5163
4907
  }
5164
4908
  function readOperations(value) {
5165
4909
  const operations = Array.isArray(value) ? value : readRecord2(value)?.operations;
@@ -6064,76 +5808,636 @@ function ensureNodeConfigsLoaded() {
6064
5808
  } else {
6065
5809
  initPromise = null;
6066
5810
  }
6067
- rebuildNodeArrays();
6068
- });
6069
- }
6070
- return initPromise;
6071
- }
6072
- function isNodeConfigsInitialized() {
6073
- return isInitialized;
6074
- }
6075
- function rebuildNodeArrays() {
6076
- NODE_CONFIGS.forEach((config2) => {
6077
- const data = getNodeMetadata(config2);
6078
- const nodeMetadata = {
6079
- ...data,
6080
- detail: { ...config2.detail }
6081
- };
6082
- if (!NODES.some((n) => n.type === nodeMetadata.type)) {
6083
- NODES.push(nodeMetadata);
6084
- NODE_TYPES[nodeMetadata.type] = nodeMetadata.node;
6085
- NODES_METADATA[nodeMetadata.type] = {
6086
- ...nodeMetadata,
6087
- __details: { type: nodeMetadata.type, ...nodeMetadata.detail }
6088
- };
6089
- }
6090
- });
6091
- const newAvailableNodes = NODES.filter(
6092
- (node) => node.available === void 0 || node.available
6093
- ).map((node) => {
6094
- const nodeConfig = NODE_CONFIGS.find((config2) => config2.type === node.type);
6095
- return {
6096
- type: node.type,
6097
- icon: node.detail.icon,
6098
- title: node.detail.title,
6099
- description: node.detail.description,
6100
- gradientColor: node.detail.gradientColor ?? "gray",
6101
- manifest: node.defaultData?.nodeData,
6102
- publishType: nodeConfig?.publishType
6103
- // Include publishType
6104
- };
6105
- });
6106
- AVAILABLE_NODES.length = 0;
6107
- AVAILABLE_NODES.push(...newAvailableNodes);
6108
- notifyNodeTypesListeners();
6109
- }
6110
- ensureNodeConfigsLoaded().catch((err) => {
6111
- console.warn(
6112
- "[Workflows] Background node config initialization failed:",
6113
- err
5811
+ rebuildNodeArrays();
5812
+ });
5813
+ }
5814
+ return initPromise;
5815
+ }
5816
+ function isNodeConfigsInitialized() {
5817
+ return isInitialized;
5818
+ }
5819
+ function rebuildNodeArrays() {
5820
+ NODE_CONFIGS.forEach((config2) => {
5821
+ const data = getNodeMetadata(config2);
5822
+ const nodeMetadata = {
5823
+ ...data,
5824
+ detail: { ...config2.detail }
5825
+ };
5826
+ if (!NODES.some((n) => n.type === nodeMetadata.type)) {
5827
+ NODES.push(nodeMetadata);
5828
+ NODE_TYPES[nodeMetadata.type] = nodeMetadata.node;
5829
+ NODES_METADATA[nodeMetadata.type] = {
5830
+ ...nodeMetadata,
5831
+ __details: { type: nodeMetadata.type, ...nodeMetadata.detail }
5832
+ };
5833
+ }
5834
+ });
5835
+ const newAvailableNodes = NODES.filter(
5836
+ (node) => node.available === void 0 || node.available
5837
+ ).map((node) => {
5838
+ const nodeConfig = NODE_CONFIGS.find((config2) => config2.type === node.type);
5839
+ return {
5840
+ type: node.type,
5841
+ icon: node.detail.icon,
5842
+ title: node.detail.title,
5843
+ description: node.detail.description,
5844
+ gradientColor: node.detail.gradientColor ?? "gray",
5845
+ manifest: node.defaultData?.nodeData,
5846
+ publishType: nodeConfig?.publishType
5847
+ // Include publishType
5848
+ };
5849
+ });
5850
+ AVAILABLE_NODES.length = 0;
5851
+ AVAILABLE_NODES.push(...newAvailableNodes);
5852
+ notifyNodeTypesListeners();
5853
+ }
5854
+ ensureNodeConfigsLoaded().catch((err) => {
5855
+ console.warn(
5856
+ "[Workflows] Background node config initialization failed:",
5857
+ err
5858
+ );
5859
+ });
5860
+ var fallbackDynamic = getNodeMetadata({ type: "__default_dynamic__" });
5861
+ if (fallbackDynamic?.node) {
5862
+ NODE_TYPES["default"] = fallbackDynamic.node;
5863
+ }
5864
+ NODE_TYPES[STICKY_NOTE_TYPE] = StickyNoteNode;
5865
+ function registerWorkflowNodeTypes(workflowNodes) {
5866
+ for (const node of workflowNodes) {
5867
+ if (node.type && !NODE_TYPES[node.type]) {
5868
+ const meta = getNodeMetadata({
5869
+ type: node.type,
5870
+ defaultData: {
5871
+ version: node.data?.version,
5872
+ nodeData: node.data?.nodeData
5873
+ },
5874
+ operations: node.data?.operations
5875
+ });
5876
+ if (meta?.node) {
5877
+ NODE_TYPES[node.type] = meta.node;
5878
+ }
5879
+ }
5880
+ }
5881
+ }
5882
+
5883
+ // src/hooks/useNodeExecution.ts
5884
+ var TRIGGER_WORKFLOW = TriggerWorkflowDocument;
5885
+ var EXECUTE_STEP = ExecuteStepDocument;
5886
+ var WORKFLOW_RUN = WorkflowRunDocument;
5887
+ var RUN_FROM_NODE = RunFromNodeDocument;
5888
+ var EXECUTION_DETAIL = ExecutionDetailDocument;
5889
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
5890
+ "COMPLETED",
5891
+ "FAILED",
5892
+ "CANCELLED",
5893
+ "TERMINATED"
5894
+ ]);
5895
+ var POLL_INTERVAL = 1500;
5896
+ var MAX_RUN_CREATION_WAIT = 6e4;
5897
+ function calculateItemCount(output) {
5898
+ if (!output || Object.keys(output).length === 0) {
5899
+ return 0;
5900
+ }
5901
+ const values = Object.values(output);
5902
+ for (const value of values) {
5903
+ if (Array.isArray(value)) {
5904
+ return value.length;
5905
+ }
5906
+ if (typeof value === "object" && value !== null) {
5907
+ const nestedValues = Object.values(value);
5908
+ for (const nestedValue of nestedValues) {
5909
+ if (Array.isArray(nestedValue)) {
5910
+ return nestedValue.length;
5911
+ }
5912
+ }
5913
+ }
5914
+ }
5915
+ return 1;
5916
+ }
5917
+ function useNodeExecution() {
5918
+ const pollTimerRef = useRef(null);
5919
+ const pollStartRef = useRef(0);
5920
+ const activeRunIdRef = useRef(null);
5921
+ const loadRequestRef = useRef(0);
5922
+ const workflow = useFlowStore((s) => s.workflow);
5923
+ const startExecution = useFlowStore(
5924
+ (s) => s.actions.execution.startExecution
5925
+ );
5926
+ const setRunMetadata = useFlowStore(
5927
+ (s) => s.actions.execution.setRunMetadata
5928
+ );
5929
+ const setStepPlan = useFlowStore((s) => s.actions.execution.setStepPlan);
5930
+ const addLog = useFlowStore((s) => s.actions.execution.addLog);
5931
+ const updateLog = useFlowStore((s) => s.actions.execution.updateLog);
5932
+ const setNodeStatus = useFlowStore((s) => s.actions.execution.setNodeStatus);
5933
+ const setNodeItemCount = useFlowStore(
5934
+ (s) => s.actions.execution.setNodeItemCount
5935
+ );
5936
+ const completeExecution = useFlowStore(
5937
+ (s) => s.actions.execution.completeExecution
5938
+ );
5939
+ const clearLogs = useFlowStore((s) => s.actions.execution.clearLogs);
5940
+ const loadDebugRun = useFlowStore((s) => s.actions.execution.loadDebugRun);
5941
+ const cleanup = useCallback(() => {
5942
+ loadRequestRef.current += 1;
5943
+ if (pollTimerRef.current) {
5944
+ clearTimeout(pollTimerRef.current);
5945
+ pollTimerRef.current = null;
5946
+ }
5947
+ activeRunIdRef.current = null;
5948
+ }, []);
5949
+ const findTriggerNode = useCallback(() => {
5950
+ const { nodes, edges } = workflow;
5951
+ const explicitTrigger = nodes.find(isNodeTrigger);
5952
+ if (explicitTrigger) {
5953
+ return explicitTrigger;
5954
+ }
5955
+ const nodesWithIncomingEdges = new Set(edges.map((e) => e.target));
5956
+ const firstNode = nodes.find(
5957
+ (node) => !nodesWithIncomingEdges.has(node.id)
5958
+ );
5959
+ if (firstNode) {
5960
+ return firstNode;
5961
+ }
5962
+ if (nodes.length > 0) {
5963
+ return nodes[0];
5964
+ }
5965
+ console.error("[useNodeExecution] No nodes found in workflow");
5966
+ return null;
5967
+ }, [workflow]);
5968
+ const getNodeName = useCallback(
5969
+ (nodeId) => {
5970
+ const node = workflow.nodes.find((n) => n.id === nodeId);
5971
+ if (!node) return `Node ${nodeId}`;
5972
+ const data = node.data;
5973
+ return data?.operation_name || data?.name || node.type || `Node ${nodeId}`;
5974
+ },
5975
+ [workflow.nodes]
5976
+ );
5977
+ const processNodeStates = useCallback(
5978
+ (runId, nodeStates) => {
5979
+ for (const nodeState of nodeStates) {
5980
+ const logId = `${runId}-${nodeState.nodeId}`;
5981
+ const rawStatus = nodeState.status.toLowerCase();
5982
+ const status = rawStatus === "completed" ? "completed" : rawStatus === "failed" || rawStatus === "timeout" ? "failed" : rawStatus === "running" || rawStatus === "waiting" || rawStatus === "delayed" || rawStatus === "waiting_subworkflow" ? "running" : "pending";
5983
+ if (status === "completed") {
5984
+ setNodeStatus(nodeState.nodeId, "success");
5985
+ if (nodeState.output) {
5986
+ const itemCount = calculateItemCount(
5987
+ nodeState.output
5988
+ );
5989
+ setNodeItemCount(nodeState.nodeId, itemCount);
5990
+ }
5991
+ } else if (status === "failed") {
5992
+ setNodeStatus(nodeState.nodeId, "error");
5993
+ } else if (status === "running") {
5994
+ setNodeStatus(nodeState.nodeId, "running");
5995
+ }
5996
+ const existingLog = useFlowStore.getState().execution.logs.find((l) => l.id === logId);
5997
+ if (!existingLog) {
5998
+ if (status === "pending") continue;
5999
+ const newLog = {
6000
+ id: logId,
6001
+ runId,
6002
+ nodeId: nodeState.nodeId,
6003
+ nodeType: nodeState.nodeType,
6004
+ nodeName: getNodeName(nodeState.nodeId),
6005
+ status,
6006
+ inputArgs: nodeState.inputArgs ?? null,
6007
+ output: nodeState.output ?? null,
6008
+ error: nodeState.error ?? null,
6009
+ duration: nodeState.duration ?? null,
6010
+ startTime: nodeState.startTime ?? (/* @__PURE__ */ new Date()).toISOString(),
6011
+ endTime: nodeState.endTime ?? null,
6012
+ usedPinnedData: nodeState.usedPinnedData ?? false,
6013
+ reusedFromRunId: nodeState.reusedFromRunId ?? null
6014
+ };
6015
+ addLog(newLog);
6016
+ } else {
6017
+ if (existingLog.status !== status || status === "completed" && !existingLog.output) {
6018
+ updateLog(logId, {
6019
+ status,
6020
+ inputArgs: nodeState.inputArgs ?? existingLog.inputArgs,
6021
+ output: nodeState.output ?? existingLog.output,
6022
+ error: nodeState.error ?? existingLog.error,
6023
+ duration: nodeState.duration ?? existingLog.duration,
6024
+ endTime: nodeState.endTime ?? existingLog.endTime,
6025
+ usedPinnedData: nodeState.usedPinnedData ?? existingLog.usedPinnedData,
6026
+ reusedFromRunId: nodeState.reusedFromRunId ?? existingLog.reusedFromRunId
6027
+ });
6028
+ }
6029
+ }
6030
+ }
6031
+ },
6032
+ [setNodeStatus, setNodeItemCount, addLog, updateLog, getNodeName]
6033
+ );
6034
+ const pollRunUpdates = useCallback(
6035
+ (runId) => {
6036
+ const client = getFluidGridsClient();
6037
+ pollStartRef.current = Date.now();
6038
+ activeRunIdRef.current = runId;
6039
+ const doPoll = async () => {
6040
+ if (activeRunIdRef.current !== runId) {
6041
+ return;
6042
+ }
6043
+ try {
6044
+ const result = await client.query({
6045
+ query: WORKFLOW_RUN,
6046
+ variables: { id: runId },
6047
+ fetchPolicy: "network-only"
6048
+ });
6049
+ if (activeRunIdRef.current !== runId) {
6050
+ return;
6051
+ }
6052
+ const run = result.data?.workflowRun;
6053
+ if (!run) {
6054
+ console.warn(
6055
+ "[useNodeExecution] Poll returned no run data for",
6056
+ runId
6057
+ );
6058
+ if (Date.now() - pollStartRef.current < MAX_RUN_CREATION_WAIT) {
6059
+ pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL);
6060
+ } else {
6061
+ console.warn(
6062
+ "[useNodeExecution] Run data not found after timeout for",
6063
+ runId
6064
+ );
6065
+ completeExecution();
6066
+ cleanup();
6067
+ }
6068
+ return;
6069
+ }
6070
+ setRunMetadata({
6071
+ runId,
6072
+ workflowKey: run.workflowKey,
6073
+ version: run.version,
6074
+ sourceMode: run.mode
6075
+ });
6076
+ if (run.nodeStates?.length) {
6077
+ processNodeStates(runId, run.nodeStates);
6078
+ }
6079
+ if (TERMINAL_STATUSES.has(run.status)) {
6080
+ completeExecution();
6081
+ cleanup();
6082
+ return;
6083
+ }
6084
+ pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL);
6085
+ } catch (error) {
6086
+ console.error("[useNodeExecution] Poll error:", error);
6087
+ if (activeRunIdRef.current !== runId) {
6088
+ return;
6089
+ }
6090
+ pollTimerRef.current = setTimeout(doPoll, POLL_INTERVAL * 2);
6091
+ }
6092
+ };
6093
+ pollTimerRef.current = setTimeout(doPoll, 800);
6094
+ },
6095
+ [processNodeStates, completeExecution, cleanup, setRunMetadata]
6096
+ );
6097
+ const executeStep = useCallback(
6098
+ async (nodeId) => {
6099
+ cleanup();
6100
+ clearLogs();
6101
+ const workflowKey = workflow.workflowKey || workflow.id;
6102
+ const version = workflow.version || "1.0.0";
6103
+ if (!workflowKey) {
6104
+ console.error(
6105
+ "[useNodeExecution] Workflow must be saved before step execution. No workflowKey found."
6106
+ );
6107
+ return {
6108
+ success: false,
6109
+ error: "Workflow must be saved before execution"
6110
+ };
6111
+ }
6112
+ setNodeStatus(nodeId, "running");
6113
+ try {
6114
+ normalizeWorkflowParameters(
6115
+ workflow.nodes.filter((node) => node.id === nodeId),
6116
+ workflow.edges,
6117
+ (type) => NODE_CONFIGS.find((config2) => config2.type === type)?.operations
6118
+ );
6119
+ const client = getFluidGridsClient();
6120
+ const result = throwIfResultError(
6121
+ await client.mutate({
6122
+ mutation: EXECUTE_STEP,
6123
+ variables: {
6124
+ input: {
6125
+ workflowKey,
6126
+ version,
6127
+ nodeId
6128
+ }
6129
+ }
6130
+ })
6131
+ );
6132
+ const stepResult = result.data?.executeStep;
6133
+ if (stepResult) {
6134
+ const backendRunId = stepResult.runId;
6135
+ startExecution(nodeId, backendRunId, {
6136
+ workflowKey: stepResult.workflowKey,
6137
+ version: stepResult.version,
6138
+ sourceMode: "TEST"
6139
+ });
6140
+ setStepPlan({
6141
+ runId: backendRunId,
6142
+ ...stepResult.plan,
6143
+ sourceRunId: stepResult.plan.sourceRunId ?? null
6144
+ });
6145
+ const initialLog = {
6146
+ id: `${backendRunId}-${nodeId}`,
6147
+ runId: backendRunId,
6148
+ nodeId,
6149
+ nodeType: workflow.nodes.find((node) => node.id === nodeId)?.type || "unknown",
6150
+ nodeName: getNodeName(nodeId),
6151
+ status: "running",
6152
+ inputArgs: null,
6153
+ output: null,
6154
+ error: null,
6155
+ duration: null,
6156
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
6157
+ endTime: null
6158
+ };
6159
+ addLog(initialLog);
6160
+ pollRunUpdates(backendRunId);
6161
+ return { success: true, runId: backendRunId, plan: stepResult.plan };
6162
+ }
6163
+ console.error("[useNodeExecution] No result from executeStep");
6164
+ setNodeStatus(nodeId, "error");
6165
+ return { success: false, error: "No result from executeStep" };
6166
+ } catch (error) {
6167
+ console.error("[useNodeExecution] Error executing step:", error);
6168
+ const errorMessage = describeOperationError(error);
6169
+ const errorRunId = nanoid$1();
6170
+ const errorLog = {
6171
+ id: `${errorRunId}-${nodeId}`,
6172
+ runId: errorRunId,
6173
+ nodeId,
6174
+ nodeType: workflow.nodes.find((node) => node.id === nodeId)?.type || "unknown",
6175
+ nodeName: getNodeName(nodeId),
6176
+ status: "failed",
6177
+ inputArgs: null,
6178
+ output: null,
6179
+ error: errorMessage,
6180
+ duration: null,
6181
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
6182
+ endTime: (/* @__PURE__ */ new Date()).toISOString()
6183
+ };
6184
+ addLog(errorLog);
6185
+ setNodeStatus(nodeId, "error");
6186
+ completeExecution();
6187
+ return { success: false, error: errorMessage };
6188
+ }
6189
+ },
6190
+ [
6191
+ cleanup,
6192
+ clearLogs,
6193
+ startExecution,
6194
+ setStepPlan,
6195
+ workflow,
6196
+ getNodeName,
6197
+ addLog,
6198
+ setNodeStatus,
6199
+ pollRunUpdates,
6200
+ completeExecution
6201
+ ]
6202
+ );
6203
+ const executeFullWorkflow = useCallback(
6204
+ async (customTriggerData) => {
6205
+ const triggerNode = findTriggerNode();
6206
+ if (!triggerNode) {
6207
+ console.error("[useNodeExecution] No trigger node found in workflow");
6208
+ return { success: false, error: "No trigger node found" };
6209
+ }
6210
+ cleanup();
6211
+ clearLogs();
6212
+ const workflowKey = workflow.workflowKey || workflow.id;
6213
+ const version = workflow.version || "1.0.0";
6214
+ if (!workflowKey) {
6215
+ console.error(
6216
+ "[useNodeExecution] Workflow must be saved before execution. No workflowKey found."
6217
+ );
6218
+ return {
6219
+ success: false,
6220
+ error: "Workflow must be saved before execution"
6221
+ };
6222
+ }
6223
+ setNodeStatus(triggerNode.id, "running");
6224
+ try {
6225
+ normalizeWorkflowParameters(
6226
+ workflow.nodes.filter((node) => node.data.disabled !== true),
6227
+ workflow.edges,
6228
+ (type) => NODE_CONFIGS.find((config2) => config2.type === type)?.operations
6229
+ );
6230
+ const client = getFluidGridsClient();
6231
+ const result = throwIfResultError(
6232
+ await client.mutate({
6233
+ mutation: TRIGGER_WORKFLOW,
6234
+ variables: {
6235
+ input: {
6236
+ workflowKey,
6237
+ version,
6238
+ triggerNodeId: triggerNode.id,
6239
+ triggerData: customTriggerData || {},
6240
+ context: {},
6241
+ // Builder "Test workflow" is always a manual/debug run.
6242
+ // Pinned node data only applies to TEST executions, never a
6243
+ // production trigger (webhook, schedule, API).
6244
+ mode: "TEST"
6245
+ }
6246
+ }
6247
+ })
6248
+ );
6249
+ const triggerResult = result.data?.triggerWorkflow;
6250
+ if (triggerResult) {
6251
+ const backendRunId = triggerResult.runId;
6252
+ startExecution(triggerNode.id, backendRunId, {
6253
+ workflowKey: triggerResult.workflowKey,
6254
+ version: triggerResult.version,
6255
+ sourceMode: "TEST"
6256
+ });
6257
+ pollRunUpdates(backendRunId);
6258
+ return { success: true, runId: backendRunId };
6259
+ }
6260
+ console.error("[useNodeExecution] No result from trigger");
6261
+ setNodeStatus(triggerNode.id, "error");
6262
+ return { success: false, error: "No result from trigger" };
6263
+ } catch (error) {
6264
+ console.error("[useNodeExecution] Error triggering workflow:", error);
6265
+ const errorMessage = describeOperationError(error);
6266
+ const errorRunId = nanoid$1();
6267
+ const errorLog = {
6268
+ id: `${errorRunId}-${triggerNode.id}`,
6269
+ runId: errorRunId,
6270
+ nodeId: triggerNode.id,
6271
+ nodeType: triggerNode.type || "unknown",
6272
+ nodeName: getNodeName(triggerNode.id),
6273
+ status: "failed",
6274
+ inputArgs: null,
6275
+ output: null,
6276
+ error: errorMessage,
6277
+ duration: null,
6278
+ startTime: (/* @__PURE__ */ new Date()).toISOString(),
6279
+ endTime: (/* @__PURE__ */ new Date()).toISOString()
6280
+ };
6281
+ addLog(errorLog);
6282
+ setNodeStatus(triggerNode.id, "error");
6283
+ completeExecution();
6284
+ return { success: false, error: errorMessage };
6285
+ }
6286
+ },
6287
+ [
6288
+ findTriggerNode,
6289
+ cleanup,
6290
+ clearLogs,
6291
+ startExecution,
6292
+ workflow,
6293
+ getNodeName,
6294
+ addLog,
6295
+ setNodeStatus,
6296
+ pollRunUpdates,
6297
+ completeExecution
6298
+ ]
6299
+ );
6300
+ const executeFromNode = useCallback(
6301
+ async (sourceRunId, nodeId) => {
6302
+ cleanup();
6303
+ clearLogs();
6304
+ setNodeStatus(nodeId, "running");
6305
+ try {
6306
+ const client = getFluidGridsClient();
6307
+ const result = throwIfResultError(
6308
+ await client.mutate({
6309
+ mutation: RUN_FROM_NODE,
6310
+ variables: {
6311
+ input: {
6312
+ sourceRunId,
6313
+ nodeId,
6314
+ // Run-from-node is a debug affordance — always TEST, same
6315
+ // rule as the other builder debug execution paths.
6316
+ mode: "TEST"
6317
+ }
6318
+ }
6319
+ })
6320
+ );
6321
+ const runFromNodeResult = result.data?.runFromNode;
6322
+ if (runFromNodeResult) {
6323
+ const backendRunId = runFromNodeResult.runId;
6324
+ startExecution(nodeId, backendRunId, {
6325
+ workflowKey: runFromNodeResult.workflowKey,
6326
+ version: runFromNodeResult.version,
6327
+ sourceMode: "TEST"
6328
+ });
6329
+ pollRunUpdates(backendRunId);
6330
+ return { success: true, runId: backendRunId };
6331
+ }
6332
+ console.error("[useNodeExecution] No result from runFromNode");
6333
+ setNodeStatus(nodeId, "error");
6334
+ return { success: false, error: "No result from runFromNode" };
6335
+ } catch (error) {
6336
+ console.error("[useNodeExecution] Error running from node:", error);
6337
+ const errorMessage = describeOperationError(error);
6338
+ setNodeStatus(nodeId, "error");
6339
+ completeExecution();
6340
+ return { success: false, error: errorMessage };
6341
+ }
6342
+ },
6343
+ [
6344
+ cleanup,
6345
+ clearLogs,
6346
+ setNodeStatus,
6347
+ startExecution,
6348
+ pollRunUpdates,
6349
+ completeExecution
6350
+ ]
6351
+ );
6352
+ const loadExecution = useCallback(
6353
+ async (runId) => {
6354
+ cleanup();
6355
+ const requestId = ++loadRequestRef.current;
6356
+ try {
6357
+ const client = getFluidGridsClient();
6358
+ const result = throwIfResultError(
6359
+ await client.query({
6360
+ query: EXECUTION_DETAIL,
6361
+ variables: { id: runId },
6362
+ fetchPolicy: "network-only"
6363
+ })
6364
+ );
6365
+ const run = result.data?.workflowRun;
6366
+ if (!run) {
6367
+ return { success: false, error: "Execution not found" };
6368
+ }
6369
+ if (run.mode !== "TEST") {
6370
+ return {
6371
+ success: false,
6372
+ error: "Only TEST runs can be loaded for continuation."
6373
+ };
6374
+ }
6375
+ if (!TERMINAL_STATUSES.has(run.status)) {
6376
+ return {
6377
+ success: false,
6378
+ error: "Only terminal TEST runs can be loaded for continuation."
6379
+ };
6380
+ }
6381
+ if (requestId !== loadRequestRef.current) {
6382
+ return { success: false, error: "Execution load superseded." };
6383
+ }
6384
+ const currentWorkflow = useFlowStore.getState().workflow;
6385
+ const currentWorkflowKey = currentWorkflow.workflowKey || currentWorkflow.id;
6386
+ if (run.workflowKey !== currentWorkflowKey || run.version !== currentWorkflow.version) {
6387
+ return {
6388
+ success: false,
6389
+ error: "The selected run belongs to a different workflow version."
6390
+ };
6391
+ }
6392
+ const logs = (run.nodeStates ?? []).map((nodeState) => {
6393
+ const rawStatus = nodeState.status.toLowerCase();
6394
+ const status = rawStatus === "completed" ? "completed" : rawStatus === "failed" || rawStatus === "timeout" ? "failed" : rawStatus === "running" || rawStatus === "waiting" || rawStatus === "delayed" || rawStatus === "waiting_subworkflow" ? "running" : "pending";
6395
+ return {
6396
+ id: `${runId}-${nodeState.nodeId}`,
6397
+ runId,
6398
+ nodeId: nodeState.nodeId,
6399
+ nodeType: nodeState.nodeType,
6400
+ nodeName: getNodeName(nodeState.nodeId),
6401
+ status,
6402
+ inputArgs: nodeState.inputArgs ?? null,
6403
+ output: nodeState.output ?? null,
6404
+ error: nodeState.error ?? null,
6405
+ duration: nodeState.duration ?? null,
6406
+ startTime: nodeState.startTime ?? run.startedAt ?? "",
6407
+ endTime: nodeState.endTime ?? null,
6408
+ usedPinnedData: nodeState.usedPinnedData ?? false,
6409
+ reusedFromRunId: nodeState.reusedFromRunId ?? null
6410
+ };
6411
+ });
6412
+ loadDebugRun(runId, logs, {
6413
+ workflowKey: run.workflowKey,
6414
+ version: run.version,
6415
+ sourceMode: run.mode
6416
+ });
6417
+ return { success: true, runId };
6418
+ } catch (error) {
6419
+ console.error("[useNodeExecution] Error loading execution:", error);
6420
+ return { success: false, error: describeOperationError(error) };
6421
+ }
6422
+ },
6423
+ [cleanup, getNodeName, loadDebugRun]
6114
6424
  );
6115
- });
6116
- var fallbackDynamic = getNodeMetadata({ type: "__default_dynamic__" });
6117
- if (fallbackDynamic?.node) {
6118
- NODE_TYPES["default"] = fallbackDynamic.node;
6119
- }
6120
- NODE_TYPES[STICKY_NOTE_TYPE] = StickyNoteNode;
6121
- function registerWorkflowNodeTypes(workflowNodes) {
6122
- for (const node of workflowNodes) {
6123
- if (node.type && !NODE_TYPES[node.type]) {
6124
- const meta = getNodeMetadata({
6125
- type: node.type,
6126
- defaultData: {
6127
- version: node.data?.version,
6128
- nodeData: node.data?.nodeData
6129
- },
6130
- operations: node.data?.operations
6131
- });
6132
- if (meta?.node) {
6133
- NODE_TYPES[node.type] = meta.node;
6425
+ useEffect(() => {
6426
+ return () => {
6427
+ if (pollTimerRef.current) {
6428
+ completeExecution();
6134
6429
  }
6135
- }
6136
- }
6430
+ cleanup();
6431
+ };
6432
+ }, [cleanup, completeExecution]);
6433
+ return {
6434
+ executeStep,
6435
+ executeFullWorkflow,
6436
+ executeFromNode,
6437
+ loadExecution,
6438
+ cleanup,
6439
+ findTriggerNode
6440
+ };
6137
6441
  }
6138
6442
  var FluidGridsContext = createContext(
6139
6443
  null
@@ -20485,116 +20789,6 @@ var ChatPanel = () => {
20485
20789
  }
20486
20790
  );
20487
20791
  };
20488
-
20489
- // src/builder/lib/field-schema.ts
20490
- function isRecord4(value) {
20491
- return typeof value === "object" && value !== null && !Array.isArray(value);
20492
- }
20493
- function isIdentifier(key) {
20494
- return /^[A-Za-z_$][\w$]*$/.test(key);
20495
- }
20496
- function joinPath(base, key) {
20497
- if (isIdentifier(key)) return base ? `${base}.${key}` : key;
20498
- const bracket = `[${JSON.stringify(key)}]`;
20499
- return base ? `${base}${bracket}` : bracket;
20500
- }
20501
- function pathToExpression(path) {
20502
- if (!path) return "${output}";
20503
- const accessor = path.startsWith("[") ? `output${path}` : `output.${path}`;
20504
- return "${" + accessor + "}";
20505
- }
20506
- function fieldFromSpec(key, path, spec) {
20507
- if (typeof spec === "string") {
20508
- return { key, path, type: spec };
20509
- }
20510
- if (isRecord4(spec)) {
20511
- const declaredType = typeof spec.type === "string" ? spec.type : void 0;
20512
- const description = typeof spec.description === "string" ? spec.description : void 0;
20513
- const required = spec.required === true;
20514
- if (isRecord4(spec.properties)) {
20515
- const children = Object.entries(spec.properties).map(
20516
- ([k, v]) => fieldFromSpec(k, joinPath(path, k), v)
20517
- );
20518
- return { key, path, type: "object", required, description, children };
20519
- }
20520
- if (spec.items !== void 0) {
20521
- const element = fieldFromSpec("0", `${path}[0]`, spec.items);
20522
- return {
20523
- key,
20524
- path,
20525
- type: "array",
20526
- required,
20527
- description,
20528
- children: [element]
20529
- };
20530
- }
20531
- return { key, path, type: declaredType ?? "any", required, description };
20532
- }
20533
- return { key, path, type: "any" };
20534
- }
20535
- function fieldsFromIO(io, root = "") {
20536
- if (!isRecord4(io)) return [];
20537
- return Object.entries(io).map(
20538
- ([key, spec]) => fieldFromSpec(key, joinPath(root, key), spec)
20539
- );
20540
- }
20541
- function typeOfValue(value) {
20542
- if (value === null || value === void 0) return "any";
20543
- if (Array.isArray(value)) return "array";
20544
- if (typeof value === "object") return "object";
20545
- if (typeof value === "number") return "number";
20546
- if (typeof value === "boolean") return "boolean";
20547
- if (typeof value === "string") return "string";
20548
- return "any";
20549
- }
20550
- function fieldFromValue(key, path, value) {
20551
- if (Array.isArray(value)) {
20552
- const children = value.length > 0 ? [fieldFromValue("0", `${path}[0]`, value[0])] : void 0;
20553
- return { key, path, type: "array", children };
20554
- }
20555
- if (isRecord4(value)) {
20556
- const children = Object.entries(value).map(
20557
- ([k, v]) => fieldFromValue(k, joinPath(path, k), v)
20558
- );
20559
- return { key, path, type: "object", children };
20560
- }
20561
- return { key, path, type: typeOfValue(value) };
20562
- }
20563
- function fieldsFromValue(value, root = "") {
20564
- if (!isRecord4(value)) return [];
20565
- return Object.entries(value).map(
20566
- ([key, v]) => fieldFromValue(key, joinPath(root, key), v)
20567
- );
20568
- }
20569
- function declaredOutputFields(nodeType, op) {
20570
- const name = op?.name;
20571
- if (!name || nodeType?.startsWith("internal:")) {
20572
- return fieldsFromIO(op?.output_fields);
20573
- }
20574
- const children = fieldsFromIO(op?.output_fields, name);
20575
- if (children.length === 0) return [];
20576
- return [{ key: name, path: name, type: "object", children }];
20577
- }
20578
- function normalizeOperations(raw) {
20579
- if (Array.isArray(raw)) return raw;
20580
- if (isRecord4(raw) && Array.isArray(raw.operations)) {
20581
- return raw.operations;
20582
- }
20583
- return [];
20584
- }
20585
- function resolveNodeOperation(data, fallbackOperations2) {
20586
- const nodeData = isRecord4(data?.nodeData) ? data?.nodeData : void 0;
20587
- let operations = normalizeOperations(nodeData?.operations);
20588
- if (operations.length === 0) {
20589
- operations = normalizeOperations(data?.operations);
20590
- }
20591
- if (operations.length === 0 && fallbackOperations2 !== void 0) {
20592
- operations = normalizeOperations(fallbackOperations2);
20593
- }
20594
- const opName = typeof data?.operation_name === "string" ? data.operation_name : void 0;
20595
- const selected = operations.find((op) => op.name === opName) ?? operations[0];
20596
- return { operations, selected };
20597
- }
20598
20792
  var FIELD_PATH_MIME = "application/x-fg-field-path";
20599
20793
  var TYPE_ICON = {
20600
20794
  string: "mdi:format-text",
@@ -21705,6 +21899,65 @@ function NodeLoadOptionsSelect({
21705
21899
  }
21706
21900
  );
21707
21901
  }
21902
+ function StructuredParameterInput({
21903
+ name,
21904
+ schema,
21905
+ value,
21906
+ onChange,
21907
+ className,
21908
+ testId
21909
+ }) {
21910
+ const tr = useTr();
21911
+ const descriptionId = useId();
21912
+ const [draft, setDraft] = useState(() => ({
21913
+ value,
21914
+ text: formatParameterValue(value)
21915
+ }));
21916
+ const text = Object.is(draft.value, value) ? draft.text : formatParameterValue(value);
21917
+ const result = parseParameterValue(schema, text);
21918
+ const type = structuredParameterType(schema);
21919
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
21920
+ /* @__PURE__ */ jsx(
21921
+ "textarea",
21922
+ {
21923
+ "aria-label": name,
21924
+ "aria-describedby": descriptionId,
21925
+ "aria-invalid": !result.valid,
21926
+ value: text,
21927
+ rows: 4,
21928
+ spellCheck: false,
21929
+ className,
21930
+ "data-testid": testId,
21931
+ placeholder: type === "array" ? "[]" : "{}",
21932
+ onChange: (event) => {
21933
+ const nextText = event.target.value;
21934
+ const nextResult = parseParameterValue(schema, nextText);
21935
+ const nextValue = nextResult.valid ? nextResult.value : nextText;
21936
+ setDraft({ value: nextValue, text: nextText });
21937
+ onChange(nextValue);
21938
+ }
21939
+ }
21940
+ ),
21941
+ /* @__PURE__ */ jsx(
21942
+ "span",
21943
+ {
21944
+ id: descriptionId,
21945
+ role: result.valid ? void 0 : "alert",
21946
+ className: result.valid ? "text-[11px] text-muted-foreground" : "text-[11px] text-destructive",
21947
+ children: result.valid ? type === "array" ? tr(
21948
+ "fluidgrids.builder.nodeParamsForm.arrayHint",
21949
+ "Enter a JSON array. String lists also accept comma-separated values."
21950
+ ) : tr(
21951
+ "fluidgrids.builder.nodeParamsForm.objectHint",
21952
+ "Enter a JSON object."
21953
+ ) : tr(
21954
+ "fluidgrids.builder.nodeParamsForm.structuredInputInvalid",
21955
+ result.error
21956
+ )
21957
+ }
21958
+ )
21959
+ ] });
21960
+ }
21708
21961
 
21709
21962
  // src/builder/lib/mapping-values.ts
21710
21963
  function isMappingValue(value) {
@@ -21822,6 +22075,7 @@ function paramsFromOperation(op) {
21822
22075
  description: typeof rec?.description === "string" ? rec.description : void 0,
21823
22076
  options,
21824
22077
  defaultValue: rec?.default,
22078
+ schema: spec,
21825
22079
  loadOptions
21826
22080
  };
21827
22081
  });
@@ -21986,18 +22240,24 @@ function useNodeConfig(target, options) {
21986
22240
  };
21987
22241
  const writeFixed = (field, value) => {
21988
22242
  if (!nodeId) return;
22243
+ const parameter = params.find((candidate) => candidate.name === field);
22244
+ const parsed = parseParameterValue(
22245
+ parameter?.schema ?? parameter?.type,
22246
+ value
22247
+ );
22248
+ const nextValue = parsed.valid ? parsed.value : value;
21989
22249
  const store = useFlowStore.getState();
21990
22250
  const next = store.workflow.nodes.map((n) => {
21991
22251
  if (n.id !== nodeId) return n;
21992
22252
  const data = { ...asRecord(n.data) ?? {} };
21993
22253
  const args = { ...asRecord(data.args) ?? {} };
21994
22254
  const state = { ...asRecord(args.state) ?? {} };
21995
- if (value === void 0 || value === null || value === "") {
22255
+ if (nextValue === void 0 || nextValue === null || nextValue === "") {
21996
22256
  delete state[field];
22257
+ args.state = state;
21997
22258
  } else {
21998
- state[field] = value;
22259
+ args.state = { ...state, [field]: nextValue };
21999
22260
  }
22000
- args.state = state;
22001
22261
  data.args = args;
22002
22262
  return { ...n, data };
22003
22263
  });
@@ -22436,6 +22696,17 @@ function NodeParamsForm({
22436
22696
  onChange: (v) => writeFixed(p.name, v),
22437
22697
  "data-testid": `${testIdPrefix}-param-${p.name}`
22438
22698
  }
22699
+ ) : structuredParameterType(p.schema ?? p.type) ? /* @__PURE__ */ jsx(
22700
+ StructuredParameterInput,
22701
+ {
22702
+ name: p.name,
22703
+ schema: p.schema ?? p.type,
22704
+ value: fixedValue,
22705
+ onChange: (value) => writeFixed(p.name, value),
22706
+ className: cn(inputCx2, "font-mono"),
22707
+ testId: `${testIdPrefix}-param-${p.name}`
22708
+ },
22709
+ `${config2.node?.id}:${config2.currentOp?.name}:${p.name}`
22439
22710
  ) : p.loadOptions ? /* @__PURE__ */ jsx(
22440
22711
  NodeLoadOptionsSelect,
22441
22712
  {
@@ -24371,7 +24642,7 @@ function QuickConfigPanelInner({
24371
24642
  }
24372
24643
 
24373
24644
  // src/utils/workflow-definition-diff.ts
24374
- function isRecord5(value) {
24645
+ function isRecord6(value) {
24375
24646
  return typeof value === "object" && value !== null && !Array.isArray(value);
24376
24647
  }
24377
24648
  function equalJson(left, right) {
@@ -24379,7 +24650,7 @@ function equalJson(left, right) {
24379
24650
  if (Array.isArray(left) && Array.isArray(right)) {
24380
24651
  return left.length === right.length && left.every((value, index) => equalJson(value, right[index]));
24381
24652
  }
24382
- if (isRecord5(left) && isRecord5(right)) {
24653
+ if (isRecord6(left) && isRecord6(right)) {
24383
24654
  const leftKeys = Object.keys(left).sort((a, b) => a.localeCompare(b));
24384
24655
  const rightKeys = Object.keys(right).sort((a, b) => a.localeCompare(b));
24385
24656
  return leftKeys.length === rightKeys.length && leftKeys.every(
@@ -24392,7 +24663,7 @@ function diffWorkflowDefinitionJson(before, after, maxEntries = 2e3) {
24392
24663
  const entries = [];
24393
24664
  const visit = (left, right, path) => {
24394
24665
  if (entries.length >= maxEntries || equalJson(left, right)) return;
24395
- if (isRecord5(left) && isRecord5(right)) {
24666
+ if (isRecord6(left) && isRecord6(right)) {
24396
24667
  const keys = [
24397
24668
  .../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])
24398
24669
  ].sort((a, b) => a.localeCompare(b));