@burdenoff/fluidgrids-fe-sdk 2026.918.2 → 2026.922.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/{WorkflowBuilder-GFFW5WSB.js → WorkflowBuilder-XZPLGBCS.js} +5 -5
  2. package/dist/{WorkflowBuilder-GFFW5WSB.js.map → WorkflowBuilder-XZPLGBCS.js.map} +1 -1
  3. package/dist/{WorkflowsList-ZBMSG2WP.js → WorkflowsList-J6IDS3SR.js} +4 -4
  4. package/dist/{WorkflowsList-ZBMSG2WP.js.map → WorkflowsList-J6IDS3SR.js.map} +1 -1
  5. package/dist/{chunk-HY7K7SLI.js → chunk-3RWKQIV4.js} +957 -53
  6. package/dist/chunk-3RWKQIV4.js.map +1 -0
  7. package/dist/chunk-I3WUYESA.js +668 -0
  8. package/dist/chunk-I3WUYESA.js.map +1 -0
  9. package/dist/{chunk-YAADZDGX.js → chunk-K4U4WX4R.js} +3 -3
  10. package/dist/{chunk-YAADZDGX.js.map → chunk-K4U4WX4R.js.map} +1 -1
  11. package/dist/{chunk-77Y5WTBD.js → chunk-SU6UYGRW.js} +4 -4
  12. package/dist/{chunk-77Y5WTBD.js.map → chunk-SU6UYGRW.js.map} +1 -1
  13. package/dist/{chunk-TOFLHYUQ.js → chunk-TKUCCNEO.js} +4 -4
  14. package/dist/{chunk-TOFLHYUQ.js.map → chunk-TKUCCNEO.js.map} +1 -1
  15. package/dist/{chunk-YH2QC6D6.js → chunk-Y4AM4RTD.js} +4 -4
  16. package/dist/{chunk-YH2QC6D6.js.map → chunk-Y4AM4RTD.js.map} +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +10 -10
  19. package/dist/templates.js +3 -3
  20. package/dist/{workflow-builder-JKkdIJoP.d.ts → workflow-builder-BNWKK8C7.d.ts} +53 -2
  21. package/dist/workflow-builder.d.ts +1 -1
  22. package/dist/workflow-builder.js +4 -4
  23. package/dist/workflows-list.js +2 -2
  24. package/package.json +1 -1
  25. package/dist/chunk-HY7K7SLI.js.map +0 -1
  26. package/dist/chunk-KT72XMAB.js +0 -379
  27. package/dist/chunk-KT72XMAB.js.map +0 -1
@@ -4,7 +4,7 @@ import { Badge as Badge$1, Button as Button$1 } from './chunk-DBOMRIP5.js';
4
4
  import { getGlobalStoreClient, createGlobalStoreClient, setGlobalStoreClient, resetGlobalStoreClient } from './chunk-BQXHJIER.js';
5
5
  import { GetMyNodeInstallationsDocument, BrowseWorkflowTemplatesDocument, InstallWorkflowNodeDocument, InstallPurchasedNodeDocument, BrowseWorkflowNodesDocument, UninstallWorkflowNodeDocument, CreateStoreOrderDocument, GetNodeInstallableVersionsDocument } from './chunk-6LXMUIR3.js';
6
6
  import { deepClone, applyTemplateInputs } from './chunk-XQP33UJR.js';
7
- import { useFlowStore, QuotaExhaustedDialog } from './chunk-KT72XMAB.js';
7
+ import { useFlowStore, QuotaExhaustedDialog } from './chunk-I3WUYESA.js';
8
8
  import { SdkPermissionGate, FLUIDGRIDS_PERMISSIONS, SdkPermissionButton } from './chunk-UIY2HBWA.js';
9
9
  import { cn as cn$1 } from './chunk-3ZYUGTIT.js';
10
10
  import { useWorkflowVersions, useRestoreWorkflowVersion, useActivateWorkflow, useDeleteWorkflow, useDeactivateWorkflow } from './chunk-ELB3LXRJ.js';
@@ -37,7 +37,7 @@ import { ApolloProvider, useQuery, useMutation } from '@apollo/client/react';
37
37
  import { TagBadge, TagsWidget } from '@burdenoff/fe-libs/tag';
38
38
  import { gql as gql$1 } from '@apollo/client/core';
39
39
  import 'jszip';
40
- import { Sparkles, Star, Download, Eye, History, Loader2, Play, RotateCcw, Trash2, Maximize2, LayoutGrid, Plus, GitBranch, AlertCircle, Search, RefreshCw, CheckCircle2, Package, Info, Copy, BadgeCheck, DollarSign, Type, Code, Hash, List, Braces, Workflow, PlusCircle, ChevronDown, ChevronRight, Layers, X, AlertTriangle } from 'lucide-react';
40
+ import { Sparkles, Star, Download, Eye, History, Loader2, Play, RotateCcw, Trash2, Maximize2, Undo2, Redo2, LayoutGrid, Keyboard, Plus, GitBranch, AlertCircle, Search, RefreshCw, CheckCircle2, Package, Info, Copy, BadgeCheck, DollarSign, Type, Code, Hash, List, Braces, Workflow, PlusCircle, ChevronDown, ChevronRight, Layers, X, AlertTriangle } from 'lucide-react';
41
41
  import { produce } from 'immer';
42
42
  import { isEmpty } from 'radash';
43
43
  import { animate, easeOut } from 'popmotion';
@@ -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
  ),
@@ -4021,6 +4409,72 @@ function nodeAccent(key) {
4021
4409
  function tint(hex, pct = 14) {
4022
4410
  return `color-mix(in srgb, ${hex} ${pct}%, transparent)`;
4023
4411
  }
4412
+ function InlineEditableLabel({
4413
+ value,
4414
+ placeholder,
4415
+ editing,
4416
+ onStartEdit,
4417
+ onCommit,
4418
+ onCancel,
4419
+ className,
4420
+ inputClassName
4421
+ }) {
4422
+ const tr = useTr();
4423
+ const [draft, setDraft] = React2.useState(value);
4424
+ const inputRef = React2.useRef(null);
4425
+ React2.useEffect(() => {
4426
+ if (!editing) return;
4427
+ setDraft(value);
4428
+ const raf = requestAnimationFrame(() => {
4429
+ inputRef.current?.focus();
4430
+ inputRef.current?.select();
4431
+ });
4432
+ return () => cancelAnimationFrame(raf);
4433
+ }, [editing, value]);
4434
+ if (editing) {
4435
+ return /* @__PURE__ */ jsx(
4436
+ "input",
4437
+ {
4438
+ ref: inputRef,
4439
+ value: draft,
4440
+ onChange: (e) => setDraft(e.target.value),
4441
+ onClick: (e) => e.stopPropagation(),
4442
+ onDoubleClick: (e) => e.stopPropagation(),
4443
+ onMouseDown: (e) => e.stopPropagation(),
4444
+ onKeyDown: (e) => {
4445
+ e.stopPropagation();
4446
+ if (e.key === "Enter") {
4447
+ e.preventDefault();
4448
+ onCommit(draft.trim());
4449
+ } else if (e.key === "Escape") {
4450
+ e.preventDefault();
4451
+ onCancel();
4452
+ }
4453
+ },
4454
+ onBlur: () => onCommit(draft.trim()),
4455
+ className: cn(
4456
+ "min-w-0 flex-1 rounded border border-primary/50 bg-card px-1 text-inherit outline-none",
4457
+ inputClassName
4458
+ )
4459
+ }
4460
+ );
4461
+ }
4462
+ return /* @__PURE__ */ jsx(
4463
+ "span",
4464
+ {
4465
+ className: cn("cursor-text truncate", className),
4466
+ title: tr(
4467
+ "fluidgrids.builder.nodeCard.renameTooltip",
4468
+ "Double-click or press F2 to rename"
4469
+ ),
4470
+ onDoubleClick: (e) => {
4471
+ e.stopPropagation();
4472
+ onStartEdit();
4473
+ },
4474
+ children: value || placeholder
4475
+ }
4476
+ );
4477
+ }
4024
4478
  var NodeCard = React2.forwardRef(
4025
4479
  ({
4026
4480
  className,
@@ -4034,6 +4488,8 @@ var NodeCard = React2.forwardRef(
4034
4488
  onDelete,
4035
4489
  isExecuting = false,
4036
4490
  infoButton,
4491
+ disabled = false,
4492
+ onToggleDisabled,
4037
4493
  children,
4038
4494
  ...props
4039
4495
  }, ref) => {
@@ -4052,7 +4508,7 @@ var NodeCard = React2.forwardRef(
4052
4508
  " ",
4053
4509
  itemCount !== 1 ? tr("fluidgrids.builder.nodeCard.itemCountOther", "items") : tr("fluidgrids.builder.nodeCard.itemCountOne", "item")
4054
4510
  ] }) }),
4055
- (onSettings || onExecute || onDelete || infoButton) && /* @__PURE__ */ jsxs(
4511
+ (onSettings || onExecute || onDelete || onToggleDisabled || infoButton) && /* @__PURE__ */ jsxs(
4056
4512
  "div",
4057
4513
  {
4058
4514
  className: cn(
@@ -4106,6 +4562,29 @@ var NodeCard = React2.forwardRef(
4106
4562
  }
4107
4563
  ),
4108
4564
  infoButton,
4565
+ onToggleDisabled && /* @__PURE__ */ jsx(
4566
+ ToolbarButton2,
4567
+ {
4568
+ title: disabled ? tr(
4569
+ "fluidgrids.builder.nodeCard.enableTooltip",
4570
+ "Enable node"
4571
+ ) : tr(
4572
+ "fluidgrids.builder.nodeCard.disableTooltip",
4573
+ "Disable node (skip on run)"
4574
+ ),
4575
+ onClick: onToggleDisabled,
4576
+ children: /* @__PURE__ */ jsx(
4577
+ Icon,
4578
+ {
4579
+ icon: disabled ? "mdi:power" : "mdi:power-off",
4580
+ className: cn(
4581
+ "size-4",
4582
+ disabled ? "text-[var(--color-status-warning-text)]" : "text-muted-foreground"
4583
+ )
4584
+ }
4585
+ )
4586
+ }
4587
+ ),
4109
4588
  onDelete && /* @__PURE__ */ jsx(
4110
4589
  ToolbarButton2,
4111
4590
  {
@@ -4126,6 +4605,10 @@ var NodeCard = React2.forwardRef(
4126
4605
  ]
4127
4606
  }
4128
4607
  ),
4608
+ 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: [
4609
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:power-off", className: "size-2.5" }),
4610
+ tr("fluidgrids.builder.nodeCard.disabledBadge", "Disabled")
4611
+ ] }) }),
4129
4612
  /* @__PURE__ */ jsxs(
4130
4613
  "div",
4131
4614
  {
@@ -4135,6 +4618,7 @@ var NodeCard = React2.forwardRef(
4135
4618
  "relative w-72 rounded-xl border border-border bg-card shadow-sm will-change-transform",
4136
4619
  statusRing,
4137
4620
  executionStatus === "success" && "animate-success-glow",
4621
+ disabled && "opacity-50 grayscale-[0.6]",
4138
4622
  className
4139
4623
  ),
4140
4624
  style: {
@@ -4221,7 +4705,17 @@ function StatusCorner({
4221
4705
  ) });
4222
4706
  }
4223
4707
  var NodeCardHeader = React2.forwardRef(
4224
- ({ icon, appName, accentKey, missingCredentials = false }, ref) => {
4708
+ ({
4709
+ icon,
4710
+ appName,
4711
+ accentKey,
4712
+ missingCredentials = false,
4713
+ label,
4714
+ isRenaming = false,
4715
+ onStartRename,
4716
+ onCommitRename,
4717
+ onCancelRename
4718
+ }, ref) => {
4225
4719
  const tr = useTr();
4226
4720
  const accent = nodeAccent(accentKey);
4227
4721
  return /* @__PURE__ */ jsxs("div", { ref, className: "flex items-center gap-2.5 px-3 pb-3 pt-4", children: [
@@ -4237,7 +4731,19 @@ var NodeCardHeader = React2.forwardRef(
4237
4731
  children: /* @__PURE__ */ jsx(NodeTypeIcon, { icon, alt: appName, className: "size-[18px]" })
4238
4732
  }
4239
4733
  ),
4240
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: appName }),
4734
+ onStartRename && onCommitRename && onCancelRename ? /* @__PURE__ */ jsx(
4735
+ InlineEditableLabel,
4736
+ {
4737
+ value: label ?? "",
4738
+ placeholder: appName,
4739
+ editing: isRenaming,
4740
+ onStartEdit: onStartRename,
4741
+ onCommit: onCommitRename,
4742
+ onCancel: onCancelRename,
4743
+ className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground",
4744
+ inputClassName: "text-[11px] font-semibold uppercase tracking-wide"
4745
+ }
4746
+ ) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: label || appName }),
4241
4747
  missingCredentials && /* @__PURE__ */ jsx(
4242
4748
  "span",
4243
4749
  {
@@ -4607,7 +5113,13 @@ function LogicNodeCard({
4607
5113
  onSettings,
4608
5114
  onExecute,
4609
5115
  onDelete,
4610
- infoButton
5116
+ infoButton,
5117
+ disabled = false,
5118
+ onToggleDisabled,
5119
+ isRenaming = false,
5120
+ onStartRename,
5121
+ onCommitRename,
5122
+ onCancelRename
4611
5123
  }) {
4612
5124
  const minHeight = Math.max(
4613
5125
  64,
@@ -4630,6 +5142,8 @@ function LogicNodeCard({
4630
5142
  onExecute,
4631
5143
  onDelete,
4632
5144
  infoButton,
5145
+ disabled,
5146
+ onToggleDisabled,
4633
5147
  children: [
4634
5148
  /* @__PURE__ */ jsxs(
4635
5149
  "div",
@@ -4638,7 +5152,19 @@ function LogicNodeCard({
4638
5152
  style: { minHeight },
4639
5153
  children: [
4640
5154
  /* @__PURE__ */ jsx("div", { className: "flex size-9 items-center justify-center rounded-lg bg-muted", children: /* @__PURE__ */ jsx(Icon, { icon, className: "size-5", style: { color: accent } }) }),
4641
- /* @__PURE__ */ jsx("span", { className: "max-w-full truncate text-sm font-semibold leading-tight", children: title }),
5155
+ onStartRename && onCommitRename && onCancelRename ? /* @__PURE__ */ jsx(
5156
+ InlineEditableLabel,
5157
+ {
5158
+ value: title,
5159
+ placeholder: title,
5160
+ editing: isRenaming,
5161
+ onStartEdit: onStartRename,
5162
+ onCommit: onCommitRename,
5163
+ onCancel: onCancelRename,
5164
+ className: "max-w-full truncate text-sm font-semibold leading-tight",
5165
+ inputClassName: "text-sm font-semibold text-center"
5166
+ }
5167
+ ) : /* @__PURE__ */ jsx("span", { className: "max-w-full truncate text-sm font-semibold leading-tight", children: title }),
4642
5168
  subtitle && /* @__PURE__ */ jsx(
4643
5169
  "span",
4644
5170
  {
@@ -5927,14 +6453,24 @@ var getNodeMetadata = (value) => {
5927
6453
  executingNodeId,
5928
6454
  nodeStatuses,
5929
6455
  nodeItemCounts,
5930
- zustandSetNodes
6456
+ zustandSetNodes,
6457
+ renamingNodeId,
6458
+ toggleNodeDisabled,
6459
+ renameNode,
6460
+ startRename,
6461
+ endRename
5931
6462
  ] = useFlowStore(
5932
6463
  useShallow((s) => [
5933
6464
  s.execution.isExecuting,
5934
6465
  s.execution.executingNodeId,
5935
6466
  s.execution.nodeStatuses,
5936
6467
  s.execution.nodeItemCounts,
5937
- s.actions.nodes.setNodes
6468
+ s.actions.nodes.setNodes,
6469
+ s.workflow.renamingNodeId,
6470
+ s.actions.nodes.toggleDisabled,
6471
+ s.actions.nodes.renameNode,
6472
+ s.actions.nodes.startRename,
6473
+ s.actions.nodes.endRename
5938
6474
  ])
5939
6475
  );
5940
6476
  useEffect(() => {
@@ -5958,6 +6494,22 @@ var getNodeMetadata = (value) => {
5958
6494
  const handleDeleteNode = () => {
5959
6495
  deleteNode(id);
5960
6496
  };
6497
+ const handleToggleDisabled = useCallback(() => {
6498
+ toggleNodeDisabled(id);
6499
+ }, [id, toggleNodeDisabled]);
6500
+ const isRenamingThisNode = renamingNodeId === id;
6501
+ const handleStartRename = useCallback(() => {
6502
+ startRename(id);
6503
+ }, [id, startRename]);
6504
+ const handleCommitRename = useCallback(
6505
+ (value2) => {
6506
+ renameNode(id, value2);
6507
+ },
6508
+ [id, renameNode]
6509
+ );
6510
+ const handleCancelRename = useCallback(() => {
6511
+ endRename();
6512
+ }, [endRename]);
5961
6513
  const openNdv = useFlowStore((s) => s.actions.ndv.open);
5962
6514
  const openQuickConfig = useFlowStore((s) => s.actions.quickConfig.open);
5963
6515
  const clickTimerRef = useRef(null);
@@ -6020,6 +6572,8 @@ var getNodeMetadata = (value) => {
6020
6572
  const nodeStatus = nodeStatuses[id] || "idle";
6021
6573
  const executionStatus = nodeStatus === "success" ? "success" : nodeStatus === "error" ? "error" : nodeStatus === "running" ? "running" : "idle";
6022
6574
  const itemCount = nodeItemCounts[id];
6575
+ const isDisabled = data?.disabled === true;
6576
+ const nodeLabel = data?.label || "";
6023
6577
  const nodeKinds = data?.nodeData?.kind;
6024
6578
  const pinnedOpType = (data?.operation_type || "").toLowerCase();
6025
6579
  const isTrigger = pinnedOpType === "trigger" || (value?.type || "").toLowerCase().includes("trigger") || // Kind-based detection only when NO operation is pinned: dual-role
@@ -6084,7 +6638,13 @@ var getNodeMetadata = (value) => {
6084
6638
  onSettings: handleOpenNdv,
6085
6639
  onExecute: handleExecuteStep,
6086
6640
  onDelete: handleDeleteNode,
6087
- infoButton: /* @__PURE__ */ jsx(NodeInfoButtonWithModal, { metadata: nodeInfoMetadata })
6641
+ infoButton: /* @__PURE__ */ jsx(NodeInfoButtonWithModal, { metadata: nodeInfoMetadata }),
6642
+ disabled: isDisabled,
6643
+ onToggleDisabled: handleToggleDisabled,
6644
+ isRenaming: isRenamingThisNode,
6645
+ onStartRename: handleStartRename,
6646
+ onCommitRename: handleCommitRename,
6647
+ onCancelRename: handleCancelRename
6088
6648
  }
6089
6649
  ),
6090
6650
  /* @__PURE__ */ jsx(
@@ -6113,6 +6673,8 @@ var getNodeMetadata = (value) => {
6113
6673
  onExecute: handleExecuteStep,
6114
6674
  onDelete: handleDeleteNode,
6115
6675
  infoButton: /* @__PURE__ */ jsx(NodeInfoButtonWithModal, { metadata: nodeInfoMetadata }),
6676
+ disabled: isDisabled,
6677
+ onToggleDisabled: handleToggleDisabled,
6116
6678
  children: [
6117
6679
  /* @__PURE__ */ jsx(
6118
6680
  NodeCardHeader,
@@ -6120,7 +6682,12 @@ var getNodeMetadata = (value) => {
6120
6682
  icon: meta.icon,
6121
6683
  appName,
6122
6684
  accentKey: value?.type || meta.title,
6123
- missingCredentials
6685
+ missingCredentials,
6686
+ label: nodeLabel,
6687
+ isRenaming: isRenamingThisNode,
6688
+ onStartRename: handleStartRename,
6689
+ onCommitRename: handleCommitRename,
6690
+ onCancelRename: handleCancelRename
6124
6691
  }
6125
6692
  ),
6126
6693
  operationDisplay === "execute" && (value?.type || "").startsWith("internal:") || isTrigger ? (
@@ -13819,7 +14386,7 @@ var DEFAULT_CONFIG = {
13819
14386
  maxTemplateLength: 1e4,
13820
14387
  debug: false
13821
14388
  };
13822
- var SAFE_GLOBALS = {
14389
+ var SAFE_GLOBALS2 = {
13823
14390
  // Type conversion
13824
14391
  parseInt,
13825
14392
  parseFloat,
@@ -13866,7 +14433,7 @@ function parseTemplate(template) {
13866
14433
  }
13867
14434
  function buildSandbox(scope) {
13868
14435
  return {
13869
- ...SAFE_GLOBALS,
14436
+ ...SAFE_GLOBALS2,
13870
14437
  output: scope.output,
13871
14438
  TriggerInput: scope.TriggerInput,
13872
14439
  WorkflowState: scope.WorkflowState,
@@ -17518,6 +18085,10 @@ function CustomDeletableEdge(props) {
17518
18085
  const nodeStatuses = useFlowStore(
17519
18086
  useShallow((s) => s.execution.nodeStatuses)
17520
18087
  );
18088
+ const nodeItemCounts = useFlowStore(
18089
+ useShallow((s) => s.execution.nodeItemCounts)
18090
+ );
18091
+ const sourceItemCount = nodeItemCounts[source];
17521
18092
  const sourceNodeStatus = nodeStatuses[source] || "idle";
17522
18093
  const isRunning = sourceNodeStatus === "running";
17523
18094
  const isSuccess = sourceNodeStatus === "success";
@@ -18417,6 +18988,26 @@ function CustomDeletableEdge(props) {
18417
18988
  }
18418
18989
  ),
18419
18990
  /* @__PURE__ */ jsxs(EdgeLabelRenderer, { children: [
18991
+ sourceItemCount !== void 0 && sourceItemCount > 0 && /* @__PURE__ */ jsx(
18992
+ "div",
18993
+ {
18994
+ className: "pointer-events-none absolute z-[999]",
18995
+ style: {
18996
+ transform: `translate(${labelX}px, ${labelY}px) translate(-50%, -100%) translateY(-10px)`
18997
+ },
18998
+ children: /* @__PURE__ */ jsxs("div", { className: "rounded-full border border-border bg-card px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm", children: [
18999
+ sourceItemCount,
19000
+ " ",
19001
+ sourceItemCount !== 1 ? tr(
19002
+ "fluidgrids.builder.edges.customDeletableEdge.itemCountOther",
19003
+ "items"
19004
+ ) : tr(
19005
+ "fluidgrids.builder.edges.customDeletableEdge.itemCountOne",
19006
+ "item"
19007
+ )
19008
+ ] })
19009
+ }
19010
+ ),
18420
19011
  /* @__PURE__ */ jsxs(
18421
19012
  "div",
18422
19013
  {
@@ -18809,6 +19400,276 @@ function computeMinimapRect(panelRect, bounds, margin) {
18809
19400
  top = Math.min(Math.max(top, bounds.top + margin), maxTop);
18810
19401
  return { left, top };
18811
19402
  }
19403
+ var SHORTCUT_ROWS = [
19404
+ {
19405
+ labelKey: "fluidgrids.builder.shortcutsHelp.undo",
19406
+ labelDefault: "Undo",
19407
+ mac: "\u2318Z",
19408
+ other: "Ctrl+Z"
19409
+ },
19410
+ {
19411
+ labelKey: "fluidgrids.builder.shortcutsHelp.redo",
19412
+ labelDefault: "Redo",
19413
+ mac: "\u2318\u21E7Z",
19414
+ other: "Ctrl+Shift+Z / Ctrl+Y"
19415
+ },
19416
+ {
19417
+ labelKey: "fluidgrids.builder.shortcutsHelp.copy",
19418
+ labelDefault: "Copy selected node(s)",
19419
+ mac: "\u2318C",
19420
+ other: "Ctrl+C"
19421
+ },
19422
+ {
19423
+ labelKey: "fluidgrids.builder.shortcutsHelp.cut",
19424
+ labelDefault: "Cut selected node(s)",
19425
+ mac: "\u2318X",
19426
+ other: "Ctrl+X"
19427
+ },
19428
+ {
19429
+ labelKey: "fluidgrids.builder.shortcutsHelp.paste",
19430
+ labelDefault: "Paste \u2014 works across workflows/tabs",
19431
+ mac: "\u2318V",
19432
+ other: "Ctrl+V"
19433
+ },
19434
+ {
19435
+ labelKey: "fluidgrids.builder.shortcutsHelp.duplicate",
19436
+ labelDefault: "Duplicate selected node(s)",
19437
+ mac: "\u2318D",
19438
+ other: "Ctrl+D"
19439
+ },
19440
+ {
19441
+ labelKey: "fluidgrids.builder.shortcutsHelp.delete",
19442
+ labelDefault: "Delete selected node(s)/connection(s)",
19443
+ mac: "\u232B / Delete",
19444
+ other: "Backspace / Delete"
19445
+ },
19446
+ {
19447
+ labelKey: "fluidgrids.builder.shortcutsHelp.rename",
19448
+ labelDefault: "Rename the selected node",
19449
+ mac: "F2 or double-click its title",
19450
+ other: "F2 or double-click its title"
19451
+ },
19452
+ {
19453
+ labelKey: "fluidgrids.builder.shortcutsHelp.multiSelectClick",
19454
+ labelDefault: "Add a node to the selection",
19455
+ mac: "\u2318-click / \u2303-click",
19456
+ other: "Ctrl-click"
19457
+ },
19458
+ {
19459
+ labelKey: "fluidgrids.builder.shortcutsHelp.multiSelectBox",
19460
+ labelDefault: "Box-select multiple nodes",
19461
+ mac: "\u21E7-drag on empty canvas",
19462
+ other: "Shift-drag on empty canvas"
19463
+ },
19464
+ {
19465
+ labelKey: "fluidgrids.builder.shortcutsHelp.stickyNote",
19466
+ labelDefault: "Add a sticky note",
19467
+ mac: "\u21E7S",
19468
+ other: "Shift+S"
19469
+ },
19470
+ {
19471
+ labelKey: "fluidgrids.builder.shortcutsHelp.cancel",
19472
+ labelDefault: "Close a panel / cancel an edit",
19473
+ mac: "Esc",
19474
+ other: "Esc"
19475
+ },
19476
+ {
19477
+ labelKey: "fluidgrids.builder.shortcutsHelp.help",
19478
+ labelDefault: "Open this shortcuts panel",
19479
+ mac: "?",
19480
+ other: "?"
19481
+ }
19482
+ ];
19483
+ function Kbd({ children }) {
19484
+ return /* @__PURE__ */ jsx("kbd", { className: "rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-[11px] font-medium text-muted-foreground", children });
19485
+ }
19486
+ function ShortcutsHelpPanel({
19487
+ open,
19488
+ onOpenChange
19489
+ }) {
19490
+ const tr = useTr();
19491
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
19492
+ return /* @__PURE__ */ jsx(Dialog$1, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent$1, { className: "max-w-lg", children: [
19493
+ /* @__PURE__ */ jsxs(DialogHeader$1, { children: [
19494
+ /* @__PURE__ */ jsx(DialogTitle$1, { children: tr("fluidgrids.builder.shortcutsHelp.title", "Keyboard shortcuts") }),
19495
+ /* @__PURE__ */ jsx(DialogDescription$1, { children: tr(
19496
+ "fluidgrids.builder.shortcutsHelp.description",
19497
+ "Speed up editing the workflow canvas."
19498
+ ) })
19499
+ ] }),
19500
+ /* @__PURE__ */ jsx("div", { className: "max-h-[60vh] space-y-0.5 overflow-y-auto pr-1", children: SHORTCUT_ROWS.map((row) => /* @__PURE__ */ jsxs(
19501
+ "div",
19502
+ {
19503
+ className: "flex items-center justify-between gap-4 rounded-md px-2 py-1.5 text-sm hover:bg-muted/50",
19504
+ children: [
19505
+ /* @__PURE__ */ jsx("span", { className: "text-foreground/80", children: tr(row.labelKey, row.labelDefault) }),
19506
+ /* @__PURE__ */ jsx(Kbd, { children: isMac ? row.mac : row.other })
19507
+ ]
19508
+ },
19509
+ row.labelKey
19510
+ )) })
19511
+ ] }) });
19512
+ }
19513
+
19514
+ // src/builder/lib/clipboard-payload.ts
19515
+ var CLIPBOARD_MARKER = "fluidgrids/flow-clipboard@1";
19516
+ function buildClipboardPayload(nodes, edges) {
19517
+ const selected = nodes.filter((n) => n.selected);
19518
+ const idSet = new Set(selected.map((n) => n.id));
19519
+ const internalEdges = edges.filter(
19520
+ (e) => idSet.has(e.source) && idSet.has(e.target)
19521
+ );
19522
+ return { marker: CLIPBOARD_MARKER, nodes: selected, edges: internalEdges };
19523
+ }
19524
+ function parseClipboardPayload(text) {
19525
+ if (!text) return null;
19526
+ let parsed;
19527
+ try {
19528
+ parsed = JSON.parse(text);
19529
+ } catch {
19530
+ return null;
19531
+ }
19532
+ if (!parsed || typeof parsed !== "object") return null;
19533
+ const rec = parsed;
19534
+ if (rec.marker !== CLIPBOARD_MARKER || !Array.isArray(rec.nodes)) {
19535
+ return null;
19536
+ }
19537
+ return {
19538
+ marker: CLIPBOARD_MARKER,
19539
+ nodes: rec.nodes,
19540
+ edges: Array.isArray(rec.edges) ? rec.edges : []
19541
+ };
19542
+ }
19543
+
19544
+ // src/builder/hooks/use-flow-clipboard.ts
19545
+ function useFlowClipboard() {
19546
+ const { getNodes, getEdges } = useReactFlow();
19547
+ const pasteNodes = useFlowStore((s) => s.actions.nodes.pasteNodes);
19548
+ const deleteNodes = useFlowStore((s) => s.actions.nodes.deleteNodes);
19549
+ const copySelection = useCallback(async () => {
19550
+ const payload = buildClipboardPayload(getNodes(), getEdges());
19551
+ if (payload.nodes.length === 0) return false;
19552
+ try {
19553
+ await navigator.clipboard.writeText(JSON.stringify(payload));
19554
+ return true;
19555
+ } catch {
19556
+ return false;
19557
+ }
19558
+ }, [getNodes, getEdges]);
19559
+ const cutSelection = useCallback(async () => {
19560
+ const selected = getNodes().filter((n) => n.selected);
19561
+ if (selected.length === 0) return false;
19562
+ const copied = await copySelection();
19563
+ if (!copied) return false;
19564
+ deleteNodes(selected);
19565
+ return true;
19566
+ }, [copySelection, deleteNodes, getNodes]);
19567
+ const pasteFromClipboard = useCallback(async () => {
19568
+ let text;
19569
+ try {
19570
+ text = await navigator.clipboard.readText();
19571
+ } catch {
19572
+ return false;
19573
+ }
19574
+ const payload = parseClipboardPayload(text);
19575
+ if (!payload || payload.nodes.length === 0) return false;
19576
+ pasteNodes({ nodes: payload.nodes, edges: payload.edges });
19577
+ return true;
19578
+ }, [pasteNodes]);
19579
+ return { copySelection, cutSelection, pasteFromClipboard };
19580
+ }
19581
+
19582
+ // src/builder/hooks/use-editor-shortcuts.ts
19583
+ function isEditableTarget(target) {
19584
+ const el = target;
19585
+ if (!el) return false;
19586
+ return el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" || el.isContentEditable;
19587
+ }
19588
+ function hasTextSelection() {
19589
+ const sel = typeof window !== "undefined" ? window.getSelection?.() : null;
19590
+ return !!sel && sel.toString().length > 0;
19591
+ }
19592
+ function useEditorShortcuts(options) {
19593
+ const readOnly = options?.readOnly ?? false;
19594
+ const onOpenHelp = options?.onOpenHelp;
19595
+ const { getNodes } = useReactFlow();
19596
+ const undo = useFlowStore((s) => s.actions.history.undo);
19597
+ const redo = useFlowStore((s) => s.actions.history.redo);
19598
+ const duplicateNodes = useFlowStore((s) => s.actions.nodes.duplicateNodes);
19599
+ const startRename = useFlowStore((s) => s.actions.nodes.startRename);
19600
+ const { copySelection, cutSelection, pasteFromClipboard } = useFlowClipboard();
19601
+ const insertStickyNote = useInsertStickyNote();
19602
+ useEffect(() => {
19603
+ if (readOnly) return;
19604
+ const onKeyDown = (e) => {
19605
+ const editable = isEditableTarget(e.target);
19606
+ const mod = e.metaKey || e.ctrlKey;
19607
+ if (!mod && !e.altKey && e.shiftKey && e.key.toLowerCase() === "s" && !editable) {
19608
+ e.preventDefault();
19609
+ insertStickyNote();
19610
+ return;
19611
+ }
19612
+ if (!mod && !editable && onOpenHelp && (e.key === "?" || e.shiftKey && e.key === "/")) {
19613
+ e.preventDefault();
19614
+ onOpenHelp();
19615
+ return;
19616
+ }
19617
+ if (!mod && !editable && e.key === "F2") {
19618
+ const selected = getNodes().filter((n) => n.selected);
19619
+ if (selected.length === 1 && selected[0]) {
19620
+ e.preventDefault();
19621
+ startRename(selected[0].id);
19622
+ }
19623
+ return;
19624
+ }
19625
+ if (!mod || editable) return;
19626
+ const key = e.key.toLowerCase();
19627
+ if (key === "z") {
19628
+ e.preventDefault();
19629
+ if (e.shiftKey) redo();
19630
+ else undo();
19631
+ return;
19632
+ }
19633
+ if (key === "y") {
19634
+ e.preventDefault();
19635
+ redo();
19636
+ return;
19637
+ }
19638
+ if ((key === "c" || key === "x") && !hasTextSelection()) {
19639
+ const hasSelection = getNodes().some((n) => n.selected);
19640
+ if (!hasSelection) return;
19641
+ e.preventDefault();
19642
+ void (key === "c" ? copySelection() : cutSelection());
19643
+ return;
19644
+ }
19645
+ if (key === "v") {
19646
+ e.preventDefault();
19647
+ void pasteFromClipboard();
19648
+ return;
19649
+ }
19650
+ if (key === "d") {
19651
+ const selectedIds = getNodes().filter((n) => n.selected).map((n) => n.id);
19652
+ if (selectedIds.length === 0) return;
19653
+ e.preventDefault();
19654
+ duplicateNodes(selectedIds);
19655
+ }
19656
+ };
19657
+ window.addEventListener("keydown", onKeyDown);
19658
+ return () => window.removeEventListener("keydown", onKeyDown);
19659
+ }, [
19660
+ readOnly,
19661
+ getNodes,
19662
+ undo,
19663
+ redo,
19664
+ duplicateNodes,
19665
+ startRename,
19666
+ copySelection,
19667
+ cutSelection,
19668
+ pasteFromClipboard,
19669
+ insertStickyNote,
19670
+ onOpenHelp
19671
+ ]);
19672
+ }
18812
19673
  var ZOOM_DURATION = 500;
18813
19674
  var NODE_WIDTH = 220;
18814
19675
  var NODE_HEIGHT = 80;
@@ -18869,6 +19730,7 @@ function CanvasControls() {
18869
19730
  const isReadOnly = searchParams.get("readonly") === "true";
18870
19731
  const [showMinimap, setShowMinimap] = useState(false);
18871
19732
  const [edgesHidden, setEdgesHidden] = useState(false);
19733
+ const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false);
18872
19734
  const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
18873
19735
  const [isDragging, setIsDragging] = useState(false);
18874
19736
  const panelRef = useRef(null);
@@ -18878,6 +19740,18 @@ function CanvasControls() {
18878
19740
  shallow
18879
19741
  );
18880
19742
  const { zoomIn, zoomOut, fitView, setEdges, getEdges, getNodes, setNodes } = useReactFlow();
19743
+ const [canUndo, canRedo, undo, redo] = useFlowStore(
19744
+ useShallow((s) => [
19745
+ s.history.past.length > 0,
19746
+ s.history.future.length > 0,
19747
+ s.actions.history.undo,
19748
+ s.actions.history.redo
19749
+ ])
19750
+ );
19751
+ useEditorShortcuts({
19752
+ readOnly: isReadOnly,
19753
+ onOpenHelp: () => setShortcutsHelpOpen(true)
19754
+ });
18881
19755
  const toggleEdges = useCallback(() => {
18882
19756
  setEdgesHidden((prev) => {
18883
19757
  const next = !prev;
@@ -18886,20 +19760,6 @@ function CanvasControls() {
18886
19760
  });
18887
19761
  }, [setEdges]);
18888
19762
  const insertStickyNote = useInsertStickyNote();
18889
- useEffect(() => {
18890
- if (isReadOnly) return;
18891
- const onKeyDown = (e) => {
18892
- if (!e.shiftKey || e.key.toLowerCase() !== "s") return;
18893
- if (e.metaKey || e.ctrlKey || e.altKey) return;
18894
- const t = e.target;
18895
- if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable))
18896
- return;
18897
- e.preventDefault();
18898
- insertStickyNote();
18899
- };
18900
- window.addEventListener("keydown", onKeyDown);
18901
- return () => window.removeEventListener("keydown", onKeyDown);
18902
- }, [insertStickyNote, isReadOnly]);
18903
19763
  const tidyUp = useCallback(() => {
18904
19764
  const nodes = getNodes().filter((n) => n.type !== STICKY_NOTE_TYPE);
18905
19765
  const edges = getEdges();
@@ -19101,6 +19961,31 @@ function CanvasControls() {
19101
19961
  }
19102
19962
  ),
19103
19963
  !isReadOnly && /* @__PURE__ */ jsxs(Fragment, { children: [
19964
+ /* @__PURE__ */ jsx("div", { className: "w-px h-5 bg-card-foreground/10 mx-0.5" }),
19965
+ /* @__PURE__ */ jsx(
19966
+ ControlBtn,
19967
+ {
19968
+ title: tr(
19969
+ "fluidgrids.builder.canvasControls.undo",
19970
+ "Undo (Ctrl/Cmd+Z)"
19971
+ ),
19972
+ disabled: !canUndo,
19973
+ onClick: undo,
19974
+ children: /* @__PURE__ */ jsx(Undo2, { className: "size-4" })
19975
+ }
19976
+ ),
19977
+ /* @__PURE__ */ jsx(
19978
+ ControlBtn,
19979
+ {
19980
+ title: tr(
19981
+ "fluidgrids.builder.canvasControls.redo",
19982
+ "Redo (Ctrl/Cmd+Shift+Z)"
19983
+ ),
19984
+ disabled: !canRedo,
19985
+ onClick: redo,
19986
+ children: /* @__PURE__ */ jsx(Redo2, { className: "size-4" })
19987
+ }
19988
+ ),
19104
19989
  /* @__PURE__ */ jsx("div", { className: "w-px h-5 bg-card-foreground/10 mx-0.5" }),
19105
19990
  /* @__PURE__ */ jsx(
19106
19991
  ControlBtn,
@@ -19144,10 +20029,29 @@ function CanvasControls() {
19144
20029
  onClick: insertStickyNote,
19145
20030
  children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:note-plus-outline", className: "size-4" })
19146
20031
  }
20032
+ ),
20033
+ /* @__PURE__ */ jsx("div", { className: "w-px h-5 bg-card-foreground/10 mx-0.5" }),
20034
+ /* @__PURE__ */ jsx(
20035
+ ControlBtn,
20036
+ {
20037
+ title: tr(
20038
+ "fluidgrids.builder.canvasControls.shortcutsHelp",
20039
+ "Keyboard shortcuts (?)"
20040
+ ),
20041
+ onClick: () => setShortcutsHelpOpen(true),
20042
+ children: /* @__PURE__ */ jsx(Keyboard, { className: "size-4" })
20043
+ }
19147
20044
  )
19148
20045
  ] })
19149
20046
  ]
19150
20047
  }
20048
+ ),
20049
+ !isReadOnly && /* @__PURE__ */ jsx(
20050
+ ShortcutsHelpPanel,
20051
+ {
20052
+ open: shortcutsHelpOpen,
20053
+ onOpenChange: setShortcutsHelpOpen
20054
+ }
19151
20055
  )
19152
20056
  ] });
19153
20057
  }
@@ -23310,7 +24214,7 @@ var FlowBuilder = ({ readOnly = false }) => {
23310
24214
  nodesDraggable: !isReadOnly,
23311
24215
  nodesConnectable: !isReadOnly,
23312
24216
  elementsSelectable: !isReadOnly,
23313
- multiSelectionKeyCode: null,
24217
+ multiSelectionKeyCode: isReadOnly ? null : ["Meta", "Control"],
23314
24218
  deleteKeyCode: isReadOnly ? null : deleteKeyCode,
23315
24219
  snapGrid: [16, 16],
23316
24220
  snapToGrid: true,
@@ -23436,5 +24340,5 @@ var FlowBuilderPage = ({
23436
24340
  };
23437
24341
 
23438
24342
  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
24343
+ //# sourceMappingURL=chunk-3RWKQIV4.js.map
24344
+ //# sourceMappingURL=chunk-3RWKQIV4.js.map