@revenexx/studio-integrations 0.5.1 → 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 (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  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 +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -2,7 +2,6 @@
2
2
  import { computed, markRaw, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
3
3
  import {
4
4
  MarkerType,
5
- PanOnScrollMode,
6
5
  VueFlow,
7
6
  useVueFlow
8
7
  } from "@vue-flow/core";
@@ -27,6 +26,7 @@ import StickyNoteView from "./StickyNoteView.vue";
27
26
  import GroupFrameView from "./GroupFrameView.vue";
28
27
  import WorkflowEdgeView from "./WorkflowEdgeView.vue";
29
28
  import { findManifest } from "../../data/integrationNodeCatalog";
29
+ import { SAFETY_POLL_MS } from "../../utils/pollBudget";
30
30
  import { runStatusLabel, statusClass } from "../../utils/statusStyle";
31
31
  const props = defineProps({
32
32
  workflow: { type: Object, required: true },
@@ -34,9 +34,14 @@ const props = defineProps({
34
34
  single: { type: Boolean, required: false }
35
35
  });
36
36
  const { loadForWorkflow, getSteps } = useIntegrationsRuns();
37
+ const t = useIntegrationsT();
37
38
  const TERMINAL = /* @__PURE__ */ new Set(["completed", "succeeded", "failed", "cancelled", "terminated"]);
38
39
  const STEPS_POLL_MS = 4e3;
39
40
  const PLAY_INTERVAL_MS = 3e3;
41
+ const { delivering: realtimeDelivering, runTick, lastRunEvent } = useIntegrationsRealtime();
42
+ function stepsPollDelay() {
43
+ return realtimeDelivering.value ? SAFETY_POLL_MS : STEPS_POLL_MS;
44
+ }
40
45
  const runs = ref([]);
41
46
  const runsPage = ref(1);
42
47
  const runsLastPage = ref(1);
@@ -54,7 +59,7 @@ async function loadRuns(reset = false) {
54
59
  runsPage.value = res.page;
55
60
  runsLastPage.value = res.lastPage;
56
61
  } catch (e) {
57
- toast.error(e?.message ?? "Could not load the runs.");
62
+ toast.error(e?.message ?? t("integration.replay.toast.loadRunsFailed"));
58
63
  } finally {
59
64
  runsLoading.value = false;
60
65
  }
@@ -92,16 +97,23 @@ async function loadSteps(runId, isPoll = false) {
92
97
  if (runStatus.value && !TERMINAL.has(runStatus.value)) {
93
98
  pollTimer = window.setTimeout(() => {
94
99
  void loadSteps(runId, true);
95
- }, STEPS_POLL_MS);
100
+ }, stepsPollDelay());
96
101
  }
97
102
  } catch (e) {
98
103
  if (selectedRunId.value === runId) {
99
- stepsError.value = e?.message ?? "Could not load the run steps.";
104
+ stepsError.value = e?.message ?? t("integration.replay.error.loadStepsFailed");
100
105
  }
101
106
  } finally {
102
107
  if (!isPoll) stepsLoading.value = false;
103
108
  }
104
109
  }
110
+ watch(runTick, () => {
111
+ const runId = selectedRunId.value;
112
+ if (!runId) return;
113
+ const event = lastRunEvent.value;
114
+ if (event && event.topicId !== runId) return;
115
+ void loadSteps(runId, true);
116
+ });
105
117
  function selectRun(id) {
106
118
  if (selectedRunId.value === id) return;
107
119
  selectedRunId.value = id;
@@ -150,9 +162,7 @@ preview.onPaneReady(() => {
150
162
  preview.updateNodeInternals(vfNodes.value.map((n) => n.id));
151
163
  void preview.fitView({ padding: 0.2, maxZoom: 1 });
152
164
  });
153
- provide("workflow-node-actions", { duplicate: () => {
154
- }, remove: () => {
155
- } });
165
+ provide("workflow-node-actions", null);
156
166
  const dynamicOutputs = ref({});
157
167
  const dynamicOutputNodes = computed(() => props.workflow.blob.nodes.filter((n) => {
158
168
  const m = findManifest(n.nodeSlug, n.nodeVersion);
@@ -176,22 +186,9 @@ const finalStatus = computed(() => {
176
186
  return m;
177
187
  });
178
188
  const runNodeIds = computed(() => new Set(steps.value.map((s) => s.node_id)));
179
- const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t) => t.id)));
189
+ const triggerIds = computed(() => new Set(props.workflow.blob.triggers.map((t2) => t2.id)));
180
190
  function edgeColorByKind(kind) {
181
- switch (kind) {
182
- case "success":
183
- return "#10b981";
184
- case "error":
185
- return "#f43f5e";
186
- case "branch-true":
187
- return "#10b981";
188
- case "branch-false":
189
- return "#f59e0b";
190
- case "branch":
191
- return "#8b5cf6";
192
- default:
193
- return "#94a3b8";
194
- }
191
+ return kind === "error" ? "#cbd5e1" : "#94a3b8";
195
192
  }
196
193
  const GROUP_PAD = 24;
197
194
  const GROUP_LABEL_ROOM = 26;
@@ -246,7 +243,7 @@ const vfNodes = computed(() => {
246
243
  const posOf = (id) => {
247
244
  const wn = blob.nodes.find((n) => n.id === id);
248
245
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
249
- const ti = blob.triggers.findIndex((t) => t.id === id);
246
+ const ti = blob.triggers.findIndex((t2) => t2.id === id);
250
247
  if (ti >= 0) return triggerPos(id, ti);
251
248
  return { x: 0, y: 0 };
252
249
  };
@@ -260,17 +257,17 @@ const vfNodes = computed(() => {
260
257
  zIndex: -100 + i,
261
258
  draggable: false,
262
259
  class: "replay-dim",
263
- data: { sticky: s, onUpdate: () => {
264
- }, onRemove: () => {
265
- } }
260
+ // No callbacks: that absence is what makes the note read-only — it then
261
+ // draws no toolbar, no resize handle and does not open for typing (PO-284).
262
+ data: { sticky: s }
266
263
  })),
267
- ...blob.triggers.map((t, i) => ({
268
- id: t.id,
264
+ ...blob.triggers.map((t2, i) => ({
265
+ id: t2.id,
269
266
  type: "trigger",
270
- position: triggerPos(t.id, i),
267
+ position: triggerPos(t2.id, i),
271
268
  draggable: false,
272
- class: replayNodeClass(t.id, "trigger"),
273
- data: t
269
+ class: replayNodeClass(t2.id, "trigger"),
270
+ data: t2
274
271
  })),
275
272
  ...blob.nodes.map((n) => ({
276
273
  id: n.id,
@@ -290,7 +287,7 @@ const vfEdges = computed(() => {
290
287
  const nodePos = (id) => {
291
288
  const wn = blob.nodes.find((n) => n.id === id);
292
289
  if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
293
- const ti = blob.triggers.findIndex((t) => t.id === id);
290
+ const ti = blob.triggers.findIndex((t2) => t2.id === id);
294
291
  if (ti >= 0) return blob.triggerPositions?.[id] ?? { x: -40, y: 60 + ti * 140 };
295
292
  return { x: 0, y: 0 };
296
293
  };
@@ -304,6 +301,7 @@ const vfEdges = computed(() => {
304
301
  if (output === "true") return "branch-true";
305
302
  if (output === "false") return "branch-false";
306
303
  if (output === "error") return "error";
304
+ if (output === "done") return "success";
307
305
  const outs = [...manifest.outputs ?? [], ...dynamicOutputs.value[sourceNodeId] ?? []];
308
306
  const portDef = outs.find((o) => o.name === output);
309
307
  if (portDef) {
@@ -323,7 +321,7 @@ const vfEdges = computed(() => {
323
321
  const kind = edgeKind(e.from.nodeId, e.from.output);
324
322
  const wn = blob.nodes.find((n) => n.id === e.from.nodeId);
325
323
  const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
326
- const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false", "error"].includes(e.from.output);
324
+ const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(e.from.output);
327
325
  const srcSide = pickSourceSide(e.from.nodeId, e.to.nodeId);
328
326
  const sourceRan = triggerIds.value.has(e.from.nodeId) ? steps.value.length > 0 : runNodeIds.value.has(e.from.nodeId);
329
327
  const traversed = sourceRan && runNodeIds.value.has(e.to.nodeId);
@@ -451,7 +449,6 @@ watch(() => props.initialRunId, (id) => {
451
449
  :nodes-draggable="false"
452
450
  :nodes-connectable="false"
453
451
  :edges-updatable="false"
454
- :pan-on-scroll-mode="PanOnScrollMode.Free"
455
452
  :delete-key-code="null"
456
453
  :min-zoom="0.25"
457
454
  :max-zoom="2"
@@ -474,12 +471,12 @@ watch(() => props.initialRunId, (id) => {
474
471
  <!-- Which run is on the canvas -->
475
472
  <div class="absolute top-3 left-3 z-10 flex items-center gap-2 rounded-lg border border-black/[0.08] bg-white/95 px-2.5 py-1.5 text-sm shadow-sm backdrop-blur-sm">
476
473
  <Play :size="13" class="text-muted-foreground" />
477
- <span class="font-medium">{{ selectedRunId ? `Run #${selectedRunId}` : "Pick a run" }}</span>
474
+ <span class="font-medium">{{ selectedRunId ? t("integration.replay.canvas.runLabel", { id: selectedRunId }) : t("integration.replay.canvas.pickRun") }}</span>
478
475
  <Badge v-if="runStatus" variant="outline" class="border text-xs font-medium uppercase" :class="statusClass(runStatus)">
479
476
  {{ runStatusLabel(runStatus) }}
480
477
  </Badge>
481
478
  <Badge v-if="stepsSource" variant="outline" class="border text-xs font-medium uppercase text-muted-foreground">
482
- {{ stepsSource === "temporal" ? "Live" : "Snapshot" }}
479
+ {{ stepsSource === "temporal" ? t("integration.replay.badge.live") : t("integration.replay.badge.snapshot") }}
483
480
  </Badge>
484
481
  <Loader2 v-if="stepsLoading" :size="13" class="animate-spin text-muted-foreground" />
485
482
  </div>
@@ -495,14 +492,14 @@ watch(() => props.initialRunId, (id) => {
495
492
  <div class="flex items-center gap-1">
496
493
  <Button variant="ghost" size="sm" class="h-7" :disabled="safeCursor === 0" @click="seek(safeCursor - 1)">
497
494
  <Prev :size="13" />
498
- Prev
495
+ {{ t("integration.replay.transport.action.prev") }}
499
496
  </Button>
500
497
  <Button size="sm" class="h-7" @click="togglePlay">
501
498
  <component :is="playing ? Pause : Play" :size="13" class="mr-1" />
502
- {{ playing ? "Pause" : "Play" }}
499
+ {{ playing ? t("integration.replay.transport.action.pause") : t("integration.replay.transport.action.play") }}
503
500
  </Button>
504
501
  <Button variant="ghost" size="sm" class="h-7" :disabled="safeCursor >= steps.length - 1" @click="seek(safeCursor + 1)">
505
- Next
502
+ {{ t("integration.replay.transport.action.next") }}
506
503
  <Next :size="13" />
507
504
  </Button>
508
505
  </div>
@@ -513,17 +510,17 @@ watch(() => props.initialRunId, (id) => {
513
510
  :max="steps.length - 1"
514
511
  :value="safeCursor"
515
512
  class="h-1.5 flex-1 cursor-pointer accent-primary"
516
- aria-label="Replay step"
513
+ :aria-label="t('integration.replay.transport.scrubberAria')"
517
514
  @input="seek(Number($event.target.value))"
518
515
  >
519
516
 
520
517
  <span class="shrink-0 text-sm tabular-nums text-muted-foreground">
521
- Step {{ safeCursor + 1 }}/{{ steps.length }}
518
+ {{ t("integration.replay.transport.stepCounter", { current: safeCursor + 1, total: steps.length }) }}
522
519
  <span class="font-mono">· {{ activeStepTitle }}</span>
523
520
  </span>
524
521
  </template>
525
522
  <p v-else-if="!stepsLoading" class="text-sm text-muted-foreground">
526
- {{ selectedRunId ? "No steps recorded for this run." : "Pick a run on the right to replay it." }}
523
+ {{ selectedRunId ? t("integration.replay.state.noSteps") : t("integration.replay.state.pickRunHint") }}
527
524
  </p>
528
525
  </div>
529
526
  </div>
@@ -533,10 +530,10 @@ watch(() => props.initialRunId, (id) => {
533
530
  <aside class="flex w-[32rem] shrink-0 flex-col border-l border-black/[0.06] bg-white">
534
531
  <div class="flex items-center gap-2 border-b border-black/[0.06] px-3 py-2">
535
532
  <div class="min-w-0 flex-1">
536
- <div class="truncate font-mono text-sm font-medium">{{ activeStepTitle || "Debug" }}</div>
533
+ <div class="truncate font-mono text-sm font-medium">{{ activeStepTitle || t("integration.replay.debug.titleFallback") }}</div>
537
534
  <div class="text-xs tabular-nums text-muted-foreground">
538
- <template v-if="steps.length">Step {{ safeCursor + 1 }} / {{ steps.length }}</template>
539
- <template v-else>No step selected</template>
535
+ <template v-if="steps.length">{{ t("integration.replay.debug.stepCounter", { current: safeCursor + 1, total: steps.length }) }}</template>
536
+ <template v-else>{{ t("integration.replay.debug.noStepSelected") }}</template>
540
537
  </div>
541
538
  </div>
542
539
  <Badge v-if="activeStep" variant="outline" class="shrink-0 border text-xs font-medium uppercase" :class="statusClass(activeStep.status)">
@@ -547,59 +544,59 @@ watch(() => props.initialRunId, (id) => {
547
544
  <div v-if="activeStep" class="min-h-0 flex-1 space-y-4 overflow-y-auto p-3">
548
545
  <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
549
546
  <div>
550
- <div class="text-mono text-muted-foreground">Attempt</div>
547
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.attempt") }}</div>
551
548
  <div class="tabular-nums">{{ activeStep.attempt + 1 }}</div>
552
549
  </div>
553
550
  <div>
554
- <div class="text-mono text-muted-foreground">Duration</div>
551
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.duration") }}</div>
555
552
  <div class="tabular-nums">{{ formatDuration(activeStep.duration_ms) }}</div>
556
553
  </div>
557
554
  <div>
558
- <div class="text-mono text-muted-foreground">Started</div>
555
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.started") }}</div>
559
556
  <div class="tabular-nums">{{ formatTime(activeStep.started_at) }}</div>
560
557
  </div>
561
558
  <div>
562
- <div class="text-mono text-muted-foreground">Completed</div>
559
+ <div class="text-mono text-muted-foreground">{{ t("integration.replay.debug.field.completed") }}</div>
563
560
  <div class="tabular-nums">{{ formatTime(activeStep.completed_at) }}</div>
564
561
  </div>
565
562
  </div>
566
563
 
567
564
  <div v-if="activeStep.branch" class="text-sm">
568
- <div class="text-mono text-muted-foreground mb-1">Branch</div>
565
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.branch") }}</div>
569
566
  <code class="rounded bg-black/[0.05] px-1.5 py-0.5 font-mono">{{ activeStep.branch }}</code>
570
567
  </div>
571
568
 
572
569
  <div v-if="activeStep.error" class="text-sm">
573
- <div class="text-mono text-muted-foreground mb-1">Error</div>
570
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.error") }}</div>
574
571
  <pre class="max-h-40 overflow-auto rounded-md border border-rose-500/20 bg-rose-500/5 p-2 font-mono text-sm text-rose-700">{{ activeStep.error }}</pre>
575
572
  </div>
576
573
 
577
574
  <div v-if="activeStep.input !== void 0 && activeStep.input !== null" class="text-sm">
578
- <div class="text-mono text-muted-foreground mb-1">Input</div>
575
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.input") }}</div>
579
576
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.input) }}</pre>
580
577
  </div>
581
578
 
582
579
  <div v-if="activeStep.config !== void 0 && activeStep.config !== null" class="text-sm">
583
- <div class="text-mono text-muted-foreground mb-1">Config</div>
580
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.config") }}</div>
584
581
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.config) }}</pre>
585
582
  </div>
586
583
 
587
584
  <div v-if="activeStep.output !== void 0 && activeStep.output !== null" class="text-sm">
588
- <div class="text-mono text-muted-foreground mb-1">Output</div>
585
+ <div class="text-mono text-muted-foreground mb-1">{{ t("integration.replay.debug.field.output") }}</div>
589
586
  <pre class="max-h-56 overflow-auto rounded-md border border-black/[0.06] bg-muted/20 p-2 font-mono text-sm">{{ pretty(activeStep.output) }}</pre>
590
587
  </div>
591
588
  </div>
592
589
  <p v-else class="p-4 text-sm text-muted-foreground">
593
- The active step's status, timings and payloads land here while the replay walks the run.
590
+ {{ t("integration.replay.debug.emptyHint") }}
594
591
  </p>
595
592
  </aside>
596
593
 
597
594
  <!-- Run list — hidden in single mode (the run detail replays ONE run). -->
598
595
  <aside v-if="!single" class="flex w-80 shrink-0 flex-col border-l border-black/[0.06] bg-white">
599
596
  <div class="border-b border-black/[0.06] px-3 py-2">
600
- <div class="text-sm font-semibold">Runs</div>
597
+ <div class="text-sm font-semibold">{{ t("integration.replay.runsList.heading") }}</div>
601
598
  <p class="text-sm text-muted-foreground">
602
- Click through the executions to replay them on the canvas.
599
+ {{ t("integration.replay.runsList.description") }}
603
600
  </p>
604
601
  </div>
605
602
 
@@ -628,7 +625,7 @@ watch(() => props.initialRunId, (id) => {
628
625
  </button>
629
626
 
630
627
  <p v-if="!runs.length && !runsLoading" class="px-2 py-6 text-center text-sm text-muted-foreground">
631
- No runs yet — trigger one from the editor's Run button.
628
+ {{ t("integration.replay.runsList.empty") }}
632
629
  </p>
633
630
 
634
631
  <div v-if="runsLoading" class="flex justify-center py-3">
@@ -641,7 +638,7 @@ watch(() => props.initialRunId, (id) => {
641
638
  class="w-full text-sm"
642
639
  @click="loadMoreRuns"
643
640
  >
644
- Load older runs
641
+ {{ t("integration.replay.runsList.loadMore") }}
645
642
  </Button>
646
643
  </div>
647
644
  </aside>
@@ -9,6 +9,7 @@ import {
9
9
  DialogHeader,
10
10
  DialogTitle,
11
11
  Input,
12
+ Label,
12
13
  Select,
13
14
  SelectContent,
14
15
  SelectItem,
@@ -28,6 +29,7 @@ const props = defineProps({
28
29
  tagSuggestions: { type: Array, required: false }
29
30
  });
30
31
  const emit = defineEmits(["update:open", "change-mode", "save-meta"]);
32
+ const t = useIntegrationsT();
31
33
  const nameDraft = ref("");
32
34
  const descriptionDraft = ref("");
33
35
  const tagsDraft = ref([]);
@@ -53,23 +55,25 @@ function formatDateTime(iso) {
53
55
  <Dialog :open="open" @update:open="(v) => emit('update:open', v)">
54
56
  <DialogContent class="sm:max-w-lg">
55
57
  <DialogHeader>
56
- <DialogTitle>Workflow settings</DialogTitle>
58
+ <DialogTitle>{{ t("integration.workflows.settings.dialog.title") }}</DialogTitle>
57
59
  <DialogDescription class="text-sm">
58
- Execution behavior and metadata for “{{ workflow.name }}”.
60
+ {{ t("integration.workflows.settings.dialog.description", { name: workflow.name }) }}
59
61
  </DialogDescription>
60
62
  </DialogHeader>
61
63
 
62
64
  <div class="space-y-4 text-sm">
63
65
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
64
- <div class="text-mono text-muted-foreground">Execution</div>
66
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.execution") }}</div>
65
67
  <div class="flex items-center gap-2">
66
68
  <Select
67
69
  :model-value="modeDraft"
68
70
  :disabled="savingMode"
69
71
  @update:model-value="(v) => emit('change-mode', v)"
70
72
  >
71
- <SelectTrigger class="h-8 w-full bg-white text-sm">
72
- <SelectValue placeholder="Pick a mode" />
73
+ <!-- Named explicitly: the section caption above says "Execution",
74
+ which is not a name the control itself carries. -->
75
+ <SelectTrigger :aria-label="t('integration.workflows.settings.field.executionMode.aria')" class="h-8 w-full bg-white text-sm">
76
+ <SelectValue :placeholder="t('integration.workflows.settings.field.executionMode.placeholder')" />
73
77
  </SelectTrigger>
74
78
  <SelectContent>
75
79
  <SelectItem v-for="m in modes" :key="m.value" :value="m.value">
@@ -85,22 +89,25 @@ function formatDateTime(iso) {
85
89
  </div>
86
90
 
87
91
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-3">
88
- <div class="text-mono text-muted-foreground">Appearance</div>
92
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.appearance") }}</div>
93
+ <!-- Real labels, not styled captions: each one names its own control
94
+ (`for`/`id`), otherwise a screen reader reads four unlabelled
95
+ fields in a row. -->
89
96
  <div class="space-y-1.5">
90
- <div class="text-sm text-muted-foreground">Name</div>
91
- <Input v-model="nameDraft" class="bg-white" placeholder="Workflow name" />
97
+ <Label for="wf-set-name" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.name.label") }}</Label>
98
+ <Input id="wf-set-name" v-model="nameDraft" class="bg-white" :placeholder="t('integration.workflows.settings.field.name.placeholder')" />
92
99
  </div>
93
100
  <div class="space-y-1.5">
94
- <div class="text-sm text-muted-foreground">Description</div>
95
- <Textarea v-model="descriptionDraft" rows="2" class="bg-white" placeholder="What this workflow does and why it exists." />
101
+ <Label for="wf-set-desc" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.description.label") }}</Label>
102
+ <Textarea id="wf-set-desc" v-model="descriptionDraft" rows="2" class="bg-white" :placeholder="t('integration.workflows.settings.field.description.placeholder')" />
96
103
  </div>
97
104
  <div class="space-y-1.5">
98
- <div class="text-sm text-muted-foreground">Tags</div>
99
- <WorkflowTagsInput v-model="tagsDraft" :suggestions="tagSuggestions ?? []" />
105
+ <Label for="wf-set-tags" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.tags.label") }}</Label>
106
+ <WorkflowTagsInput v-model="tagsDraft" input-id="wf-set-tags" :suggestions="tagSuggestions ?? []" />
100
107
  </div>
101
108
  <div class="space-y-1.5">
102
- <div class="text-sm text-muted-foreground">Icon</div>
103
- <WorkflowIconPicker v-model="iconDraft" />
109
+ <Label id="wf-set-icon-label" class="text-sm font-normal text-muted-foreground">{{ t("integration.workflows.settings.field.icon.label") }}</Label>
110
+ <WorkflowIconPicker v-model="iconDraft" trigger-labelledby="wf-set-icon-label" />
104
111
  </div>
105
112
  <div class="flex justify-end">
106
113
  <Button
@@ -110,23 +117,23 @@ function formatDateTime(iso) {
110
117
  @click="emit('save-meta', { name: nameDraft.trim(), description: descriptionDraft.trim(), tags: tagsDraft, icon: iconDraft })"
111
118
  >
112
119
  <Loader2 v-if="savingMeta" :size="12" class="animate-spin" />
113
- Save changes
120
+ {{ t("integration.workflows.settings.action.save") }}
114
121
  </Button>
115
122
  </div>
116
123
  </div>
117
124
 
118
125
  <div class="rounded-lg border border-black/[0.06] bg-white p-4 space-y-2">
119
- <div class="text-mono text-muted-foreground">Metadata</div>
126
+ <div class="text-mono text-muted-foreground">{{ t("integration.workflows.settings.section.metadata") }}</div>
120
127
  <div class="grid grid-cols-[110px_1fr] gap-x-3 gap-y-2">
121
- <span class="text-muted-foreground">ID</span>
128
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.id") }}</span>
122
129
  <span class="font-mono">{{ workflow.id }}</span>
123
- <span class="text-muted-foreground">Definition</span>
130
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.definition") }}</span>
124
131
  <span class="font-mono">{{ workflow.blob_definition_version }}</span>
125
- <span class="text-muted-foreground">Revision</span>
132
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.revision") }}</span>
126
133
  <span class="font-mono">r{{ workflow.revision }}</span>
127
- <span class="text-muted-foreground">Created</span>
134
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.created") }}</span>
128
135
  <span>{{ formatDateTime(workflow.created_at) }}</span>
129
- <span class="text-muted-foreground">Updated</span>
136
+ <span class="text-muted-foreground">{{ t("integration.workflows.settings.meta.updated") }}</span>
130
137
  <span>{{ formatDateTime(workflow.updated_at) }}</span>
131
138
  </div>
132
139
  </div>
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  suggestions?: string[];
3
+ inputId?: string;
3
4
  };
4
5
  type __VLS_PublicProps = __VLS_Props & {
5
6
  modelValue?: string[];
@@ -2,8 +2,10 @@
2
2
  import { ref, useId } from "vue";
3
3
  const model = defineModel({ type: Array, ...{ default: () => [] } });
4
4
  const props = defineProps({
5
- suggestions: { type: Array, required: false, default: () => [] }
5
+ suggestions: { type: Array, required: false, default: () => [] },
6
+ inputId: { type: String, required: false }
6
7
  });
8
+ const t = useIntegrationsT();
7
9
  const draft = ref("");
8
10
  const inputRef = ref(null);
9
11
  const listId = useId();
@@ -14,7 +16,7 @@ function commit() {
14
16
  if (!model.value.includes(tag)) model.value = [...model.value, tag];
15
17
  }
16
18
  function remove(tag) {
17
- model.value = model.value.filter((t) => t !== tag);
19
+ model.value = model.value.filter((t2) => t2 !== tag);
18
20
  }
19
21
  function onKeydown(e) {
20
22
  if (e.key === "Enter" || e.key === ",") {
@@ -28,31 +30,35 @@ function onKeydown(e) {
28
30
 
29
31
  <template>
30
32
  <div
31
- class="flex min-h-9 cursor-text flex-wrap items-center gap-1 rounded-md border border-black/[0.08] bg-white px-2 py-1.5"
33
+ class="flex min-h-9 cursor-text flex-wrap items-center gap-1 rounded-md border border-black/[0.08] bg-white px-2 py-1.5 focus-within:ring-2 focus-within:ring-primary/40"
32
34
  @click="inputRef?.focus()"
33
35
  >
34
36
  <span
35
- v-for="t in model"
36
- :key="t"
37
+ v-for="tag in model"
38
+ :key="tag"
37
39
  class="inline-flex items-center gap-1 rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs text-foreground"
38
40
  >
39
- {{ t }}
41
+ {{ tag }}
40
42
  <button
41
43
  type="button"
42
44
  class="text-muted-foreground transition hover:text-foreground"
43
- :title="`Remove ${t}`"
44
- @click.stop="remove(t)"
45
+ :title="t('integration.workflows.tagsInput.action.remove', { tag })"
46
+ :aria-label="t('integration.workflows.tagsInput.action.remove', { tag })"
47
+ @click.stop="remove(tag)"
45
48
  >
46
49
  ×
47
50
  </button>
48
51
  </span>
52
+ <!-- No ring of its own: the wrapper above is the field the user sees, and
53
+ it carries the focus ring via focus-within. -->
49
54
  <input
55
+ :id="props.inputId"
50
56
  ref="inputRef"
51
57
  v-model="draft"
52
58
  type="text"
53
59
  :list="listId"
54
60
  class="h-6 min-w-24 flex-1 bg-transparent text-sm focus:outline-none"
55
- :placeholder="model.length ? 'Add tag\u2026' : 'Add tags \u2014 Enter confirms\u2026'"
61
+ :placeholder="model.length ? t('integration.workflows.tagsInput.placeholder.add') : t('integration.workflows.tagsInput.placeholder.addFirst')"
56
62
  @keydown="onKeydown"
57
63
  @blur="commit()"
58
64
  >
@@ -1,5 +1,6 @@
1
1
  type __VLS_Props = {
2
2
  suggestions?: string[];
3
+ inputId?: string;
3
4
  };
4
5
  type __VLS_PublicProps = __VLS_Props & {
5
6
  modelValue?: string[];
@@ -2,7 +2,10 @@
2
2
  import { computed, ref } from "vue";
3
3
  import { Handle, Position, useVueFlow } from "@vue-flow/core";
4
4
  import { NodeToolbar } from "@vue-flow/node-toolbar";
5
- import { ClockCircle as Clock, Play, TrashBinMinimalistic as Trash2, LinkRound as Webhook, Bolt } from "#solar-icons";
5
+ import { ClockCircle as Clock, Play, TrashBinMinimalistic as TrashBin, LinkRound as Webhook, Bolt } from "#solar-icons";
6
+ import { resolveDisplayName } from "../../utils/localize";
7
+ import { triggerTypeLabel } from "../../data/integrationNodeCatalog";
8
+ import { describeCron } from "../../utils/schedule";
6
9
  const props = defineProps({
7
10
  id: { type: String, required: true },
8
11
  data: { type: Object, required: true },
@@ -10,20 +13,21 @@ const props = defineProps({
10
13
  });
11
14
  defineOptions({ inheritAttrs: false });
12
15
  defineEmits(["updateNodeInternals"]);
16
+ const t = useIntegrationsT();
13
17
  const icon = computed(() => {
14
18
  return props.data.type === "schedule" ? Clock : props.data.type === "webhook" ? Webhook : props.data.type === "event" ? Bolt : Play;
15
19
  });
16
20
  const subtitle = computed(() => {
17
21
  const c = props.data.config;
18
- if (props.data.type === "schedule") return c.cron ?? "no cron set";
22
+ if (props.data.type === "schedule") return describeCron(c.cron) ?? c.cron ?? t("integration.canvas.trigger.subtitle.noSchedule");
19
23
  if (props.data.type === "webhook") return `${c.method ?? "POST"} ${c.path ?? "/"}`;
20
- if (props.data.type === "event") return c.event_type ? `on ${c.event_type}` : "no event set";
21
- return "On demand";
24
+ if (props.data.type === "event") return c.event_type ? t("integration.canvas.trigger.subtitle.onEvent", { event: c.event_type }) : t("integration.canvas.trigger.subtitle.noEvent");
25
+ return t("integration.canvas.trigger.subtitle.onDemand");
22
26
  });
23
27
  const title = computed(() => {
24
28
  const n = props.data.name;
25
- if (!n) return props.data.type.charAt(0).toUpperCase() + props.data.type.slice(1);
26
- return typeof n === "string" ? n : n.en ?? Object.values(n)[0];
29
+ if (!n) return triggerTypeLabel(props.data.type);
30
+ return resolveDisplayName(n);
27
31
  });
28
32
  const hovered = ref(false);
29
33
  let hoverTimer = null;
@@ -52,10 +56,11 @@ function sideToPosition(s) {
52
56
  <button
53
57
  type="button"
54
58
  class="flex h-6 items-center gap-1 rounded px-1.5 text-sm text-rose-600 hover:bg-rose-50"
55
- title="Delete"
59
+ :title="t('integration.common.action.delete')"
60
+ :aria-label="t('integration.canvas.trigger.action.deleteAria')"
56
61
  @click="remove"
57
62
  >
58
- <Trash2 :size="11" />
63
+ <TrashBin :size="11" />
59
64
  </button>
60
65
  </NodeToolbar>
61
66
 
@@ -84,8 +89,8 @@ function sideToPosition(s) {
84
89
  </div>
85
90
  </div>
86
91
  <div class="flex items-center justify-between border-t border-emerald-500/15 bg-emerald-500/[0.04] rounded-b-md px-3 py-1 text-mono text-emerald-700/80 font-semibold">
87
- <span>Trigger · {{ data.type }}</span>
88
- <span v-if="data.active === false" class="text-amber-600">Inactive</span>
92
+ <span>{{ t("integration.canvas.badge.trigger") }} · {{ data.type }}</span>
93
+ <span v-if="data.active === false" class="text-amber-600">{{ t("integration.canvas.trigger.badge.inactive") }}</span>
89
94
  </div>
90
95
  </div>
91
96