@revenexx/studio-integrations 0.5.1 → 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 +20 -1
  49. package/dist/runtime/components/Integrations/WorkflowEditor.vue +167 -46
  50. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -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 +196 -8
  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,10 +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
59
  runBusy: { type: Boolean, required: false },
54
- runWaiting: { type: Boolean, required: false }
60
+ runWaiting: { type: Boolean, required: false },
61
+ overlayOpen: { type: Boolean, required: false }
55
62
  });
56
63
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
57
64
  const { keys: secretKeys } = useIntegrationsSecrets();
@@ -66,7 +73,20 @@ function clone(v) {
66
73
  const vfNodes = ref([]);
67
74
  const vfEdges = ref([]);
68
75
  const flow = useVueFlow();
76
+ const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.warnings ?? [], props.workflow.blob.nodes));
69
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
+ });
70
90
  const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
71
91
  const m = findManifest(n.nodeSlug, n.nodeVersion);
72
92
  return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
@@ -79,6 +99,7 @@ function edgeKindFromPort(sourceNodeId, output) {
79
99
  if (output === "true") return "branch-true";
80
100
  if (output === "false") return "branch-false";
81
101
  if (output === "error") return "error";
102
+ if (output === "done") return "success";
82
103
  const portDef = (manifest.outputs ?? []).find((o) => o.name === output);
83
104
  if (portDef) {
84
105
  if (portDef.kind === "error") return "error";
@@ -93,10 +114,10 @@ function edgeKindFromPort(sourceNodeId, output) {
93
114
  }
94
115
  return "default";
95
116
  }
96
- function stripSide(handleId) {
97
- if (!handleId) return null;
98
- const i = handleId.indexOf("__");
99
- 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);
100
121
  }
101
122
  function triggerPosition(id, index) {
102
123
  const saved = blob.value.triggerPositions?.[id];
@@ -121,10 +142,6 @@ function stickyArea(s) {
121
142
  }
122
143
  const GROUP_PAD = 24;
123
144
  const GROUP_LABEL_ROOM = 26;
124
- function locText(v) {
125
- if (!v) return "";
126
- return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
127
- }
128
145
  function memberDims(id) {
129
146
  const d = flow.findNode(id)?.dimensions;
130
147
  return d && d.width > 0 ? d : { width: 220, height: 64 };
@@ -262,7 +279,11 @@ function syncFromBlob() {
262
279
  type: "workflow",
263
280
  position: n.position ?? { x: 240, y: 80 },
264
281
  class: bornClass(n.id),
265
- data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id] }
282
+ data: {
283
+ ...n,
284
+ __resolvedOutputs: dynamicOutputs.value[n.id],
285
+ __invalidFields: invalidNodeFields.value[n.id]
286
+ }
266
287
  }))
267
288
  ];
268
289
  const seenEdge = /* @__PURE__ */ new Set();
@@ -285,6 +306,18 @@ function syncFromBlob() {
285
306
  targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
286
307
  });
287
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
+ }
288
321
  vfEdges.value = mapped;
289
322
  void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
290
323
  }
@@ -292,9 +325,11 @@ function makeVfEdge(id, sourceId, targetId, output) {
292
325
  const kind = edgeKindFromPort(sourceId, output);
293
326
  const wn = blob.value.nodes.find((n) => n.id === sourceId);
294
327
  const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
295
- 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);
296
329
  const srcSide = pickSourceSide(sourceId, targetId);
297
330
  const tgtSide = oppositeSide(srcSide);
331
+ const order = orderedOutputPorts(sourceId);
332
+ const portIndex = order.indexOf(output);
298
333
  return {
299
334
  id,
300
335
  source: sourceId,
@@ -304,7 +339,8 @@ function makeVfEdge(id, sourceId, targetId, output) {
304
339
  type: "flow",
305
340
  data: {
306
341
  kind,
307
- label: isBranchPort ? output : void 0
342
+ label: isBranchPort ? output : void 0,
343
+ portIndex: portIndex < 0 ? 0 : portIndex
308
344
  },
309
345
  markerEnd: {
310
346
  type: MarkerType.ArrowClosed,
@@ -315,24 +351,21 @@ function makeVfEdge(id, sourceId, targetId, output) {
315
351
  };
316
352
  }
317
353
  function edgeColorByKind(kind) {
318
- switch (kind) {
319
- case "success":
320
- return "#10b981";
321
- case "error":
322
- return "#f43f5e";
323
- case "branch-true":
324
- return "#10b981";
325
- case "branch-false":
326
- return "#f59e0b";
327
- case "branch":
328
- return "#8b5cf6";
329
- default:
330
- return "#94a3b8";
331
- }
354
+ return kind === "error" ? "#cbd5e1" : "#94a3b8";
332
355
  }
333
356
  syncFromBlob();
334
- 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
+ });
335
367
  flow.onNodeDragStart(({ node }) => {
368
+ dragInFlight = true;
336
369
  if (node.type !== "group") return;
337
370
  const g = blob.value.groups?.find((x) => x.id === node.id);
338
371
  if (!g) return;
@@ -350,6 +383,7 @@ flow.onNodeDrag(({ node }) => {
350
383
  }
351
384
  });
352
385
  function persistDraggedNodes(evt) {
386
+ dragInFlight = false;
353
387
  const moved = evt.nodes?.length ? evt.nodes : [evt.node];
354
388
  for (const n of moved) {
355
389
  const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
@@ -395,7 +429,7 @@ flow.onConnect((conn) => {
395
429
  connectCompleted = true;
396
430
  if (!conn.source || !conn.target) return;
397
431
  const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
398
- const output = stripSide(conn.sourceHandle) ?? "default";
432
+ const output = portFromHandleId(conn.sourceHandle) ?? "default";
399
433
  blob.value.edges.push({
400
434
  id,
401
435
  from: { nodeId: conn.source, output },
@@ -409,7 +443,7 @@ let connectStart = null;
409
443
  let connectCompleted = false;
410
444
  flow.onConnectStart(({ nodeId, handleId, handleType }) => {
411
445
  connectCompleted = false;
412
- connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: stripSide(handleId) ?? "default" } : null;
446
+ connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: portFromHandleId(handleId) ?? "default" } : null;
413
447
  });
414
448
  flow.onConnectEnd((event) => {
415
449
  const start = connectStart;
@@ -429,7 +463,7 @@ flow.onEdgeUpdate(({ edge, connection }) => {
429
463
  if (connection.source === connection.target) return;
430
464
  const be = blob.value.edges.find((e) => e.id === edge.id);
431
465
  if (!be) return;
432
- 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 };
433
467
  be.to = { nodeId: connection.target, input: be.to.input };
434
468
  syncFromBlob();
435
469
  push();
@@ -548,9 +582,18 @@ function duplicateBlobNodes(ids) {
548
582
  });
549
583
  }
550
584
  provide("workflow-node-actions", {
585
+ configure: (id) => {
586
+ renameIntent.value = false;
587
+ selectedId.value = id;
588
+ },
551
589
  duplicate: (id) => duplicateBlobNodes([id]),
552
590
  remove: (id) => removeBlobNode(id)
553
591
  });
592
+ const controlBar = ref(null);
593
+ provide(
594
+ "workflow-canvas-bottom-inset",
595
+ () => controlBar.value ? controlBar.value.offsetHeight + 12 : 0
596
+ );
554
597
  const CLIP_KIND = "revenexx.workflow.nodes";
555
598
  async function copyNodes(ids) {
556
599
  const nodes = blob.value.nodes.filter((n) => ids.includes(n.id));
@@ -698,17 +741,33 @@ const selection = computed(() => {
698
741
  if (!selectedId.value) return null;
699
742
  return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
700
743
  });
701
- flow.onNodeClick((evt) => {
744
+ const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
745
+ flow.onNodeDoubleClick((evt) => {
702
746
  if (evt.node.type === "sticky" || evt.node.type === "group") return;
703
747
  selectedId.value = evt.node.id;
704
748
  });
705
749
  flow.onPaneClick(() => {
706
750
  selectedId.value = null;
707
751
  });
708
- function localLabel(v) {
709
- if (!v) return "";
710
- 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
+ );
711
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 });
712
771
  const selectedUpstreamOutputs = computed(() => {
713
772
  const id = selectedId.value;
714
773
  if (!id) return [];
@@ -720,7 +779,9 @@ const selectedUpstreamOutputs = computed(() => {
720
779
  const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
721
780
  return [{
722
781
  nodeId: source.id,
723
- 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,
724
785
  output: e.from.output,
725
786
  fields: portDef?.fields
726
787
  }];
@@ -819,7 +880,6 @@ function addFromPayload(payload, pos) {
819
880
  adoptIntoGroupAt(id2, at);
820
881
  markBorn(id2);
821
882
  syncFromBlob();
822
- selectedId.value = id2;
823
883
  push();
824
884
  return;
825
885
  }
@@ -836,7 +896,6 @@ function addFromPayload(payload, pos) {
836
896
  adoptIntoGroupAt(id, at);
837
897
  markBorn(id);
838
898
  syncFromBlob();
839
- selectedId.value = id;
840
899
  push();
841
900
  }
842
901
  function onDrop(ev) {
@@ -871,7 +930,6 @@ function onPaletteInsert(payload) {
871
930
  });
872
931
  markBorn(id);
873
932
  syncFromBlob();
874
- selectedId.value = id;
875
933
  push();
876
934
  return;
877
935
  }
@@ -903,6 +961,10 @@ function spliceIntoEdge(edgeId, payload) {
903
961
  const edge = blob.value.edges.find((e) => e.id === edgeId);
904
962
  const manifest = findManifest(payload.slug, payload.version);
905
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
+ }
906
968
  const a = nodePosition(edge.from.nodeId);
907
969
  const b = nodePosition(edge.to.nodeId);
908
970
  const id = `n-${Date.now()}`;
@@ -921,7 +983,6 @@ function spliceIntoEdge(edgeId, payload) {
921
983
  );
922
984
  markBorn(id);
923
985
  syncFromBlob();
924
- selectedId.value = id;
925
986
  push();
926
987
  }
927
988
  function defaultConfigFor(fields = []) {
@@ -935,6 +996,34 @@ function nodeDims(id) {
935
996
  const d = flow.findNode(id)?.dimensions;
936
997
  return d && d.width > 0 ? d : { width: 220, height: 64 };
937
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
+ }
938
1027
  async function tidyUp() {
939
1028
  const containment = /* @__PURE__ */ new Map();
940
1029
  for (const s of blob.value.stickies ?? []) {
@@ -1014,6 +1103,7 @@ async function tidyUp() {
1014
1103
  if (!inner) return n;
1015
1104
  return { ...n, position: { x: anchor.x + GROUP_PAD + inner.position.x, y: anchor.y + GROUP_PAD + GROUP_LABEL_ROOM + inner.position.y } };
1016
1105
  });
1106
+ reorderBranchesByHandle(laid, memberToGroup);
1017
1107
  for (const n of laid) {
1018
1108
  const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
1019
1109
  if (n.type === "workflow") {
@@ -1120,6 +1210,7 @@ async function focusPalette() {
1120
1210
  paletteRef.value?.focusSearch();
1121
1211
  }
1122
1212
  const lightboxOpen = computed(() => !!selection.value);
1213
+ const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1123
1214
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1124
1215
  useCanvasShortcuts(() => ({
1125
1216
  "enter": {
@@ -1181,7 +1272,7 @@ useCanvasShortcuts(() => ({
1181
1272
  pendingConnect.value = null;
1182
1273
  flow.removeSelectedElements();
1183
1274
  }
1184
- }), { disabled: lightboxOpen });
1275
+ }), { disabled: () => !canvasHasKeyboard.value });
1185
1276
  const nodeTypes = {
1186
1277
  workflow: markRaw(WorkflowNodeView),
1187
1278
  trigger: markRaw(WorkflowTriggerView),
@@ -1219,15 +1310,15 @@ const edgeTypes = {
1219
1310
  :default-edge-options="{ type: 'flow' }"
1220
1311
  :connection-line-type="ConnectionLineType.Step"
1221
1312
  :connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
1313
+ :connection-mode="ConnectionMode.Strict"
1222
1314
  :is-valid-connection="isValidConnection"
1223
1315
  :snap-to-grid="true"
1224
1316
  :snap-grid="[20, 20]"
1225
1317
  :selection-mode="SelectionMode.Partial"
1226
1318
  :selection-key-code="pointerMode === 'select' ? true : 'Shift'"
1227
1319
  :pan-on-drag="pointerMode === 'hand' ? true : [1]"
1228
- :pan-on-scroll="true"
1229
- :pan-on-scroll-mode="PanOnScrollMode.Free"
1230
- :delete-key-code="['Backspace', 'Delete']"
1320
+ :zoom-on-double-click="false"
1321
+ :delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
1231
1322
  :edges-updatable="true"
1232
1323
  :min-zoom="0.25"
1233
1324
  :max-zoom="4"
@@ -1466,7 +1557,7 @@ const edgeTypes = {
1466
1557
  </Transition>
1467
1558
 
1468
1559
  <!-- Floating control pane — bottom center. -->
1469
- <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">
1470
1561
  <TooltipProvider :delay-duration="250">
1471
1562
  <div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
1472
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">
@@ -1489,9 +1580,13 @@ const edgeTypes = {
1489
1580
  <div class="mx-1 h-5 w-px bg-black/10" />
1490
1581
 
1491
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. -->
1492
1585
  <IntegrationsEditorTip label="Select" shortcut="v">
1493
1586
  <button
1494
1587
  type="button"
1588
+ aria-label="Select"
1589
+ :aria-pressed="pointerMode === 'select'"
1495
1590
  class="flex size-7 items-center justify-center rounded-lg transition"
1496
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'"
1497
1592
  @click="setPointerMode('select')"
@@ -1502,6 +1597,8 @@ const edgeTypes = {
1502
1597
  <IntegrationsEditorTip label="Hand (pan)" shortcut="h">
1503
1598
  <button
1504
1599
  type="button"
1600
+ aria-label="Hand (pan)"
1601
+ :aria-pressed="pointerMode === 'hand'"
1505
1602
  class="flex size-7 items-center justify-center rounded-lg transition"
1506
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'"
1507
1604
  @click="setPointerMode('hand')"
@@ -1578,6 +1675,7 @@ const edgeTypes = {
1578
1675
  <IntegrationsEditorTip label="Zoom out" shortcut="-">
1579
1676
  <button
1580
1677
  type="button"
1678
+ aria-label="Zoom out"
1581
1679
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1582
1680
  @click="flow.zoomOut({ duration: 150 })"
1583
1681
  >
@@ -1591,8 +1689,13 @@ const edgeTypes = {
1591
1689
  <span class="inline-flex">
1592
1690
  <DropdownMenu v-model:open="zoomMenuOpen">
1593
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. -->
1594
1696
  <button
1595
1697
  type="button"
1698
+ :aria-label="`Zoom level ${zoomPct}`"
1596
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"
1597
1700
  >
1598
1701
  {{ zoomPct }}
@@ -1604,6 +1707,7 @@ const edgeTypes = {
1604
1707
  v-model="zoomInputText"
1605
1708
  type="text"
1606
1709
  inputmode="numeric"
1710
+ aria-label="Zoom level in percent"
1607
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"
1608
1712
  placeholder="%"
1609
1713
  @keydown.stop
@@ -1626,6 +1730,7 @@ const edgeTypes = {
1626
1730
  <IntegrationsEditorTip label="Zoom in" shortcut="=">
1627
1731
  <button
1628
1732
  type="button"
1733
+ aria-label="Zoom in"
1629
1734
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1630
1735
  @click="flow.zoomIn({ duration: 150 })"
1631
1736
  >
@@ -1669,7 +1774,22 @@ const edgeTypes = {
1669
1774
  renameIntent = false;
1670
1775
  }
1671
1776
  }">
1672
- <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
+ >
1673
1793
  <NodeInspector
1674
1794
  v-if="selection"
1675
1795
  :selection="selection"
@@ -1682,6 +1802,7 @@ const edgeTypes = {
1682
1802
  :triggers="blob.triggers"
1683
1803
  :dynamic-outputs="dynamicOutputs"
1684
1804
  :workflow-id="workflow.id"
1805
+ :refused-fields="selectedId ? invalidNodeMessages[selectedId] : void 0"
1685
1806
  @update="applyInspectorUpdate"
1686
1807
  @close="selectedId = null"
1687
1808
  @remove="selectedId && removeBlobNode(selectedId)"
@@ -8,8 +8,27 @@ type __VLS_Props = {
8
8
  runBusy?: boolean;
9
9
  /** The run is queued behind a bundle build — busy, but worth saying why. */
10
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;
11
18
  };
12
- 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, {
13
32
  change: (blob: WorkflowBlob) => any;
14
33
  run: () => any;
15
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
  —
@@ -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, {