@revenexx/studio-integrations 1.1.1 → 1.3.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 (119) hide show
  1. package/CHANGELOG.md +638 -0
  2. package/README.md +3 -1
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
  5. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
  6. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
  7. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
  8. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  9. package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
  10. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  11. package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
  12. package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
  13. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
  14. package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  16. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +88 -1
  18. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  19. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +7 -3
  20. package/dist/runtime/components/Integrations/NodeInspector.vue +125 -54
  21. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +7 -3
  22. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  23. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  24. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  25. package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
  26. package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
  27. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
  28. package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
  29. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
  30. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
  31. package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
  32. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
  33. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
  34. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  35. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  36. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  37. package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
  38. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
  39. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
  40. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
  41. package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
  42. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
  43. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
  44. package/dist/runtime/components/Integrations/WorkflowEditor.vue +78 -30
  45. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
  46. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
  47. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
  48. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  49. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  50. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  51. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
  52. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
  53. package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
  54. package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
  55. package/dist/runtime/components/Integrations/canvasSides.js +22 -0
  56. package/dist/runtime/composables/useCredentialTypes.js +2 -6
  57. package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
  58. package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
  59. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
  60. package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
  61. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  62. package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
  63. package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
  64. package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
  65. package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
  66. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
  67. package/dist/runtime/composables/useIntegrationsTemplates.js +7 -5
  68. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
  69. package/dist/runtime/composables/useIntegrationsWorkflows.js +10 -7
  70. package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
  71. package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
  72. package/dist/runtime/composables/useOAuthConnect.js +33 -0
  73. package/dist/runtime/data/expressionEditor.d.ts +17 -5
  74. package/dist/runtime/data/expressionEditor.js +11 -5
  75. package/dist/runtime/data/handleId.d.ts +61 -1
  76. package/dist/runtime/data/handleId.js +8 -1
  77. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  78. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  79. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  80. package/dist/runtime/data/nodeVersionMove.js +65 -0
  81. package/dist/runtime/data/payloadForm.js +3 -2
  82. package/dist/runtime/i18n/messages/de.json +79 -12
  83. package/dist/runtime/i18n/messages/en.json +79 -12
  84. package/dist/runtime/pages/integrations/audit.vue +7 -6
  85. package/dist/runtime/pages/integrations/credentials.vue +9 -21
  86. package/dist/runtime/pages/integrations/index.vue +55 -8
  87. package/dist/runtime/pages/integrations/runs.vue +116 -7
  88. package/dist/runtime/pages/integrations/secrets.vue +2 -1
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
  90. package/dist/runtime/pages/integrations/templates/index.vue +18 -15
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +169 -24
  92. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  93. package/dist/runtime/types/integrations.d.ts +49 -1
  94. package/dist/runtime/utils/apiError.d.ts +21 -1
  95. package/dist/runtime/utils/apiError.js +1 -1
  96. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  97. package/dist/runtime/utils/buildSettle.js +29 -0
  98. package/dist/runtime/utils/clipboard.d.ts +33 -0
  99. package/dist/runtime/utils/clipboard.js +11 -0
  100. package/dist/runtime/utils/deepClone.d.ts +41 -0
  101. package/dist/runtime/utils/deepClone.js +3 -0
  102. package/dist/runtime/utils/formatDuration.d.ts +44 -0
  103. package/dist/runtime/utils/formatDuration.js +13 -0
  104. package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
  105. package/dist/runtime/utils/nodeDisplayName.js +6 -0
  106. package/dist/runtime/utils/notify.d.ts +1 -1
  107. package/dist/runtime/utils/runEnvelope.d.ts +31 -0
  108. package/dist/runtime/utils/runEnvelope.js +12 -0
  109. package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
  110. package/dist/runtime/utils/runFeedQuery.js +66 -0
  111. package/dist/runtime/utils/runOutputs.d.ts +33 -0
  112. package/dist/runtime/utils/runOutputs.js +10 -0
  113. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  114. package/dist/runtime/utils/settingCondition.js +84 -0
  115. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  116. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  117. package/dist/runtime/utils/validationFindings.d.ts +26 -3
  118. package/dist/runtime/utils/validationFindings.js +18 -3
  119. package/package.json +1 -1
@@ -146,20 +146,20 @@ function spliceNode() {
146
146
  >
147
147
  <button
148
148
  type="button"
149
- class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
149
+ class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
150
150
  :title="t('integration.canvas.edge.action.insertNode')"
151
151
  @click.stop="spliceNode"
152
152
  >
153
- <Plus :size="12" />
153
+ <Plus :size="13" />
154
154
  </button>
155
155
  <div class="h-3.5 w-px bg-black/10" />
156
156
  <button
157
157
  type="button"
158
- class="flex size-5 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
158
+ class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
159
159
  :title="t('integration.canvas.edge.action.delete')"
160
160
  @click.stop="removeEdge"
161
161
  >
162
- <TrashBin :size="12" />
162
+ <TrashBin :size="13" />
163
163
  </button>
164
164
  </div>
165
165
  </EdgeLabelRenderer>
@@ -28,7 +28,7 @@ import {
28
28
  DropdownMenuTrigger,
29
29
  TooltipProvider
30
30
  } from "@revenexx/studio";
31
- import { Braces, EllipsisVertical, GripVertical, Group as GroupIcon, Hand, Map as MapIcon, Minus, MousePointer2, Plus, Settings2, StickyNote } from "lucide-vue-next";
31
+ import { Braces, Database, EllipsisVertical, GripVertical, Group as GroupIcon, Hand, Map as MapIcon, Minus, MousePointer2, Plus, Settings2, StickyNote } from "lucide-vue-next";
32
32
  import "@vue-flow/core/dist/style.css";
33
33
  import "@vue-flow/core/dist/theme-default.css";
34
34
  import "@vue-flow/minimap/dist/style.css";
@@ -40,6 +40,7 @@ import WorkflowTriggerView from "./WorkflowTriggerView.vue";
40
40
  import StickyNoteView from "./StickyNoteView.vue";
41
41
  import GroupFrameView from "./GroupFrameView.vue";
42
42
  import WorkflowContextMenu, {} from "./WorkflowContextMenu.vue";
43
+ import WorkflowStateNamespacesDialog from "./WorkflowStateNamespacesDialog.vue";
43
44
  import WorkflowVariablesDialog from "./WorkflowVariablesDialog.vue";
44
45
  import { useCanvasShortcuts } from "../../composables/useCanvasShortcuts";
45
46
  import { usePaletteLayout } from "../../composables/usePaletteLayout";
@@ -50,7 +51,10 @@ import { layoutWorkflowNodes } from "./workflowLayout";
50
51
  import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
51
52
  import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
52
53
  import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
53
- import { portFromHandleId } from "../../data/handleId";
54
+ import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
55
+ import { OPPOSITE_SIDE } from "./canvasSides";
56
+ import { deepClone } from "../../utils/deepClone";
57
+ import { copyToClipboard } from "../../utils/clipboard";
54
58
  import { groupFindingsByNode } from "../../utils/validationFindings";
55
59
  import { reorderBranchSubtrees } from "../../data/branchReorder";
56
60
  import { findingsByNodeField } from "../../utils/validationFindings";
@@ -65,14 +69,11 @@ const props = defineProps({
65
69
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
66
70
  const t = useIntegrationsT();
67
71
  const { keys: secretKeys } = useIntegrationsSecrets();
68
- const blob = ref(clone(props.draft ?? props.workflow.blob));
72
+ const blob = ref(deepClone(props.draft ?? props.workflow.blob));
69
73
  watch(() => props.workflow.id, () => {
70
- blob.value = clone(props.workflow.blob);
74
+ blob.value = deepClone(props.workflow.blob);
71
75
  syncFromBlob();
72
76
  });
73
- function clone(v) {
74
- return JSON.parse(JSON.stringify(v));
75
- }
76
77
  const vfNodes = ref([]);
77
78
  const vfEdges = ref([]);
78
79
  const flow = useVueFlow();
@@ -80,7 +81,7 @@ const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.wa
80
81
  const dynamicOutputs = ref({});
81
82
  const invalidNodeFields = computed(() => {
82
83
  const out = {};
83
- for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "")) {
84
+ for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "", (_n, key) => key)) {
84
85
  if (g.nodeId) out[g.nodeId] = g.fields;
85
86
  }
86
87
  return out;
@@ -160,9 +161,6 @@ function nodePosition(id) {
160
161
  function pickSourceSide(_sourceId, _targetId) {
161
162
  return "bottom";
162
163
  }
163
- function oppositeSide(s) {
164
- return s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
165
- }
166
164
  function stickyArea(s) {
167
165
  return (s.size?.width ?? 240) * (s.size?.height ?? 160);
168
166
  }
@@ -252,6 +250,7 @@ function markBorn(...ids) {
252
250
  function bornClass(id) {
253
251
  return bornIds.value.has(id) ? "node-pop-in" : void 0;
254
252
  }
253
+ const NODE_FALLBACK_POSITION = { x: 240, y: 80 };
255
254
  function syncFromBlob() {
256
255
  const loops = nodeLoopData(blob.value.nodes, blob.value.edges);
257
256
  const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
@@ -304,7 +303,7 @@ function syncFromBlob() {
304
303
  ...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
305
304
  id: n.id,
306
305
  type: "workflow",
307
- position: n.position ?? { x: 240, y: 80 },
306
+ position: n.position ?? NODE_FALLBACK_POSITION,
308
307
  class: bornClass(n.id),
309
308
  data: {
310
309
  ...n,
@@ -330,8 +329,8 @@ function syncFromBlob() {
330
329
  ...vf,
331
330
  source,
332
331
  target,
333
- sourceHandle: srcGroup ? "default__bottom" : vf.sourceHandle,
334
- targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
332
+ sourceHandle: srcGroup ? outputHandleId("default", "bottom") : vf.sourceHandle,
333
+ targetHandle: tgtGroup ? inputHandleId("top") : vf.targetHandle
335
334
  });
336
335
  }
337
336
  const bySource = /* @__PURE__ */ new Map();
@@ -355,15 +354,15 @@ function makeVfEdge(id, sourceId, targetId, output) {
355
354
  const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
356
355
  const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
357
356
  const srcSide = pickSourceSide(sourceId, targetId);
358
- const tgtSide = oppositeSide(srcSide);
357
+ const tgtSide = OPPOSITE_SIDE[srcSide];
359
358
  const order = orderedOutputPorts(sourceId);
360
359
  const portIndex = order.indexOf(output);
361
360
  return {
362
361
  id,
363
362
  source: sourceId,
364
363
  target: targetId,
365
- sourceHandle: `${output}__${srcSide}`,
366
- targetHandle: `__in__${tgtSide}`,
364
+ sourceHandle: outputHandleId(output, srcSide),
365
+ targetHandle: inputHandleId(tgtSide),
367
366
  type: "flow",
368
367
  data: {
369
368
  kind,
@@ -561,7 +560,7 @@ function removeBlobNode(id) {
561
560
  push();
562
561
  }
563
562
  function push() {
564
- emit("change", clone(blob.value));
563
+ emit("change", deepClone(blob.value));
565
564
  }
566
565
  const STICKY_DEFAULT_SIZE = { width: 240, height: 160 };
567
566
  function updateSticky(id, patch) {
@@ -572,6 +571,18 @@ function updateSticky(id, patch) {
572
571
  push();
573
572
  }
574
573
  const variablesOpen = ref(false);
574
+ const stateOpen = ref(false);
575
+ const stateCount = computed(() => (blob.value.state ?? []).length);
576
+ function updateStateNamespaces(next) {
577
+ blob.value.state = next.length ? next : void 0;
578
+ push();
579
+ }
580
+ function declareStateNamespace(namespace, role, visibility) {
581
+ const declared = blob.value.state ?? [];
582
+ if (declared.some((n) => n.namespace === namespace)) return;
583
+ blob.value.state = [...declared, { namespace, role, visibility }];
584
+ push();
585
+ }
575
586
  const variableCount = computed(() => Object.keys(blob.value.variables ?? {}).length);
576
587
  function updateVariables(next) {
577
588
  blob.value.variables = Object.keys(next).length ? next : void 0;
@@ -622,7 +633,7 @@ function duplicateBlobNodes(ids) {
622
633
  src.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
623
634
  const offset = 40;
624
635
  const copies = src.map((n) => ({
625
- ...clone(n),
636
+ ...deepClone(n),
626
637
  id: idMap.get(n.id),
627
638
  position: { x: (n.position?.x ?? 0) + offset, y: (n.position?.y ?? 0) + offset }
628
639
  }));
@@ -661,12 +672,10 @@ async function copyNodes(ids) {
661
672
  if (!nodes.length) return;
662
673
  const inner = new Set(nodes.map((n) => n.id));
663
674
  const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
664
- try {
665
- await navigator.clipboard.writeText(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2));
666
- toast.success(t("integration.editor.clipboard.toast.copied", nodes.length));
667
- } catch {
668
- toast.error(t("integration.editor.clipboard.error.write"));
669
- }
675
+ await copyToClipboard(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2), {
676
+ copied: t("integration.editor.clipboard.toast.copied", nodes.length),
677
+ failed: t("integration.editor.clipboard.error.write")
678
+ });
670
679
  }
671
680
  async function pasteNodes(at) {
672
681
  let text = "";
@@ -695,7 +704,7 @@ async function pasteNodes(at) {
695
704
  const dx = at ? Math.round(at.x - minX) : 40;
696
705
  const dy = at ? Math.round(at.y - minY) : 40;
697
706
  blob.value.nodes.push(...payload.nodes.map((n) => ({
698
- ...clone(n),
707
+ ...deepClone(n),
699
708
  id: idMap.get(n.id),
700
709
  position: { x: (n.position?.x ?? 0) + dx, y: (n.position?.y ?? 0) + dy }
701
710
  })));
@@ -906,10 +915,10 @@ function groupAtPoint(pos) {
906
915
  function addGroupFrame() {
907
916
  const rect = flow.vueFlowRef.value?.getBoundingClientRect();
908
917
  const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
909
- addFromPayload({ kind: "group" }, {
918
+ addFromPayload({ kind: "group" }, freeSpotNear({
910
919
  x: center.x - EMPTY_GROUP_SIZE.width / 2,
911
920
  y: center.y - EMPTY_GROUP_SIZE.height / 2
912
- });
921
+ }));
913
922
  }
914
923
  function adoptIntoGroupAt(id, pos) {
915
924
  const g = groupAtPoint(pos);
@@ -970,6 +979,21 @@ function onDrop(ev) {
970
979
  const payload = JSON.parse(raw);
971
980
  addFromPayload(payload, flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY }));
972
981
  }
982
+ const CASCADE_STEP = 2 * GRID;
983
+ function freeSpotNear(at) {
984
+ const taken = [
985
+ ...vfNodes.value.map((n) => n.position),
986
+ ...blob.value.nodes.map((n) => n.position ?? NODE_FALLBACK_POSITION),
987
+ ...Object.values(blob.value.triggerPositions ?? {})
988
+ ];
989
+ const occupied = (p) => taken.some((q) => Math.abs(q.x - p.x) < GRID && Math.abs(q.y - p.y) < GRID);
990
+ const spot = { x: snap(at.x), y: snap(at.y) };
991
+ for (let i = 0; i <= taken.length && occupied(spot); i++) {
992
+ spot.x += CASCADE_STEP;
993
+ spot.y += CASCADE_STEP;
994
+ }
995
+ return spot;
996
+ }
973
997
  function onPaletteInsert(payload) {
974
998
  if (pendingConnect.value && payload.kind === "manifest") {
975
999
  const pc = pendingConnect.value;
@@ -1003,7 +1027,7 @@ function onPaletteInsert(payload) {
1003
1027
  pendingSpliceEdgeId.value = null;
1004
1028
  const rect = flow.vueFlowRef.value?.getBoundingClientRect();
1005
1029
  const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
1006
- addFromPayload(payload, center);
1030
+ addFromPayload(payload, freeSpotNear(center));
1007
1031
  }
1008
1032
  const pendingSpliceEdgeId = ref(null);
1009
1033
  provide("workflow-edge-actions", {
@@ -1279,7 +1303,7 @@ function onConfigOpenAutoFocus(event) {
1279
1303
  event.preventDefault();
1280
1304
  event.target?.focus();
1281
1305
  }
1282
- const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1306
+ const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !stateOpen.value && !props.overlayOpen);
1283
1307
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1284
1308
  useCanvasShortcuts(() => ({
1285
1309
  "enter": {
@@ -1324,6 +1348,9 @@ useCanvasShortcuts(() => ({
1324
1348
  "shift+v": () => {
1325
1349
  variablesOpen.value = true;
1326
1350
  },
1351
+ "shift+t": () => {
1352
+ stateOpen.value = true;
1353
+ },
1327
1354
  "shift+alt+s": () => emit("openSettings"),
1328
1355
  "-": () => {
1329
1356
  void flow.zoomOut({ duration: 150 });
@@ -1728,6 +1755,17 @@ const edgeTypes = {
1728
1755
  <span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
1729
1756
  </button>
1730
1757
  </IntegrationsEditorTip>
1758
+ <IntegrationsEditorTip :label="t('integration.editor.tool.state.tooltip')" shortcut="shift+t">
1759
+ <button
1760
+ type="button"
1761
+ class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
1762
+ @click="stateOpen = true"
1763
+ >
1764
+ <Database :size="13" />
1765
+ {{ t("integration.editor.tool.state.label") }}
1766
+ <span v-if="stateCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ stateCount }}</span>
1767
+ </button>
1768
+ </IntegrationsEditorTip>
1731
1769
  <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
1732
1770
  <button
1733
1771
  type="button"
@@ -1827,6 +1865,14 @@ const edgeTypes = {
1827
1865
  </div>
1828
1866
  </div>
1829
1867
 
1868
+ <!-- State namespaces lightbox (control-pane entry) — what this workflow may
1869
+ remember between runs. -->
1870
+ <WorkflowStateNamespacesDialog
1871
+ v-model:open="stateOpen"
1872
+ :state="blob.state ?? []"
1873
+ @update:state="updateStateNamespaces"
1874
+ />
1875
+
1830
1876
  <!-- Workflow variables lightbox (control-pane entry). -->
1831
1877
  <WorkflowVariablesDialog
1832
1878
  v-model:open="variablesOpen"
@@ -1865,6 +1911,7 @@ const edgeTypes = {
1865
1911
  :selection="selection"
1866
1912
  :secret-keys="secretKeys"
1867
1913
  :variables="blob.variables"
1914
+ :state-namespaces="blob.state ?? []"
1868
1915
  :upstream-outputs="selectedUpstreamOutputs"
1869
1916
  :autofocus-name="renameIntent"
1870
1917
  :nodes="blob.nodes"
@@ -1880,6 +1927,7 @@ const edgeTypes = {
1880
1927
  selectedId = id;
1881
1928
  renameIntent = false;
1882
1929
  }"
1930
+ @declare-state="declareStateNamespace"
1883
1931
  />
1884
1932
  </DialogContent>
1885
1933
  </Dialog>
@@ -11,6 +11,7 @@ import { Badge, Button } from "@revenexx/studio";
11
11
  import { toast } from "vue-sonner";
12
12
  import "@vue-flow/core/dist/style.css";
13
13
  import "@vue-flow/core/dist/theme-default.css";
14
+ import LoadingAnnouncement from "./LoadingAnnouncement.vue";
14
15
  import WorkflowNodeView from "./WorkflowNodeView.vue";
15
16
  import WorkflowTriggerView from "./WorkflowTriggerView.vue";
16
17
  import StickyNoteView from "./StickyNoteView.vue";
@@ -18,8 +19,11 @@ import GroupFrameView from "./GroupFrameView.vue";
18
19
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
19
20
  import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
20
21
  import { findManifest } from "../../data/integrationNodeCatalog";
22
+ import { OPPOSITE_SIDE } from "./canvasSides";
23
+ import { inputHandleId, outputHandleId } from "../../data/handleId";
21
24
  import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
22
25
  import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
26
+ import { errorReason } from "../../utils/apiError";
23
27
  const props = defineProps({
24
28
  workflow: { type: Object, required: true },
25
29
  draftPending: { type: Boolean, required: false }
@@ -118,7 +122,6 @@ function buildPreview(blob) {
118
122
  return { x: 0, y: 0 };
119
123
  };
120
124
  const pickSourceSide = (_sourceId, _targetId) => "bottom";
121
- const opposite = (s) => s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
122
125
  const edgeKind = (sourceNodeId, output) => {
123
126
  const wn = blob.nodes.find((n) => n.id === sourceNodeId);
124
127
  if (!wn) return "default";
@@ -182,8 +185,8 @@ function buildPreview(blob) {
182
185
  id: e.id,
183
186
  source: e.from.nodeId,
184
187
  target: e.to.nodeId,
185
- sourceHandle: `${e.from.output}__${srcSide}`,
186
- targetHandle: `__in__${opposite(srcSide)}`,
188
+ sourceHandle: outputHandleId(e.from.output, srcSide),
189
+ targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
187
190
  type: "flow",
188
191
  selectable: false,
189
192
  data: { kind, label: isBranchPort ? e.from.output : void 0 },
@@ -222,7 +225,7 @@ async function select(revision) {
222
225
  buildPreview(detail.blob);
223
226
  }
224
227
  } catch (e) {
225
- toast.error(e?.message ?? t("integration.history.toast.loadRevisionFailed"));
228
+ toast.error(errorReason(e, t("integration.history.toast.loadRevisionFailed")));
226
229
  } finally {
227
230
  detailLoading.value = false;
228
231
  }
@@ -239,7 +242,7 @@ async function restoreRevision(rev) {
239
242
  await loadList(true);
240
243
  void select(null);
241
244
  } catch (e) {
242
- toast.error(e?.message ?? t("integration.history.toast.restoreFailed"));
245
+ toast.error(errorReason(e, t("integration.history.toast.restoreFailed")));
243
246
  } finally {
244
247
  restoring.value = null;
245
248
  }
@@ -328,6 +331,14 @@ onMounted(() => {
328
331
  </p>
329
332
  </div>
330
333
 
334
+ <!-- In the aside's chrome rather than in the scrolling list, so it is on the
335
+ page while the revisions are on their way (PO-251). -->
336
+ <LoadingAnnouncement
337
+ :label="t('integration.history.heading.title')"
338
+ :loading="listLoading && !entries.length"
339
+ :text="t('integration.history.state.loading')"
340
+ />
341
+
331
342
  <div class="flex-1 overflow-y-auto p-2 space-y-1">
332
343
  <!-- Current (live) version -->
333
344
  <button
@@ -385,7 +396,7 @@ onMounted(() => {
385
396
  {{ revisionsApi.error.value }}
386
397
  </p>
387
398
 
388
- <div v-if="listLoading && !entries.length" class="space-y-2 px-2 py-2">
399
+ <div v-if="listLoading && !entries.length" aria-hidden="true" class="space-y-2 px-2 py-2">
389
400
  <div v-for="n in 5" :key="`sk-rev-${n}`" class="flex items-center gap-3 py-1.5">
390
401
  <div class="size-7 shrink-0 rounded-md bg-black/[0.07] animate-pulse" />
391
402
  <div class="min-w-0 flex-1 space-y-1.5">
@@ -5,8 +5,12 @@ import { NodeToolbar } from "@vue-flow/node-toolbar";
5
5
  import * as Lucide from "lucide-vue-next";
6
6
  import { Copy, DangerTriangle as AlertTriangle, RefreshCircle as Loader2, Repeat, TrashBinMinimalistic as TrashBin } from "#solar-icons";
7
7
  import { NODE_CATEGORIES, findManifest, isVersionExecutable } from "../../data/integrationNodeCatalog";
8
+ import { inputHandleId, outputHandleId } from "../../data/handleId";
8
9
  import { isTerminalManifest } from "../../data/nodeInputData";
9
- import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
10
+ import { resolveLocalized } from "../../utils/localize";
11
+ import { SIDE_POSITION, distributedStyle } from "./canvasSides";
12
+ import { fieldDisplayLabel } from "../../utils/validationFindings";
13
+ import { nodeDisplayName } from "../../utils/nodeDisplayName";
10
14
  const props = defineProps({
11
15
  id: { type: String, required: true },
12
16
  data: { type: Object, required: true },
@@ -17,11 +21,11 @@ defineEmits(["updateNodeInternals"]);
17
21
  const t = useIntegrationsT();
18
22
  const manifest = computed(() => findManifest(props.data.nodeSlug, props.data.nodeVersion));
19
23
  const category = computed(() => NODE_CATEGORIES.find((c) => c.key === manifest.value?.category));
20
- const title = computed(
21
- () => resolveDisplayName(props.data.name) || resolveLocalized(manifest.value?.name) || props.data.nodeSlug
22
- );
24
+ const title = computed(() => nodeDisplayName(props.data));
23
25
  const invalidFields = computed(() => props.data.__invalidFields ?? []);
24
- const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, { fields: invalidFields.value.join(", ") }));
26
+ const invalidLabel = computed(() => t("integration.canvas.node.badge.invalidConfig", invalidFields.value.length, {
27
+ fields: invalidFields.value.map((key) => fieldDisplayLabel(props.data, key)).join(", ")
28
+ }));
25
29
  const insideLoops = computed(() => props.data.__loop?.inside ?? []);
26
30
  const repeatsLabel = computed(() => t("integration.canvas.node.badge.repeats", insideLoops.value.length, { iterators: insideLoops.value.join(", ") }));
27
31
  const bodySize = computed(() => props.data.__loop?.bodySize);
@@ -111,14 +115,6 @@ function portTone(p) {
111
115
  const showPortLabels = computed(() => resolvePending.value || outputs.value.length > 1 || outputs.value.some((p) => p.kind !== "default"));
112
116
  const IN_SIDES = ["top"];
113
117
  const OUT_SIDES = ["bottom"];
114
- function sideToPosition(s) {
115
- return s === "top" ? Position.Top : s === "right" ? Position.Right : s === "bottom" ? Position.Bottom : Position.Left;
116
- }
117
- function distributedStyle(side, i, total) {
118
- const pct = `${(i + 1) / (total + 1) * 100}%`;
119
- if (side === "top" || side === "bottom") return { left: pct };
120
- return { top: pct };
121
- }
122
118
  const flow = useVueFlow();
123
119
  const hovered = ref(false);
124
120
  let hoverTimer = null;
@@ -192,22 +188,22 @@ function remove() {
192
188
  <NodeToolbar v-if="nodeActions" :is-visible="hovered || selected" :position="Position.Top" :offset="8" class="flex items-center gap-1 rounded-md border border-black/[0.08] bg-white px-1 py-1 shadow-md" @mouseenter="onHoverEnter" @mouseleave="onHoverLeave">
193
189
  <button
194
190
  type="button"
195
- class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-foreground/80 hover:bg-muted/50"
191
+ class="flex size-6 items-center justify-center rounded text-sm text-foreground/80 hover:bg-muted/50"
196
192
  :title="t('integration.canvas.node.action.duplicate')"
197
193
  :aria-label="t('integration.canvas.node.action.duplicateAria')"
198
194
  @click="duplicate"
199
195
  >
200
- <Copy :size="11" />
196
+ <Copy :size="13" />
201
197
  </button>
202
198
  <div class="h-4 w-px bg-black/10" />
203
199
  <button
204
200
  type="button"
205
- class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
201
+ class="flex size-6 items-center justify-center rounded text-sm text-rose-600 hover:bg-rose-50"
206
202
  :title="t('integration.common.action.delete')"
207
203
  :aria-label="t('integration.canvas.node.action.deleteAria')"
208
204
  @click="remove"
209
205
  >
210
- <TrashBin :size="11" />
206
+ <TrashBin :size="13" />
211
207
  </button>
212
208
  </NodeToolbar>
213
209
 
@@ -239,10 +235,10 @@ function remove() {
239
235
  dots feed the same `__in` port. Clearly visible, ring + shadow. -->
240
236
  <Handle
241
237
  v-for="side in IN_SIDES"
242
- :id="`__in__${side}`"
238
+ :id="inputHandleId(side)"
243
239
  :key="`in-${side}`"
244
240
  type="target"
245
- :position="sideToPosition(side)"
241
+ :position="SIDE_POSITION[side]"
246
242
  :title="t('integration.canvas.node.handle.input')"
247
243
  class="!size-3 !rounded-full !border-2 !border-white !bg-slate-500 !shadow-sm transition-all hover:!size-3.5 hover:!bg-slate-700"
248
244
  />
@@ -252,10 +248,10 @@ function remove() {
252
248
  <template v-for="side in OUT_SIDES" :key="`side-${side}`">
253
249
  <Handle
254
250
  v-for="(p, i) in outputs"
255
- :id="`${p.name}__${side}`"
251
+ :id="outputHandleId(p.name, side)"
256
252
  :key="`${p.name}-${side}`"
257
253
  type="source"
258
- :position="sideToPosition(side)"
254
+ :position="SIDE_POSITION[side]"
259
255
  :title="handleTitle(p)"
260
256
  :style="distributedStyle(side, i, outputs.length)"
261
257
  class="!size-3 !rounded-full !border-2 !border-white !shadow-sm transition-all hover:!size-3.5"
@@ -24,10 +24,14 @@ import StickyNoteView from "./StickyNoteView.vue";
24
24
  import GroupFrameView from "./GroupFrameView.vue";
25
25
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
26
26
  import { findManifest } from "../../data/integrationNodeCatalog";
27
+ import { OPPOSITE_SIDE } from "./canvasSides";
28
+ import { inputHandleId, outputHandleId } from "../../data/handleId";
27
29
  import { nodeLoopData } from "../../data/loopBody";
28
30
  import { triggerGlyph } from "../../data/triggerGlyphs";
31
+ import { formatDuration } from "../../utils/formatDuration";
29
32
  import { SAFETY_POLL_MS } from "../../utils/pollBudget";
30
33
  import { runStatusClass, runStatusLabel } from "../../utils/statusStyle";
34
+ import { errorReason } from "../../utils/apiError";
31
35
  const props = defineProps({
32
36
  workflow: { type: Object, required: true },
33
37
  initialRunId: { type: [String, null], required: false },
@@ -59,7 +63,7 @@ async function loadRuns(reset = false) {
59
63
  runsPage.value = res.page;
60
64
  runsLastPage.value = res.lastPage;
61
65
  } catch (e) {
62
- toast.error(e?.message ?? t("integration.replay.toast.loadRunsFailed"));
66
+ toast.error(errorReason(e, t("integration.replay.toast.loadRunsFailed")));
63
67
  } finally {
64
68
  runsLoading.value = false;
65
69
  }
@@ -101,7 +105,7 @@ async function loadSteps(runId, isPoll = false) {
101
105
  }
102
106
  } catch (e) {
103
107
  if (selectedRunId.value === runId) {
104
- stepsError.value = e?.message ?? t("integration.replay.error.loadStepsFailed");
108
+ stepsError.value = errorReason(e, t("integration.replay.error.loadStepsFailed"));
105
109
  }
106
110
  } finally {
107
111
  if (!isPoll) stepsLoading.value = false;
@@ -293,7 +297,6 @@ const vfEdges = computed(() => {
293
297
  return { x: 0, y: 0 };
294
298
  };
295
299
  const pickSourceSide = (_sourceId, _targetId) => "bottom";
296
- const opposite = (s) => s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
297
300
  const edgeKind = (sourceNodeId, output) => {
298
301
  const wn = blob.nodes.find((n) => n.id === sourceNodeId);
299
302
  if (!wn) return "default";
@@ -331,8 +334,8 @@ const vfEdges = computed(() => {
331
334
  id: e.id,
332
335
  source: e.from.nodeId,
333
336
  target: e.to.nodeId,
334
- sourceHandle: `${e.from.output}__${srcSide}`,
335
- targetHandle: `__in__${opposite(srcSide)}`,
337
+ sourceHandle: outputHandleId(e.from.output, srcSide),
338
+ targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
336
339
  type: "flow",
337
340
  selectable: false,
338
341
  animated: traversed && !targetReached,
@@ -395,12 +398,6 @@ function formatTime(iso) {
395
398
  return String(iso);
396
399
  }
397
400
  }
398
- function formatDuration(ms) {
399
- if (ms == null) return "\u2014";
400
- if (ms < 1e3) return `${ms} ms`;
401
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)} s`;
402
- return `${Math.floor(ms / 6e4)}m ${Math.round(ms % 6e4 / 1e3)}s`;
403
- }
404
401
  function pretty(v) {
405
402
  try {
406
403
  return JSON.stringify(v, null, 2);
@@ -0,0 +1,13 @@
1
+ import type { WorkflowStateNamespace } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ state: WorkflowStateNamespace[];
5
+ };
6
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ "update:open": (args_0: boolean) => any;
8
+ "update:state": (next: WorkflowStateNamespace[]) => any;
9
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ "onUpdate:open"?: ((args_0: boolean) => any) | undefined;
11
+ "onUpdate:state"?: ((next: WorkflowStateNamespace[]) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ export default _default;