@revenexx/studio-integrations 0.5.0 → 0.6.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 (129) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/README.md +17 -4
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +15 -14
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +91 -19
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -21
  7. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +4 -8
  8. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  9. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -2
  10. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  11. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +54 -25
  13. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  14. package/dist/runtime/components/Integrations/GroupFrameView.vue +12 -12
  15. package/dist/runtime/components/Integrations/Layout.vue +21 -2
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +10 -11
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +2 -5
  18. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  19. package/dist/runtime/components/Integrations/NodeConfigField.vue +160 -67
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  21. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.vue +1075 -303
  23. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  24. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  25. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  26. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  27. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  28. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  29. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  30. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  31. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +42 -0
  32. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  33. package/dist/runtime/components/Integrations/NodePalette.vue +100 -34
  34. package/dist/runtime/components/Integrations/QueryBuilder.vue +8 -1
  35. package/dist/runtime/components/Integrations/RunActionsMenu.vue +3 -1
  36. package/dist/runtime/components/Integrations/RunPayloadInput.vue +17 -4
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +2 -1
  38. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  39. package/dist/runtime/components/Integrations/TemplateCard.vue +109 -0
  40. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  41. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  42. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +7 -10
  43. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  44. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +21 -37
  45. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  46. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +23 -7
  47. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  48. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +22 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +170 -48
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +22 -1
  51. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +4 -20
  52. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  53. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +11 -2
  54. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  55. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  56. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +189 -46
  57. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  58. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -20
  59. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +15 -9
  60. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  61. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +7 -2
  62. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  63. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +4 -2
  64. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +30 -17
  65. package/dist/runtime/components/Integrations/WorkflowsTable.vue +47 -7
  66. package/dist/runtime/composables/useCredentialTypes.js +4 -4
  67. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  68. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -4
  69. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  70. package/dist/runtime/composables/useIntegrationsNodes.js +4 -4
  71. package/dist/runtime/composables/useIntegrationsRuns.js +11 -6
  72. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  73. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  74. package/dist/runtime/composables/useIntegrationsSecrets.js +10 -7
  75. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -1
  76. package/dist/runtime/composables/useIntegrationsTemplates.js +12 -8
  77. package/dist/runtime/composables/useIntegrationsTriggers.js +6 -6
  78. package/dist/runtime/composables/useIntegrationsWorkflows.js +8 -4
  79. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  80. package/dist/runtime/composables/useLoadGate.js +8 -0
  81. package/dist/runtime/data/branchReorder.d.ts +37 -0
  82. package/dist/runtime/data/branchReorder.js +71 -0
  83. package/dist/runtime/data/handleId.d.ts +23 -0
  84. package/dist/runtime/data/handleId.js +6 -0
  85. package/dist/runtime/data/integrationTemplates.d.ts +39 -0
  86. package/dist/runtime/data/integrationTemplates.js +15 -0
  87. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  88. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  89. package/dist/runtime/data/nodeIcons.js +73 -0
  90. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  91. package/dist/runtime/data/nodeInputData.js +4 -0
  92. package/dist/runtime/data/payloadForm.d.ts +9 -2
  93. package/dist/runtime/data/payloadForm.js +27 -3
  94. package/dist/runtime/data/portTone.d.ts +17 -0
  95. package/dist/runtime/data/portTone.js +7 -0
  96. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  97. package/dist/runtime/data/webhookAuth.js +19 -0
  98. package/dist/runtime/data/workflowIconCatalog.js +2 -5
  99. package/dist/runtime/pages/integrations/audit.vue +91 -6
  100. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +6 -4
  101. package/dist/runtime/pages/integrations/credentials.vue +119 -21
  102. package/dist/runtime/pages/integrations/index.vue +49 -44
  103. package/dist/runtime/pages/integrations/runs.vue +1 -1
  104. package/dist/runtime/pages/integrations/schedules.vue +51 -7
  105. package/dist/runtime/pages/integrations/secrets.vue +29 -7
  106. package/dist/runtime/pages/integrations/templates/[slug].vue +52 -30
  107. package/dist/runtime/pages/integrations/templates/index.vue +57 -86
  108. package/dist/runtime/pages/integrations/workflows/[id].vue +243 -25
  109. package/dist/runtime/pages/integrations/workflows/index.vue +15 -2
  110. package/dist/runtime/types/integrations.d.ts +65 -6
  111. package/dist/runtime/utils/apiError.d.ts +47 -0
  112. package/dist/runtime/utils/apiError.js +32 -0
  113. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  114. package/dist/runtime/utils/cockpitSources.js +14 -0
  115. package/dist/runtime/utils/loadGate.d.ts +61 -0
  116. package/dist/runtime/utils/loadGate.js +22 -0
  117. package/dist/runtime/utils/localize.d.ts +29 -0
  118. package/dist/runtime/utils/localize.js +4 -0
  119. package/dist/runtime/utils/notify.d.ts +59 -0
  120. package/dist/runtime/utils/notify.js +21 -0
  121. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  122. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  123. package/dist/runtime/utils/schedule.d.ts +141 -0
  124. package/dist/runtime/utils/schedule.js +207 -0
  125. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  126. package/dist/runtime/utils/validationFindings.js +53 -0
  127. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  128. package/dist/runtime/utils/whenIdle.js +12 -0
  129. package/package.json +8 -2
@@ -2,8 +2,8 @@
2
2
  import { computed, markRaw, nextTick, provide, ref, watch } from "vue";
3
3
  import {
4
4
  ConnectionLineType,
5
+ ConnectionMode,
5
6
  MarkerType,
6
- PanOnScrollMode,
7
7
  SelectionMode,
8
8
  VueFlow,
9
9
  useVueFlow
@@ -48,9 +48,17 @@ import { toast } from "vue-sonner";
48
48
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
49
49
  import { layoutWorkflowNodes } from "./workflowLayout";
50
50
  import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
51
+ import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
52
+ import { portFromHandleId } from "../../data/handleId";
53
+ import { groupFindingsByNode } from "../../utils/validationFindings";
54
+ import { reorderBranchSubtrees } from "../../data/branchReorder";
55
+ import { findingsByNodeField } from "../../utils/validationFindings";
56
+ import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
51
57
  const props = defineProps({
52
58
  workflow: { type: Object, required: true },
53
- runBusy: { type: Boolean, required: false }
59
+ runBusy: { type: Boolean, required: false },
60
+ runWaiting: { type: Boolean, required: false },
61
+ overlayOpen: { type: Boolean, required: false }
54
62
  });
55
63
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
56
64
  const { keys: secretKeys } = useIntegrationsSecrets();
@@ -65,7 +73,20 @@ function clone(v) {
65
73
  const vfNodes = ref([]);
66
74
  const vfEdges = ref([]);
67
75
  const flow = useVueFlow();
76
+ const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.warnings ?? [], props.workflow.blob.nodes));
68
77
  const dynamicOutputs = ref({});
78
+ const invalidNodeFields = computed(() => {
79
+ const out = {};
80
+ for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "")) {
81
+ if (g.nodeId) out[g.nodeId] = g.fields;
82
+ }
83
+ return out;
84
+ });
85
+ let dragInFlight = false;
86
+ watch(() => JSON.stringify(invalidNodeFields.value), () => {
87
+ if (dragInFlight) return;
88
+ syncFromBlob();
89
+ });
69
90
  const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
70
91
  const m = findManifest(n.nodeSlug, n.nodeVersion);
71
92
  return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
@@ -78,6 +99,7 @@ function edgeKindFromPort(sourceNodeId, output) {
78
99
  if (output === "true") return "branch-true";
79
100
  if (output === "false") return "branch-false";
80
101
  if (output === "error") return "error";
102
+ if (output === "done") return "success";
81
103
  const portDef = (manifest.outputs ?? []).find((o) => o.name === output);
82
104
  if (portDef) {
83
105
  if (portDef.kind === "error") return "error";
@@ -92,10 +114,10 @@ function edgeKindFromPort(sourceNodeId, output) {
92
114
  }
93
115
  return "default";
94
116
  }
95
- function stripSide(handleId) {
96
- if (!handleId) return null;
97
- const i = handleId.indexOf("__");
98
- return i >= 0 ? handleId.slice(0, i) : handleId;
117
+ function orderedOutputPorts(nodeId) {
118
+ const wn = blob.value.nodes.find((n) => n.id === nodeId);
119
+ if (!wn) return ["default"];
120
+ return portsForNode(wn, findManifest(wn.nodeSlug, wn.nodeVersion), dynamicOutputs.value[nodeId]).map((p) => p.name);
99
121
  }
100
122
  function triggerPosition(id, index) {
101
123
  const saved = blob.value.triggerPositions?.[id];
@@ -120,10 +142,6 @@ function stickyArea(s) {
120
142
  }
121
143
  const GROUP_PAD = 24;
122
144
  const GROUP_LABEL_ROOM = 26;
123
- function locText(v) {
124
- if (!v) return "";
125
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
126
- }
127
145
  function memberDims(id) {
128
146
  const d = flow.findNode(id)?.dimensions;
129
147
  return d && d.width > 0 ? d : { width: 220, height: 64 };
@@ -261,7 +279,11 @@ function syncFromBlob() {
261
279
  type: "workflow",
262
280
  position: n.position ?? { x: 240, y: 80 },
263
281
  class: bornClass(n.id),
264
- data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id] }
282
+ data: {
283
+ ...n,
284
+ __resolvedOutputs: dynamicOutputs.value[n.id],
285
+ __invalidFields: invalidNodeFields.value[n.id]
286
+ }
265
287
  }))
266
288
  ];
267
289
  const seenEdge = /* @__PURE__ */ new Set();
@@ -284,6 +306,18 @@ function syncFromBlob() {
284
306
  targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
285
307
  });
286
308
  }
309
+ const bySource = /* @__PURE__ */ new Map();
310
+ for (const e of mapped) {
311
+ const list = bySource.get(e.source) ?? [];
312
+ list.push(e);
313
+ bySource.set(e.source, list);
314
+ }
315
+ for (const list of bySource.values()) {
316
+ list.sort((a, b) => (a.data?.portIndex ?? 0) - (b.data?.portIndex ?? 0));
317
+ list.forEach((e, i) => {
318
+ e.data = { ...e.data, laneIndex: i, laneCount: list.length };
319
+ });
320
+ }
287
321
  vfEdges.value = mapped;
288
322
  void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
289
323
  }
@@ -291,9 +325,11 @@ function makeVfEdge(id, sourceId, targetId, output) {
291
325
  const kind = edgeKindFromPort(sourceId, output);
292
326
  const wn = blob.value.nodes.find((n) => n.id === sourceId);
293
327
  const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
294
- const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false", "error"].includes(output);
328
+ const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
295
329
  const srcSide = pickSourceSide(sourceId, targetId);
296
330
  const tgtSide = oppositeSide(srcSide);
331
+ const order = orderedOutputPorts(sourceId);
332
+ const portIndex = order.indexOf(output);
297
333
  return {
298
334
  id,
299
335
  source: sourceId,
@@ -303,7 +339,8 @@ function makeVfEdge(id, sourceId, targetId, output) {
303
339
  type: "flow",
304
340
  data: {
305
341
  kind,
306
- label: isBranchPort ? output : void 0
342
+ label: isBranchPort ? output : void 0,
343
+ portIndex: portIndex < 0 ? 0 : portIndex
307
344
  },
308
345
  markerEnd: {
309
346
  type: MarkerType.ArrowClosed,
@@ -314,24 +351,21 @@ function makeVfEdge(id, sourceId, targetId, output) {
314
351
  };
315
352
  }
316
353
  function edgeColorByKind(kind) {
317
- switch (kind) {
318
- case "success":
319
- return "#10b981";
320
- case "error":
321
- return "#f43f5e";
322
- case "branch-true":
323
- return "#10b981";
324
- case "branch-false":
325
- return "#f59e0b";
326
- case "branch":
327
- return "#8b5cf6";
328
- default:
329
- return "#94a3b8";
330
- }
354
+ return kind === "error" ? "#cbd5e1" : "#94a3b8";
331
355
  }
332
356
  syncFromBlob();
333
- flow.onPaneReady(() => flow.fitView({ padding: 0.2, maxZoom: 1 }));
357
+ const paneReady = ref(false);
358
+ const pendingReveal = ref(null);
359
+ flow.onPaneReady(() => {
360
+ flow.fitView({ padding: 0.2, maxZoom: 1 });
361
+ paneReady.value = true;
362
+ if (pendingReveal.value) {
363
+ centerOnNode(pendingReveal.value);
364
+ pendingReveal.value = null;
365
+ }
366
+ });
334
367
  flow.onNodeDragStart(({ node }) => {
368
+ dragInFlight = true;
335
369
  if (node.type !== "group") return;
336
370
  const g = blob.value.groups?.find((x) => x.id === node.id);
337
371
  if (!g) return;
@@ -349,6 +383,7 @@ flow.onNodeDrag(({ node }) => {
349
383
  }
350
384
  });
351
385
  function persistDraggedNodes(evt) {
386
+ dragInFlight = false;
352
387
  const moved = evt.nodes?.length ? evt.nodes : [evt.node];
353
388
  for (const n of moved) {
354
389
  const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
@@ -394,7 +429,7 @@ flow.onConnect((conn) => {
394
429
  connectCompleted = true;
395
430
  if (!conn.source || !conn.target) return;
396
431
  const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
397
- const output = stripSide(conn.sourceHandle) ?? "default";
432
+ const output = portFromHandleId(conn.sourceHandle) ?? "default";
398
433
  blob.value.edges.push({
399
434
  id,
400
435
  from: { nodeId: conn.source, output },
@@ -408,7 +443,7 @@ let connectStart = null;
408
443
  let connectCompleted = false;
409
444
  flow.onConnectStart(({ nodeId, handleId, handleType }) => {
410
445
  connectCompleted = false;
411
- connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: stripSide(handleId) ?? "default" } : null;
446
+ connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: portFromHandleId(handleId) ?? "default" } : null;
412
447
  });
413
448
  flow.onConnectEnd((event) => {
414
449
  const start = connectStart;
@@ -428,7 +463,7 @@ flow.onEdgeUpdate(({ edge, connection }) => {
428
463
  if (connection.source === connection.target) return;
429
464
  const be = blob.value.edges.find((e) => e.id === edge.id);
430
465
  if (!be) return;
431
- be.from = { nodeId: connection.source, output: stripSide(connection.sourceHandle) ?? be.from.output };
466
+ be.from = { nodeId: connection.source, output: portFromHandleId(connection.sourceHandle) ?? be.from.output };
432
467
  be.to = { nodeId: connection.target, input: be.to.input };
433
468
  syncFromBlob();
434
469
  push();
@@ -547,9 +582,18 @@ function duplicateBlobNodes(ids) {
547
582
  });
548
583
  }
549
584
  provide("workflow-node-actions", {
585
+ configure: (id) => {
586
+ renameIntent.value = false;
587
+ selectedId.value = id;
588
+ },
550
589
  duplicate: (id) => duplicateBlobNodes([id]),
551
590
  remove: (id) => removeBlobNode(id)
552
591
  });
592
+ const controlBar = ref(null);
593
+ provide(
594
+ "workflow-canvas-bottom-inset",
595
+ () => controlBar.value ? controlBar.value.offsetHeight + 12 : 0
596
+ );
553
597
  const CLIP_KIND = "revenexx.workflow.nodes";
554
598
  async function copyNodes(ids) {
555
599
  const nodes = blob.value.nodes.filter((n) => ids.includes(n.id));
@@ -697,17 +741,33 @@ const selection = computed(() => {
697
741
  if (!selectedId.value) return null;
698
742
  return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
699
743
  });
700
- flow.onNodeClick((evt) => {
744
+ const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
745
+ flow.onNodeDoubleClick((evt) => {
701
746
  if (evt.node.type === "sticky" || evt.node.type === "group") return;
702
747
  selectedId.value = evt.node.id;
703
748
  });
704
749
  flow.onPaneClick(() => {
705
750
  selectedId.value = null;
706
751
  });
707
- function localLabel(v) {
708
- if (!v) return "";
709
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
752
+ function centerOnNode(id) {
753
+ const vf = vfNodes.value.find((n) => n.id === id);
754
+ if (!vf) return;
755
+ const size = flow.findNode(id)?.dimensions;
756
+ void flow.setCenter(
757
+ vf.position.x + (size?.width ?? 220) / 2,
758
+ vf.position.y + (size?.height ?? 76) / 2,
759
+ { zoom: Math.max(flow.viewport.value?.zoom ?? 1, 0.75), duration: 300 }
760
+ );
710
761
  }
762
+ function revealNode(id) {
763
+ if (paneReady.value) centerOnNode(id);
764
+ else pendingReveal.value = id;
765
+ if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t) => t.id === id)) {
766
+ renameIntent.value = false;
767
+ selectedId.value = id;
768
+ }
769
+ }
770
+ defineExpose({ revealNode });
711
771
  const selectedUpstreamOutputs = computed(() => {
712
772
  const id = selectedId.value;
713
773
  if (!id) return [];
@@ -719,7 +779,9 @@ const selectedUpstreamOutputs = computed(() => {
719
779
  const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
720
780
  return [{
721
781
  nodeId: source.id,
722
- nodeName: localLabel(source.name) || source.nodeSlug,
782
+ // A node's own name is text the user typed — never trimmed, never read
783
+ // as JSON. The manifest strings below it get the normalising resolver.
784
+ nodeName: resolveDisplayName(source.name) || source.nodeSlug,
723
785
  output: e.from.output,
724
786
  fields: portDef?.fields
725
787
  }];
@@ -818,7 +880,6 @@ function addFromPayload(payload, pos) {
818
880
  adoptIntoGroupAt(id2, at);
819
881
  markBorn(id2);
820
882
  syncFromBlob();
821
- selectedId.value = id2;
822
883
  push();
823
884
  return;
824
885
  }
@@ -835,7 +896,6 @@ function addFromPayload(payload, pos) {
835
896
  adoptIntoGroupAt(id, at);
836
897
  markBorn(id);
837
898
  syncFromBlob();
838
- selectedId.value = id;
839
899
  push();
840
900
  }
841
901
  function onDrop(ev) {
@@ -870,7 +930,6 @@ function onPaletteInsert(payload) {
870
930
  });
871
931
  markBorn(id);
872
932
  syncFromBlob();
873
- selectedId.value = id;
874
933
  push();
875
934
  return;
876
935
  }
@@ -902,6 +961,10 @@ function spliceIntoEdge(edgeId, payload) {
902
961
  const edge = blob.value.edges.find((e) => e.id === edgeId);
903
962
  const manifest = findManifest(payload.slug, payload.version);
904
963
  if (!edge || !manifest) return;
964
+ if (isTerminalManifest(manifest)) {
965
+ toast.error(`${resolveLocalized(manifest.name) || manifest.slug} ends the run and cannot be inserted into a connection.`);
966
+ return;
967
+ }
905
968
  const a = nodePosition(edge.from.nodeId);
906
969
  const b = nodePosition(edge.to.nodeId);
907
970
  const id = `n-${Date.now()}`;
@@ -920,7 +983,6 @@ function spliceIntoEdge(edgeId, payload) {
920
983
  );
921
984
  markBorn(id);
922
985
  syncFromBlob();
923
- selectedId.value = id;
924
986
  push();
925
987
  }
926
988
  function defaultConfigFor(fields = []) {
@@ -934,6 +996,34 @@ function nodeDims(id) {
934
996
  const d = flow.findNode(id)?.dimensions;
935
997
  return d && d.width > 0 ? d : { width: 220, height: 64 };
936
998
  }
999
+ function reorderBranchesByHandle(laid, memberToGroup) {
1000
+ const posById = new Map(laid.map((n) => [n.id, n]));
1001
+ const nodes = /* @__PURE__ */ new Map();
1002
+ for (const n of laid) {
1003
+ if (!memberToGroup.has(n.id)) nodes.set(n.id, { x: n.position.x });
1004
+ }
1005
+ const portOrderCache = /* @__PURE__ */ new Map();
1006
+ const portsFor = (id) => {
1007
+ let o = portOrderCache.get(id);
1008
+ if (!o) {
1009
+ o = orderedOutputPorts(id);
1010
+ portOrderCache.set(id, o);
1011
+ }
1012
+ return o;
1013
+ };
1014
+ const edges = [];
1015
+ for (const e of vfEdges.value) {
1016
+ if (!nodes.has(e.source) || !nodes.has(e.target)) continue;
1017
+ const port = portFromHandleId(e.sourceHandle) ?? "default";
1018
+ const order = portsFor(e.source);
1019
+ const idx = order.indexOf(port);
1020
+ edges.push({ source: e.source, target: e.target, portIndex: idx < 0 ? order.length : idx });
1021
+ }
1022
+ for (const [id, x] of reorderBranchSubtrees(nodes, edges)) {
1023
+ const n = posById.get(id);
1024
+ n.position = { x, y: n.position.y };
1025
+ }
1026
+ }
937
1027
  async function tidyUp() {
938
1028
  const containment = /* @__PURE__ */ new Map();
939
1029
  for (const s of blob.value.stickies ?? []) {
@@ -1013,6 +1103,7 @@ async function tidyUp() {
1013
1103
  if (!inner) return n;
1014
1104
  return { ...n, position: { x: anchor.x + GROUP_PAD + inner.position.x, y: anchor.y + GROUP_PAD + GROUP_LABEL_ROOM + inner.position.y } };
1015
1105
  });
1106
+ reorderBranchesByHandle(laid, memberToGroup);
1016
1107
  for (const n of laid) {
1017
1108
  const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
1018
1109
  if (n.type === "workflow") {
@@ -1119,6 +1210,7 @@ async function focusPalette() {
1119
1210
  paletteRef.value?.focusSearch();
1120
1211
  }
1121
1212
  const lightboxOpen = computed(() => !!selection.value);
1213
+ const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1122
1214
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1123
1215
  useCanvasShortcuts(() => ({
1124
1216
  "enter": {
@@ -1180,7 +1272,7 @@ useCanvasShortcuts(() => ({
1180
1272
  pendingConnect.value = null;
1181
1273
  flow.removeSelectedElements();
1182
1274
  }
1183
- }), { disabled: lightboxOpen });
1275
+ }), { disabled: () => !canvasHasKeyboard.value });
1184
1276
  const nodeTypes = {
1185
1277
  workflow: markRaw(WorkflowNodeView),
1186
1278
  trigger: markRaw(WorkflowTriggerView),
@@ -1218,15 +1310,15 @@ const edgeTypes = {
1218
1310
  :default-edge-options="{ type: 'flow' }"
1219
1311
  :connection-line-type="ConnectionLineType.Step"
1220
1312
  :connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
1313
+ :connection-mode="ConnectionMode.Strict"
1221
1314
  :is-valid-connection="isValidConnection"
1222
1315
  :snap-to-grid="true"
1223
1316
  :snap-grid="[20, 20]"
1224
1317
  :selection-mode="SelectionMode.Partial"
1225
1318
  :selection-key-code="pointerMode === 'select' ? true : 'Shift'"
1226
1319
  :pan-on-drag="pointerMode === 'hand' ? true : [1]"
1227
- :pan-on-scroll="true"
1228
- :pan-on-scroll-mode="PanOnScrollMode.Free"
1229
- :delete-key-code="['Backspace', 'Delete']"
1320
+ :zoom-on-double-click="false"
1321
+ :delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
1230
1322
  :edges-updatable="true"
1231
1323
  :min-zoom="0.25"
1232
1324
  :max-zoom="4"
@@ -1465,14 +1557,14 @@ const edgeTypes = {
1465
1557
  </Transition>
1466
1558
 
1467
1559
  <!-- Floating control pane — bottom center. -->
1468
- <div class="absolute bottom-3 left-1/2 z-10 -translate-x-1/2">
1560
+ <div ref="controlBar" class="absolute bottom-3 left-1/2 z-10 -translate-x-1/2">
1469
1561
  <TooltipProvider :delay-duration="250">
1470
1562
  <div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
1471
1563
  <div class="flex items-center gap-0.5 whitespace-nowrap border border-white bg-white rounded-lg bg-gradient-to-br from-white via-violet-50/40 to-white px-2 py-1">
1472
1564
  <!-- Smart Run: the page decides what "run" means — workflows that
1473
1565
  consume trigger.payload open the payload dialog, payload-free
1474
1566
  ones fire immediately. One button either way. -->
1475
- <IntegrationsEditorTip label="Run workflow" shortcut="mod+enter">
1567
+ <IntegrationsEditorTip :label="runWaiting ? 'The bundle is still building \u2014 the run starts when it is ready' : 'Run workflow'" shortcut="mod+enter">
1476
1568
  <button
1477
1569
  type="button"
1478
1570
  class="relative flex h-8 items-center gap-1.5 rounded-lg bg-gradient-to-b from-emerald-500 to-emerald-600 px-3.5 text-sm font-medium text-white transition after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_1px_0_rgba(255,255,255,0.3),inset_0_-2px_0_rgba(0,0,0,0.25)] hover:from-emerald-600 hover:to-emerald-600 active:translate-y-px disabled:opacity-60"
@@ -1481,16 +1573,20 @@ const edgeTypes = {
1481
1573
  >
1482
1574
  <Loader2 v-if="runBusy" :size="14" class="animate-spin" />
1483
1575
  <Play v-else :size="14" />
1484
- Run
1576
+ {{ runWaiting ? "Waiting for build\u2026" : "Run" }}
1485
1577
  </button>
1486
1578
  </IntegrationsEditorTip>
1487
1579
 
1488
1580
  <div class="mx-1 h-5 w-px bg-black/10" />
1489
1581
 
1490
1582
  <div class="flex items-center gap-0.5 rounded-lg bg-black/[0.05] p-0.5 shadow-[inset_0_1px_2px_rgba(15,23,42,0.12)]">
1583
+ <!-- Icon-only, and the tooltip is not an accessible name: each one
1584
+ says what it does itself, and whether it is the active mode. -->
1491
1585
  <IntegrationsEditorTip label="Select" shortcut="v">
1492
1586
  <button
1493
1587
  type="button"
1588
+ aria-label="Select"
1589
+ :aria-pressed="pointerMode === 'select'"
1494
1590
  class="flex size-7 items-center justify-center rounded-lg transition"
1495
1591
  :class="pointerMode === 'select' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
1496
1592
  @click="setPointerMode('select')"
@@ -1501,6 +1597,8 @@ const edgeTypes = {
1501
1597
  <IntegrationsEditorTip label="Hand (pan)" shortcut="h">
1502
1598
  <button
1503
1599
  type="button"
1600
+ aria-label="Hand (pan)"
1601
+ :aria-pressed="pointerMode === 'hand'"
1504
1602
  class="flex size-7 items-center justify-center rounded-lg transition"
1505
1603
  :class="pointerMode === 'hand' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
1506
1604
  @click="setPointerMode('hand')"
@@ -1577,6 +1675,7 @@ const edgeTypes = {
1577
1675
  <IntegrationsEditorTip label="Zoom out" shortcut="-">
1578
1676
  <button
1579
1677
  type="button"
1678
+ aria-label="Zoom out"
1580
1679
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1581
1680
  @click="flow.zoomOut({ duration: 150 })"
1582
1681
  >
@@ -1590,8 +1689,13 @@ const edgeTypes = {
1590
1689
  <span class="inline-flex">
1591
1690
  <DropdownMenu v-model:open="zoomMenuOpen">
1592
1691
  <DropdownMenuTrigger as-child>
1692
+ <!-- The label carries the percentage because it replaces the
1693
+ button's text as its accessible name: a bare "Zoom level"
1694
+ would leave screen reader users with the one thing sighted
1695
+ users do not need told and without the value they do. -->
1593
1696
  <button
1594
1697
  type="button"
1698
+ :aria-label="`Zoom level ${zoomPct}`"
1595
1699
  class="w-12 rounded-lg py-1 text-center text-sm tabular-nums text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground"
1596
1700
  >
1597
1701
  {{ zoomPct }}
@@ -1603,6 +1707,7 @@ const edgeTypes = {
1603
1707
  v-model="zoomInputText"
1604
1708
  type="text"
1605
1709
  inputmode="numeric"
1710
+ aria-label="Zoom level in percent"
1606
1711
  class="h-6 w-full rounded-md border border-black/[0.08] bg-white px-1 text-center text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-primary/40"
1607
1712
  placeholder="%"
1608
1713
  @keydown.stop
@@ -1625,6 +1730,7 @@ const edgeTypes = {
1625
1730
  <IntegrationsEditorTip label="Zoom in" shortcut="=">
1626
1731
  <button
1627
1732
  type="button"
1733
+ aria-label="Zoom in"
1628
1734
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1629
1735
  @click="flow.zoomIn({ duration: 150 })"
1630
1736
  >
@@ -1668,7 +1774,22 @@ const edgeTypes = {
1668
1774
  renameIntent = false;
1669
1775
  }
1670
1776
  }">
1671
- <DialogContent class="flex h-[86vh] w-[min(1280px,94vw)] max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none">
1777
+ <!-- Sized for the form, not for three columns. 1100px leaves the 1024px
1778
+ reading width a small gutter instead of floating it in the middle,
1779
+ and still gives the form ~770px with the Input panel open. Fixed
1780
+ rather than fitting the content: the tabs hold very different
1781
+ amounts, and a dialog that resizes under the pointer as you switch
1782
+ between them is worse than one with some room to spare.
1783
+
1784
+ A trigger is the exception, and gets a box its own size. It has one
1785
+ tab, no output to test and a handful of controls — a schedule is a
1786
+ frequency, a time and its days — so the node's width would leave the
1787
+ form as a strip down the left of an empty workbench. -->
1788
+ <DialogContent
1789
+ :show-close-button="false"
1790
+ class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
1791
+ :class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
1792
+ >
1672
1793
  <NodeInspector
1673
1794
  v-if="selection"
1674
1795
  :selection="selection"
@@ -1681,6 +1802,7 @@ const edgeTypes = {
1681
1802
  :triggers="blob.triggers"
1682
1803
  :dynamic-outputs="dynamicOutputs"
1683
1804
  :workflow-id="workflow.id"
1805
+ :refused-fields="selectedId ? invalidNodeMessages[selectedId] : void 0"
1684
1806
  @update="applyInspectorUpdate"
1685
1807
  @close="selectedId = null"
1686
1808
  @remove="selectedId && removeBlobNode(selectedId)"
@@ -6,8 +6,29 @@ type __VLS_Props = {
6
6
  workflow: Workflow;
7
7
  /** Busy flag for the control-pane Play button (a run is being triggered). */
8
8
  runBusy?: boolean;
9
+ /** The run is queued behind a bundle build — busy, but worth saying why. */
10
+ runWaiting?: boolean;
11
+ /**
12
+ * One of the page's own lightboxes is open above the editor (settings,
13
+ * duplicate, run payload, the sync response, the deactivation prompt). The
14
+ * canvas stands its keyboard shortcuts down while one is up — see
15
+ * `canvasHasKeyboard` below. A new page-level dialog belongs in here too.
16
+ */
17
+ overlayOpen?: boolean;
9
18
  };
10
- declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ /**
20
+ * Show a node the user was sent to from outside the canvas — the validation
21
+ * banner's per-node line (PO-209). Centres the card and opens its settings,
22
+ * which is where the fields at fault are.
23
+ *
24
+ * A node inside a collapsed group has no card on the canvas: centring would pan
25
+ * to empty space, so it only opens the settings. The reverse can't happen — an
26
+ * id that is on the canvas is in the blob by construction.
27
+ */
28
+ declare function revealNode(id: string): void;
29
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {
30
+ revealNode: typeof revealNode;
31
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
32
  change: (blob: WorkflowBlob) => any;
12
33
  run: () => any;
13
34
  toggleActive: () => any;
@@ -2,7 +2,6 @@
2
2
  import { computed, markRaw, nextTick, onMounted, provide, ref, watch } from "vue";
3
3
  import {
4
4
  MarkerType,
5
- PanOnScrollMode,
6
5
  VueFlow,
7
6
  useVueFlow
8
7
  } from "@vue-flow/core";
@@ -65,24 +64,9 @@ function onResolvedOutputs(nodeId, outputs) {
65
64
  const previewNodes = ref([]);
66
65
  const previewEdges = ref([]);
67
66
  const preview = useVueFlow("workflow-history-preview");
68
- provide("workflow-node-actions", { duplicate: () => {
69
- }, remove: () => {
70
- } });
67
+ provide("workflow-node-actions", null);
71
68
  function edgeColorByKind(kind) {
72
- switch (kind) {
73
- case "success":
74
- return "#10b981";
75
- case "error":
76
- return "#f43f5e";
77
- case "branch-true":
78
- return "#10b981";
79
- case "branch-false":
80
- return "#f59e0b";
81
- case "branch":
82
- return "#8b5cf6";
83
- default:
84
- return "#94a3b8";
85
- }
69
+ return kind === "error" ? "#cbd5e1" : "#94a3b8";
86
70
  }
87
71
  const GROUP_PAD = 24;
88
72
  const GROUP_LABEL_ROOM = 26;
@@ -140,6 +124,7 @@ function buildPreview(blob) {
140
124
  if (output === "true") return "branch-true";
141
125
  if (output === "false") return "branch-false";
142
126
  if (output === "error") return "error";
127
+ if (output === "done") return "success";
143
128
  const outs = [...manifest.outputs ?? [], ...dynamicOutputs.value[sourceNodeId] ?? []];
144
129
  const portDef = outs.find((o) => o.name === output);
145
130
  if (portDef) {
@@ -187,7 +172,7 @@ function buildPreview(blob) {
187
172
  const kind = edgeKind(e.from.nodeId, e.from.output);
188
173
  const wn = blob.nodes.find((n) => n.id === e.from.nodeId);
189
174
  const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
190
- const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false", "error"].includes(e.from.output);
175
+ const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(e.from.output);
191
176
  const srcSide = pickSourceSide(e.from.nodeId, e.to.nodeId);
192
177
  return {
193
178
  id: e.id,
@@ -296,7 +281,6 @@ onMounted(() => {
296
281
  :nodes-connectable="false"
297
282
  :elements-selectable="false"
298
283
  :edges-updatable="false"
299
- :pan-on-scroll-mode="PanOnScrollMode.Free"
300
284
  :delete-key-code="null"
301
285
  :min-zoom="0.25"
302
286
  :max-zoom="2"
@@ -1,5 +1,23 @@
1
1
  import type { WorkflowIcon } from '../../types/integrations.js';
2
- type __VLS_PublicProps = {
2
+ /**
3
+ * The caller names the picker by pointing at its own `<Label>`'s id — not with
4
+ * `<Label for>`, and not with an `id` of ours on the trigger.
5
+ *
6
+ * An `id` on the trigger would be a quiet regression: `PopoverTrigger` puts
7
+ * reka-ui's generated `triggerId` there, `PopoverContentImpl` points the opened
8
+ * dialog's `aria-labelledby` at exactly that id, and reka's `Slot` merges with
9
+ * the *child's* props winning — so ours would replace it and leave the popover
10
+ * naming an element that no longer exists.
11
+ *
12
+ * `aria-labelledby` also lists the id of the span holding the current choice, so
13
+ * the name stays "Icon Business Central" rather than dropping the value the way
14
+ * a plain label would. Attribute fallthrough is no help here either: the root
15
+ * this component renders is the popover, not the button.
16
+ */
17
+ type __VLS_Props = {
18
+ triggerLabelledby?: string;
19
+ };
20
+ type __VLS_PublicProps = __VLS_Props & {
3
21
  modelValue?: WorkflowIcon | null;
4
22
  };
5
23
  declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -1,10 +1,17 @@
1
1
  <script setup>
2
- import { computed, ref } from "vue";
2
+ import { computed, ref, useId } from "vue";
3
3
  import { AltArrowDown as ChevronDown } from "#solar-icons";
4
4
  import { Popover, PopoverContent, PopoverTrigger } from "@revenexx/studio";
5
5
  import { WORKFLOW_SOLAR_ICONS, imageIdFromSrc, nodeLogoOptions } from "../../data/workflowIconCatalog";
6
6
  import WorkflowIconView from "./WorkflowIconView.vue";
7
7
  const model = defineModel({ type: [Object, null], ...{ default: null } });
8
+ const props = defineProps({
9
+ triggerLabelledby: { type: String, required: false }
10
+ });
11
+ const valueId = useId();
12
+ const triggerLabelledby = computed(
13
+ () => props.triggerLabelledby ? `${props.triggerLabelledby} ${valueId}` : void 0
14
+ );
8
15
  const open = ref(false);
9
16
  const logos = computed(() => nodeLogoOptions());
10
17
  function pick(icon) {
@@ -30,13 +37,14 @@ const selectedLabel = computed(() => {
30
37
  <Popover v-model:open="open">
31
38
  <PopoverTrigger as-child>
32
39
  <button
40
+ :aria-labelledby="triggerLabelledby"
33
41
  type="button"
34
42
  class="flex h-9 w-full items-center gap-2 rounded-md border border-black/[0.08] bg-white px-2.5 text-left text-sm transition hover:border-black/20"
35
43
  >
36
44
  <span class="flex size-6 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
37
45
  <WorkflowIconView :icon="model" :size="14" />
38
46
  </span>
39
- <span class="min-w-0 flex-1 truncate" :class="model ? '' : 'text-muted-foreground'">
47
+ <span :id="valueId" class="min-w-0 flex-1 truncate" :class="model ? '' : 'text-muted-foreground'">
40
48
  {{ selectedLabel }}
41
49
  </span>
42
50
  <ChevronDown :size="14" class="shrink-0 text-muted-foreground" />
@@ -50,6 +58,7 @@ const selectedLabel = computed(() => {
50
58
  class="flex size-8 items-center justify-center rounded-md border text-sm text-muted-foreground transition hover:bg-muted/50"
51
59
  :class="!model ? 'border-emerald-500/40 bg-emerald-500/10' : 'border-transparent'"
52
60
  title="No icon (default glyph)"
61
+ aria-label="No icon (default glyph)"
53
62
  @click="pick(null)"
54
63
  >
55
64
  —