@revenexx/studio-integrations 1.2.0 → 1.4.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 (82) hide show
  1. package/CHANGELOG.md +581 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
  4. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
  5. package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
  6. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  7. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
  11. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  12. package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
  13. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  14. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  15. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  16. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  17. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  18. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  19. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  20. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  21. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  22. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  23. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  24. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  25. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  26. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  27. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  28. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  29. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  30. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  31. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  32. package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
  33. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  34. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  35. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  36. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  37. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  38. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  39. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  40. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  41. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  42. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  43. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  44. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  45. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  46. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  47. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  48. package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
  49. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  50. package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
  51. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  52. package/dist/runtime/data/canvasShortcuts.js +68 -0
  53. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  54. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  55. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  56. package/dist/runtime/data/nodeVersionMove.js +65 -0
  57. package/dist/runtime/i18n/messages/de.json +83 -20
  58. package/dist/runtime/i18n/messages/en.json +83 -20
  59. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  60. package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
  61. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  62. package/dist/runtime/types/integrations.d.ts +44 -1
  63. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  64. package/dist/runtime/utils/apiEnvelope.js +20 -0
  65. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  66. package/dist/runtime/utils/buildSettle.js +29 -0
  67. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  68. package/dist/runtime/utils/configDefaults.js +7 -0
  69. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  70. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  71. package/dist/runtime/utils/notify.d.ts +2 -2
  72. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  73. package/dist/runtime/utils/runEnvelope.js +2 -1
  74. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  75. package/dist/runtime/utils/selectOptions.js +19 -0
  76. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  77. package/dist/runtime/utils/settingCondition.js +84 -0
  78. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  79. package/dist/runtime/utils/shortcutLabel.js +48 -0
  80. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  81. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  82. package/package.json +1 -1
@@ -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, Keyboard, 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,14 +40,19 @@ 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 CanvasShortcutsDialog from "./CanvasShortcutsDialog.vue";
44
+ import WorkflowStateNamespacesDialog from "./WorkflowStateNamespacesDialog.vue";
43
45
  import WorkflowVariablesDialog from "./WorkflowVariablesDialog.vue";
44
46
  import { useCanvasShortcuts } from "../../composables/useCanvasShortcuts";
47
+ import { libraryKeyCodes, primaryShortcut, shortcutKeys } from "../../data/canvasShortcuts";
45
48
  import { usePaletteLayout } from "../../composables/usePaletteLayout";
46
49
  import { useMiniMapLayout } from "../../composables/useMiniMapLayout";
47
50
  import { toast } from "vue-sonner";
48
51
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
49
52
  import { layoutWorkflowNodes } from "./workflowLayout";
50
- import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
53
+ import { defaultsFor } from "../../utils/configDefaults";
54
+ import { nodeDisplayLabel } from "../../utils/validationFindings";
55
+ import { TRIGGER_TEMPLATES, findManifest, newestExecutableVersion } from "../../data/integrationNodeCatalog";
51
56
  import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
52
57
  import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
53
58
  import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
@@ -382,8 +387,32 @@ function edgeColorByKind(kind) {
382
387
  syncFromBlob();
383
388
  const paneReady = ref(false);
384
389
  const pendingReveal = ref(null);
385
- flow.onPaneReady(() => {
386
- flow.fitView({ padding: 0.2, maxZoom: 1 });
390
+ function liftAboveControlPane() {
391
+ const bar = controlBar.value?.getBoundingClientRect();
392
+ const wrap = canvasWrapRef.value?.getBoundingClientRect();
393
+ const vp = flow.viewport.value;
394
+ if (!bar || !wrap || !vp) return;
395
+ let top = Number.POSITIVE_INFINITY;
396
+ let bottom = Number.NEGATIVE_INFINITY;
397
+ for (const n of vfNodes.value) {
398
+ const height = flow.findNode(n.id)?.dimensions?.height ?? 76;
399
+ top = Math.min(top, n.position.y);
400
+ bottom = Math.max(bottom, n.position.y + height);
401
+ }
402
+ if (!Number.isFinite(top) || !Number.isFinite(bottom)) return;
403
+ const CLEARANCE = 8;
404
+ const overlap = wrap.top + vp.y + bottom * vp.zoom - bar.top + CLEARANCE;
405
+ if (overlap <= 0) return;
406
+ const roomAtTop = wrap.top + vp.y + top * vp.zoom - wrap.top - CLEARANCE;
407
+ const lift = Math.min(overlap, Math.max(roomAtTop, 0));
408
+ if (lift <= 0) return;
409
+ flow.setViewport({ ...vp, y: vp.y - lift });
410
+ }
411
+ flow.onPaneReady(async () => {
412
+ await nextTick();
413
+ void canvasWrapRef.value?.getBoundingClientRect();
414
+ await flow.fitView({ padding: { x: 0.2, top: 0.2, bottom: "96px" }, maxZoom: 1 });
415
+ liftAboveControlPane();
387
416
  paneReady.value = true;
388
417
  if (pendingReveal.value) {
389
418
  centerOnNode(pendingReveal.value);
@@ -570,6 +599,18 @@ function updateSticky(id, patch) {
570
599
  push();
571
600
  }
572
601
  const variablesOpen = ref(false);
602
+ const stateOpen = ref(false);
603
+ const stateCount = computed(() => (blob.value.state ?? []).length);
604
+ function updateStateNamespaces(next) {
605
+ blob.value.state = next.length ? next : void 0;
606
+ push();
607
+ }
608
+ function declareStateNamespace(namespace, role, visibility) {
609
+ const declared = blob.value.state ?? [];
610
+ if (declared.some((n) => n.namespace === namespace)) return;
611
+ blob.value.state = [...declared, { namespace, role, visibility }];
612
+ push();
613
+ }
573
614
  const variableCount = computed(() => Object.keys(blob.value.variables ?? {}).length);
574
615
  function updateVariables(next) {
575
616
  blob.value.variables = Object.keys(next).length ? next : void 0;
@@ -824,7 +865,17 @@ function revealNode(id) {
824
865
  selectedId.value = id;
825
866
  }
826
867
  }
827
- defineExpose({ revealNode });
868
+ function showNodeOnCanvas(id) {
869
+ const folded = collapsedGroupOf().get(id);
870
+ if (folded) patchGroup(folded.id, { collapsed: false });
871
+ if (blob.value.nodes.some((n) => n.id === id)) {
872
+ markBorn(id);
873
+ syncFromBlob();
874
+ }
875
+ if (paneReady.value) centerOnNode(id);
876
+ else pendingReveal.value = id;
877
+ }
878
+ defineExpose({ revealNode, showNodeOnCanvas, attachErrorStop });
828
879
  const selectedUpstreamOutputs = computed(() => {
829
880
  const id = selectedId.value;
830
881
  if (!id) return [];
@@ -947,7 +998,7 @@ function addFromPayload(payload, pos) {
947
998
  id,
948
999
  nodeSlug: manifest.slug,
949
1000
  nodeVersion: manifest.version,
950
- config: defaultConfigFor(manifest.config ?? []),
1001
+ config: defaultsFor(manifest.config ?? []),
951
1002
  position: at
952
1003
  });
953
1004
  adoptIntoGroupAt(id, at);
@@ -992,7 +1043,7 @@ function onPaletteInsert(payload) {
992
1043
  id,
993
1044
  nodeSlug: manifest.slug,
994
1045
  nodeVersion: manifest.version,
995
- config: defaultConfigFor(manifest.config ?? []),
1046
+ config: defaultsFor(manifest.config ?? []),
996
1047
  position: { x: pc.pos.x, y: pc.pos.y }
997
1048
  });
998
1049
  blob.value.edges.push({
@@ -1044,7 +1095,7 @@ function spliceIntoEdge(edgeId, payload) {
1044
1095
  id,
1045
1096
  nodeSlug: manifest.slug,
1046
1097
  nodeVersion: manifest.version,
1047
- config: defaultConfigFor(manifest.config ?? []),
1098
+ config: defaultsFor(manifest.config ?? []),
1048
1099
  position: { x: snap((a.x + b.x) / 2), y: snap((a.y + b.y) / 2) }
1049
1100
  });
1050
1101
  blob.value.edges = blob.value.edges.filter((e) => e.id !== edgeId);
@@ -1057,12 +1108,44 @@ function spliceIntoEdge(edgeId, payload) {
1057
1108
  syncFromBlob();
1058
1109
  push();
1059
1110
  }
1060
- function defaultConfigFor(fields = []) {
1061
- const out = {};
1062
- for (const f of fields ?? []) {
1063
- if (f.default !== void 0) out[f.key] = f.default;
1064
- }
1065
- return out;
1111
+ const STOP_AND_ERROR_SLUG = "revenexx:stop-and-error";
1112
+ function attachErrorStop(sourceNodeId, port) {
1113
+ const source = blob.value.nodes.find((n) => n.id === sourceNodeId);
1114
+ const version = newestExecutableVersion(STOP_AND_ERROR_SLUG);
1115
+ const manifest = version ? findManifest(STOP_AND_ERROR_SLUG, version) : null;
1116
+ if (!source || !manifest) return;
1117
+ const at = nodePosition(sourceNodeId);
1118
+ const id = `n-${Date.now()}`;
1119
+ blob.value.nodes.push({
1120
+ id,
1121
+ nodeSlug: manifest.slug,
1122
+ nodeVersion: manifest.version,
1123
+ config: {
1124
+ ...defaultsFor(manifest.config ?? []),
1125
+ // The template convention verbatim: name the step that failed, then quote
1126
+ // its own message, so the run record says what went wrong rather than only
1127
+ // that something did. `errorCode` keeps the node's own default.
1128
+ message: t("integration.editor.errorPath.message", {
1129
+ label: nodeDisplayLabel(source),
1130
+ nodeId: source.id,
1131
+ port
1132
+ })
1133
+ },
1134
+ // Below the source and on the grid, the way the templates place theirs.
1135
+ // `freeSpotNear` keeps two of them off each other.
1136
+ position: freeSpotNear({ x: at.x, y: at.y + 180 })
1137
+ });
1138
+ blob.value.edges.push({
1139
+ id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
1140
+ from: { nodeId: sourceNodeId, output: port },
1141
+ to: { nodeId: id, input: void 0 }
1142
+ });
1143
+ markBorn(id);
1144
+ syncFromBlob();
1145
+ if (paneReady.value) centerOnNode(id);
1146
+ else pendingReveal.value = id;
1147
+ push();
1148
+ toast.info(t("integration.editor.errorPath.toast.added", { label: nodeDisplayLabel(source) }));
1066
1149
  }
1067
1150
  function nodeDims(id) {
1068
1151
  const d = flow.findNode(id)?.dimensions;
@@ -1231,9 +1314,9 @@ function setPointerMode(mode) {
1231
1314
  }
1232
1315
  }
1233
1316
  const zoomPct = computed(() => `${Math.round((flow.viewport.value?.zoom ?? 1) * 100)}%`);
1234
- const ZOOM_PRESETS = [50, 75, 100, 150, 200, 300, 400];
1317
+ const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
1235
1318
  const MIN_ZOOM_PCT = 25;
1236
- const MAX_ZOOM_PCT = 400;
1319
+ const MAX_ZOOM_PCT = 200;
1237
1320
  const zoomMenuOpen = ref(false);
1238
1321
  const zoomInputText = ref("");
1239
1322
  function setZoomPct(pct) {
@@ -1282,6 +1365,7 @@ async function focusPalette() {
1282
1365
  paletteRef.value?.focusSearch();
1283
1366
  }
1284
1367
  const lightboxOpen = computed(() => !!selection.value);
1368
+ const shortcutsOpen = ref(false);
1285
1369
  watch(lightboxOpen, (open) => {
1286
1370
  if (!open) renameIntent.value = false;
1287
1371
  });
@@ -1290,64 +1374,71 @@ function onConfigOpenAutoFocus(event) {
1290
1374
  event.preventDefault();
1291
1375
  event.target?.focus();
1292
1376
  }
1293
- const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1377
+ const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !stateOpen.value && !shortcutsOpen.value && !props.overlayOpen);
1378
+ const DELETE_KEY_CODES = libraryKeyCodes("delete");
1294
1379
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1295
1380
  useCanvasShortcuts(() => ({
1296
- "enter": {
1381
+ [shortcutKeys("open")]: {
1297
1382
  disabled: () => !singleSelected.value,
1298
1383
  run: () => {
1299
1384
  selectedId.value = singleSelected.value.id;
1300
1385
  }
1301
1386
  },
1302
- "f2|space": {
1387
+ [shortcutKeys("rename")]: {
1303
1388
  disabled: () => !singleSelected.value,
1304
1389
  run: () => {
1305
1390
  renameIntent.value = true;
1306
1391
  selectedId.value = singleSelected.value.id;
1307
1392
  }
1308
1393
  },
1309
- "mod+a": () => flow.addSelectedNodes(flow.getNodes.value),
1310
- "mod+c": {
1394
+ [shortcutKeys("selectAll")]: () => flow.addSelectedNodes(flow.getNodes.value),
1395
+ [shortcutKeys("copy")]: {
1311
1396
  disabled: () => !flow.getSelectedNodes.value.length,
1312
1397
  run: () => {
1313
1398
  void copyNodes(flow.getSelectedNodes.value.map((n) => n.id));
1314
1399
  }
1315
1400
  },
1316
- "mod+v": () => {
1401
+ [shortcutKeys("paste")]: () => {
1317
1402
  void pasteNodes();
1318
1403
  },
1319
- "mod+d": {
1404
+ [shortcutKeys("duplicate")]: {
1320
1405
  disabled: () => !flow.getSelectedNodes.value.length,
1321
1406
  run: () => duplicateBlobNodes(flow.getSelectedNodes.value.map((n) => n.id))
1322
1407
  },
1323
- "mod+g": {
1408
+ [shortcutKeys("group")]: {
1324
1409
  disabled: () => !flow.getSelectedNodes.value.length,
1325
1410
  run: () => createGroupFromSelection(flow.getSelectedNodes.value.map((n) => n.id))
1326
1411
  },
1327
- "mod+enter": { disabled: () => !!props.runBusy, run: () => emit("run") },
1328
- "shift+alt+t": () => {
1412
+ [shortcutKeys("run")]: { disabled: () => !!props.runBusy, run: () => emit("run") },
1413
+ [shortcutKeys("tidyUp")]: () => {
1329
1414
  void tidyUp();
1330
1415
  },
1331
- "shift+s": () => addSticky(),
1332
- "v": () => setPointerMode("select"),
1333
- "h": () => setPointerMode("hand"),
1334
- "shift+a": () => emit("toggleActive"),
1335
- "shift+v": () => {
1416
+ [shortcutKeys("addSticky")]: () => addSticky(),
1417
+ [shortcutKeys("pointerSelect")]: () => setPointerMode("select"),
1418
+ [shortcutKeys("pointerHand")]: () => setPointerMode("hand"),
1419
+ [shortcutKeys("toggleActive")]: () => emit("toggleActive"),
1420
+ [shortcutKeys("variables")]: () => {
1336
1421
  variablesOpen.value = true;
1337
1422
  },
1338
- "shift+alt+s": () => emit("openSettings"),
1339
- "-": () => {
1423
+ [shortcutKeys("state")]: () => {
1424
+ stateOpen.value = true;
1425
+ },
1426
+ [shortcutKeys("settings")]: () => emit("openSettings"),
1427
+ [shortcutKeys("zoomOut")]: () => {
1340
1428
  void flow.zoomOut({ duration: 150 });
1341
1429
  },
1342
- "=": () => {
1430
+ [shortcutKeys("zoomIn")]: () => {
1343
1431
  void flow.zoomIn({ duration: 150 });
1344
1432
  },
1345
- "0": () => setZoomPct(100),
1346
- "1": () => flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 }),
1347
- "tab": () => {
1433
+ [shortcutKeys("zoomReset")]: () => setZoomPct(100),
1434
+ [shortcutKeys("fitView")]: () => flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 }),
1435
+ [shortcutKeys("addNode")]: () => {
1348
1436
  void focusPalette();
1349
1437
  },
1350
- "escape": () => {
1438
+ [shortcutKeys("overview")]: () => {
1439
+ shortcutsOpen.value = true;
1440
+ },
1441
+ [shortcutKeys("clearSelection")]: () => {
1351
1442
  pendingSpliceEdgeId.value = null;
1352
1443
  pendingConnect.value = null;
1353
1444
  flow.removeSelectedElements();
@@ -1382,6 +1473,19 @@ const edgeTypes = {
1382
1473
  :run-busy="runBusy"
1383
1474
  @action="onMenuAction"
1384
1475
  >
1476
+ <!-- `pan-activation-key-code` is null rather than left at its default, which
1477
+ is Space. The canvas library binds that key on the document and calls
1478
+ preventDefault on it, and `useCanvasShortcuts` steps aside for a key
1479
+ somebody upstream has already claimed — so Space renaming a node, which
1480
+ the shortcuts overview offers, answered nothing at all (PO-472).
1481
+ Nothing is given up by switching it off, and the reason is not that the
1482
+ drag pans anyway: in select mode `pan-on-drag` below lists the middle
1483
+ button only, and the library's own filter refuses a left-drag for a pan
1484
+ on that ground before the activation key is consulted at all. Measured:
1485
+ with the key claimed, Space held over a left-drag did nothing — no pan,
1486
+ and no rubber band either, because holding it also switched selecting
1487
+ off; with it freed, that drag draws the rubber band. What does change is
1488
+ Space over the wheel, which zooms the way the wheel alone does. -->
1385
1489
  <VueFlow
1386
1490
  v-model:nodes="vfNodes"
1387
1491
  v-model:edges="vfEdges"
@@ -1398,10 +1502,11 @@ const edgeTypes = {
1398
1502
  :selection-key-code="pointerMode === 'select' ? true : 'Shift'"
1399
1503
  :pan-on-drag="pointerMode === 'hand' ? true : [1]"
1400
1504
  :zoom-on-double-click="false"
1401
- :delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
1505
+ :delete-key-code="canvasHasKeyboard ? DELETE_KEY_CODES : null"
1506
+ :pan-activation-key-code="null"
1402
1507
  :edges-updatable="true"
1403
1508
  :min-zoom="0.25"
1404
- :max-zoom="4"
1509
+ :max-zoom="2"
1405
1510
  >
1406
1511
  <Background pattern-color="#cbd5e1" :gap="20" :size="1.2" />
1407
1512
  </VueFlow>
@@ -1641,10 +1746,11 @@ const edgeTypes = {
1641
1746
  <TooltipProvider :delay-duration="250">
1642
1747
  <div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
1643
1748
  <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">
1644
- <!-- Smart Run: the page decides what "run" means — workflows that
1645
- consume trigger.payload open the payload dialog, payload-free
1646
- ones fire immediately. One button either way. -->
1647
- <IntegrationsEditorTip :label="runWaiting ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')" shortcut="mod+enter">
1749
+ <!-- Smart Run: the page decides what "run" means — a workflow that
1750
+ consumes trigger.payload, or leaves the mode to the caller, opens
1751
+ the run dialog; one that asks neither fires immediately. One
1752
+ button either way. -->
1753
+ <IntegrationsEditorTip :label="runWaiting ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')" :shortcut="primaryShortcut('run')">
1648
1754
  <button
1649
1755
  type="button"
1650
1756
  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"
@@ -1662,7 +1768,7 @@ const edgeTypes = {
1662
1768
  <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)]">
1663
1769
  <!-- Icon-only, and the tooltip is not an accessible name: each one
1664
1770
  says what it does itself, and whether it is the active mode. -->
1665
- <IntegrationsEditorTip :label="t('integration.editor.tool.select')" shortcut="v">
1771
+ <IntegrationsEditorTip :label="t('integration.editor.tool.select')" :shortcut="primaryShortcut('pointerSelect')">
1666
1772
  <button
1667
1773
  type="button"
1668
1774
  :aria-label="t('integration.editor.tool.select')"
@@ -1674,7 +1780,7 @@ const edgeTypes = {
1674
1780
  <MousePointer2 :size="14" />
1675
1781
  </button>
1676
1782
  </IntegrationsEditorTip>
1677
- <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" shortcut="h">
1783
+ <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" :shortcut="primaryShortcut('pointerHand')">
1678
1784
  <button
1679
1785
  type="button"
1680
1786
  :aria-label="t('integration.editor.tool.hand')"
@@ -1688,7 +1794,7 @@ const edgeTypes = {
1688
1794
  </IntegrationsEditorTip>
1689
1795
  </div>
1690
1796
 
1691
- <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" shortcut="shift+alt+t">
1797
+ <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" :shortcut="primaryShortcut('tidyUp')">
1692
1798
  <button
1693
1799
  type="button"
1694
1800
  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]"
@@ -1698,7 +1804,7 @@ const edgeTypes = {
1698
1804
  {{ t("integration.editor.tool.tidyUp") }}
1699
1805
  </button>
1700
1806
  </IntegrationsEditorTip>
1701
- <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" shortcut="1">
1807
+ <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" :shortcut="primaryShortcut('fitView')">
1702
1808
  <button
1703
1809
  type="button"
1704
1810
  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]"
@@ -1708,7 +1814,7 @@ const edgeTypes = {
1708
1814
  {{ t("integration.editor.tool.fitView.label") }}
1709
1815
  </button>
1710
1816
  </IntegrationsEditorTip>
1711
- <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" shortcut="shift+s">
1817
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" :shortcut="primaryShortcut('addSticky')">
1712
1818
  <button
1713
1819
  type="button"
1714
1820
  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]"
@@ -1728,7 +1834,7 @@ const edgeTypes = {
1728
1834
  {{ t("integration.editor.tool.addGroup.label") }}
1729
1835
  </button>
1730
1836
  </IntegrationsEditorTip>
1731
- <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" shortcut="shift+v">
1837
+ <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" :shortcut="primaryShortcut('variables')">
1732
1838
  <button
1733
1839
  type="button"
1734
1840
  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]"
@@ -1739,7 +1845,18 @@ const edgeTypes = {
1739
1845
  <span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
1740
1846
  </button>
1741
1847
  </IntegrationsEditorTip>
1742
- <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
1848
+ <IntegrationsEditorTip :label="t('integration.editor.tool.state.tooltip')" :shortcut="primaryShortcut('state')">
1849
+ <button
1850
+ type="button"
1851
+ 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]"
1852
+ @click="stateOpen = true"
1853
+ >
1854
+ <Database :size="13" />
1855
+ {{ t("integration.editor.tool.state.label") }}
1856
+ <span v-if="stateCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ stateCount }}</span>
1857
+ </button>
1858
+ </IntegrationsEditorTip>
1859
+ <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" :shortcut="primaryShortcut('settings')">
1743
1860
  <button
1744
1861
  type="button"
1745
1862
  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]"
@@ -1749,10 +1866,25 @@ const edgeTypes = {
1749
1866
  {{ t("integration.editor.tool.settings.label") }}
1750
1867
  </button>
1751
1868
  </IntegrationsEditorTip>
1869
+ <!-- The overview, and the one control that has to be reachable without
1870
+ already knowing a key: it is where the other keys are learnt. Its
1871
+ tooltip carries `?` for the same reason — the pane teaches the key
1872
+ that teaches the rest. Icon-only, so it carries a name of its own
1873
+ (`specs/accessibility.md` AC-2). -->
1874
+ <IntegrationsEditorTip :label="t('integration.canvas.shortcuts.dialog.title')" :shortcut="primaryShortcut('overview')">
1875
+ <button
1876
+ type="button"
1877
+ :aria-label="t('integration.canvas.shortcuts.dialog.title')"
1878
+ class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1879
+ @click="shortcutsOpen = true"
1880
+ >
1881
+ <Keyboard :size="14" />
1882
+ </button>
1883
+ </IntegrationsEditorTip>
1752
1884
 
1753
1885
  <div class="mx-1 h-5 w-px bg-black/10" />
1754
1886
 
1755
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" shortcut="-">
1887
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" :shortcut="primaryShortcut('zoomOut')">
1756
1888
  <button
1757
1889
  type="button"
1758
1890
  :aria-label="t('integration.editor.tool.zoomOut')"
@@ -1765,7 +1897,7 @@ const edgeTypes = {
1765
1897
  <!-- Tooltip sits on a wrapper span so its as-child merge never
1766
1898
  touches the dropdown trigger (merging both onto one button
1767
1899
  swallowed the trigger's pointerdown and the menu never opened). -->
1768
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" shortcut="0">
1900
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" :shortcut="primaryShortcut('zoomReset')">
1769
1901
  <span class="inline-flex">
1770
1902
  <DropdownMenu v-model:open="zoomMenuOpen">
1771
1903
  <DropdownMenuTrigger as-child>
@@ -1807,7 +1939,7 @@ const edgeTypes = {
1807
1939
  </DropdownMenu>
1808
1940
  </span>
1809
1941
  </IntegrationsEditorTip>
1810
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" shortcut="=">
1942
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" :shortcut="primaryShortcut('zoomIn')">
1811
1943
  <button
1812
1944
  type="button"
1813
1945
  :aria-label="t('integration.editor.tool.zoomIn')"
@@ -1838,6 +1970,14 @@ const edgeTypes = {
1838
1970
  </div>
1839
1971
  </div>
1840
1972
 
1973
+ <!-- State namespaces lightbox (control-pane entry) — what this workflow may
1974
+ remember between runs. -->
1975
+ <WorkflowStateNamespacesDialog
1976
+ v-model:open="stateOpen"
1977
+ :state="blob.state ?? []"
1978
+ @update:state="updateStateNamespaces"
1979
+ />
1980
+
1841
1981
  <!-- Workflow variables lightbox (control-pane entry). -->
1842
1982
  <WorkflowVariablesDialog
1843
1983
  v-model:open="variablesOpen"
@@ -1845,6 +1985,10 @@ const edgeTypes = {
1845
1985
  @update:variables="updateVariables"
1846
1986
  />
1847
1987
 
1988
+ <!-- Every key this canvas answers, opened with `?`. Rendered here rather
1989
+ than at the page: the keys are the canvas's, and so is the list. -->
1990
+ <CanvasShortcutsDialog v-model:open="shortcutsOpen" />
1991
+
1848
1992
  <!-- Node/trigger configuration — a near-fullscreen workbench lightbox
1849
1993
  (Input pane / Parameters+Settings tabs / Save+Cancel footer). The
1850
1994
  inspector edits a draft; closing without Save discards it. -->
@@ -1876,6 +2020,7 @@ const edgeTypes = {
1876
2020
  :selection="selection"
1877
2021
  :secret-keys="secretKeys"
1878
2022
  :variables="blob.variables"
2023
+ :state-namespaces="blob.state ?? []"
1879
2024
  :upstream-outputs="selectedUpstreamOutputs"
1880
2025
  :autofocus-name="renameIntent"
1881
2026
  :nodes="blob.nodes"
@@ -1891,6 +2036,7 @@ const edgeTypes = {
1891
2036
  selectedId = id;
1892
2037
  renameIntent = false;
1893
2038
  }"
2039
+ @declare-state="declareStateNamespace"
1894
2040
  />
1895
2041
  </DialogContent>
1896
2042
  </Dialog>
@@ -42,17 +42,51 @@ type __VLS_Props = {
42
42
  * id that is on the canvas is in the blob by construction.
43
43
  */
44
44
  declare function revealNode(id: string): void;
45
+ /**
46
+ * Point at a node without opening it — for a reader who has to look at the
47
+ * card's *surroundings* rather than at its settings.
48
+ *
49
+ * The deliberate difference from {@link revealNode} is the missing selection,
50
+ * and with it the missing settings lightbox. That is the whole reason there are
51
+ * two functions: the validation banner sends someone to a field at fault, and
52
+ * the dialog is where that field is. The error-path advice sends them to a port
53
+ * with nothing on it, and the question they have to answer — where should this
54
+ * failure go? — is answered by the graph around the card, which a dialog over
55
+ * the canvas covers up.
56
+ *
57
+ * Marked with the same pulse a newly created card gets, because the job is the
58
+ * same one: say which of thirty cards is the one meant.
59
+ */
60
+ declare function showNodeOnCanvas(id: string): void;
61
+ /**
62
+ * Hang a Stop-and-Error step off a node's error output and wire it up
63
+ * (specs/workflow-advice.md AC-3).
64
+ *
65
+ * The advice strip can tell an author their error output leads nowhere; on its
66
+ * own that is a reprimand without a remedy, because the repair — drop a node,
67
+ * pick the right one, connect it, write an expression quoting the failing step's
68
+ * message — is four steps of tedium they have to already know. Every template we
69
+ * ship does exactly this, twenty-five times over, so the shape is settled and
70
+ * worth one click.
71
+ *
72
+ * Leaves the result unsaved and unopened, like every other insertion here: the
73
+ * new step is highlighted and the author decides whether to keep it. Deleting it
74
+ * is the undo, because this editor has no undo stack to hook into.
75
+ */
76
+ declare function attachErrorStop(sourceNodeId: string, port: string): void;
45
77
  declare const _default: import("vue").DefineComponent<__VLS_Props, {
46
78
  revealNode: typeof revealNode;
79
+ showNodeOnCanvas: typeof showNodeOnCanvas;
80
+ attachErrorStop: typeof attachErrorStop;
47
81
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
48
- change: (blob: WorkflowBlob) => any;
49
82
  run: () => any;
50
83
  toggleActive: () => any;
84
+ change: (blob: WorkflowBlob) => any;
51
85
  openSettings: () => any;
52
86
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
53
- onChange?: ((blob: WorkflowBlob) => any) | undefined;
54
87
  onRun?: (() => any) | undefined;
55
88
  onToggleActive?: (() => any) | undefined;
89
+ onChange?: ((blob: WorkflowBlob) => any) | undefined;
56
90
  onOpenSettings?: (() => any) | undefined;
57
91
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
58
92
  export default _default;
@@ -3,9 +3,13 @@ import '@vue-flow/core/dist/theme-default.css';
3
3
  import type { Workflow } from '../../types/integrations.js';
4
4
  type __VLS_Props = {
5
5
  workflow: Workflow;
6
- /** Preselect a run (deep-link from the Runs table). */
6
+ /** The run to replay (deep-link from the Runs table). */
7
7
  initialRunId?: string | null;
8
- /** Replay ONLY the initial run: no run list, no browsing (run detail view). */
8
+ /**
9
+ * @deprecated Accepted and ignored since PO-350 — the panel has no other mode
10
+ * to switch out of. Kept so a host passing it keeps working; drop it at the
11
+ * next major.
12
+ */
9
13
  single?: boolean;
10
14
  };
11
15
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;