@revenexx/studio-integrations 0.6.0 → 0.7.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 (110) hide show
  1. package/CHANGELOG.md +227 -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 +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +29 -28
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +38 -37
  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 +16 -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.vue +15 -14
  16. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +9 -8
  17. package/dist/runtime/components/Integrations/NodeConfigField.vue +43 -42
  18. package/dist/runtime/components/Integrations/NodeInspector.vue +225 -216
  19. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +1 -1
  20. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +3 -2
  21. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +1 -1
  22. package/dist/runtime/components/Integrations/NodePalette.vue +30 -29
  23. package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
  24. package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
  25. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  26. package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
  27. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  28. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  29. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  30. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  31. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  32. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  33. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  34. package/dist/runtime/components/Integrations/StickyNoteView.vue +37 -23
  35. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  36. package/dist/runtime/components/Integrations/TemplateCard.vue +16 -6
  37. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +10 -9
  38. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  39. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +36 -35
  40. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  41. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
  42. package/dist/runtime/components/Integrations/WorkflowEditor.vue +82 -79
  43. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +24 -23
  44. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
  45. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +19 -20
  46. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +52 -39
  47. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +20 -19
  48. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
  49. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +12 -9
  50. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +27 -26
  51. package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
  52. package/dist/runtime/composables/integrationsApi.js +4 -2
  53. package/dist/runtime/composables/useCredentialTypes.js +4 -2
  54. package/dist/runtime/composables/useIntegrationsCredentials.js +6 -4
  55. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  56. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  57. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  58. package/dist/runtime/composables/useIntegrationsNodes.js +3 -1
  59. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  60. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  61. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  62. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  63. package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
  64. package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
  65. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  66. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  67. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  68. package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
  69. package/dist/runtime/composables/useIntegrationsTriggers.js +4 -2
  70. package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -3
  71. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  72. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  73. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  74. package/dist/runtime/data/integrationTemplates.d.ts +9 -0
  75. package/dist/runtime/data/integrationTemplates.js +7 -1
  76. package/dist/runtime/data/payloadForm.js +2 -1
  77. package/dist/runtime/data/workflowIconCatalog.js +2 -1
  78. package/dist/runtime/i18n/messages/de.json +1207 -0
  79. package/dist/runtime/i18n/messages/en.json +1207 -0
  80. package/dist/runtime/i18n/translate.d.ts +83 -0
  81. package/dist/runtime/i18n/translate.js +41 -0
  82. package/dist/runtime/pages/integrations/audit.vue +52 -34
  83. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
  84. package/dist/runtime/pages/integrations/credentials.vue +70 -52
  85. package/dist/runtime/pages/integrations/index.vue +50 -56
  86. package/dist/runtime/pages/integrations/runs.vue +33 -21
  87. package/dist/runtime/pages/integrations/schedules.vue +49 -28
  88. package/dist/runtime/pages/integrations/secrets.vue +34 -34
  89. package/dist/runtime/pages/integrations/templates/[slug].vue +77 -68
  90. package/dist/runtime/pages/integrations/templates/index.vue +35 -27
  91. package/dist/runtime/pages/integrations/workflows/[id].vue +164 -99
  92. package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
  93. package/dist/runtime/plugins/locale.d.ts +10 -0
  94. package/dist/runtime/plugins/locale.js +6 -0
  95. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  96. package/dist/runtime/plugins/realtime.client.js +4 -0
  97. package/dist/runtime/utils/credentialState.d.ts +24 -1
  98. package/dist/runtime/utils/credentialState.js +26 -11
  99. package/dist/runtime/utils/localize.d.ts +18 -5
  100. package/dist/runtime/utils/localize.js +32 -9
  101. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  102. package/dist/runtime/utils/pollBudget.js +17 -0
  103. package/dist/runtime/utils/schedule.d.ts +0 -25
  104. package/dist/runtime/utils/schedule.js +37 -25
  105. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  106. package/dist/runtime/utils/statusStyle.js +9 -1
  107. package/dist/runtime/utils/validationFindings.d.ts +2 -2
  108. package/dist/runtime/utils/validationFindings.js +4 -3
  109. package/dist/types.d.mts +2 -6
  110. package/package.json +3 -2
@@ -61,6 +61,7 @@ const props = defineProps({
61
61
  overlayOpen: { type: Boolean, required: false }
62
62
  });
63
63
  const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
64
+ const t = useIntegrationsT();
64
65
  const { keys: secretKeys } = useIntegrationsSecrets();
65
66
  const blob = ref(clone(props.workflow.blob));
66
67
  watch(() => props.workflow.id, () => {
@@ -127,7 +128,7 @@ function triggerPosition(id, index) {
127
128
  function nodePosition(id) {
128
129
  const wn = blob.value.nodes.find((n) => n.id === id);
129
130
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
130
- const ti = blob.value.triggers.findIndex((t) => t.id === id);
131
+ const ti = blob.value.triggers.findIndex((t2) => t2.id === id);
131
132
  if (ti >= 0) return triggerPosition(id, ti);
132
133
  return { x: 0, y: 0 };
133
134
  }
@@ -197,7 +198,7 @@ function ungroup(id) {
197
198
  push();
198
199
  }
199
200
  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));
201
+ const valid = ids.filter((id2) => blob.value.nodes.some((n) => n.id === id2) || blob.value.triggers.some((t2) => t2.id === id2));
201
202
  if (valid.length < 1) return;
202
203
  for (const g of blob.value.groups ?? []) {
203
204
  g.nodeIds = g.nodeIds.filter((x) => !valid.includes(x));
@@ -206,7 +207,7 @@ function createGroupFromSelection(ids) {
206
207
  const id = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
207
208
  blob.value.groups = [...blob.value.groups ?? [], {
208
209
  id,
209
- label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
210
+ label: t("integration.editor.group.defaultLabel", { n: (blob.value.groups?.length ?? 0) + 1 }),
210
211
  nodeIds: valid
211
212
  }];
212
213
  syncFromBlob();
@@ -267,12 +268,12 @@ function syncFromBlob() {
267
268
  onRemove: () => removeBlobNode(s.id)
268
269
  }
269
270
  })),
270
- ...blob.value.triggers.filter((t) => !collapsedBy.has(t.id)).map((t, i) => ({
271
- id: t.id,
271
+ ...blob.value.triggers.filter((t2) => !collapsedBy.has(t2.id)).map((t2, i) => ({
272
+ id: t2.id,
272
273
  type: "trigger",
273
- position: triggerPosition(t.id, i),
274
- class: bornClass(t.id),
275
- data: t
274
+ position: triggerPosition(t2.id, i),
275
+ class: bornClass(t2.id),
276
+ data: t2
276
277
  })),
277
278
  ...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
278
279
  id: n.id,
@@ -394,7 +395,7 @@ function persistDraggedNodes(evt) {
394
395
  const dy = Math.round(n.position.y - origin.frame.y);
395
396
  for (const [id, memberPos] of Object.entries(origin.members)) {
396
397
  const target = { x: Math.round(memberPos.x + dx), y: Math.round(memberPos.y + dy) };
397
- if (blob.value.triggers.some((t) => t.id === id)) {
398
+ if (blob.value.triggers.some((t2) => t2.id === id)) {
398
399
  blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [id]: target };
399
400
  } else {
400
401
  const wn = blob.value.nodes.find((b) => b.id === id);
@@ -407,7 +408,7 @@ function persistDraggedNodes(evt) {
407
408
  }
408
409
  continue;
409
410
  }
410
- if (blob.value.triggers.some((t) => t.id === n.id)) {
411
+ if (blob.value.triggers.some((t2) => t2.id === n.id)) {
411
412
  blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [n.id]: pos };
412
413
  } else if (blob.value.stickies?.some((s) => s.id === n.id)) {
413
414
  const st = blob.value.stickies.find((s) => s.id === n.id);
@@ -456,7 +457,7 @@ flow.onConnectEnd((event) => {
456
457
  pendingConnect.value = { sourceId: start.sourceId, output: start.output, pos: { x: snap(pos.x), y: snap(pos.y) } };
457
458
  pendingSpliceEdgeId.value = null;
458
459
  void focusPalette();
459
- toast.info("Pick a node \u2014 it will be added right there, already connected.");
460
+ toast.info(t("integration.editor.quickConnect.toast"));
460
461
  });
461
462
  flow.onEdgeUpdate(({ edge, connection }) => {
462
463
  if (!connection.source || !connection.target) return;
@@ -479,9 +480,11 @@ flow.onEdgesChange((changes) => {
479
480
  if (mutated) push();
480
481
  });
481
482
  flow.onNodesChange((changes) => {
482
- for (const c of changes) {
483
- if (c.type === "remove") removeBlobNode(c.id);
484
- }
483
+ const removed = changes.filter((c) => c.type === "remove").map((c) => c.id);
484
+ if (!removed.length) return;
485
+ void nextTick(() => {
486
+ for (const id of removed) removeBlobNode(id);
487
+ });
485
488
  });
486
489
  function stripFromGroups(drop) {
487
490
  if (!blob.value.groups?.length) return;
@@ -496,7 +499,7 @@ function removeBlobNode(id) {
496
499
  stripFromGroups(/* @__PURE__ */ new Set([id]));
497
500
  blob.value.nodes = blob.value.nodes.filter((n) => n.id !== id);
498
501
  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);
502
+ blob.value.triggers = blob.value.triggers.filter((t2) => t2.id !== id);
500
503
  blob.value.edges = blob.value.edges.filter((e) => e.from.nodeId !== id && e.to.nodeId !== id);
501
504
  if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
502
505
  if (selectedId.value === id) selectedId.value = null;
@@ -527,7 +530,7 @@ function addSticky(at) {
527
530
  id: `s-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
528
531
  // n8n-style starter content: teaches the edit gesture and the supported
529
532
  // 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",
533
+ content: t("integration.editor.sticky.starterContent"),
531
534
  position: at ? { x: Math.round(at.x), y: Math.round(at.y) } : {
532
535
  x: Math.round(center.x - STICKY_DEFAULT_SIZE.width / 2),
533
536
  y: Math.round(center.y - STICKY_DEFAULT_SIZE.height / 2)
@@ -543,7 +546,7 @@ function removeBlobNodes(ids) {
543
546
  const drop = new Set(ids);
544
547
  stripFromGroups(drop);
545
548
  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));
549
+ blob.value.triggers = blob.value.triggers.filter((t2) => !drop.has(t2.id));
547
550
  blob.value.edges = blob.value.edges.filter((e) => !drop.has(e.from.nodeId) && !drop.has(e.to.nodeId));
548
551
  if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => !drop.has(s.id));
549
552
  for (const id of ids) {
@@ -602,9 +605,9 @@ async function copyNodes(ids) {
602
605
  const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
603
606
  try {
604
607
  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`);
608
+ toast.success(t("integration.editor.clipboard.toast.copied", nodes.length));
606
609
  } catch {
607
- toast.error("Could not write to the clipboard");
610
+ toast.error(t("integration.editor.clipboard.error.write"));
608
611
  }
609
612
  }
610
613
  async function pasteNodes(at) {
@@ -612,18 +615,18 @@ async function pasteNodes(at) {
612
615
  try {
613
616
  text = await navigator.clipboard.readText();
614
617
  } catch {
615
- toast.error("Could not read the clipboard");
618
+ toast.error(t("integration.editor.clipboard.error.read"));
616
619
  return;
617
620
  }
618
621
  let payload;
619
622
  try {
620
623
  payload = JSON.parse(text);
621
624
  } catch {
622
- toast.error("Clipboard does not contain nodes");
625
+ toast.error(t("integration.editor.clipboard.error.empty"));
623
626
  return;
624
627
  }
625
628
  if (payload?.kind !== CLIP_KIND || !Array.isArray(payload.nodes) || !payload.nodes.length) {
626
- toast.error("Clipboard does not contain nodes");
629
+ toast.error(t("integration.editor.clipboard.error.empty"));
627
630
  return;
628
631
  }
629
632
  const idMap = /* @__PURE__ */ new Map();
@@ -739,9 +742,9 @@ function isValidConnection(conn) {
739
742
  const selectedId = ref(null);
740
743
  const selection = computed(() => {
741
744
  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;
745
+ return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t2) => t2.id === selectedId.value) ?? null;
743
746
  });
744
- const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t) => t.id === selectedId.value));
747
+ const selectionIsTrigger = computed(() => !!selectedId.value && blob.value.triggers.some((t2) => t2.id === selectedId.value));
745
748
  flow.onNodeDoubleClick((evt) => {
746
749
  if (evt.node.type === "sticky" || evt.node.type === "group") return;
747
750
  selectedId.value = evt.node.id;
@@ -762,7 +765,7 @@ function centerOnNode(id) {
762
765
  function revealNode(id) {
763
766
  if (paneReady.value) centerOnNode(id);
764
767
  else pendingReveal.value = id;
765
- if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t) => t.id === id)) {
768
+ if (blob.value.nodes.some((n) => n.id === id) || blob.value.triggers.some((t2) => t2.id === id)) {
766
769
  renameIntent.value = false;
767
770
  selectedId.value = id;
768
771
  }
@@ -789,7 +792,7 @@ const selectedUpstreamOutputs = computed(() => {
789
792
  });
790
793
  function applyInspectorUpdate(next) {
791
794
  const id = next.id;
792
- const triggerIdx = blob.value.triggers.findIndex((t) => t.id === id);
795
+ const triggerIdx = blob.value.triggers.findIndex((t2) => t2.id === id);
793
796
  if (triggerIdx >= 0) {
794
797
  blob.value.triggers[triggerIdx] = next;
795
798
  } else {
@@ -862,7 +865,7 @@ function addFromPayload(payload, pos) {
862
865
  const id2 = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
863
866
  blob.value.groups = [...blob.value.groups ?? [], {
864
867
  id: id2,
865
- label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
868
+ label: t("integration.editor.group.defaultLabel", { n: (blob.value.groups?.length ?? 0) + 1 }),
866
869
  nodeIds: [],
867
870
  rect: { x: at.x, y: at.y, ...EMPTY_GROUP_SIZE }
868
871
  }];
@@ -872,7 +875,7 @@ function addFromPayload(payload, pos) {
872
875
  return;
873
876
  }
874
877
  if (payload.kind === "trigger") {
875
- const tpl = TRIGGER_TEMPLATES.find((t) => t.type === payload.type);
878
+ const tpl = TRIGGER_TEMPLATES.find((t2) => t2.type === payload.type);
876
879
  if (!tpl) return;
877
880
  const id2 = crypto.randomUUID();
878
881
  blob.value.triggers.push({ id: id2, type: tpl.type, config: { ...tpl.defaultConfig } });
@@ -950,7 +953,7 @@ provide("workflow-edge-actions", {
950
953
  if (!blob.value.edges.some((e) => e.id === edgeId)) return;
951
954
  pendingSpliceEdgeId.value = edgeId;
952
955
  void focusPalette();
953
- toast.info("Pick a node from the palette \u2014 it will be inserted into the connection.");
956
+ toast.info(t("integration.editor.splice.toast"));
954
957
  }
955
958
  });
956
959
  function primaryOutputName(slug, version) {
@@ -962,7 +965,7 @@ function spliceIntoEdge(edgeId, payload) {
962
965
  const manifest = findManifest(payload.slug, payload.version);
963
966
  if (!edge || !manifest) return;
964
967
  if (isTerminalManifest(manifest)) {
965
- toast.error(`${resolveLocalized(manifest.name) || manifest.slug} ends the run and cannot be inserted into a connection.`);
968
+ toast.error(t("integration.editor.splice.error.terminal", { name: resolveLocalized(manifest.name) || manifest.slug }));
966
969
  return;
967
970
  }
968
971
  const a = nodePosition(edge.from.nodeId);
@@ -1298,7 +1301,7 @@ const edgeTypes = {
1298
1301
  :kind="menu.kind"
1299
1302
  :selection-count="flow.getSelectedNodes.value.length"
1300
1303
  :total-nodes="vfNodes.length"
1301
- :target-is-trigger="!!menu.targetId && blob.triggers.some((t) => t.id === menu.targetId)"
1304
+ :target-is-trigger="!!menu.targetId && blob.triggers.some((t2) => t2.id === menu.targetId)"
1302
1305
  :run-busy="runBusy"
1303
1306
  @action="onMenuAction"
1304
1307
  >
@@ -1351,7 +1354,7 @@ const edgeTypes = {
1351
1354
  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
1355
  :class="nodeDrag.dragging.value?.kind === 'trigger' ? 'bg-emerald-500' : nodeDrag.dragging.value?.kind === 'group' ? 'bg-slate-500' : 'bg-violet-500'"
1353
1356
  >
1354
- Drop to add
1357
+ {{ t("integration.editor.dropGhost.label") }}
1355
1358
  </span>
1356
1359
  </div>
1357
1360
  </Transition>
@@ -1404,11 +1407,11 @@ const edgeTypes = {
1404
1407
  <button
1405
1408
  type="button"
1406
1409
  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"
1410
+ :title="t('integration.editor.palette.action.expand.title')"
1408
1411
  @click="void focusPalette()"
1409
1412
  >
1410
1413
  <Plus :size="14" class="text-emerald-600" />
1411
- Add node
1414
+ {{ t("integration.editor.palette.title") }}
1412
1415
  </button>
1413
1416
  </div>
1414
1417
  </div>
@@ -1427,29 +1430,29 @@ const edgeTypes = {
1427
1430
  @pointerdown="palette.startDrag($event, paletteShellRef)"
1428
1431
  >
1429
1432
  <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>
1433
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.editor.palette.title") }}</span>
1431
1434
  <DropdownMenu>
1432
1435
  <DropdownMenuTrigger as-child>
1433
1436
  <button
1434
1437
  type="button"
1435
1438
  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"
1439
+ :title="t('integration.editor.palette.action.placement.title')"
1437
1440
  >
1438
1441
  <EllipsisVertical :size="14" />
1439
1442
  </button>
1440
1443
  </DropdownMenuTrigger>
1441
1444
  <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>
1445
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('left')">{{ t("integration.editor.palette.action.dockLeft") }}</DropdownMenuItem>
1446
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('right')">{{ t("integration.editor.palette.action.dockRight") }}</DropdownMenuItem>
1447
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.floatHere(paletteShellRef)">{{ t("integration.editor.panel.action.floatHere") }}</DropdownMenuItem>
1445
1448
  <DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
1446
- <DropdownMenuItem class="rounded-md text-sm" @select="palette.setCollapsed(true)">Collapse</DropdownMenuItem>
1449
+ <DropdownMenuItem class="rounded-md text-sm" @select="palette.setCollapsed(true)">{{ t("integration.editor.panel.action.collapse") }}</DropdownMenuItem>
1447
1450
  </DropdownMenuContent>
1448
1451
  </DropdownMenu>
1449
1452
  <button
1450
1453
  type="button"
1451
1454
  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"
1455
+ :title="t('integration.editor.palette.action.collapse.title')"
1453
1456
  @click="palette.setCollapsed(true)"
1454
1457
  >
1455
1458
  <Minus :size="14" />
@@ -1491,11 +1494,11 @@ const edgeTypes = {
1491
1494
  <button
1492
1495
  type="button"
1493
1496
  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"
1497
+ :title="t('integration.editor.minimap.action.expand.title')"
1495
1498
  @click="minimapLayout.setCollapsed(false)"
1496
1499
  >
1497
1500
  <MapIcon :size="14" class="text-emerald-600" />
1498
- Map
1501
+ {{ t("integration.editor.minimap.title") }}
1499
1502
  </button>
1500
1503
  </div>
1501
1504
  </div>
@@ -1514,31 +1517,31 @@ const edgeTypes = {
1514
1517
  @pointerdown="minimapLayout.startDrag($event, minimapShellRef)"
1515
1518
  >
1516
1519
  <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>
1520
+ <span class="min-w-0 flex-1 truncate text-sm font-medium">{{ t("integration.editor.minimap.title") }}</span>
1518
1521
  <DropdownMenu>
1519
1522
  <DropdownMenuTrigger as-child>
1520
1523
  <button
1521
1524
  type="button"
1522
1525
  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"
1526
+ :title="t('integration.editor.minimap.action.placement.title')"
1524
1527
  >
1525
1528
  <EllipsisVertical :size="14" />
1526
1529
  </button>
1527
1530
  </DropdownMenuTrigger>
1528
1531
  <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>
1532
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-left')">{{ t("integration.editor.minimap.action.topLeft") }}</DropdownMenuItem>
1533
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-right')">{{ t("integration.editor.minimap.action.topRight") }}</DropdownMenuItem>
1534
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-left')">{{ t("integration.editor.minimap.action.bottomLeft") }}</DropdownMenuItem>
1535
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-right')">{{ t("integration.editor.minimap.action.bottomRight") }}</DropdownMenuItem>
1536
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.floatHere(minimapShellRef)">{{ t("integration.editor.panel.action.floatHere") }}</DropdownMenuItem>
1534
1537
  <DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
1535
- <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.setCollapsed(true)">Collapse</DropdownMenuItem>
1538
+ <DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.setCollapsed(true)">{{ t("integration.editor.panel.action.collapse") }}</DropdownMenuItem>
1536
1539
  </DropdownMenuContent>
1537
1540
  </DropdownMenu>
1538
1541
  <button
1539
1542
  type="button"
1540
1543
  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"
1544
+ :title="t('integration.editor.minimap.action.collapse.title')"
1542
1545
  @click="minimapLayout.setCollapsed(true)"
1543
1546
  >
1544
1547
  <Minus :size="14" />
@@ -1564,7 +1567,7 @@ const edgeTypes = {
1564
1567
  <!-- Smart Run: the page decides what "run" means — workflows that
1565
1568
  consume trigger.payload open the payload dialog, payload-free
1566
1569
  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">
1570
+ <IntegrationsEditorTip :label="runWaiting ? t('integration.editor.tool.run.tooltipWaiting') : t('integration.editor.tool.run.tooltip')" shortcut="mod+enter">
1568
1571
  <button
1569
1572
  type="button"
1570
1573
  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 +1576,7 @@ const edgeTypes = {
1573
1576
  >
1574
1577
  <Loader2 v-if="runBusy" :size="14" class="animate-spin" />
1575
1578
  <Play v-else :size="14" />
1576
- {{ runWaiting ? "Waiting for build\u2026" : "Run" }}
1579
+ {{ runWaiting ? t("integration.editor.run.waitingForBuild") : t("integration.editor.run.button") }}
1577
1580
  </button>
1578
1581
  </IntegrationsEditorTip>
1579
1582
 
@@ -1582,10 +1585,10 @@ const edgeTypes = {
1582
1585
  <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
1586
  <!-- Icon-only, and the tooltip is not an accessible name: each one
1584
1587
  says what it does itself, and whether it is the active mode. -->
1585
- <IntegrationsEditorTip label="Select" shortcut="v">
1588
+ <IntegrationsEditorTip :label="t('integration.editor.tool.select')" shortcut="v">
1586
1589
  <button
1587
1590
  type="button"
1588
- aria-label="Select"
1591
+ :aria-label="t('integration.editor.tool.select')"
1589
1592
  :aria-pressed="pointerMode === 'select'"
1590
1593
  class="flex size-7 items-center justify-center rounded-lg transition"
1591
1594
  :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 +1597,10 @@ const edgeTypes = {
1594
1597
  <MousePointer2 :size="14" />
1595
1598
  </button>
1596
1599
  </IntegrationsEditorTip>
1597
- <IntegrationsEditorTip label="Hand (pan)" shortcut="h">
1600
+ <IntegrationsEditorTip :label="t('integration.editor.tool.hand')" shortcut="h">
1598
1601
  <button
1599
1602
  type="button"
1600
- aria-label="Hand (pan)"
1603
+ :aria-label="t('integration.editor.tool.hand')"
1601
1604
  :aria-pressed="pointerMode === 'hand'"
1602
1605
  class="flex size-7 items-center justify-center rounded-lg transition"
1603
1606
  :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 +1611,74 @@ const edgeTypes = {
1608
1611
  </IntegrationsEditorTip>
1609
1612
  </div>
1610
1613
 
1611
- <IntegrationsEditorTip label="Tidy up" shortcut="shift+alt+t">
1614
+ <IntegrationsEditorTip :label="t('integration.editor.tool.tidyUp')" shortcut="shift+alt+t">
1612
1615
  <button
1613
1616
  type="button"
1614
1617
  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
1618
  @click="tidyUp"
1616
1619
  >
1617
1620
  <LayoutGrid :size="13" />
1618
- Tidy up
1621
+ {{ t("integration.editor.tool.tidyUp") }}
1619
1622
  </button>
1620
1623
  </IntegrationsEditorTip>
1621
- <IntegrationsEditorTip label="Fit view" shortcut="1">
1624
+ <IntegrationsEditorTip :label="t('integration.editor.tool.fitView.tooltip')" shortcut="1">
1622
1625
  <button
1623
1626
  type="button"
1624
1627
  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
1628
  @click="flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 })"
1626
1629
  >
1627
1630
  <Sparkles :size="13" />
1628
- Fit
1631
+ {{ t("integration.editor.tool.fitView.label") }}
1629
1632
  </button>
1630
1633
  </IntegrationsEditorTip>
1631
- <IntegrationsEditorTip label="Add sticky note" shortcut="shift+s">
1634
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addSticky.tooltip')" shortcut="shift+s">
1632
1635
  <button
1633
1636
  type="button"
1634
1637
  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
1638
  @click="addSticky()"
1636
1639
  >
1637
1640
  <StickyNote :size="13" />
1638
- Note
1641
+ {{ t("integration.editor.tool.addSticky.label") }}
1639
1642
  </button>
1640
1643
  </IntegrationsEditorTip>
1641
- <IntegrationsEditorTip label="Add group frame — nodes dropped inside join it">
1644
+ <IntegrationsEditorTip :label="t('integration.editor.tool.addGroup.tooltip')">
1642
1645
  <button
1643
1646
  type="button"
1644
1647
  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
1648
  @click="addGroupFrame"
1646
1649
  >
1647
1650
  <GroupIcon :size="13" />
1648
- Group
1651
+ {{ t("integration.editor.tool.addGroup.label") }}
1649
1652
  </button>
1650
1653
  </IntegrationsEditorTip>
1651
- <IntegrationsEditorTip label="Workflow variables" shortcut="shift+v">
1654
+ <IntegrationsEditorTip :label="t('integration.editor.tool.variables.tooltip')" shortcut="shift+v">
1652
1655
  <button
1653
1656
  type="button"
1654
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]"
1655
1658
  @click="variablesOpen = true"
1656
1659
  >
1657
1660
  <Braces :size="13" />
1658
- Variables
1661
+ {{ t("integration.editor.tool.variables.label") }}
1659
1662
  <span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
1660
1663
  </button>
1661
1664
  </IntegrationsEditorTip>
1662
- <IntegrationsEditorTip label="Workflow settings" shortcut="shift+alt+s">
1665
+ <IntegrationsEditorTip :label="t('integration.editor.tool.settings.tooltip')" shortcut="shift+alt+s">
1663
1666
  <button
1664
1667
  type="button"
1665
1668
  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
1669
  @click="emit('openSettings')"
1667
1670
  >
1668
1671
  <Settings2 :size="13" />
1669
- Settings
1672
+ {{ t("integration.editor.tool.settings.label") }}
1670
1673
  </button>
1671
1674
  </IntegrationsEditorTip>
1672
1675
 
1673
1676
  <div class="mx-1 h-5 w-px bg-black/10" />
1674
1677
 
1675
- <IntegrationsEditorTip label="Zoom out" shortcut="-">
1678
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomOut')" shortcut="-">
1676
1679
  <button
1677
1680
  type="button"
1678
- aria-label="Zoom out"
1681
+ :aria-label="t('integration.editor.tool.zoomOut')"
1679
1682
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1680
1683
  @click="flow.zoomOut({ duration: 150 })"
1681
1684
  >
@@ -1685,7 +1688,7 @@ const edgeTypes = {
1685
1688
  <!-- Tooltip sits on a wrapper span so its as-child merge never
1686
1689
  touches the dropdown trigger (merging both onto one button
1687
1690
  swallowed the trigger's pointerdown and the menu never opened). -->
1688
- <IntegrationsEditorTip label="Zoom level, 0 resets to 100%" shortcut="0">
1691
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomLevel.tooltip')" shortcut="0">
1689
1692
  <span class="inline-flex">
1690
1693
  <DropdownMenu v-model:open="zoomMenuOpen">
1691
1694
  <DropdownMenuTrigger as-child>
@@ -1695,7 +1698,7 @@ const edgeTypes = {
1695
1698
  users do not need told and without the value they do. -->
1696
1699
  <button
1697
1700
  type="button"
1698
- :aria-label="`Zoom level ${zoomPct}`"
1701
+ :aria-label="t('integration.editor.tool.zoomLevel.aria', { pct: zoomPct })"
1699
1702
  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
1703
  >
1701
1704
  {{ zoomPct }}
@@ -1707,7 +1710,7 @@ const edgeTypes = {
1707
1710
  v-model="zoomInputText"
1708
1711
  type="text"
1709
1712
  inputmode="numeric"
1710
- aria-label="Zoom level in percent"
1713
+ :aria-label="t('integration.editor.tool.zoomLevel.inputAria')"
1711
1714
  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
1715
  placeholder="%"
1713
1716
  @keydown.stop
@@ -1727,10 +1730,10 @@ const edgeTypes = {
1727
1730
  </DropdownMenu>
1728
1731
  </span>
1729
1732
  </IntegrationsEditorTip>
1730
- <IntegrationsEditorTip label="Zoom in" shortcut="=">
1733
+ <IntegrationsEditorTip :label="t('integration.editor.tool.zoomIn')" shortcut="=">
1731
1734
  <button
1732
1735
  type="button"
1733
- aria-label="Zoom in"
1736
+ :aria-label="t('integration.editor.tool.zoomIn')"
1734
1737
  class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
1735
1738
  @click="flow.zoomIn({ duration: 150 })"
1736
1739
  >
@@ -1749,10 +1752,10 @@ const edgeTypes = {
1749
1752
  >
1750
1753
  <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
1754
  <div class="font-medium text-foreground mb-1">
1752
- Empty canvas
1755
+ {{ t("integration.editor.emptyState.title") }}
1753
1756
  </div>
1754
1757
  <p>
1755
- Drag a trigger and an action from the panel on the left.
1758
+ {{ t("integration.editor.emptyState.hint") }}
1756
1759
  </p>
1757
1760
  </div>
1758
1761
  </div>