@revenexx/studio-integrations 0.6.0 → 1.0.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 (137) hide show
  1. package/CHANGELOG.md +643 -0
  2. package/README.md +31 -0
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -3
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
  12. package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
  13. package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
  14. package/dist/runtime/components/Integrations/Layout.vue +28 -3
  15. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
  17. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
  18. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
  19. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
  20. package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
  21. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
  22. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
  23. package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
  24. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
  25. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
  26. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
  27. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
  28. package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
  29. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  30. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  31. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  32. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  33. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  34. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  35. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  36. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  37. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  38. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  39. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  40. package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
  41. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  42. package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
  43. package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
  44. package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
  45. package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
  46. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
  47. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  48. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
  49. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  50. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  51. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
  52. package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
  53. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
  54. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
  55. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
  56. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
  57. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  58. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
  59. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
  60. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
  61. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
  62. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
  63. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  64. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
  65. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
  66. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  67. package/dist/runtime/composables/integrationsApi.js +4 -2
  68. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  69. package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
  70. package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
  71. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  72. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  73. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  74. package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
  75. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  76. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  77. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  78. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  79. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  80. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
  81. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  82. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  83. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  84. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  85. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  86. package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
  87. package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
  88. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
  89. package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
  90. package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
  91. package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
  92. package/dist/runtime/data/integrationNodeCatalog.js +22 -4
  93. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  94. package/dist/runtime/data/integrationTemplates.js +7 -1
  95. package/dist/runtime/data/nodeInputData.d.ts +58 -1
  96. package/dist/runtime/data/nodeInputData.js +30 -5
  97. package/dist/runtime/data/payloadForm.js +2 -1
  98. package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
  99. package/dist/runtime/data/triggerGlyphs.js +11 -0
  100. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  101. package/dist/runtime/i18n/messages/de.json +1224 -0
  102. package/dist/runtime/i18n/messages/en.json +1224 -0
  103. package/dist/runtime/i18n/translate.d.ts +83 -0
  104. package/dist/runtime/i18n/translate.js +41 -0
  105. package/dist/runtime/pages/integrations/audit.vue +52 -34
  106. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  107. package/dist/runtime/pages/integrations/credentials.vue +73 -52
  108. package/dist/runtime/pages/integrations/index.vue +66 -73
  109. package/dist/runtime/pages/integrations/runs.vue +33 -21
  110. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  111. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  112. package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
  113. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  114. package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
  115. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  116. package/dist/runtime/plugins/locale.d.ts +10 -0
  117. package/dist/runtime/plugins/locale.js +6 -0
  118. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  119. package/dist/runtime/plugins/realtime.client.js +4 -0
  120. package/dist/runtime/types/integrations.d.ts +25 -1
  121. package/dist/runtime/utils/cockpitSources.d.ts +22 -3
  122. package/dist/runtime/utils/credentialState.d.ts +30 -2
  123. package/dist/runtime/utils/credentialState.js +26 -11
  124. package/dist/runtime/utils/localize.d.ts +52 -5
  125. package/dist/runtime/utils/localize.js +47 -9
  126. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  127. package/dist/runtime/utils/pollBudget.js +17 -0
  128. package/dist/runtime/utils/schedule.d.ts +0 -25
  129. package/dist/runtime/utils/schedule.js +37 -25
  130. package/dist/runtime/utils/servedPaths.d.ts +22 -0
  131. package/dist/runtime/utils/servedPaths.js +3 -0
  132. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  133. package/dist/runtime/utils/statusStyle.js +9 -1
  134. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  135. package/dist/runtime/utils/validationFindings.js +4 -3
  136. package/dist/types.d.mts +2 -6
  137. package/package.json +3 -2
@@ -56,13 +56,15 @@ import { findingsByNodeField } from "../../utils/validationFindings";
56
56
  import { resolveDisplayName, resolveLocalized } from "../../utils/localize";
57
57
  const props = defineProps({
58
58
  workflow: { type: Object, required: true },
59
+ draft: { type: [Object, null], required: false },
59
60
  runBusy: { type: Boolean, required: false },
60
61
  runWaiting: { type: Boolean, required: false },
61
62
  overlayOpen: { type: Boolean, required: false }
62
63
  });
63
64
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
65
+ const t = useIntegrationsT();
64
66
  const { keys: secretKeys } = useIntegrationsSecrets();
65
- const blob = ref(clone(props.workflow.blob));
67
+ const blob = ref(clone(props.draft ?? props.workflow.blob));
66
68
  watch(() => props.workflow.id, () => {
67
69
  blob.value = clone(props.workflow.blob);
68
70
  syncFromBlob();
@@ -127,7 +129,7 @@ function triggerPosition(id, index) {
127
129
  function nodePosition(id) {
128
130
  const wn = blob.value.nodes.find((n) => n.id === id);
129
131
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
130
- const ti = blob.value.triggers.findIndex((t) => t.id === id);
132
+ const ti = blob.value.triggers.findIndex((t2) => t2.id === id);
131
133
  if (ti >= 0) return triggerPosition(id, ti);
132
134
  return { x: 0, y: 0 };
133
135
  }
@@ -197,7 +199,7 @@ function ungroup(id) {
197
199
  push();
198
200
  }
199
201
  function createGroupFromSelection(ids) {
200
- const valid = ids.filter((id2) => blob.value.nodes.some((n) => n.id === id2) || blob.value.triggers.some((t) => t.id === id2));
202
+ const valid = ids.filter((id2) => blob.value.nodes.some((n) => n.id === id2) || blob.value.triggers.some((t2) => t2.id === id2));
201
203
  if (valid.length < 1) return;
202
204
  for (const g of blob.value.groups ?? []) {
203
205
  g.nodeIds = g.nodeIds.filter((x) => !valid.includes(x));
@@ -206,7 +208,7 @@ function createGroupFromSelection(ids) {
206
208
  const id = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
207
209
  blob.value.groups = [...blob.value.groups ?? [], {
208
210
  id,
209
- label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
211
+ label: t("integration.editor.group.defaultLabel", { n: (blob.value.groups?.length ?? 0) + 1 }),
210
212
  nodeIds: valid
211
213
  }];
212
214
  syncFromBlob();
@@ -267,12 +269,12 @@ function syncFromBlob() {
267
269
  onRemove: () => removeBlobNode(s.id)
268
270
  }
269
271
  })),
270
- ...blob.value.triggers.filter((t) => !collapsedBy.has(t.id)).map((t, i) => ({
271
- id: t.id,
272
+ ...blob.value.triggers.filter((t2) => !collapsedBy.has(t2.id)).map((t2, i) => ({
273
+ id: t2.id,
272
274
  type: "trigger",
273
- position: triggerPosition(t.id, i),
274
- class: bornClass(t.id),
275
- data: t
275
+ position: triggerPosition(t2.id, i),
276
+ class: bornClass(t2.id),
277
+ data: t2
276
278
  })),
277
279
  ...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
278
280
  id: n.id,
@@ -394,7 +396,7 @@ function persistDraggedNodes(evt) {
394
396
  const dy = Math.round(n.position.y - origin.frame.y);
395
397
  for (const [id, memberPos] of Object.entries(origin.members)) {
396
398
  const target = { x: Math.round(memberPos.x + dx), y: Math.round(memberPos.y + dy) };
397
- if (blob.value.triggers.some((t) => t.id === id)) {
399
+ if (blob.value.triggers.some((t2) => t2.id === id)) {
398
400
  blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [id]: target };
399
401
  } else {
400
402
  const wn = blob.value.nodes.find((b) => b.id === id);
@@ -407,7 +409,7 @@ function persistDraggedNodes(evt) {
407
409
  }
408
410
  continue;
409
411
  }
410
- if (blob.value.triggers.some((t) => t.id === n.id)) {
412
+ if (blob.value.triggers.some((t2) => t2.id === n.id)) {
411
413
  blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [n.id]: pos };
412
414
  } else if (blob.value.stickies?.some((s) => s.id === n.id)) {
413
415
  const st = blob.value.stickies.find((s) => s.id === n.id);
@@ -456,7 +458,7 @@ flow.onConnectEnd((event) => {
456
458
  pendingConnect.value = { sourceId: start.sourceId, output: start.output, pos: { x: snap(pos.x), y: snap(pos.y) } };
457
459
  pendingSpliceEdgeId.value = null;
458
460
  void focusPalette();
459
- toast.info("Pick a node \u2014 it will be added right there, already connected.");
461
+ toast.info(t("integration.editor.quickConnect.toast"));
460
462
  });
461
463
  flow.onEdgeUpdate(({ edge, connection }) => {
462
464
  if (!connection.source || !connection.target) return;
@@ -468,20 +470,49 @@ flow.onEdgeUpdate(({ edge, connection }) => {
468
470
  syncFromBlob();
469
471
  push();
470
472
  });
471
- flow.onEdgesChange((changes) => {
473
+ let pendingEdgeRemovals = [];
474
+ let pendingNodeRemovals = [];
475
+ let removalFlushQueued = false;
476
+ function queueRemovalFlush() {
477
+ if (removalFlushQueued) return;
478
+ removalFlushQueued = true;
479
+ void nextTick(flushRemovals);
480
+ }
481
+ function flushRemovals() {
482
+ removalFlushQueued = false;
483
+ const edges = pendingEdgeRemovals;
484
+ const nodes = pendingNodeRemovals;
485
+ pendingEdgeRemovals = [];
486
+ pendingNodeRemovals = [];
487
+ const goneNodes = new Set(nodes);
472
488
  let mutated = false;
489
+ for (const c of edges) {
490
+ if (goneNodes.has(c.source) || goneNodes.has(c.target)) continue;
491
+ blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
492
+ mutated = true;
493
+ }
494
+ if (nodes.length) {
495
+ dropFromBlob(nodes);
496
+ mutated = true;
497
+ }
498
+ if (mutated) {
499
+ syncFromBlob();
500
+ push();
501
+ }
502
+ }
503
+ flow.onEdgesChange((changes) => {
473
504
  for (const c of changes) {
474
505
  if (c.type === "remove") {
475
- blob.value.edges = blob.value.edges.filter((e) => e.id !== c.id);
476
- mutated = true;
506
+ pendingEdgeRemovals.push({ id: c.id, source: c.source, target: c.target });
477
507
  }
478
508
  }
479
- if (mutated) push();
509
+ if (pendingEdgeRemovals.length) queueRemovalFlush();
480
510
  });
481
511
  flow.onNodesChange((changes) => {
482
- for (const c of changes) {
483
- if (c.type === "remove") removeBlobNode(c.id);
484
- }
512
+ const removed = changes.filter((c) => c.type === "remove").map((c) => c.id);
513
+ if (!removed.length) return;
514
+ pendingNodeRemovals.push(...removed);
515
+ queueRemovalFlush();
485
516
  });
486
517
  function stripFromGroups(drop) {
487
518
  if (!blob.value.groups?.length) return;
@@ -496,7 +527,7 @@ function removeBlobNode(id) {
496
527
  stripFromGroups(/* @__PURE__ */ new Set([id]));
497
528
  blob.value.nodes = blob.value.nodes.filter((n) => n.id !== id);
498
529
  if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => s.id !== id);
499
- blob.value.triggers = blob.value.triggers.filter((t) => t.id !== id);
530
+ blob.value.triggers = blob.value.triggers.filter((t2) => t2.id !== id);
500
531
  blob.value.edges = blob.value.edges.filter((e) => e.from.nodeId !== id && e.to.nodeId !== id);
501
532
  if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
502
533
  if (selectedId.value === id) selectedId.value = null;
@@ -527,7 +558,7 @@ function addSticky(at) {
527
558
  id: `s-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
528
559
  // n8n-style starter content: teaches the edit gesture and the supported
529
560
  // markdown by example.
530
- content: "## I'm a note\n**Double-click** to edit me. **Bold**, *italic*, `code`, [links](https://revenexx.com) and\n- lists work",
561
+ content: t("integration.editor.sticky.starterContent"),
531
562
  position: at ? { x: Math.round(at.x), y: Math.round(at.y) } : {
532
563
  x: Math.round(center.x - STICKY_DEFAULT_SIZE.width / 2),
533
564
  y: Math.round(center.y - STICKY_DEFAULT_SIZE.height / 2)
@@ -538,18 +569,22 @@ function addSticky(at) {
538
569
  syncFromBlob();
539
570
  push();
540
571
  }
541
- function removeBlobNodes(ids) {
572
+ function dropFromBlob(ids) {
542
573
  if (!ids.length) return;
543
574
  const drop = new Set(ids);
544
575
  stripFromGroups(drop);
545
576
  blob.value.nodes = blob.value.nodes.filter((n) => !drop.has(n.id));
546
- blob.value.triggers = blob.value.triggers.filter((t) => !drop.has(t.id));
577
+ blob.value.triggers = blob.value.triggers.filter((t2) => !drop.has(t2.id));
547
578
  blob.value.edges = blob.value.edges.filter((e) => !drop.has(e.from.nodeId) && !drop.has(e.to.nodeId));
548
579
  if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => !drop.has(s.id));
549
580
  for (const id of ids) {
550
581
  if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
551
582
  }
552
583
  if (selectedId.value && drop.has(selectedId.value)) selectedId.value = null;
584
+ }
585
+ function removeBlobNodes(ids) {
586
+ if (!ids.length) return;
587
+ dropFromBlob(ids);
553
588
  syncFromBlob();
554
589
  push();
555
590
  }
@@ -602,9 +637,9 @@ async function copyNodes(ids) {
602
637
  const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
603
638
  try {
604
639
  await navigator.clipboard.writeText(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2));
605
- toast.success(nodes.length === 1 ? "Node copied" : `${nodes.length} nodes copied`);
640
+ toast.success(t("integration.editor.clipboard.toast.copied", nodes.length));
606
641
  } catch {
607
- toast.error("Could not write to the clipboard");
642
+ toast.error(t("integration.editor.clipboard.error.write"));
608
643
  }
609
644
  }
610
645
  async function pasteNodes(at) {
@@ -612,18 +647,18 @@ async function pasteNodes(at) {
612
647
  try {
613
648
  text = await navigator.clipboard.readText();
614
649
  } catch {
615
- toast.error("Could not read the clipboard");
650
+ toast.error(t("integration.editor.clipboard.error.read"));
616
651
  return;
617
652
  }
618
653
  let payload;
619
654
  try {
620
655
  payload = JSON.parse(text);
621
656
  } catch {
622
- toast.error("Clipboard does not contain nodes");
657
+ toast.error(t("integration.editor.clipboard.error.empty"));
623
658
  return;
624
659
  }
625
660
  if (payload?.kind !== CLIP_KIND || !Array.isArray(payload.nodes) || !payload.nodes.length) {
626
- toast.error("Clipboard does not contain nodes");
661
+ toast.error(t("integration.editor.clipboard.error.empty"));
627
662
  return;
628
663
  }
629
664
  const idMap = /* @__PURE__ */ new Map();
@@ -739,9 +774,9 @@ function isValidConnection(conn) {
739
774
  const selectedId = ref(null);
740
775
  const selection = computed(() => {
741
776
  if (!selectedId.value) return null;
742
- return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
777
+ return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t2) => t2.id === selectedId.value) ?? null;
743
778
  });
744
- const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
779
+ const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t2) => t2.id === selectedId.value));
745
780
  flow.onNodeDoubleClick((evt) => {
746
781
  if (evt.node.type === "sticky" || evt.node.type === "group") return;
747
782
  selectedId.value = evt.node.id;
@@ -762,7 +797,7 @@ function centerOnNode(id) {
762
797
  function revealNode(id) {
763
798
  if (paneReady.value) centerOnNode(id);
764
799
  else pendingReveal.value = id;
765
- if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t) => t.id === id)) {
800
+ if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t2) => t2.id === id)) {
766
801
  renameIntent.value = false;
767
802
  selectedId.value = id;
768
803
  }
@@ -776,20 +811,20 @@ const selectedUpstreamOutputs = computed(() => {
776
811
  return blob.value.edges.filter((e) => e.to.nodeId === id).flatMap((e) => {
777
812
  const source = blob.value.nodes.find((n) => n.id === e.from.nodeId);
778
813
  if (!source) return [];
779
- const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
814
+ const port = portsForNode(source, findManifest(source.nodeSlug, source.nodeVersion), dynamicOutputs.value[source.id]).find((p) => p.name === e.from.output);
780
815
  return [{
781
816
  nodeId: source.id,
782
817
  // A node's own name is text the user typed — never trimmed, never read
783
818
  // as JSON. The manifest strings below it get the normalising resolver.
784
819
  nodeName: resolveDisplayName(source.name) || source.nodeSlug,
785
820
  output: e.from.output,
786
- fields: portDef?.fields
821
+ fields: port?.fields.map((f) => f.name)
787
822
  }];
788
823
  });
789
824
  });
790
825
  function applyInspectorUpdate(next) {
791
826
  const id = next.id;
792
- const triggerIdx = blob.value.triggers.findIndex((t) => t.id === id);
827
+ const triggerIdx = blob.value.triggers.findIndex((t2) => t2.id === id);
793
828
  if (triggerIdx >= 0) {
794
829
  blob.value.triggers[triggerIdx] = next;
795
830
  } else {
@@ -862,7 +897,7 @@ function addFromPayload(payload, pos) {
862
897
  const id2 = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
863
898
  blob.value.groups = [...blob.value.groups ?? [], {
864
899
  id: id2,
865
- label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
900
+ label: t("integration.editor.group.defaultLabel", { n: (blob.value.groups?.length ?? 0) + 1 }),
866
901
  nodeIds: [],
867
902
  rect: { x: at.x, y: at.y, ...EMPTY_GROUP_SIZE }
868
903
  }];
@@ -872,7 +907,7 @@ function addFromPayload(payload, pos) {
872
907
  return;
873
908
  }
874
909
  if (payload.kind === "trigger") {
875
- const tpl = TRIGGER_TEMPLATES.find((t) => t.type === payload.type);
910
+ const tpl = TRIGGER_TEMPLATES.find((t2) => t2.type === payload.type);
876
911
  if (!tpl) return;
877
912
  const id2 = crypto.randomUUID();
878
913
  blob.value.triggers.push({ id: id2, type: tpl.type, config: { ...tpl.defaultConfig } });
@@ -950,7 +985,7 @@ provide("workflow-edge-actions", {
950
985
  if (!blob.value.edges.some((e) => e.id === edgeId)) return;
951
986
  pendingSpliceEdgeId.value = edgeId;
952
987
  void focusPalette();
953
- toast.info("Pick a node from the palette \u2014 it will be inserted into the connection.");
988
+ toast.info(t("integration.editor.splice.toast"));
954
989
  }
955
990
  });
956
991
  function primaryOutputName(slug, version) {
@@ -962,7 +997,7 @@ function spliceIntoEdge(edgeId, payload) {
962
997
  const manifest = findManifest(payload.slug, payload.version);
963
998
  if (!edge || !manifest) return;
964
999
  if (isTerminalManifest(manifest)) {
965
- toast.error(`${resolveLocalized(manifest.name) || manifest.slug} ends the run and cannot be inserted into a connection.`);
1000
+ toast.error(t("integration.editor.splice.error.terminal", { name: resolveLocalized(manifest.name) || manifest.slug }));
966
1001
  return;
967
1002
  }
968
1003
  const a = nodePosition(edge.from.nodeId);
@@ -1210,6 +1245,14 @@ async function focusPalette() {
1210
1245
  paletteRef.value?.focusSearch();
1211
1246
  }
1212
1247
  const lightboxOpen = computed(() => !!selection.value);
1248
+ watch(lightboxOpen, (open) => {
1249
+ if (!open) renameIntent.value = false;
1250
+ });
1251
+ function onConfigOpenAutoFocus(event) {
1252
+ if (renameIntent.value) return;
1253
+ event.preventDefault();
1254
+ event.target?.focus();
1255
+ }
1213
1256
  const canvasHasKeyboard = computed(() => !lightboxOpen.value && !variablesOpen.value && !props.overlayOpen);
1214
1257
  const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
1215
1258
  useCanvasShortcuts(() => ({
@@ -1298,7 +1341,7 @@ const edgeTypes = {
1298
1341
  :kind="menu.kind"
1299
1342
  :selection-count="flow.getSelectedNodes.value.length"
1300
1343
  :total-nodes="vfNodes.length"
1301
- :target-is-trigger="!!menu.targetId && blob.triggers.some((t) => t.id === menu.targetId)"
1344
+ :target-is-trigger="!!menu.targetId && blob.triggers.some((t2) => t2.id === menu.targetId)"
1302
1345
  :run-busy="runBusy"
1303
1346
  @action="onMenuAction"
1304
1347
  >
@@ -1351,7 +1394,7 @@ const edgeTypes = {
1351
1394
  class="absolute -top-0.5 left-1/2 -translate-x-1/2 -translate-y-full rounded-md px-1.5 py-0.5 text-xs font-medium text-white shadow-sm"
1352
1395
  :class="nodeDrag.dragging.value?.kind === 'trigger' ? 'bg-emerald-500' : nodeDrag.dragging.value?.kind === 'group' ? 'bg-slate-500' : 'bg-violet-500'"
1353
1396
  >
1354
- Drop to add
1397
+ {{ t("integration.editor.dropGhost.label") }}
1355
1398
  </span>
1356
1399
  </div>
1357
1400
  </Transition>
@@ -1404,11 +1447,11 @@ const edgeTypes = {
1404
1447
  <button
1405
1448
  type="button"
1406
1449
  class="flex h-9 items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75 hover:cursor-pointer"
1407
- title="Expand the node palette"
1450
+ :title="t('integration.editor.palette.action.expand.title')"
1408
1451
  @click="void focusPalette()"
1409
1452
  >
1410
1453
  <Plus :size="14" class="text-emerald-600" />
1411
- Add node
1454
+ {{ t("integration.editor.palette.title") }}
1412
1455
  </button>
1413
1456
  </div>
1414
1457
  </div>
@@ -1427,29 +1470,29 @@ const edgeTypes = {
1427
1470
  @pointerdown="palette.startDrag($event, paletteShellRef)"
1428
1471
  >
1429
1472
  <GripVertical :size="14" class="shrink-0 text-muted-foreground/60" />
1430
- <span class="min-w-0 flex-1 truncate text-sm font-medium">Add node</span>
1473
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.editor.palette.title") }}</span>
1431
1474
  <DropdownMenu>
1432
1475
  <DropdownMenuTrigger as-child>
1433
1476
  <button
1434
1477
  type="button"
1435
1478
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
1436
- title="Palette placement"
1479
+ :title="t('integration.editor.palette.action.placement.title')"
1437
1480
  >
1438
1481
  <EllipsisVertical :size="14" />
1439
1482
  </button>
1440
1483
  </DropdownMenuTrigger>
1441
1484
  <DropdownMenuContent align="end" class="w-36 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
1442
- <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('left')">Dock left</DropdownMenuItem>
1443
- <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('right')">Dock right</DropdownMenuItem>
1444
- <DropdownMenuItem class="rounded-md text-sm" @select="palette.floatHere(paletteShellRef)">Float here</DropdownMenuItem>
1485
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('left')">{{ t("integration.editor.palette.action.dockLeft") }}</DropdownMenuItem>
1486
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('right')">{{ t("integration.editor.palette.action.dockRight") }}</DropdownMenuItem>
1487
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.floatHere(paletteShellRef)">{{ t("integration.editor.panel.action.floatHere") }}</DropdownMenuItem>
1445
1488
  <DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
1446
- <DropdownMenuItem class="rounded-md text-sm" @select="palette.setCollapsed(true)">Collapse</DropdownMenuItem>
1489
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.setCollapsed(true)">{{ t("integration.editor.panel.action.collapse") }}</DropdownMenuItem>
1447
1490
  </DropdownMenuContent>
1448
1491
  </DropdownMenu>
1449
1492
  <button
1450
1493
  type="button"
1451
1494
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
1452
- title="Collapse the palette"
1495
+ :title="t('integration.editor.palette.action.collapse.title')"
1453
1496
  @click="palette.setCollapsed(true)"
1454
1497
  >
1455
1498
  <Minus :size="14" />
@@ -1491,11 +1534,11 @@ const edgeTypes = {
1491
1534
  <button
1492
1535
  type="button"
1493
1536
  class="flex h-9 items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75 hover:cursor-pointer"
1494
- title="Expand the minimap"
1537
+ :title="t('integration.editor.minimap.action.expand.title')"
1495
1538
  @click="minimapLayout.setCollapsed(false)"
1496
1539
  >
1497
1540
  <MapIcon :size="14" class="text-emerald-600" />
1498
- Map
1541
+ {{ t("integration.editor.minimap.title") }}
1499
1542
  </button>
1500
1543
  </div>
1501
1544
  </div>
@@ -1514,31 +1557,31 @@ const edgeTypes = {
1514
1557
  @pointerdown="minimapLayout.startDrag($event, minimapShellRef)"
1515
1558
  >
1516
1559
  <GripVertical :size="14" class="shrink-0 text-muted-foreground/60" />
1517
- <span class="min-w-0 flex-1 truncate text-sm font-medium">Map</span>
1560
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.editor.minimap.title") }}</span>
1518
1561
  <DropdownMenu>
1519
1562
  <DropdownMenuTrigger as-child>
1520
1563
  <button
1521
1564
  type="button"
1522
1565
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
1523
- title="Minimap placement"
1566
+ :title="t('integration.editor.minimap.action.placement.title')"
1524
1567
  >
1525
1568
  <EllipsisVertical :size="14" />
1526
1569
  </button>
1527
1570
  </DropdownMenuTrigger>
1528
1571
  <DropdownMenuContent align="end" class="w-40 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
1529
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-left')">Top left</DropdownMenuItem>
1530
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-right')">Top right</DropdownMenuItem>
1531
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-left')">Bottom left</DropdownMenuItem>
1532
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-right')">Bottom right</DropdownMenuItem>
1533
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.floatHere(minimapShellRef)">Float here</DropdownMenuItem>
1572
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-left')">{{ t("integration.editor.minimap.action.topLeft") }}</DropdownMenuItem>
1573
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-right')">{{ t("integration.editor.minimap.action.topRight") }}</DropdownMenuItem>
1574
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-left')">{{ t("integration.editor.minimap.action.bottomLeft") }}</DropdownMenuItem>
1575
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-right')">{{ t("integration.editor.minimap.action.bottomRight") }}</DropdownMenuItem>
1576
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.floatHere(minimapShellRef)">{{ t("integration.editor.panel.action.floatHere") }}</DropdownMenuItem>
1534
1577
  <DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
1535
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.setCollapsed(true)">Collapse</DropdownMenuItem>
1578
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.setCollapsed(true)">{{ t("integration.editor.panel.action.collapse") }}</DropdownMenuItem>
1536
1579
  </DropdownMenuContent>
1537
1580
  </DropdownMenu>
1538
1581
  <button
1539
1582
  type="button"
1540
1583
  class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
1541
- title="Collapse the minimap"
1584
+ :title="t('integration.editor.minimap.action.collapse.title')"
1542
1585
  @click="minimapLayout.setCollapsed(true)"
1543
1586
  >
1544
1587
  <Minus :size="14" />
@@ -1564,7 +1607,7 @@ const edgeTypes = {
1564
1607
  <!-- Smart Run: the page decides what "run" means — workflows that
1565
1608
  consume trigger.payload open the payload dialog, payload-free
1566
1609
  ones fire immediately. One button either way. -->
1567
- <IntegrationsEditorTip :label="runWaiting ? 'The bundle is still building \u2014 the run starts when it is ready' : 'Run workflow'" shortcut="mod+enter">
1610
+ <IntegrationsEditorTip :label="runWaiting ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')" shortcut="mod+enter">
1568
1611
  <button
1569
1612
  type="button"
1570
1613
  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"
@@ -1573,7 +1616,7 @@ const edgeTypes = {
1573
1616
  >
1574
1617
  <Loader2 v-if="runBusy" :size="14" class="animate-spin" />
1575
1618
  <Play v-else :size="14" />
1576
- {{ runWaiting ? "Waiting for build\u2026" : "Run" }}
1619
+ {{ runWaiting ? t("integration.editor.run.waitingForBuild") : t("integration.editor.run.button") }}
1577
1620
  </button>
1578
1621
  </IntegrationsEditorTip>
1579
1622
 
@@ -1582,10 +1625,10 @@ const edgeTypes = {
1582
1625
  <div class="flex items-center gap-0.5 rounded-lg bg-black/[0.05] p-0.5 shadow-[inset_0_1px_2px_rgba(15,23,42,0.12)]">
1583
1626
  <!-- Icon-only, and the tooltip is not an accessible name: each one
1584
1627
  says what it does itself, and whether it is the active mode. -->
1585
- <IntegrationsEditorTip label="Select" shortcut="v">
1628
+ <IntegrationsEditorTip :label="t('integration.editor.tool.select')" shortcut="v">
1586
1629
  <button
1587
1630
  type="button"
1588
- aria-label="Select"
1631
+ :aria-label="t('integration.editor.tool.select')"
1589
1632
  :aria-pressed="pointerMode === 'select'"
1590
1633
  class="flex size-7 items-center justify-center rounded-lg transition"
1591
1634
  :class="pointerMode === 'select' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
@@ -1594,10 +1637,10 @@ const edgeTypes = {
1594
1637
  <MousePointer2 :size="14" />
1595
1638
  </button>
1596
1639
  </IntegrationsEditorTip>
1597
- <IntegrationsEditorTip label="Hand (pan)" shortcut="h">
1640
+ <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" shortcut="h">
1598
1641
  <button
1599
1642
  type="button"
1600
- aria-label="Hand (pan)"
1643
+ :aria-label="t('integration.editor.tool.hand')"
1601
1644
  :aria-pressed="pointerMode === 'hand'"
1602
1645
  class="flex size-7 items-center justify-center rounded-lg transition"
1603
1646
  :class="pointerMode === 'hand' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
@@ -1608,74 +1651,74 @@ const edgeTypes = {
1608
1651
  </IntegrationsEditorTip>
1609
1652
  </div>
1610
1653
 
1611
- <IntegrationsEditorTip label="Tidy up" shortcut="shift+alt+t">
1654
+ <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" shortcut="shift+alt+t">
1612
1655
  <button
1613
1656
  type="button"
1614
1657
  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]"
1615
1658
  @click="tidyUp"
1616
1659
  >
1617
1660
  <LayoutGrid :size="13" />
1618
- Tidy up
1661
+ {{ t("integration.editor.tool.tidyUp") }}
1619
1662
  </button>
1620
1663
  </IntegrationsEditorTip>
1621
- <IntegrationsEditorTip label="Fit view" shortcut="1">
1664
+ <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" shortcut="1">
1622
1665
  <button
1623
1666
  type="button"
1624
1667
  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]"
1625
1668
  @click="flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 })"
1626
1669
  >
1627
1670
  <Sparkles :size="13" />
1628
- Fit
1671
+ {{ t("integration.editor.tool.fitView.label") }}
1629
1672
  </button>
1630
1673
  </IntegrationsEditorTip>
1631
- <IntegrationsEditorTip label="Add sticky note" shortcut="shift+s">
1674
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" shortcut="shift+s">
1632
1675
  <button
1633
1676
  type="button"
1634
1677
  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]"
1635
1678
  @click="addSticky()"
1636
1679
  >
1637
1680
  <StickyNote :size="13" />
1638
- Note
1681
+ {{ t("integration.editor.tool.addSticky.label") }}
1639
1682
  </button>
1640
1683
  </IntegrationsEditorTip>
1641
- <IntegrationsEditorTip label="Add group frame — nodes dropped inside join it">
1684
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addGroup.tooltip')">
1642
1685
  <button
1643
1686
  type="button"
1644
1687
  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]"
1645
1688
  @click="addGroupFrame"
1646
1689
  >
1647
1690
  <GroupIcon :size="13" />
1648
- Group
1691
+ {{ t("integration.editor.tool.addGroup.label") }}
1649
1692
  </button>
1650
1693
  </IntegrationsEditorTip>
1651
- <IntegrationsEditorTip label="Workflow variables" shortcut="shift+v">
1694
+ <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" shortcut="shift+v">
1652
1695
  <button
1653
1696
  type="button"
1654
1697
  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]"
1655
1698
  @click="variablesOpen = true"
1656
1699
  >
1657
1700
  <Braces :size="13" />
1658
- Variables
1701
+ {{ t("integration.editor.tool.variables.label") }}
1659
1702
  <span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
1660
1703
  </button>
1661
1704
  </IntegrationsEditorTip>
1662
- <IntegrationsEditorTip label="Workflow settings" shortcut="shift+alt+s">
1705
+ <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
1663
1706
  <button
1664
1707
  type="button"
1665
1708
  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]"
1666
1709
  @click="emit('openSettings')"
1667
1710
  >
1668
1711
  <Settings2 :size="13" />
1669
- Settings
1712
+ {{ t("integration.editor.tool.settings.label") }}
1670
1713
  </button>
1671
1714
  </IntegrationsEditorTip>
1672
1715
 
1673
1716
  <div class="mx-1 h-5 w-px bg-black/10" />
1674
1717
 
1675
- <IntegrationsEditorTip label="Zoom out" shortcut="-">
1718
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" shortcut="-">
1676
1719
  <button
1677
1720
  type="button"
1678
- aria-label="Zoom out"
1721
+ :aria-label="t('integration.editor.tool.zoomOut')"
1679
1722
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1680
1723
  @click="flow.zoomOut({ duration: 150 })"
1681
1724
  >
@@ -1685,7 +1728,7 @@ const edgeTypes = {
1685
1728
  <!-- Tooltip sits on a wrapper span so its as-child merge never
1686
1729
  touches the dropdown trigger (merging both onto one button
1687
1730
  swallowed the trigger's pointerdown and the menu never opened). -->
1688
- <IntegrationsEditorTip label="Zoom level, 0 resets to 100%" shortcut="0">
1731
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" shortcut="0">
1689
1732
  <span class="inline-flex">
1690
1733
  <DropdownMenu v-model:open="zoomMenuOpen">
1691
1734
  <DropdownMenuTrigger as-child>
@@ -1695,7 +1738,7 @@ const edgeTypes = {
1695
1738
  users do not need told and without the value they do. -->
1696
1739
  <button
1697
1740
  type="button"
1698
- :aria-label="`Zoom level ${zoomPct}`"
1741
+ :aria-label="t('integration.editor.tool.zoomLevel.aria', { pct: zoomPct })"
1699
1742
  class="w-12 rounded-lg py-1 text-center text-sm tabular-nums text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground"
1700
1743
  >
1701
1744
  {{ zoomPct }}
@@ -1707,7 +1750,7 @@ const edgeTypes = {
1707
1750
  v-model="zoomInputText"
1708
1751
  type="text"
1709
1752
  inputmode="numeric"
1710
- aria-label="Zoom level in percent"
1753
+ :aria-label="t('integration.editor.tool.zoomLevel.inputAria')"
1711
1754
  class="h-6 w-full rounded-md border border-black/[0.08] bg-white px-1 text-center text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-primary/40"
1712
1755
  placeholder="%"
1713
1756
  @keydown.stop
@@ -1727,10 +1770,10 @@ const edgeTypes = {
1727
1770
  </DropdownMenu>
1728
1771
  </span>
1729
1772
  </IntegrationsEditorTip>
1730
- <IntegrationsEditorTip label="Zoom in" shortcut="=">
1773
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" shortcut="=">
1731
1774
  <button
1732
1775
  type="button"
1733
- aria-label="Zoom in"
1776
+ :aria-label="t('integration.editor.tool.zoomIn')"
1734
1777
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1735
1778
  @click="flow.zoomIn({ duration: 150 })"
1736
1779
  >
@@ -1749,10 +1792,10 @@ const edgeTypes = {
1749
1792
  >
1750
1793
  <div class="rounded-lg border border-dashed border-black/[0.12] bg-white/80 px-5 py-4 text-center text-sm text-muted-foreground shadow-sm backdrop-blur-sm">
1751
1794
  <div class="font-medium text-foreground mb-1">
1752
- Empty canvas
1795
+ {{ t("integration.editor.emptyState.title") }}
1753
1796
  </div>
1754
1797
  <p>
1755
- Drag a trigger and an action from the panel on the left.
1798
+ {{ t("integration.editor.emptyState.hint") }}
1756
1799
  </p>
1757
1800
  </div>
1758
1801
  </div>
@@ -1787,6 +1830,7 @@ const edgeTypes = {
1787
1830
  form as a strip down the left of an empty workbench. -->
1788
1831
  <DialogContent
1789
1832
  :show-close-button="false"
1833
+ @open-auto-focus="onConfigOpenAutoFocus"
1790
1834
  class="flex max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none"
1791
1835
  :class="selectionIsTrigger ? 'h-[min(520px,86vh)] w-[min(600px,94vw)]' : 'h-[min(680px,86vh)] w-[min(1100px,94vw)]'"
1792
1836
  >