@revenexx/studio-integrations 2.8.0 → 2.9.0

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 (44) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/ExpressionInput.vue +31 -0
  4. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +44 -11
  5. package/dist/runtime/components/Integrations/NodeConfigField.vue +5 -1
  6. package/dist/runtime/components/Integrations/NodePalette.vue +72 -64
  7. package/dist/runtime/components/Integrations/RunDetailPanel.vue +11 -5
  8. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +2 -2
  9. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +6 -0
  10. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -2
  11. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +6 -0
  12. package/dist/runtime/components/Integrations/WorkflowEditor.vue +147 -27
  13. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +36 -12
  14. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +14 -0
  15. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +162 -13
  16. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +14 -0
  17. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +34 -12
  18. package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -1
  19. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +3 -1
  20. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -1
  21. package/dist/runtime/components/Integrations/canvasSides.d.ts +27 -0
  22. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  23. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +4 -1
  24. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -3
  25. package/dist/runtime/data/canvasShapes.d.ts +139 -0
  26. package/dist/runtime/data/canvasShapes.js +130 -0
  27. package/dist/runtime/data/connectionRules.d.ts +110 -0
  28. package/dist/runtime/data/connectionRules.js +110 -0
  29. package/dist/runtime/data/graphShapes.d.ts +175 -0
  30. package/dist/runtime/data/graphShapes.js +359 -0
  31. package/dist/runtime/data/handleId.d.ts +12 -0
  32. package/dist/runtime/data/handleId.js +3 -0
  33. package/dist/runtime/data/refusalMessage.d.ts +32 -0
  34. package/dist/runtime/data/refusalMessage.js +42 -0
  35. package/dist/runtime/data/saveRefusal.d.ts +31 -0
  36. package/dist/runtime/data/saveRefusal.js +25 -0
  37. package/dist/runtime/i18n/messages/de.json +36 -1
  38. package/dist/runtime/i18n/messages/en.json +36 -1
  39. package/dist/runtime/pages/integrations/schedules.vue +1 -1
  40. package/dist/runtime/pages/integrations/workflows/[id].vue +38 -3
  41. package/dist/runtime/types/integrations.d.ts +13 -4
  42. package/dist/runtime/utils/skippedStep.d.ts +39 -0
  43. package/dist/runtime/utils/skippedStep.js +22 -0
  44. package/package.json +2 -2
@@ -49,16 +49,19 @@ import { toast } from "vue-sonner";
49
49
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
50
50
  import { layoutWorkflowNodes } from "./workflowLayout";
51
51
  import { defaultsFor } from "../../utils/configDefaults";
52
- import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
52
+ import { TRIGGER_TEMPLATES, findManifest, triggerTypeLabel } from "../../data/integrationNodeCatalog";
53
53
  import { collectUpstreamNodes, isTerminalManifest, portsForNode } from "../../data/nodeInputData";
54
54
  import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
55
+ import { canvasShapes } from "../../data/canvasShapes";
56
+ import { connectionRefusal, lastingCrossings, refusalJudge, refusalOfChange } from "../../data/connectionRules";
57
+ import { copiedRefusalText, joinRefusalText, refusalText } from "../../data/refusalMessage";
55
58
  import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
56
59
  import { OPPOSITE_SIDE } from "./canvasSides";
57
60
  import { deepClone } from "../../utils/deepClone";
58
61
  import { copyToClipboard } from "../../utils/clipboard";
59
- import { groupFindingsByNode, findingsByNodeField } from "../../utils/validationFindings";
62
+ import { groupFindingsByNode, findingsByNodeField, nodeDisplayLabel } from "../../utils/validationFindings";
60
63
  import { reorderBranchSubtrees } from "../../data/branchReorder";
61
- import { resolveLocalized } from "../../utils/localize";
64
+ import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
62
65
  import { nodeDisplayName } from "../../utils/nodeDisplayName";
63
66
  import { keepOpenBeside } from "../../utils/dialog";
64
67
  const props = defineProps({
@@ -79,6 +82,7 @@ watch(() => props.workflow.id, () => {
79
82
  const vfNodes = ref([]);
80
83
  const vfEdges = ref([]);
81
84
  const flow = useVueFlow();
85
+ let joinRefusals = [];
82
86
  const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.warnings ?? [], props.workflow.blob.nodes));
83
87
  const dynamicOutputs = ref({});
84
88
  const invalidNodeFields = computed(() => {
@@ -255,6 +259,8 @@ function bornClass(id) {
255
259
  const NODE_FALLBACK_POSITION = { x: 240, y: 80 };
256
260
  function syncFromBlob() {
257
261
  const loops = nodeLoopData(blob.value.nodes, blob.value.edges);
262
+ const shapes = canvasShapes(blob.value.nodes, blob.value.edges, blob.value.triggers);
263
+ joinRefusals = shapes.refusals;
258
264
  const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
259
265
  const collapsedBy = collapsedGroupOf();
260
266
  const groupNodes = (blob.value.groups ?? []).map((g, i) => {
@@ -300,7 +306,7 @@ function syncFromBlob() {
300
306
  type: "trigger",
301
307
  position: triggerPosition(t2.id, i),
302
308
  class: bornClass(t2.id),
303
- data: t2
309
+ data: { ...t2, __split: shapes.splitTriggers.has(t2.id) }
304
310
  })),
305
311
  ...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
306
312
  id: n.id,
@@ -311,7 +317,8 @@ function syncFromBlob() {
311
317
  ...n,
312
318
  __resolvedOutputs: dynamicOutputs.value[n.id],
313
319
  __invalidFields: invalidNodeFields.value[n.id],
314
- __loop: loops.get(n.id)
320
+ __loop: loops.get(n.id),
321
+ __shape: shapes.byNode.get(n.id)
315
322
  }
316
323
  }))
317
324
  ];
@@ -329,10 +336,14 @@ function syncFromBlob() {
329
336
  const vf = makeVfEdge(e.id, e.from.nodeId, e.to.nodeId, e.from.output);
330
337
  mapped.push({
331
338
  ...vf,
339
+ // Where branches of a split cross, drawn as the connection to change.
340
+ data: { ...vf.data, crossing: shapes.crossingEdges.has(e.id) },
332
341
  source,
333
342
  target,
334
343
  sourceHandle: srcGroup ? outputHandleId("default", "bottom") : vf.sourceHandle,
335
- targetHandle: tgtGroup ? inputHandleId("top") : vf.targetHandle
344
+ // A connection of a nested merge's level arrives at the virtual merger
345
+ // drawn in front of the input, not at the input dot (PO-750).
346
+ targetHandle: tgtGroup ? inputHandleId("top") : shapes.arrivalHandle(e) ?? vf.targetHandle
336
347
  });
337
348
  }
338
349
  const bySource = /* @__PURE__ */ new Map();
@@ -350,6 +361,13 @@ function syncFromBlob() {
350
361
  vfEdges.value = mapped;
351
362
  void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
352
363
  }
364
+ watch(
365
+ () => (blob.value.groups ?? []).flatMap((g) => g.nodeIds.map((id) => {
366
+ const { width, height } = memberDims(id);
367
+ return `${id}:${width}x${height}`;
368
+ })).join("|"),
369
+ () => syncFromBlob()
370
+ );
353
371
  function makeVfEdge(id, sourceId, targetId, output) {
354
372
  const kind = edgeKindFromPort(sourceId, output);
355
373
  const wn = blob.value.nodes.find((n) => n.id === sourceId);
@@ -483,6 +501,7 @@ flow.onConnect((conn) => {
483
501
  if (!conn.source || !conn.target) return;
484
502
  const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
485
503
  const output = portFromHandleId(conn.sourceHandle) ?? "default";
504
+ const before = joinRefusals;
486
505
  blob.value.edges.push({
487
506
  id,
488
507
  from: { nodeId: conn.source, output },
@@ -490,17 +509,26 @@ flow.onConnect((conn) => {
490
509
  });
491
510
  syncFromBlob();
492
511
  push();
512
+ warnNewJoinRefusal(before);
493
513
  });
494
514
  const pendingConnect = ref(null);
495
515
  let connectStart = null;
496
516
  let connectCompleted = false;
497
517
  flow.onConnectStart(({ nodeId, handleId, handleType }) => {
498
518
  connectCompleted = false;
519
+ lastRefusal = null;
520
+ prepareDragJudge();
499
521
  connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: portFromHandleId(handleId) ?? "default" } : null;
500
522
  });
501
523
  flow.onConnectEnd((event) => {
502
524
  const start = connectStart;
503
525
  connectStart = null;
526
+ dragJudge = null;
527
+ const refused = connectCompleted ? null : droppedOnRefusal(event);
528
+ if (refused) {
529
+ sayRefused(refused);
530
+ return;
531
+ }
504
532
  if (!start || connectCompleted) return;
505
533
  if (!(event instanceof MouseEvent)) return;
506
534
  const target = event.target;
@@ -511,15 +539,42 @@ flow.onConnectEnd((event) => {
511
539
  void focusPalette();
512
540
  toast.info(t("integration.editor.quickConnect.toast"));
513
541
  });
542
+ flow.onClickConnectStart(() => {
543
+ lastRefusal = null;
544
+ prepareDragJudge();
545
+ });
546
+ flow.onClickConnectEnd((event) => {
547
+ dragJudge = null;
548
+ const refused = droppedOnRefusal(event);
549
+ if (refused) sayRefused(refused);
550
+ });
551
+ flow.onEdgeUpdateStart(({ edge }) => {
552
+ updatingEdgeId = edge.id;
553
+ lastRefusal = null;
554
+ prepareDragJudge();
555
+ });
556
+ flow.onEdgeUpdateEnd(({ event }) => {
557
+ updatingEdgeId = null;
558
+ dragJudge = null;
559
+ const refused = droppedOnRefusal(event);
560
+ if (refused) sayRefused(refused);
561
+ });
514
562
  flow.onEdgeUpdate(({ edge, connection }) => {
515
563
  if (!connection.source || !connection.target) return;
516
564
  if (connection.source === connection.target) return;
517
565
  const be = blob.value.edges.find((e) => e.id === edge.id);
518
566
  if (!be) return;
567
+ const prior = updatingEdgeId;
568
+ updatingEdgeId = edge.id;
569
+ const refused = refusalOf(connection.source, connection.sourceHandle ?? null, connection.target);
570
+ updatingEdgeId = prior;
571
+ if (refused) return;
572
+ const before = joinRefusals;
519
573
  be.from = { nodeId: connection.source, output: portFromHandleId(connection.sourceHandle) ?? be.from.output };
520
574
  be.to = { nodeId: connection.target, input: be.to.input };
521
575
  syncFromBlob();
522
576
  push();
577
+ warnNewJoinRefusal(before);
523
578
  });
524
579
  let pendingEdgeRemovals = [];
525
580
  let pendingNodeRemovals = [];
@@ -531,6 +586,7 @@ function queueRemovalFlush() {
531
586
  }
532
587
  function flushRemovals() {
533
588
  removalFlushQueued = false;
589
+ const before = joinRefusals;
534
590
  const edges = pendingEdgeRemovals;
535
591
  const nodes = pendingNodeRemovals;
536
592
  pendingEdgeRemovals = [];
@@ -549,6 +605,7 @@ function flushRemovals() {
549
605
  if (mutated) {
550
606
  syncFromBlob();
551
607
  push();
608
+ warnNewJoinRefusal(before);
552
609
  }
553
610
  }
554
611
  flow.onEdgesChange((changes) => {
@@ -575,6 +632,7 @@ function removeBlobNode(id) {
575
632
  ungroup(id);
576
633
  return;
577
634
  }
635
+ const before = joinRefusals;
578
636
  stripFromGroups(/* @__PURE__ */ new Set([id]));
579
637
  blob.value.nodes = blob.value.nodes.filter((n) => n.id !== id);
580
638
  if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => s.id !== id);
@@ -584,6 +642,7 @@ function removeBlobNode(id) {
584
642
  if (selectedId.value === id) selectedId.value = null;
585
643
  syncFromBlob();
586
644
  push();
645
+ warnNewJoinRefusal(before);
587
646
  }
588
647
  function push() {
589
648
  emit("change", deepClone(blob.value));
@@ -647,13 +706,16 @@ function dropFromBlob(ids) {
647
706
  }
648
707
  function removeBlobNodes(ids) {
649
708
  if (!ids.length) return;
709
+ const before = joinRefusals;
650
710
  dropFromBlob(ids);
651
711
  syncFromBlob();
652
712
  push();
713
+ warnNewJoinRefusal(before);
653
714
  }
654
715
  function duplicateBlobNodes(ids) {
655
716
  const src = blob.value.nodes.filter((n) => ids.includes(n.id));
656
717
  if (!src.length) return;
718
+ const before = joinRefusals;
657
719
  const idMap = /* @__PURE__ */ new Map();
658
720
  const stamp = Date.now();
659
721
  src.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
@@ -664,21 +726,29 @@ function duplicateBlobNodes(ids) {
664
726
  position: { x: (n.position?.x ?? 0) + offset, y: (n.position?.y ?? 0) + offset }
665
727
  }));
666
728
  blob.value.nodes.push(...copies);
667
- for (const e of blob.value.edges.filter((e2) => idMap.has(e2.from.nodeId) && idMap.has(e2.to.nodeId))) {
668
- blob.value.edges.push({
669
- id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
670
- from: { nodeId: idMap.get(e.from.nodeId), output: e.from.output },
671
- to: { nodeId: idMap.get(e.to.nodeId), input: e.to.input }
672
- });
673
- }
729
+ addCopiedEdges(blob.value.edges.filter((e) => idMap.has(e.from.nodeId) && idMap.has(e.to.nodeId)), idMap);
674
730
  markBorn(...idMap.values());
675
731
  syncFromBlob();
676
732
  push();
733
+ warnNewJoinRefusal(before);
677
734
  void nextTick(() => {
678
735
  flow.removeSelectedElements();
679
736
  flow.addSelectedNodes(flow.getNodes.value.filter((n) => [...idMap.values()].includes(n.id)));
680
737
  });
681
738
  }
739
+ function addCopiedEdges(edges, idMap) {
740
+ const iterators = iteratorNodeIds(blob.value.nodes);
741
+ const copies = edges.map((e) => ({
742
+ id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
743
+ from: { nodeId: idMap.get(e.from.nodeId), output: e.from.output },
744
+ to: { nodeId: idMap.get(e.to.nodeId), input: e.to.input }
745
+ }));
746
+ const lasting = lastingCrossings({ nodes: blob.value.nodes, edges: [...blob.value.edges, ...copies] }, iterators);
747
+ const kept = copies.filter((e) => !lasting.has(e.id));
748
+ blob.value.edges.push(...kept);
749
+ const refused = copies.length - kept.length;
750
+ if (refused) showRefusal(copiedRefusalText(refused), refused);
751
+ }
682
752
  provide("workflow-node-actions", {
683
753
  configure: (id) => {
684
754
  renameIntent.value = false;
@@ -722,6 +792,7 @@ async function pasteNodes(at) {
722
792
  toast.error(t("integration.editor.clipboard.error.empty"));
723
793
  return;
724
794
  }
795
+ const before = joinRefusals;
725
796
  const idMap = /* @__PURE__ */ new Map();
726
797
  const stamp = Date.now();
727
798
  payload.nodes.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
@@ -734,17 +805,11 @@ async function pasteNodes(at) {
734
805
  id: idMap.get(n.id),
735
806
  position: { x: (n.position?.x ?? 0) + dx, y: (n.position?.y ?? 0) + dy }
736
807
  })));
737
- for (const e of payload.edges ?? []) {
738
- if (!idMap.has(e.from.nodeId) || !idMap.has(e.to.nodeId)) continue;
739
- blob.value.edges.push({
740
- id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
741
- from: { nodeId: idMap.get(e.from.nodeId), output: e.from.output },
742
- to: { nodeId: idMap.get(e.to.nodeId), input: e.to.input }
743
- });
744
- }
808
+ addCopiedEdges((payload.edges ?? []).filter((e) => idMap.has(e.from.nodeId) && idMap.has(e.to.nodeId)), idMap);
745
809
  markBorn(...idMap.values());
746
810
  syncFromBlob();
747
811
  push();
812
+ warnNewJoinRefusal(before);
748
813
  void nextTick(() => {
749
814
  flow.removeSelectedElements();
750
815
  flow.addSelectedNodes(flow.getNodes.value.filter((n) => [...idMap.values()].includes(n.id)));
@@ -827,10 +892,55 @@ function onResolvedOutputs(nodeId, outputs) {
827
892
  syncFromBlob();
828
893
  nextTick(() => flow.updateNodeInternals([nodeId]));
829
894
  }
895
+ let updatingEdgeId = null;
896
+ let lastRefusal = null;
897
+ let dragJudge = null;
898
+ function prepareDragJudge() {
899
+ dragJudge = { replacing: updatingEdgeId, judge: refusalJudge(blob.value, iteratorNodeIds(blob.value.nodes), updatingEdgeId) };
900
+ }
901
+ function refusalOf(source, sourceHandle, target) {
902
+ const candidate = { from: { nodeId: source, output: portFromHandleId(sourceHandle) ?? "default" }, to: { nodeId: target } };
903
+ if (dragJudge && dragJudge.replacing === updatingEdgeId) return dragJudge.judge(candidate);
904
+ return connectionRefusal(blob.value, iteratorNodeIds(blob.value.nodes), candidate, updatingEdgeId);
905
+ }
830
906
  function isValidConnection(conn) {
831
907
  if (!conn.source || !conn.target) return false;
832
908
  if (conn.source === conn.target) return false;
833
- return true;
909
+ if ("id" in conn && conn.id) return true;
910
+ const reason = refusalOf(conn.source, conn.sourceHandle, conn.target);
911
+ lastRefusal = reason ? { reason, dots: [`${conn.source}|${conn.sourceHandle ?? ""}`, `${conn.target}|${conn.targetHandle ?? ""}`] } : null;
912
+ return reason === null;
913
+ }
914
+ function canvasLabel(id) {
915
+ const node = blob.value.nodes.find((n) => n.id === id);
916
+ if (node) return nodeDisplayLabel(node);
917
+ const trigger = blob.value.triggers.find((tr) => tr.id === id);
918
+ return trigger ? resolveDisplayName(trigger.name) || triggerTypeLabel(trigger.type) : id;
919
+ }
920
+ function showRefusal(text, count) {
921
+ const description = count === void 0 ? t(text.description, text.params) : t(text.description, count, text.params);
922
+ toast.error(t(text.title), { description, duration: 8e3 });
923
+ }
924
+ function warnNewJoinRefusal(before) {
925
+ const key = (r) => `${r.nodeId}|${r.rule}`;
926
+ const known = new Set(before.map(key));
927
+ const added = joinRefusals.find((r) => !known.has(key(r)));
928
+ if (!added) return;
929
+ const text = joinRefusalText(added, canvasLabel(added.nodeId));
930
+ toast.warning(t(text.title), { description: t(text.description, text.params), duration: 8e3 });
931
+ }
932
+ function sayRefused(refusal) {
933
+ showRefusal(refusalText(refusal, canvasLabel));
934
+ }
935
+ function droppedOnRefusal(event) {
936
+ const refusal = lastRefusal;
937
+ lastRefusal = null;
938
+ if (!refusal || !event) return null;
939
+ const point = "changedTouches" in event ? event.changedTouches[0] : event;
940
+ const under = point ? document.elementFromPoint(point.clientX, point.clientY) : null;
941
+ const dot = (under ?? event.target)?.closest(".vue-flow__handle");
942
+ if (!dot) return null;
943
+ return refusal.dots.includes(`${dot.dataset.nodeid}|${dot.dataset.handleid ?? ""}`) ? refusal.reason : null;
834
944
  }
835
945
  const selectedId = ref(null);
836
946
  const selection = computed(() => {
@@ -1095,22 +1205,32 @@ function spliceIntoEdge(edgeId, payload) {
1095
1205
  const a = nodePosition(edge.from.nodeId);
1096
1206
  const b = nodePosition(edge.to.nodeId);
1097
1207
  const id = `n-${Date.now()}`;
1098
- blob.value.nodes.push({
1208
+ const inserted = {
1099
1209
  id,
1100
1210
  nodeSlug: manifest.slug,
1101
1211
  nodeVersion: manifest.version,
1102
1212
  config: defaultsFor(manifest.config ?? []),
1103
1213
  position: { x: snap((a.x + b.x) / 2), y: snap((a.y + b.y) / 2) }
1104
- });
1105
- blob.value.edges = blob.value.edges.filter((e) => e.id !== edgeId);
1214
+ };
1106
1215
  const stamp = Date.now().toString(36);
1107
- blob.value.edges.push(
1216
+ const edges = [
1217
+ ...blob.value.edges.filter((e) => e.id !== edgeId),
1108
1218
  { id: `e-${stamp}a${Math.random().toString(36).slice(2, 6)}`, from: { nodeId: edge.from.nodeId, output: edge.from.output }, to: { nodeId: id, input: void 0 } },
1109
1219
  { id: `e-${stamp}b${Math.random().toString(36).slice(2, 6)}`, from: { nodeId: id, output: primaryOutputName(payload.slug, payload.version) }, to: { nodeId: edge.to.nodeId, input: edge.to.input } }
1110
- );
1220
+ ];
1221
+ const nodes = [...blob.value.nodes, inserted];
1222
+ const refused = refusalOfChange({ nodes, edges: blob.value.edges }, { nodes, edges }, iteratorNodeIds(nodes));
1223
+ if (refused) {
1224
+ sayRefused(refused);
1225
+ return;
1226
+ }
1227
+ const before = joinRefusals;
1228
+ blob.value.nodes.push(inserted);
1229
+ blob.value.edges = edges;
1111
1230
  markBorn(id);
1112
1231
  syncFromBlob();
1113
1232
  push();
1233
+ warnNewJoinRefusal(before);
1114
1234
  }
1115
1235
  function nodeDims(id) {
1116
1236
  const d = flow.findNode(id)?.dimensions;
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed, markRaw, nextTick, onMounted, provide, ref, watch } from "vue";
2
+ import { computed, markRaw, nextTick, onMounted, provide, ref, shallowRef, watch } from "vue";
3
3
  import {
4
4
  MarkerType,
5
5
  VueFlow,
@@ -22,6 +22,7 @@ import { findManifest } from "../../data/integrationNodeCatalog";
22
22
  import { OPPOSITE_SIDE } from "./canvasSides";
23
23
  import { inputHandleId, outputHandleId } from "../../data/handleId";
24
24
  import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
25
+ import { canvasShapes } from "../../data/canvasShapes";
25
26
  import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
26
27
  import { errorReason } from "../../utils/apiError";
27
28
  import { personLabel } from "../../utils/personLabel";
@@ -81,7 +82,7 @@ function edgeColorByKind(kind) {
81
82
  }
82
83
  const GROUP_PAD = 24;
83
84
  const GROUP_LABEL_ROOM = 26;
84
- function previewGroupNodes(blob, nodePos) {
85
+ function previewGroupNodes(blob, nodePos, nodeSize) {
85
86
  return (blob.groups ?? []).map((g, i) => {
86
87
  let minX = Number.POSITIVE_INFINITY;
87
88
  let minY = Number.POSITIVE_INFINITY;
@@ -89,10 +90,11 @@ function previewGroupNodes(blob, nodePos) {
89
90
  let maxY = Number.NEGATIVE_INFINITY;
90
91
  for (const id of g.nodeIds) {
91
92
  const pos = nodePos(id);
93
+ const size = nodeSize(id);
92
94
  minX = Math.min(minX, pos.x);
93
95
  minY = Math.min(minY, pos.y);
94
- maxX = Math.max(maxX, pos.x + 220);
95
- maxY = Math.max(maxY, pos.y + 64);
96
+ maxX = Math.max(maxX, pos.x + size.width);
97
+ maxY = Math.max(maxY, pos.y + size.height);
96
98
  }
97
99
  if (!Number.isFinite(minX)) return null;
98
100
  return {
@@ -116,15 +118,36 @@ function previewGroupNodes(blob, nodePos) {
116
118
  function stickyArea(s) {
117
119
  return (s.size?.width ?? 240) * (s.size?.height ?? 160);
118
120
  }
119
- function buildPreview(blob) {
121
+ function snapshotPos(blob) {
120
122
  const triggerPos = (id, i) => blob.triggerPositions?.[id] ?? { x: -40, y: 60 + i * 140 };
121
- const nodePos = (id) => {
123
+ return (id) => {
122
124
  const wn = blob.nodes.find((n) => n.id === id);
123
125
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
124
126
  const ti = blob.triggers.findIndex((t2) => t2.id === id);
125
127
  if (ti >= 0) return triggerPos(id, ti);
126
128
  return { x: 0, y: 0 };
127
129
  };
130
+ }
131
+ function memberSize(id) {
132
+ const d = preview.findNode(id)?.dimensions;
133
+ return d && d.width > 0 ? d : { width: 220, height: 64 };
134
+ }
135
+ const builtBlob = shallowRef(null);
136
+ watch(
137
+ () => (builtBlob.value?.groups ?? []).flatMap((g) => g.nodeIds.map((id) => {
138
+ const { width, height } = memberSize(id);
139
+ return `${id}:${width}x${height}`;
140
+ })).join("|"),
141
+ () => {
142
+ const blob = builtBlob.value;
143
+ if (!blob) return;
144
+ const frames = new Map(previewGroupNodes(blob, snapshotPos(blob), memberSize).map((n) => [n.id, n]));
145
+ if (frames.size) previewNodes.value = previewNodes.value.map((n) => frames.get(n.id) ?? n);
146
+ }
147
+ );
148
+ function buildPreview(blob) {
149
+ builtBlob.value = blob;
150
+ const nodePos = snapshotPos(blob);
128
151
  const pickSourceSide = (_sourceId, _targetId) => "bottom";
129
152
  const edgeKind = (sourceNodeId, output) => {
130
153
  const wn = blob.nodes.find((n) => n.id === sourceNodeId);
@@ -151,9 +174,10 @@ function buildPreview(blob) {
151
174
  return "default";
152
175
  };
153
176
  const loops = nodeLoopData(blob.nodes, blob.edges);
177
+ const shapes = canvasShapes(blob.nodes, blob.edges, blob.triggers);
154
178
  const stickies = [...blob.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
155
179
  previewNodes.value = [
156
- ...previewGroupNodes(blob, nodePos),
180
+ ...previewGroupNodes(blob, nodePos, memberSize),
157
181
  ...stickies.map((s, i) => ({
158
182
  id: s.id,
159
183
  type: "sticky",
@@ -164,19 +188,19 @@ function buildPreview(blob) {
164
188
  // draws no toolbar, no resize handle and does not open for typing (PO-284).
165
189
  data: { sticky: s }
166
190
  })),
167
- ...blob.triggers.map((t2, i) => ({
191
+ ...blob.triggers.map((t2) => ({
168
192
  id: t2.id,
169
193
  type: "trigger",
170
- position: triggerPos(t2.id, i),
194
+ position: nodePos(t2.id),
171
195
  draggable: false,
172
- data: t2
196
+ data: { ...t2, __split: shapes.splitTriggers.has(t2.id) }
173
197
  })),
174
198
  ...blob.nodes.map((n) => ({
175
199
  id: n.id,
176
200
  type: "workflow",
177
201
  position: n.position ?? { x: 240, y: 80 },
178
202
  draggable: false,
179
- data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.get(n.id) }
203
+ data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id], __loop: loops.get(n.id), __shape: shapes.byNode.get(n.id) }
180
204
  }))
181
205
  ];
182
206
  previewEdges.value = blob.edges.map((e) => {
@@ -190,7 +214,7 @@ function buildPreview(blob) {
190
214
  source: e.from.nodeId,
191
215
  target: e.to.nodeId,
192
216
  sourceHandle: outputHandleId(e.from.output, srcSide),
193
- targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
217
+ targetHandle: shapes.arrivalHandle(e) ?? inputHandleId(OPPOSITE_SIDE[srcSide]),
194
218
  type: "flow",
195
219
  selectable: false,
196
220
  data: { kind, label: isBranchPort ? e.from.output : void 0 },
@@ -1,3 +1,4 @@
1
+ import type { NodeShapeData } from '../../data/canvasShapes.js';
1
2
  import type { NodeManifestOutput, WorkflowNode } from '../../types/integrations.js';
2
3
  interface NodeData extends WorkflowNode {
3
4
  __selected?: boolean;
@@ -22,6 +23,19 @@ interface NodeData extends WorkflowNode {
22
23
  /** Set on an iterator itself: how many nodes hang off its `item` port. */
23
24
  bodySize?: number;
24
25
  };
26
+ /**
27
+ * Run replay only (PO-750): the node's step was skipped, and this says why —
28
+ * "Skipped: dead end in branch 2", or a plain "Skipped" for a reason the studio
29
+ * does not name. Injected like the markers above; no other canvas passes it.
30
+ */
31
+ __skipped?: string;
32
+ /**
33
+ * Where the workflow splits and merges at this node (PO-750), computed per sync
34
+ * from the blob's edges (`data/canvasShapes.ts`) and injected into all three
35
+ * canvases like `__loop`: a split port and a merging input are drawn as boxes,
36
+ * and the levels of a nested merge as small boxes in front of the input.
37
+ */
38
+ __shape?: NodeShapeData;
25
39
  }
26
40
  type __VLS_Props = {
27
41
  id: string;