@burdenoff/fluidgrids-fe-sdk 2026.918.2 → 2026.922.1

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.
@@ -224,6 +224,245 @@ function stripSubmitActions(card) {
224
224
  }
225
225
  return result;
226
226
  }
227
+
228
+ // src/builder/components/lib/expression-preview.ts
229
+ var PURE_EXPRESSION_REGEX = /^\{\{(.*)\}\}$/s;
230
+ var PURE_LEGACY_REGEX = /^\$\{(.*)\}$/s;
231
+ function containsExpressionSyntax(str) {
232
+ return typeof str === "string" && str.includes("{{");
233
+ }
234
+ function findEmbeddedExpressions(str) {
235
+ const matches = [];
236
+ let searchFrom = 0;
237
+ while (searchFrom < str.length) {
238
+ const start = str.indexOf("{{", searchFrom);
239
+ if (start === -1) break;
240
+ const end = str.indexOf("}}", start + 2);
241
+ if (end === -1) break;
242
+ matches.push({
243
+ full: str.slice(start, end + 2),
244
+ expression: str.slice(start + 2, end).trim()
245
+ });
246
+ searchFrom = end + 2;
247
+ }
248
+ return matches;
249
+ }
250
+ function toItems(value) {
251
+ if (value === void 0 || value === null) return [];
252
+ if (Array.isArray(value)) return value.map((entry) => ({ json: entry }));
253
+ return [{ json: value }];
254
+ }
255
+ var SAFE_GLOBALS = {
256
+ parseInt,
257
+ parseFloat,
258
+ isNaN,
259
+ isFinite,
260
+ Number,
261
+ String,
262
+ Boolean,
263
+ JSON,
264
+ Date,
265
+ Math,
266
+ Array,
267
+ Object,
268
+ console: {
269
+ log: () => void 0,
270
+ error: () => void 0,
271
+ warn: () => void 0
272
+ }
273
+ };
274
+ function buildExpressionSandbox(scope) {
275
+ const inputItems = toItems(scope.input);
276
+ const currentItem = inputItems[0] ?? { json: {} };
277
+ const itemsForNode = (nodeName) => toItems(scope.nodeOutputs[nodeName]);
278
+ const dollarNode = (nodeName) => {
279
+ const items = itemsForNode(nodeName);
280
+ const at = items[0] ?? { json: {} };
281
+ return {
282
+ item: at.json,
283
+ all: () => items,
284
+ first: () => items[0]?.json ?? {},
285
+ last: () => items[items.length - 1]?.json ?? {}
286
+ };
287
+ };
288
+ const nodeObject = {};
289
+ for (const nodeName of Object.keys(scope.nodeOutputs)) {
290
+ nodeObject[nodeName] = dollarNode(nodeName);
291
+ }
292
+ const now = /* @__PURE__ */ new Date();
293
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
294
+ return {
295
+ ...SAFE_GLOBALS,
296
+ $json: currentItem.json,
297
+ $node: nodeObject,
298
+ $: dollarNode,
299
+ $items: (nodeName) => nodeName ? itemsForNode(nodeName) : inputItems,
300
+ $input: {
301
+ item: currentItem.json,
302
+ all: () => inputItems,
303
+ first: () => inputItems[0]?.json ?? {},
304
+ last: () => inputItems[inputItems.length - 1]?.json ?? {}
305
+ },
306
+ $workflow: {
307
+ id: scope.workflowKey,
308
+ name: scope.workflowKey,
309
+ version: scope.version
310
+ },
311
+ $execution: { id: scope.runId },
312
+ // Stubbed pending BOFF-7517 (Variables model) — mirrors the backend stub.
313
+ $vars: {},
314
+ // Never real env vars in the browser preview.
315
+ $env: {},
316
+ $now: now,
317
+ $today: today
318
+ };
319
+ }
320
+ function evaluateBare(expression, sandbox) {
321
+ const paramNames = Object.keys(sandbox);
322
+ const paramValues = Object.values(sandbox);
323
+ const fn = new Function(
324
+ ...paramNames,
325
+ `"use strict"; return (${expression});`
326
+ );
327
+ return fn(...paramValues);
328
+ }
329
+ function stringifyForInterpolation(value) {
330
+ if (value === null || value === void 0) return "";
331
+ if (typeof value === "object") return JSON.stringify(value);
332
+ return String(value);
333
+ }
334
+ function previewNewSyntax(str, scope) {
335
+ const sandbox = buildExpressionSandbox(scope);
336
+ const pure = PURE_EXPRESSION_REGEX.exec(str);
337
+ try {
338
+ if (pure) {
339
+ const expression = pure[1]?.trim();
340
+ if (!expression) {
341
+ return { isTemplate: true, error: "Empty expression" };
342
+ }
343
+ return { isTemplate: true, value: evaluateBare(expression, sandbox) };
344
+ }
345
+ let result = str;
346
+ for (const match of findEmbeddedExpressions(str)) {
347
+ if (!match.expression) continue;
348
+ const value = evaluateBare(match.expression, sandbox);
349
+ result = result.replace(match.full, stringifyForInterpolation(value));
350
+ }
351
+ return { isTemplate: true, value: result };
352
+ } catch (error) {
353
+ return {
354
+ isTemplate: true,
355
+ error: error instanceof Error ? error.message : String(error)
356
+ };
357
+ }
358
+ }
359
+ function previewLegacySyntax(str, scope) {
360
+ const sandbox = {
361
+ ...SAFE_GLOBALS,
362
+ output: scope.input,
363
+ TriggerInput: {},
364
+ WorkflowState: Object.fromEntries(
365
+ Object.entries(scope.nodeOutputs).map(([nodeId, output]) => [
366
+ nodeId,
367
+ { output, status: "COMPLETED" }
368
+ ])
369
+ ),
370
+ RunContext: {
371
+ runId: scope.runId ?? "preview",
372
+ workflowKey: scope.workflowKey ?? "preview",
373
+ version: scope.version ?? "1.0.0"
374
+ }
375
+ };
376
+ const pure = PURE_LEGACY_REGEX.exec(str);
377
+ try {
378
+ if (pure) {
379
+ const expression = pure[1]?.trim();
380
+ if (!expression) return { isTemplate: true, error: "Empty expression" };
381
+ return { isTemplate: true, value: evaluateBare(expression, sandbox) };
382
+ }
383
+ if (!str.includes("${")) {
384
+ return { isTemplate: false };
385
+ }
386
+ let result = str;
387
+ const templateRegex = /\$\{([^}]+)\}/g;
388
+ for (const match of str.matchAll(templateRegex)) {
389
+ const expression = match[1]?.trim();
390
+ if (!expression) continue;
391
+ const value = evaluateBare(expression, sandbox);
392
+ result = result.replace(match[0], stringifyForInterpolation(value));
393
+ }
394
+ return { isTemplate: true, value: result };
395
+ } catch (error) {
396
+ return {
397
+ isTemplate: true,
398
+ error: error instanceof Error ? error.message : String(error)
399
+ };
400
+ }
401
+ }
402
+ function evaluateExpressionPreview(str, scope) {
403
+ if (typeof str !== "string" || str.trim() === "") {
404
+ return { isTemplate: false };
405
+ }
406
+ if (containsExpressionSyntax(str)) {
407
+ return previewNewSyntax(str, scope);
408
+ }
409
+ return previewLegacySyntax(str, scope);
410
+ }
411
+ var EXPRESSION_BUILTINS = [
412
+ { insert: "$json", label: "$json", description: "Current item's data" },
413
+ {
414
+ insert: '$node["NodeName"]',
415
+ label: '$node["NodeName"]',
416
+ description: "Another node's current item"
417
+ },
418
+ {
419
+ insert: '$("NodeName")',
420
+ label: '$("NodeName")',
421
+ description: "Lineage helper: .item / .all() / .first() / .last()"
422
+ },
423
+ {
424
+ insert: "$items()",
425
+ label: "$items()",
426
+ description: "This node's input as a list of items"
427
+ },
428
+ {
429
+ insert: "$input",
430
+ label: "$input",
431
+ description: "Input helper: .item / .all() / .first() / .last()"
432
+ },
433
+ {
434
+ insert: "$workflow",
435
+ label: "$workflow",
436
+ description: "Workflow id / name / version"
437
+ },
438
+ {
439
+ insert: "$execution",
440
+ label: "$execution",
441
+ description: "Current execution id"
442
+ },
443
+ {
444
+ insert: "$vars",
445
+ label: "$vars",
446
+ description: "Workflow variables (coming soon)"
447
+ },
448
+ {
449
+ insert: "$env",
450
+ label: "$env",
451
+ description: "Allowlisted environment values"
452
+ },
453
+ { insert: "$now", label: "$now", description: "Current date/time" },
454
+ { insert: "$today", label: "$today", description: "Start of today" }
455
+ ];
456
+ var AUTOCOMPLETE_LOOKBACK = 40;
457
+ function openExpressionPrefix(value, cursor) {
458
+ const windowStart = Math.max(0, cursor - AUTOCOMPLETE_LOOKBACK);
459
+ const before = value.slice(windowStart, cursor);
460
+ const openIndex = before.lastIndexOf("{{");
461
+ if (openIndex === -1) return null;
462
+ const between = before.slice(openIndex + 2);
463
+ if (between.includes("}}")) return null;
464
+ return between.trimStart();
465
+ }
227
466
  function ExpressionEditor({
228
467
  value,
229
468
  onChange,
@@ -231,14 +470,18 @@ function ExpressionEditor({
231
470
  className,
232
471
  rows = 4,
233
472
  label,
234
- helpText
473
+ helpText,
474
+ nodeId
235
475
  }) {
236
476
  const tr = useTr();
237
477
  const editorRef = useRef(null);
238
478
  const [isFocused, setIsFocused] = useState(false);
479
+ const [suggestionsOpen, setSuggestionsOpen] = useState(false);
239
480
  const handleChange = useCallback(
240
481
  (e) => {
241
482
  onChange(e.target.value);
483
+ const cursor = e.target.selectionStart;
484
+ setSuggestionsOpen(openExpressionPrefix(e.target.value, cursor) !== null);
242
485
  },
243
486
  [onChange]
244
487
  );
@@ -260,6 +503,83 @@ function ExpressionEditor({
260
503
  },
261
504
  [value, onChange]
262
505
  );
506
+ const applySuggestion = useCallback(
507
+ (insert) => {
508
+ const textarea = editorRef.current;
509
+ if (!textarea) {
510
+ insertTemplate(`{{ ${insert} }}`);
511
+ return;
512
+ }
513
+ const cursor = textarea.selectionStart;
514
+ const windowStart = Math.max(0, cursor - AUTOCOMPLETE_LOOKBACK);
515
+ const before = value.slice(windowStart, cursor);
516
+ const openIndex = before.lastIndexOf("{{");
517
+ if (openIndex === -1) {
518
+ insertTemplate(`{{ ${insert} }}`);
519
+ return;
520
+ }
521
+ const replaceStart = windowStart + openIndex;
522
+ const newValue = value.slice(0, replaceStart) + `{{ ${insert} }}` + value.slice(cursor);
523
+ onChange(newValue);
524
+ setSuggestionsOpen(false);
525
+ const cursorAfter = replaceStart + `{{ ${insert} }}`.length;
526
+ setTimeout(() => {
527
+ textarea.focus();
528
+ textarea.setSelectionRange(cursorAfter, cursorAfter);
529
+ }, 0);
530
+ },
531
+ [value, onChange, insertTemplate]
532
+ );
533
+ const cursorPos = editorRef.current?.selectionStart ?? value.length;
534
+ const filterText = suggestionsOpen ? openExpressionPrefix(value, cursorPos) ?? "" : "";
535
+ const suggestions = useMemo(() => {
536
+ if (!suggestionsOpen) return [];
537
+ const needle = filterText.toLowerCase();
538
+ return EXPRESSION_BUILTINS.filter(
539
+ (b) => b.label.toLowerCase().startsWith(needle || "$")
540
+ ).slice(0, 8);
541
+ }, [suggestionsOpen, filterText]);
542
+ const logs = useFlowStore((s) => s.execution.logs);
543
+ const workflowKey = useFlowStore((s) => s.workflow.workflowKey);
544
+ const workflowVersion = useFlowStore((s) => s.workflow.version);
545
+ const previewScope = useMemo(() => {
546
+ const nodeOutputs = {};
547
+ let input = {};
548
+ let runId;
549
+ for (const log of logs) {
550
+ if (log.status !== "completed" || !log.output) continue;
551
+ nodeOutputs[log.nodeId] = log.output;
552
+ if (log.nodeId === nodeId) {
553
+ input = log.inputArgs ?? {};
554
+ runId = log.runId;
555
+ }
556
+ }
557
+ return { input, nodeOutputs, workflowKey, version: workflowVersion, runId };
558
+ }, [logs, nodeId, workflowKey, workflowVersion]);
559
+ const hasPreviewData = Object.keys(previewScope.input).length > 0;
560
+ const [preview, setPreview] = useState(null);
561
+ useEffect(() => {
562
+ if (!nodeId || !value.trim()) {
563
+ setPreview(null);
564
+ return;
565
+ }
566
+ const timer = setTimeout(() => {
567
+ setPreview(evaluateExpressionPreview(value, previewScope));
568
+ }, 200);
569
+ return () => clearTimeout(timer);
570
+ }, [value, previewScope, nodeId]);
571
+ const previewText = useMemo(() => {
572
+ if (!preview) return null;
573
+ if (!preview.isTemplate) return null;
574
+ if (preview.error) return null;
575
+ if (preview.value === void 0) return "undefined";
576
+ if (typeof preview.value === "string") return preview.value;
577
+ try {
578
+ return JSON.stringify(preview.value);
579
+ } catch {
580
+ return String(preview.value);
581
+ }
582
+ }, [preview]);
263
583
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
264
584
  label && /* @__PURE__ */ jsx("label", { className: "text-sm font-medium text-foreground", children: label }),
265
585
  /* @__PURE__ */ jsxs(
@@ -270,7 +590,7 @@ function ExpressionEditor({
270
590
  isFocused && "ring-2 ring-primary ring-offset-1"
271
591
  ),
272
592
  children: [
273
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5 border-b bg-muted/30", children: [
593
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1 px-2 py-1.5 border-b bg-muted/30", children: [
274
594
  /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground mr-2", children: tr("fluidgrids.builder.expressionEditor.insertLabel", "Insert:") }),
275
595
  /* @__PURE__ */ jsx(
276
596
  "button",
@@ -298,29 +618,97 @@ function ExpressionEditor({
298
618
  className: "text-xs px-2 py-0.5 rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors",
299
619
  children: "${WorkflowState}"
300
620
  }
621
+ ),
622
+ /* @__PURE__ */ jsx("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
623
+ /* @__PURE__ */ jsx(
624
+ "button",
625
+ {
626
+ type: "button",
627
+ onClick: () => insertTemplate("{{ $json. }}"),
628
+ className: "text-xs px-2 py-0.5 rounded bg-secondary/40 text-secondary-foreground hover:bg-secondary/60 transition-colors",
629
+ children: "{{ $json }}"
630
+ }
301
631
  )
302
632
  ] }),
303
- /* @__PURE__ */ jsx(
304
- "textarea",
305
- {
306
- ref: editorRef,
307
- value,
308
- onChange: handleChange,
309
- onFocus: () => setIsFocused(true),
310
- onBlur: () => setIsFocused(false),
311
- placeholder: placeholder ?? tr(
312
- "fluidgrids.builder.expressionEditor.defaultPlaceholder",
313
- "Enter expression..."
314
- ),
315
- rows,
316
- spellCheck: false,
317
- className: cn(
318
- "w-full p-3 font-mono text-sm bg-background resize-none",
319
- "focus:outline-none",
320
- "placeholder:text-muted-foreground/50"
321
- )
322
- }
323
- )
633
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
634
+ /* @__PURE__ */ jsx(
635
+ "textarea",
636
+ {
637
+ ref: editorRef,
638
+ value,
639
+ onChange: handleChange,
640
+ onKeyUp: (e) => {
641
+ const target = e.currentTarget;
642
+ setSuggestionsOpen(
643
+ openExpressionPrefix(target.value, target.selectionStart) !== null
644
+ );
645
+ },
646
+ onFocus: () => setIsFocused(true),
647
+ onBlur: () => {
648
+ setIsFocused(false);
649
+ setTimeout(() => setSuggestionsOpen(false), 150);
650
+ },
651
+ placeholder: placeholder ?? tr(
652
+ "fluidgrids.builder.expressionEditor.defaultPlaceholder",
653
+ "Enter expression..."
654
+ ),
655
+ rows,
656
+ spellCheck: false,
657
+ className: cn(
658
+ "w-full p-3 font-mono text-sm bg-background resize-none",
659
+ "focus:outline-none",
660
+ "placeholder:text-muted-foreground/50"
661
+ )
662
+ }
663
+ ),
664
+ suggestionsOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
665
+ "div",
666
+ {
667
+ className: "absolute z-20 left-2 right-2 top-full mt-1 rounded-md border bg-popover shadow-md overflow-hidden",
668
+ role: "listbox",
669
+ "aria-label": tr(
670
+ "fluidgrids.builder.expressionEditor.autocompleteLabel",
671
+ "Expression variable suggestions"
672
+ ),
673
+ children: suggestions.map((s) => /* @__PURE__ */ jsxs(
674
+ "button",
675
+ {
676
+ type: "button",
677
+ role: "option",
678
+ "aria-selected": false,
679
+ onMouseDown: (e) => {
680
+ e.preventDefault();
681
+ applySuggestion(s.insert);
682
+ },
683
+ className: "flex w-full items-baseline gap-2 px-3 py-1.5 text-left hover:bg-accent",
684
+ children: [
685
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-primary", children: s.label }),
686
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground truncate", children: s.description })
687
+ ]
688
+ },
689
+ s.insert
690
+ ))
691
+ }
692
+ )
693
+ ] }),
694
+ nodeId && value.trim() && /* @__PURE__ */ jsx("div", { className: "border-t bg-muted/20 px-3 py-1.5 text-xs", children: preview?.error ? /* @__PURE__ */ jsxs("span", { className: "text-destructive", children: [
695
+ tr(
696
+ "fluidgrids.builder.expressionEditor.previewError",
697
+ "Preview error:"
698
+ ),
699
+ " ",
700
+ preview.error
701
+ ] }) : previewText !== null ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
702
+ tr(
703
+ "fluidgrids.builder.expressionEditor.previewLabel",
704
+ "Preview:"
705
+ ),
706
+ " ",
707
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-foreground", children: previewText })
708
+ ] }) : !hasPreviewData ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/70 italic", children: tr(
709
+ "fluidgrids.builder.expressionEditor.previewNoData",
710
+ 'Run "Execute step" on this node to preview real values.'
711
+ ) }) : null })
324
712
  ]
325
713
  }
326
714
  ),
@@ -13819,7 +14207,7 @@ var DEFAULT_CONFIG = {
13819
14207
  maxTemplateLength: 1e4,
13820
14208
  debug: false
13821
14209
  };
13822
- var SAFE_GLOBALS = {
14210
+ var SAFE_GLOBALS2 = {
13823
14211
  // Type conversion
13824
14212
  parseInt,
13825
14213
  parseFloat,
@@ -13866,7 +14254,7 @@ function parseTemplate(template) {
13866
14254
  }
13867
14255
  function buildSandbox(scope) {
13868
14256
  return {
13869
- ...SAFE_GLOBALS,
14257
+ ...SAFE_GLOBALS2,
13870
14258
  output: scope.output,
13871
14259
  TriggerInput: scope.TriggerInput,
13872
14260
  WorkflowState: scope.WorkflowState,
@@ -23436,5 +23824,5 @@ var FlowBuilderPage = ({
23436
23824
  };
23437
23825
 
23438
23826
  export { Card2 as Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ConnectionFormDialog, ConnectionSelector, EntityTagPicker, FLUIDGRIDS_ENTITY_TYPES, FlowBuilderPage, FluidGridsProvider, ProviderPicker, SchemaConnectionForm, ScopePicker, TemplateCard, buildNodeCheck, buildRequiredNodeTypes, listCustomBlocks, mergeStickyNotes, refetchStoreNodes, registerCustomBlock, resolveUiSpec, stripSubmitActions, tagLabel, useAvailableTags, useConnection, useConnectionDialog, useConnections, useConnectionsByProvider, useCreateConnection, useCreateTag, useDraftEntityTags, useEntityTags, useFluidGridsContext, useImportWorkflowTemplate, useInitiateOAuth, useInstalledNodes, useNodeExecution, useProviderByKey, useProviderCatalog, useProviders, useReplaceEntityTags, useSearchTags, useTagEntity, useUntagEntity, useWorkflowTemplates };
23439
- //# sourceMappingURL=chunk-HY7K7SLI.js.map
23440
- //# sourceMappingURL=chunk-HY7K7SLI.js.map
23827
+ //# sourceMappingURL=chunk-XASNLRFQ.js.map
23828
+ //# sourceMappingURL=chunk-XASNLRFQ.js.map