@revenexx/studio-integrations 1.3.0 → 1.4.1

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 (62) hide show
  1. package/CHANGELOG.md +457 -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/CredentialDetailDialog.vue +61 -29
  7. package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
  8. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +10 -0
  9. package/dist/runtime/components/Integrations/NodeConfigField.vue +94 -49
  10. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +10 -0
  11. package/dist/runtime/components/Integrations/NodeInspector.vue +37 -60
  12. package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
  13. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
  14. package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
  15. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
  16. package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
  17. package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
  18. package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
  19. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
  20. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
  21. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
  22. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
  23. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
  24. package/dist/runtime/components/Integrations/WorkflowEditor.vue +165 -56
  25. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
  26. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
  27. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
  28. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
  29. package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
  30. package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
  31. package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
  32. package/dist/runtime/composables/useCredentialTypes.js +3 -9
  33. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
  34. package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
  35. package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
  36. package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
  37. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -8
  38. package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
  39. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -7
  40. package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
  41. package/dist/runtime/data/canvasShortcuts.js +68 -0
  42. package/dist/runtime/i18n/messages/de.json +30 -16
  43. package/dist/runtime/i18n/messages/en.json +30 -16
  44. package/dist/runtime/pages/integrations/credentials.vue +40 -18
  45. package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
  46. package/dist/runtime/pages/integrations/workflows/[id].vue +128 -33
  47. package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
  48. package/dist/runtime/utils/apiEnvelope.js +20 -0
  49. package/dist/runtime/utils/apiError.d.ts +35 -8
  50. package/dist/runtime/utils/apiError.js +12 -12
  51. package/dist/runtime/utils/configDefaults.d.ts +18 -0
  52. package/dist/runtime/utils/configDefaults.js +7 -0
  53. package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
  54. package/dist/runtime/utils/errorPathAdvice.js +19 -0
  55. package/dist/runtime/utils/notify.d.ts +2 -2
  56. package/dist/runtime/utils/runEnvelope.d.ts +12 -0
  57. package/dist/runtime/utils/runEnvelope.js +2 -1
  58. package/dist/runtime/utils/selectOptions.d.ts +48 -0
  59. package/dist/runtime/utils/selectOptions.js +19 -0
  60. package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
  61. package/dist/runtime/utils/shortcutLabel.js +48 -0
  62. package/package.json +1 -1
@@ -28,7 +28,7 @@ import {
28
28
  DropdownMenuTrigger,
29
29
  TooltipProvider
30
30
  } from "@revenexx/studio";
31
- import { Braces, Database, 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,15 +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";
43
44
  import WorkflowStateNamespacesDialog from "./WorkflowStateNamespacesDialog.vue";
44
45
  import WorkflowVariablesDialog from "./WorkflowVariablesDialog.vue";
45
46
  import { useCanvasShortcuts } from "../../composables/useCanvasShortcuts";
47
+ import { libraryKeyCodes, primaryShortcut, shortcutKeys } from "../../data/canvasShortcuts";
46
48
  import { usePaletteLayout } from "../../composables/usePaletteLayout";
47
49
  import { useMiniMapLayout } from "../../composables/useMiniMapLayout";
48
50
  import { toast } from "vue-sonner";
49
51
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
50
52
  import { layoutWorkflowNodes } from "./workflowLayout";
51
- 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";
52
56
  import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
53
57
  import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
54
58
  import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
@@ -383,8 +387,32 @@ function edgeColorByKind(kind) {
383
387
  syncFromBlob();
384
388
  const paneReady = ref(false);
385
389
  const pendingReveal = ref(null);
386
- flow.onPaneReady(() => {
387
- 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();
388
416
  paneReady.value = true;
389
417
  if (pendingReveal.value) {
390
418
  centerOnNode(pendingReveal.value);
@@ -837,7 +865,17 @@ function revealNode(id) {
837
865
  selectedId.value = id;
838
866
  }
839
867
  }
840
- 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 });
841
879
  const selectedUpstreamOutputs = computed(() => {
842
880
  const id = selectedId.value;
843
881
  if (!id) return [];
@@ -960,7 +998,7 @@ function addFromPayload(payload, pos) {
960
998
  id,
961
999
  nodeSlug: manifest.slug,
962
1000
  nodeVersion: manifest.version,
963
- config: defaultConfigFor(manifest.config ?? []),
1001
+ config: defaultsFor(manifest.config ?? []),
964
1002
  position: at
965
1003
  });
966
1004
  adoptIntoGroupAt(id, at);
@@ -1005,7 +1043,7 @@ function onPaletteInsert(payload) {
1005
1043
  id,
1006
1044
  nodeSlug: manifest.slug,
1007
1045
  nodeVersion: manifest.version,
1008
- config: defaultConfigFor(manifest.config ?? []),
1046
+ config: defaultsFor(manifest.config ?? []),
1009
1047
  position: { x: pc.pos.x, y: pc.pos.y }
1010
1048
  });
1011
1049
  blob.value.edges.push({
@@ -1057,7 +1095,7 @@ function spliceIntoEdge(edgeId, payload) {
1057
1095
  id,
1058
1096
  nodeSlug: manifest.slug,
1059
1097
  nodeVersion: manifest.version,
1060
- config: defaultConfigFor(manifest.config ?? []),
1098
+ config: defaultsFor(manifest.config ?? []),
1061
1099
  position: { x: snap((a.x + b.x) / 2), y: snap((a.y + b.y) / 2) }
1062
1100
  });
1063
1101
  blob.value.edges = blob.value.edges.filter((e) => e.id !== edgeId);
@@ -1070,12 +1108,44 @@ function spliceIntoEdge(edgeId, payload) {
1070
1108
  syncFromBlob();
1071
1109
  push();
1072
1110
  }
1073
- function defaultConfigFor(fields = []) {
1074
- const out = {};
1075
- for (const f of fields ?? []) {
1076
- if (f.default !== void 0) out[f.key] = f.default;
1077
- }
1078
- 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) }));
1079
1149
  }
1080
1150
  function nodeDims(id) {
1081
1151
  const d = flow.findNode(id)?.dimensions;
@@ -1244,9 +1314,9 @@ function setPointerMode(mode) {
1244
1314
  }
1245
1315
  }
1246
1316
  const zoomPct = computed(() => `${Math.round((flow.viewport.value?.zoom ?? 1) * 100)}%`);
1247
- const ZOOM_PRESETS = [50, 75, 100, 150, 200, 300, 400];
1317
+ const ZOOM_PRESETS = [25, 50, 75, 100, 150, 200];
1248
1318
  const MIN_ZOOM_PCT = 25;
1249
- const MAX_ZOOM_PCT = 400;
1319
+ const MAX_ZOOM_PCT = 200;
1250
1320
  const zoomMenuOpen = ref(false);
1251
1321
  const zoomInputText = ref("");
1252
1322
  function setZoomPct(pct) {
@@ -1295,6 +1365,7 @@ async function focusPalette() {
1295
1365
  paletteRef.value?.focusSearch();
1296
1366
  }
1297
1367
  const lightboxOpen = computed(() => !!selection.value);
1368
+ const shortcutsOpen = ref(false);
1298
1369
  watch(lightboxOpen, (open) => {
1299
1370
  if (!open) renameIntent.value = false;
1300
1371
  });
@@ -1303,67 +1374,71 @@ function onConfigOpenAutoFocus(event) {
1303
1374
  event.preventDefault();
1304
1375
  event.target?.focus();
1305
1376
  }
1306
- const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !stateOpen.value && !props.overlayOpen);
1377
+ const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !stateOpen.value && !shortcutsOpen.value && !props.overlayOpen);
1378
+ const DELETE_KEY_CODES = libraryKeyCodes("delete");
1307
1379
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1308
1380
  useCanvasShortcuts(() => ({
1309
- "enter": {
1381
+ [shortcutKeys("open")]: {
1310
1382
  disabled: () => !singleSelected.value,
1311
1383
  run: () => {
1312
1384
  selectedId.value = singleSelected.value.id;
1313
1385
  }
1314
1386
  },
1315
- "f2|space": {
1387
+ [shortcutKeys("rename")]: {
1316
1388
  disabled: () => !singleSelected.value,
1317
1389
  run: () => {
1318
1390
  renameIntent.value = true;
1319
1391
  selectedId.value = singleSelected.value.id;
1320
1392
  }
1321
1393
  },
1322
- "mod+a": () => flow.addSelectedNodes(flow.getNodes.value),
1323
- "mod+c": {
1394
+ [shortcutKeys("selectAll")]: () => flow.addSelectedNodes(flow.getNodes.value),
1395
+ [shortcutKeys("copy")]: {
1324
1396
  disabled: () => !flow.getSelectedNodes.value.length,
1325
1397
  run: () => {
1326
1398
  void copyNodes(flow.getSelectedNodes.value.map((n) => n.id));
1327
1399
  }
1328
1400
  },
1329
- "mod+v": () => {
1401
+ [shortcutKeys("paste")]: () => {
1330
1402
  void pasteNodes();
1331
1403
  },
1332
- "mod+d": {
1404
+ [shortcutKeys("duplicate")]: {
1333
1405
  disabled: () => !flow.getSelectedNodes.value.length,
1334
1406
  run: () => duplicateBlobNodes(flow.getSelectedNodes.value.map((n) => n.id))
1335
1407
  },
1336
- "mod+g": {
1408
+ [shortcutKeys("group")]: {
1337
1409
  disabled: () => !flow.getSelectedNodes.value.length,
1338
1410
  run: () => createGroupFromSelection(flow.getSelectedNodes.value.map((n) => n.id))
1339
1411
  },
1340
- "mod+enter": { disabled: () => !!props.runBusy, run: () => emit("run") },
1341
- "shift+alt+t": () => {
1412
+ [shortcutKeys("run")]: { disabled: () => !!props.runBusy, run: () => emit("run") },
1413
+ [shortcutKeys("tidyUp")]: () => {
1342
1414
  void tidyUp();
1343
1415
  },
1344
- "shift+s": () => addSticky(),
1345
- "v": () => setPointerMode("select"),
1346
- "h": () => setPointerMode("hand"),
1347
- "shift+a": () => emit("toggleActive"),
1348
- "shift+v": () => {
1416
+ [shortcutKeys("addSticky")]: () => addSticky(),
1417
+ [shortcutKeys("pointerSelect")]: () => setPointerMode("select"),
1418
+ [shortcutKeys("pointerHand")]: () => setPointerMode("hand"),
1419
+ [shortcutKeys("toggleActive")]: () => emit("toggleActive"),
1420
+ [shortcutKeys("variables")]: () => {
1349
1421
  variablesOpen.value = true;
1350
1422
  },
1351
- "shift+t": () => {
1423
+ [shortcutKeys("state")]: () => {
1352
1424
  stateOpen.value = true;
1353
1425
  },
1354
- "shift+alt+s": () => emit("openSettings"),
1355
- "-": () => {
1426
+ [shortcutKeys("settings")]: () => emit("openSettings"),
1427
+ [shortcutKeys("zoomOut")]: () => {
1356
1428
  void flow.zoomOut({ duration: 150 });
1357
1429
  },
1358
- "=": () => {
1430
+ [shortcutKeys("zoomIn")]: () => {
1359
1431
  void flow.zoomIn({ duration: 150 });
1360
1432
  },
1361
- "0": () => setZoomPct(100),
1362
- "1": () => flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 }),
1363
- "tab": () => {
1433
+ [shortcutKeys("zoomReset")]: () => setZoomPct(100),
1434
+ [shortcutKeys("fitView")]: () => flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 }),
1435
+ [shortcutKeys("addNode")]: () => {
1364
1436
  void focusPalette();
1365
1437
  },
1366
- "escape": () => {
1438
+ [shortcutKeys("overview")]: () => {
1439
+ shortcutsOpen.value = true;
1440
+ },
1441
+ [shortcutKeys("clearSelection")]: () => {
1367
1442
  pendingSpliceEdgeId.value = null;
1368
1443
  pendingConnect.value = null;
1369
1444
  flow.removeSelectedElements();
@@ -1398,6 +1473,19 @@ const edgeTypes = {
1398
1473
  :run-busy="runBusy"
1399
1474
  @action="onMenuAction"
1400
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. -->
1401
1489
  <VueFlow
1402
1490
  v-model:nodes="vfNodes"
1403
1491
  v-model:edges="vfEdges"
@@ -1414,10 +1502,11 @@ const edgeTypes = {
1414
1502
  :selection-key-code="pointerMode === 'select' ? true : 'Shift'"
1415
1503
  :pan-on-drag="pointerMode === 'hand' ? true : [1]"
1416
1504
  :zoom-on-double-click="false"
1417
- :delete-key-code="canvasHasKeyboard ? ['Backspace', 'Delete'] : null"
1505
+ :delete-key-code="canvasHasKeyboard ? DELETE_KEY_CODES : null"
1506
+ :pan-activation-key-code="null"
1418
1507
  :edges-updatable="true"
1419
1508
  :min-zoom="0.25"
1420
- :max-zoom="4"
1509
+ :max-zoom="2"
1421
1510
  >
1422
1511
  <Background pattern-color="#cbd5e1" :gap="20" :size="1.2" />
1423
1512
  </VueFlow>
@@ -1657,10 +1746,11 @@ const edgeTypes = {
1657
1746
  <TooltipProvider :delay-duration="250">
1658
1747
  <div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
1659
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">
1660
- <!-- Smart Run: the page decides what "run" means — workflows that
1661
- consume trigger.payload open the payload dialog, payload-free
1662
- ones fire immediately. One button either way. -->
1663
- <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')">
1664
1754
  <button
1665
1755
  type="button"
1666
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"
@@ -1678,7 +1768,7 @@ const edgeTypes = {
1678
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)]">
1679
1769
  <!-- Icon-only, and the tooltip is not an accessible name: each one
1680
1770
  says what it does itself, and whether it is the active mode. -->
1681
- <IntegrationsEditorTip :label="t('integration.editor.tool.select')" shortcut="v">
1771
+ <IntegrationsEditorTip :label="t('integration.editor.tool.select')" :shortcut="primaryShortcut('pointerSelect')">
1682
1772
  <button
1683
1773
  type="button"
1684
1774
  :aria-label="t('integration.editor.tool.select')"
@@ -1690,7 +1780,7 @@ const edgeTypes = {
1690
1780
  <MousePointer2 :size="14" />
1691
1781
  </button>
1692
1782
  </IntegrationsEditorTip>
1693
- <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" shortcut="h">
1783
+ <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" :shortcut="primaryShortcut('pointerHand')">
1694
1784
  <button
1695
1785
  type="button"
1696
1786
  :aria-label="t('integration.editor.tool.hand')"
@@ -1704,7 +1794,7 @@ const edgeTypes = {
1704
1794
  </IntegrationsEditorTip>
1705
1795
  </div>
1706
1796
 
1707
- <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" shortcut="shift+alt+t">
1797
+ <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" :shortcut="primaryShortcut('tidyUp')">
1708
1798
  <button
1709
1799
  type="button"
1710
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]"
@@ -1714,7 +1804,7 @@ const edgeTypes = {
1714
1804
  {{ t("integration.editor.tool.tidyUp") }}
1715
1805
  </button>
1716
1806
  </IntegrationsEditorTip>
1717
- <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" shortcut="1">
1807
+ <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" :shortcut="primaryShortcut('fitView')">
1718
1808
  <button
1719
1809
  type="button"
1720
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]"
@@ -1724,7 +1814,7 @@ const edgeTypes = {
1724
1814
  {{ t("integration.editor.tool.fitView.label") }}
1725
1815
  </button>
1726
1816
  </IntegrationsEditorTip>
1727
- <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" shortcut="shift+s">
1817
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" :shortcut="primaryShortcut('addSticky')">
1728
1818
  <button
1729
1819
  type="button"
1730
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]"
@@ -1744,7 +1834,7 @@ const edgeTypes = {
1744
1834
  {{ t("integration.editor.tool.addGroup.label") }}
1745
1835
  </button>
1746
1836
  </IntegrationsEditorTip>
1747
- <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" shortcut="shift+v">
1837
+ <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" :shortcut="primaryShortcut('variables')">
1748
1838
  <button
1749
1839
  type="button"
1750
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]"
@@ -1755,7 +1845,7 @@ const edgeTypes = {
1755
1845
  <span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
1756
1846
  </button>
1757
1847
  </IntegrationsEditorTip>
1758
- <IntegrationsEditorTip :label="t('integration.editor.tool.state.tooltip')" shortcut="shift+t">
1848
+ <IntegrationsEditorTip :label="t('integration.editor.tool.state.tooltip')" :shortcut="primaryShortcut('state')">
1759
1849
  <button
1760
1850
  type="button"
1761
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]"
@@ -1766,7 +1856,7 @@ const edgeTypes = {
1766
1856
  <span v-if="stateCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ stateCount }}</span>
1767
1857
  </button>
1768
1858
  </IntegrationsEditorTip>
1769
- <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
1859
+ <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" :shortcut="primaryShortcut('settings')">
1770
1860
  <button
1771
1861
  type="button"
1772
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]"
@@ -1776,10 +1866,25 @@ const edgeTypes = {
1776
1866
  {{ t("integration.editor.tool.settings.label") }}
1777
1867
  </button>
1778
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>
1779
1884
 
1780
1885
  <div class="mx-1 h-5 w-px bg-black/10" />
1781
1886
 
1782
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" shortcut="-">
1887
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" :shortcut="primaryShortcut('zoomOut')">
1783
1888
  <button
1784
1889
  type="button"
1785
1890
  :aria-label="t('integration.editor.tool.zoomOut')"
@@ -1792,7 +1897,7 @@ const edgeTypes = {
1792
1897
  <!-- Tooltip sits on a wrapper span so its as-child merge never
1793
1898
  touches the dropdown trigger (merging both onto one button
1794
1899
  swallowed the trigger's pointerdown and the menu never opened). -->
1795
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" shortcut="0">
1900
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" :shortcut="primaryShortcut('zoomReset')">
1796
1901
  <span class="inline-flex">
1797
1902
  <DropdownMenu v-model:open="zoomMenuOpen">
1798
1903
  <DropdownMenuTrigger as-child>
@@ -1834,7 +1939,7 @@ const edgeTypes = {
1834
1939
  </DropdownMenu>
1835
1940
  </span>
1836
1941
  </IntegrationsEditorTip>
1837
- <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" shortcut="=">
1942
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" :shortcut="primaryShortcut('zoomIn')">
1838
1943
  <button
1839
1944
  type="button"
1840
1945
  :aria-label="t('integration.editor.tool.zoomIn')"
@@ -1880,6 +1985,10 @@ const edgeTypes = {
1880
1985
  @update:variables="updateVariables"
1881
1986
  />
1882
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
+
1883
1992
  <!-- Node/trigger configuration — a near-fullscreen workbench lightbox
1884
1993
  (Input pane / Parameters+Settings tabs / Save+Cancel footer). The
1885
1994
  inspector edits a draft; closing without Save discards it. -->
@@ -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>;