@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
@@ -29,18 +29,19 @@ const props = defineProps({
29
29
  type: { type: [Object, null], required: true }
30
30
  });
31
31
  const emit = defineEmits(["update:active", "apply"]);
32
+ const t = useIntegrationsT();
32
33
  const kind = computed(() => props.type ? REVENEXX_SOURCE_KIND_BY_SLUG[props.type.slug] ?? null : null);
33
34
  const uid = useId();
34
- const SWITCH_LABEL = {
35
- sftp: "Use revenexx SFTP Servers",
36
- smtp: "Use revenexx Mail Servers",
37
- api: "Use revenexx API Keys"
38
- };
39
- const NEW_LABEL = {
40
- sftp: "New SFTP account",
41
- smtp: "New mail credential",
42
- api: "New API key"
43
- };
35
+ const SWITCH_LABEL = computed(() => ({
36
+ sftp: t("integration.credentials.revenexxSource.switch.sftp"),
37
+ smtp: t("integration.credentials.revenexxSource.switch.smtp"),
38
+ api: t("integration.credentials.revenexxSource.switch.api")
39
+ }));
40
+ const NEW_LABEL = computed(() => ({
41
+ sftp: t("integration.credentials.revenexxSource.new.sftp"),
42
+ smtp: t("integration.credentials.revenexxSource.new.smtp"),
43
+ api: t("integration.credentials.revenexxSource.new.api")
44
+ }));
44
45
  const sources = cockpitSources();
45
46
  const available = sources !== null;
46
47
  const tenants = computed(() => sources?.tenants.tenants.value ?? []);
@@ -74,7 +75,7 @@ watch(kind, () => {
74
75
  });
75
76
  const sftpOptions = computed(() => (sftp?.accounts.value ?? []).filter((a) => (a.is_active || a.status === "active") && a.login_username).map((a) => ({ value: String(a.id), label: a.login_username })));
76
77
  const marketOptions = computed(() => [
77
- { code: GLOBAL_MARKET_CODE, label: "Global (tenant baseline)" },
78
+ { code: GLOBAL_MARKET_CODE, label: t("integration.credentials.revenexxSource.market.global") },
78
79
  ...markets.value.map((m) => ({ code: m.code, label: `${m.name} (${m.code})` }))
79
80
  ]);
80
81
  async function ensureLists() {
@@ -92,7 +93,7 @@ async function ensureLists() {
92
93
  await fetchMarkets();
93
94
  }
94
95
  } catch (e) {
95
- toast.error(e?.message ?? "Could not load revenexx resources.");
96
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.loadResources"));
96
97
  } finally {
97
98
  loadingList.value = false;
98
99
  }
@@ -105,14 +106,14 @@ function marketSlug() {
105
106
  }
106
107
  async function applySftp() {
107
108
  if (!sftp) return;
108
- const t = tenant.value;
109
+ const tenantSlug = tenant.value;
109
110
  const acc = sftp.accounts.value.find((a) => String(a.id) === selectedId.value);
110
- if (!t || !acc) return;
111
+ if (!tenantSlug || !acc) return;
111
112
  applying.value = true;
112
113
  try {
113
- const creds = await sftp.revealPassword(t, acc.id);
114
+ const creds = await sftp.revealPassword(tenantSlug, acc.id);
114
115
  if (!creds) {
115
- toast.error("Account still provisioning \u2014 try again in a moment.");
116
+ toast.error(t("integration.credentials.revenexxSource.error.accountProvisioning"));
116
117
  return;
117
118
  }
118
119
  emit("apply", {
@@ -123,7 +124,7 @@ async function applySftp() {
123
124
  });
124
125
  applied.value = `${acc.login_username}@${sftpHost}:${sftpPort}`;
125
126
  } catch (e) {
126
- toast.error(e?.message ?? "Could not read the SFTP password.");
127
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readSftpPassword"));
127
128
  } finally {
128
129
  applying.value = false;
129
130
  }
@@ -134,7 +135,7 @@ async function applySmtp() {
134
135
  try {
135
136
  const cred = await mail.getCredential(selectedId.value);
136
137
  if (!cred) {
137
- toast.error("That mail credential is no longer there.");
138
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialGone"));
138
139
  return;
139
140
  }
140
141
  emit("apply", {
@@ -146,7 +147,7 @@ async function applySmtp() {
146
147
  });
147
148
  applied.value = `${cred.smtp_username}@${mailSmtpHost}:${mailSmtpPort}`;
148
149
  } catch (e) {
149
- toast.error(e?.message ?? "Could not read the mail credential.");
150
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.readMailCredential"));
150
151
  } finally {
151
152
  applying.value = false;
152
153
  }
@@ -178,30 +179,30 @@ function openCreate() {
178
179
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
179
180
  async function createSftp() {
180
181
  if (!sftp) return;
181
- const t = tenant.value;
182
- if (!t) {
183
- toast.error("No tenant selected.");
182
+ const tenantSlug = tenant.value;
183
+ if (!tenantSlug) {
184
+ toast.error(t("integration.credentials.revenexxSource.error.noTenant"));
184
185
  return;
185
186
  }
186
187
  creating.value = true;
187
188
  try {
188
- let acc = await sftp.create(t, createName.value.trim() || void 0);
189
+ let acc = await sftp.create(tenantSlug, createName.value.trim() || void 0);
189
190
  for (let i = 0; i < 24 && !(acc.is_active || acc.status === "active"); i++) {
190
- provisioningNote.value = "Provisioning the account\u2026";
191
+ provisioningNote.value = t("integration.credentials.revenexxSource.status.provisioning");
191
192
  await sleep(2500);
192
- acc = await sftp.refresh(t, acc.id);
193
- if (acc.status === "failed") throw new Error(acc.error_message || "Provisioning failed.");
193
+ acc = await sftp.refresh(tenantSlug, acc.id);
194
+ if (acc.status === "failed") throw new Error(acc.error_message || t("integration.credentials.revenexxSource.error.provisioningFailed"));
194
195
  }
195
196
  if (!(acc.is_active || acc.status === "active")) {
196
- throw new Error("Still provisioning \u2014 pick it from the list once it turns active.");
197
+ throw new Error(t("integration.credentials.revenexxSource.error.stillProvisioning"));
197
198
  }
198
- await sftp.load(t);
199
+ await sftp.load(tenantSlug);
199
200
  selectedId.value = String(acc.id);
200
201
  await applySftp();
201
202
  createOpen.value = false;
202
- toast.success("SFTP account created");
203
+ toast.success(t("integration.credentials.revenexxSource.toast.sftpCreated"));
203
204
  } catch (e) {
204
- toast.error(e?.message ?? "Could not create the SFTP account.");
205
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSftp"));
205
206
  } finally {
206
207
  creating.value = false;
207
208
  provisioningNote.value = "";
@@ -213,7 +214,7 @@ async function createSmtp() {
213
214
  try {
214
215
  const cred = await mail.createCredential({ name: createName.value.trim() || "SMTP", type: "SMTP" });
215
216
  if (!cred) {
216
- toast.error("The mail credential was not returned \u2014 pick it from the list once it appears.");
217
+ toast.error(t("integration.credentials.revenexxSource.error.mailCredentialNotReturned"));
217
218
  return;
218
219
  }
219
220
  emit("apply", {
@@ -227,9 +228,9 @@ async function createSmtp() {
227
228
  smtpCreds.value = [{ uuid: cred.uuid, name: cred.name }, ...smtpCreds.value];
228
229
  selectedId.value = cred.uuid;
229
230
  createOpen.value = false;
230
- toast.success("Mail credential created");
231
+ toast.success(t("integration.credentials.revenexxSource.toast.smtpCreated"));
231
232
  } catch (e) {
232
- toast.error(e?.message ?? "Could not create the mail credential.");
233
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createSmtp"));
233
234
  } finally {
234
235
  creating.value = false;
235
236
  }
@@ -241,11 +242,11 @@ async function createApiKey() {
241
242
  const { secret } = await consoleKeys.create(createName.value.trim() || "Integrations key", ["*"]);
242
243
  emit("apply", { apiKey: secret, tenant: tenant.value ?? "", market: marketSlug() });
243
244
  apiKeyReady.value = true;
244
- applied.value = `Key for ${tenant.value}${marketSlug() ? ` \xB7 ${marketSlug()}` : " \xB7 global"}`;
245
+ applied.value = marketSlug() ? t("integration.credentials.revenexxSource.status.keyForMarket", { tenant: tenant.value, market: marketSlug() }) : t("integration.credentials.revenexxSource.status.keyForGlobal", { tenant: tenant.value });
245
246
  createOpen.value = false;
246
- toast.success("API key created");
247
+ toast.success(t("integration.credentials.revenexxSource.toast.apiKeyCreated"));
247
248
  } catch (e) {
248
- toast.error(e?.message ?? "Could not create the API key.");
249
+ toast.error(e?.message ?? t("integration.credentials.revenexxSource.error.createApiKey"));
249
250
  } finally {
250
251
  creating.value = false;
251
252
  }
@@ -269,11 +270,10 @@ function submitCreate() {
269
270
  belongs somewhere else. The manual fields stay on screen either way. -->
270
271
  <p class="text-xs text-muted-foreground">
271
272
  <template v-if="available">
272
- Reuse an existing revenexx resource instead of typing access data by hand.
273
+ {{ t("integration.credentials.revenexxSource.hint.available") }}
273
274
  </template>
274
275
  <template v-else>
275
- Not available here — the managed revenexx resources live in the cockpit.
276
- Enter the access data by hand instead.
276
+ {{ t("integration.credentials.revenexxSource.hint.unavailable") }}
277
277
  </template>
278
278
  </p>
279
279
  </div>
@@ -291,14 +291,14 @@ function submitCreate() {
291
291
  <div v-if="active" class="mt-3 space-y-3">
292
292
  <!-- Tenant (preselected) -->
293
293
  <div class="space-y-1.5">
294
- <Label :for="`${uid}-tenant`">Tenant</Label>
294
+ <Label :for="`${uid}-tenant`">{{ t("integration.credentials.revenexxSource.field.tenant.label") }}</Label>
295
295
  <Select :model-value="tenant ?? ''" @update:model-value="(v) => tenant = v">
296
296
  <SelectTrigger :id="`${uid}-tenant`" class="bg-white">
297
- <SelectValue placeholder="Select tenant" />
297
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.tenant.placeholder')" />
298
298
  </SelectTrigger>
299
299
  <SelectContent>
300
- <SelectItem v-for="t in tenants" :key="t.slug" :value="t.slug">
301
- {{ t.name }} ({{ t.slug }})
300
+ <SelectItem v-for="tn in tenants" :key="tn.slug" :value="tn.slug">
301
+ {{ tn.name }} ({{ tn.slug }})
302
302
  </SelectItem>
303
303
  </SelectContent>
304
304
  </Select>
@@ -306,10 +306,10 @@ function submitCreate() {
306
306
 
307
307
  <!-- Market scope — only the Revenexx API credential is market-scoped -->
308
308
  <div v-if="kind === 'api'" class="space-y-1.5">
309
- <Label :for="`${uid}-market`">Market</Label>
309
+ <Label :for="`${uid}-market`">{{ t("integration.credentials.revenexxSource.field.market.label") }}</Label>
310
310
  <Select :model-value="marketCode" @update:model-value="(v) => marketCode = v">
311
311
  <SelectTrigger :id="`${uid}-market`" class="bg-white">
312
- <SelectValue placeholder="Select market" />
312
+ <SelectValue :placeholder="t('integration.credentials.revenexxSource.field.market.placeholder')" />
313
313
  </SelectTrigger>
314
314
  <SelectContent>
315
315
  <SelectItem v-for="m in marketOptions" :key="m.code" :value="m.code">
@@ -321,11 +321,11 @@ function submitCreate() {
321
321
 
322
322
  <!-- Resource select (SFTP / Mail) -->
323
323
  <div v-if="kind === 'sftp' || kind === 'smtp'" class="space-y-1.5">
324
- <Label :for="`${uid}-resource`">{{ kind === "sftp" ? "SFTP account" : "Mail credential" }}</Label>
324
+ <Label :for="`${uid}-resource`">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.field.resource.sftpLabel") : t("integration.credentials.revenexxSource.field.resource.mailLabel") }}</Label>
325
325
  <div class="flex items-center gap-2">
326
326
  <Select :model-value="selectedId" class="flex-1" @update:model-value="(v) => onSelect(v)">
327
327
  <SelectTrigger :id="`${uid}-resource`" class="bg-white">
328
- <SelectValue :placeholder="loadingList ? 'Loading\u2026' : 'Select one'" />
328
+ <SelectValue :placeholder="loadingList ? t('integration.common.state.loading') : t('integration.credentials.revenexxSource.field.resource.placeholder')" />
329
329
  </SelectTrigger>
330
330
  <SelectContent>
331
331
  <template v-if="kind === 'sftp'">
@@ -338,7 +338,7 @@ function submitCreate() {
338
338
  </Select>
339
339
  <Button variant="outline" size="sm" class="shrink-0 bg-white" :disabled="!tenant" @click="openCreate">
340
340
  <Plus :size="14" class="mr-1" />
341
- New
341
+ {{ t("integration.credentials.revenexxSource.action.new") }}
342
342
  </Button>
343
343
  </div>
344
344
  </div>
@@ -347,7 +347,7 @@ function submitCreate() {
347
347
  <div v-else-if="kind === 'api'">
348
348
  <Button variant="outline" size="sm" class="bg-white" :disabled="!tenant" @click="openCreate">
349
349
  <Plus :size="14" class="mr-1" />
350
- Create new API key
350
+ {{ t("integration.credentials.revenexxSource.action.createApiKey") }}
351
351
  </Button>
352
352
  </div>
353
353
 
@@ -355,7 +355,7 @@ function submitCreate() {
355
355
  <div v-if="applied || applying" class="flex items-center gap-2 rounded-md bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
356
356
  <Loader2 v-if="applying" :size="14" class="animate-spin" />
357
357
  <Check v-else :size="14" />
358
- <span class="truncate">{{ applying ? "Copying access data\u2026" : `Using ${applied}` }}</span>
358
+ <span class="truncate">{{ applying ? t("integration.credentials.revenexxSource.status.copying") : t("integration.credentials.revenexxSource.status.using", { value: applied }) }}</span>
359
359
  </div>
360
360
  </div>
361
361
 
@@ -363,36 +363,36 @@ function submitCreate() {
363
363
  <Dialog v-model:open="createOpen">
364
364
  <DialogContent class="sm:max-w-md">
365
365
  <DialogHeader>
366
- <DialogTitle>{{ kind ? NEW_LABEL[kind] : "New" }}</DialogTitle>
366
+ <DialogTitle>{{ kind ? NEW_LABEL[kind] : t("integration.credentials.revenexxSource.new.fallback") }}</DialogTitle>
367
367
  <DialogDescription class="text-sm">
368
368
  <template v-if="kind === 'api'">
369
- A full-access key is minted for <b>{{ tenant }}</b> and copied straight into this credential.
369
+ {{ t("integration.credentials.revenexxSource.createDialog.description.api", { tenant }) }}
370
370
  </template>
371
371
  <template v-else-if="kind === 'sftp'">
372
- Reserves a new SFTP account for <b>{{ tenant }}</b> and fills its login here.
372
+ {{ t("integration.credentials.revenexxSource.createDialog.description.sftp", { tenant }) }}
373
373
  </template>
374
374
  <template v-else>
375
- Creates a new SMTP credential and fills its login here.
375
+ {{ t("integration.credentials.revenexxSource.createDialog.description.smtp") }}
376
376
  </template>
377
377
  </DialogDescription>
378
378
  </DialogHeader>
379
379
 
380
380
  <div class="space-y-1.5 py-1">
381
- <Label for="rvnxx-new-name">{{ kind === "sftp" ? "Label" : "Name" }}</Label>
381
+ <Label for="rvnxx-new-name">{{ kind === "sftp" ? t("integration.credentials.revenexxSource.createDialog.field.label") : t("integration.credentials.revenexxSource.createDialog.field.name") }}</Label>
382
382
  <Input
383
383
  id="rvnxx-new-name"
384
384
  v-model="createName"
385
385
  class="bg-white"
386
- :placeholder="kind === 'sftp' ? 'e.g. imports' : 'e.g. Production'"
386
+ :placeholder="kind === 'sftp' ? t('integration.credentials.revenexxSource.createDialog.field.placeholder.sftp') : t('integration.credentials.revenexxSource.createDialog.field.placeholder.default')"
387
387
  />
388
388
  <p v-if="provisioningNote" class="text-xs text-muted-foreground">{{ provisioningNote }}</p>
389
389
  </div>
390
390
 
391
391
  <DialogFooter>
392
- <Button variant="ghost" :disabled="creating" @click="createOpen = false">Cancel</Button>
392
+ <Button variant="ghost" :disabled="creating" @click="createOpen = false">{{ t("integration.common.action.cancel") }}</Button>
393
393
  <Button :disabled="creating" @click="submitCreate">
394
394
  <Loader2 v-if="creating" :size="14" class="mr-1 animate-spin" />
395
- Create
395
+ {{ t("integration.credentials.revenexxSource.action.create") }}
396
396
  </Button>
397
397
  </DialogFooter>
398
398
  </DialogContent>
@@ -7,20 +7,21 @@ const props = defineProps({
7
7
  types: { type: Array, required: true }
8
8
  });
9
9
  const emit = defineEmits(["pick"]);
10
+ const t = useIntegrationsT();
10
11
  const query = ref("");
11
12
  function authKindLabel(k) {
12
13
  switch (k) {
13
14
  case "api-key":
14
- return "API Key";
15
+ return t("integration.credentials.shared.authKind.apiKey");
15
16
  case "oauth2-authcode":
16
17
  case "oauth2-client-credentials":
17
- return "OAuth2";
18
+ return t("integration.credentials.shared.authKind.oauth2");
18
19
  case "basic-auth":
19
- return "Basic Auth";
20
+ return t("integration.credentials.shared.authKind.basicAuth");
20
21
  case "static":
21
- return "Static";
22
+ return t("integration.credentials.shared.authKind.static");
22
23
  case "secret":
23
- return "Secret";
24
+ return t("integration.credentials.shared.authKind.secret");
24
25
  default:
25
26
  return k ?? "";
26
27
  }
@@ -28,7 +29,7 @@ function authKindLabel(k) {
28
29
  const filtered = computed(() => {
29
30
  const q = query.value.trim().toLowerCase();
30
31
  if (!q) return props.types;
31
- return props.types.filter((t) => resolveLocalized(t.name).toLowerCase().includes(q));
32
+ return props.types.filter((t2) => resolveLocalized(t2.name).toLowerCase().includes(q));
32
33
  });
33
34
  </script>
34
35
 
@@ -38,16 +39,16 @@ const filtered = computed(() => {
38
39
  <Search :size="15" class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
39
40
  <Input
40
41
  v-model="query"
41
- aria-label="Search for an app"
42
+ :aria-label="t('integration.credentials.typePicker.search.aria')"
42
43
  class="bg-white pl-9"
43
- placeholder="Search for an app…"
44
+ :placeholder="t('integration.credentials.typePicker.search.placeholder')"
44
45
  autofocus
45
46
  />
46
47
  </div>
47
48
 
48
49
  <div class="mt-2 max-h-80 overflow-y-auto -mx-1 px-1">
49
50
  <p v-if="!filtered.length" class="px-2 py-6 text-center text-sm text-muted-foreground">
50
- No credential type matches “{{ query }}”.
51
+ {{ t("integration.credentials.typePicker.empty", { query }) }}
51
52
  </p>
52
53
  <button
53
54
  v-for="t in filtered"
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import { onMounted, ref, watch } from "vue";
3
- import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as Trash2 } from "#solar-icons";
3
+ import { RefreshCircle as Loader2, Refresh as ReplayIcon, TrashBinMinimalistic as TrashBin } from "#solar-icons";
4
4
  import { Badge, Button } from "@revenexx/studio";
5
5
  import { toast } from "vue-sonner";
6
6
  const props = defineProps({
7
7
  workflowId: { type: Number, required: true }
8
8
  });
9
+ const t = useIntegrationsT();
9
10
  const { list, replay, discard } = useIntegrationsDeadLetters();
10
11
  const PER_PAGE = 25;
11
12
  const items = ref([]);
@@ -25,7 +26,7 @@ async function load(p = page.value) {
25
26
  lastPage.value = res.lastPage;
26
27
  page.value = res.page;
27
28
  } catch (e) {
28
- error.value = e?.message ?? "Could not load dead letters.";
29
+ error.value = e?.message ?? t("integration.deadLetters.error.load");
29
30
  } finally {
30
31
  loading.value = false;
31
32
  }
@@ -41,10 +42,10 @@ async function onReplay(dl) {
41
42
  try {
42
43
  const res = await replay(dl.id);
43
44
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
44
- toast.success(newId ? `Replayed \u2014 started run #${newId}` : "Replayed");
45
+ toast.success(newId ? t("integration.deadLetters.toast.replay.started", { id: newId }) : t("integration.deadLetters.toast.replay.done"));
45
46
  await load();
46
47
  } catch (e) {
47
- toast.error(e?.message ?? "Could not replay.");
48
+ toast.error(e?.message ?? t("integration.deadLetters.toast.replay.error"));
48
49
  } finally {
49
50
  busyId.value = null;
50
51
  }
@@ -53,10 +54,10 @@ async function onDiscard(dl) {
53
54
  busyId.value = dl.id;
54
55
  try {
55
56
  await discard(dl.id);
56
- toast.success("Discarded");
57
+ toast.success(t("integration.deadLetters.toast.discard.done"));
57
58
  await load();
58
59
  } catch (e) {
59
- toast.error(e?.message ?? "Could not discard.");
60
+ toast.error(e?.message ?? t("integration.deadLetters.toast.discard.error"));
60
61
  } finally {
61
62
  busyId.value = null;
62
63
  }
@@ -80,14 +81,14 @@ function fmt(iso) {
80
81
  <div class="space-y-3">
81
82
  <div class="flex items-center justify-between">
82
83
  <div>
83
- <h3 class="text-sm font-semibold">Missed runs</h3>
84
+ <h3 class="text-sm font-semibold">{{ t("integration.deadLetters.panel.title") }}</h3>
84
85
  <p class="mt-0.5 text-sm text-muted-foreground">
85
- Triggers that fired while this workflow couldn't run (bundle building or failed). Replay starts a fresh run from the captured payload.
86
+ {{ t("integration.deadLetters.panel.description") }}
86
87
  </p>
87
88
  </div>
88
89
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="loading" @click="() => load()">
89
90
  <Loader2 v-if="loading" :size="12" class="animate-spin" />
90
- Refresh
91
+ {{ t("integration.common.action.refresh") }}
91
92
  </Button>
92
93
  </div>
93
94
 
@@ -107,7 +108,7 @@ function fmt(iso) {
107
108
  </div>
108
109
  </div>
109
110
  <div v-else-if="!items.length" class="rounded-lg border border-black/[0.06] bg-white p-6 text-center text-sm text-muted-foreground">
110
- No missed runs. Every trigger firing made it into a run.
111
+ {{ t("integration.deadLetters.state.empty") }}
111
112
  </div>
112
113
 
113
114
  <div v-else class="rounded-lg border border-black/[0.06] bg-white divide-y divide-black/[0.04]">
@@ -118,7 +119,7 @@ function fmt(iso) {
118
119
  >
119
120
  <Badge variant="outline" :class="['border text-xs font-medium uppercase shrink-0 mt-0.5', statusClass(dl.status)]">{{ dl.status }}</Badge>
120
121
  <div class="min-w-0 flex-1">
121
- <div class="text-sm break-words">{{ dl.reason || "No reason recorded." }}</div>
122
+ <div class="text-sm break-words">{{ dl.reason || t("integration.deadLetters.reason.fallback") }}</div>
122
123
  <div class="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground font-mono">
123
124
  <span>#{{ dl.id }}</span>
124
125
  <span v-if="dl.source">{{ dl.source }}</span>
@@ -130,21 +131,21 @@ function fmt(iso) {
130
131
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="busyId === dl.id" @click="onReplay(dl)">
131
132
  <Loader2 v-if="busyId === dl.id" :size="12" class="animate-spin" />
132
133
  <ReplayIcon v-else :size="12" />
133
- Replay
134
+ {{ t("integration.deadLetters.action.replay") }}
134
135
  </Button>
135
136
  <Button variant="ghost" size="sm" class="h-7 text-sm text-rose-600 hover:bg-rose-500/10" :disabled="busyId === dl.id" @click="onDiscard(dl)">
136
- <Trash2 :size="12" />
137
- Discard
137
+ <TrashBin :size="12" />
138
+ {{ t("integration.deadLetters.action.discard") }}
138
139
  </Button>
139
140
  </div>
140
141
  </div>
141
142
  </div>
142
143
 
143
144
  <div v-if="lastPage > 1" class="flex items-center justify-between text-sm text-muted-foreground">
144
- <span class="tabular-nums">Page {{ page }} / {{ lastPage }} · {{ total }} total</span>
145
+ <span class="tabular-nums">{{ t("integration.deadLetters.pagination.summary", { page, lastPage, total }) }}</span>
145
146
  <div class="flex gap-2">
146
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">← Newer</Button>
147
- <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">Older →</Button>
147
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page <= 1 || loading" @click="goPage(page - 1)">{{ t("integration.deadLetters.pagination.newer") }}</Button>
148
+ <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="page >= lastPage || loading" @click="goPage(page + 1)">{{ t("integration.deadLetters.pagination.older") }}</Button>
148
149
  </div>
149
150
  </div>
150
151
  </div>
@@ -6,6 +6,7 @@ const props = defineProps({
6
6
  inputId: { type: String, required: false }
7
7
  });
8
8
  const emit = defineEmits(["update:modelValue"]);
9
+ const t = useIntegrationsT();
9
10
  const catalog = typeof useDefinedEvents === "function" ? useDefinedEvents() : { emits: ref([]), listens: ref([]), loading: ref(false), load: async () => {
10
11
  } };
11
12
  onMounted(() => {
@@ -15,8 +16,8 @@ const focused = ref(false);
15
16
  const options = computed(() => {
16
17
  const out = /* @__PURE__ */ new Map();
17
18
  for (const e of catalog.emits.value) out.set(e.topic, { topic: e.topic, source: e.app });
18
- for (const t of catalog.listens.value) {
19
- if (!out.has(t)) out.set(t, { topic: t, source: null });
19
+ for (const t2 of catalog.listens.value) {
20
+ if (!out.has(t2)) out.set(t2, { topic: t2, source: null });
20
21
  }
21
22
  return [...out.values()].sort((a, b) => a.topic.localeCompare(b.topic));
22
23
  });
@@ -48,7 +49,7 @@ function pick(topic) {
48
49
  class="absolute left-0 right-0 top-full z-30 mt-1 max-h-56 overflow-y-auto rounded-md border border-black/[0.08] bg-white py-1 shadow-md"
49
50
  >
50
51
  <div v-if="catalog.loading.value && !filtered.length" class="px-2.5 py-1.5 text-sm text-muted-foreground">
51
- Loading event catalogue…
52
+ {{ t("integration.canvas.eventCombobox.loading") }}
52
53
  </div>
53
54
  <button
54
55
  v-for="o in filtered"
@@ -17,6 +17,7 @@ const props = defineProps({
17
17
  label: { type: String, required: false }
18
18
  });
19
19
  const emit = defineEmits(["update:modelValue"]);
20
+ const t = useIntegrationsT();
20
21
  const text = computed(() => String(props.modelValue ?? ""));
21
22
  const OPEN = "${{ ";
22
23
  const CLOSE = " }}";
@@ -34,12 +35,12 @@ function pillLabel(expr) {
34
35
  }
35
36
  if (path[0] === "trigger" && path[1] === "payload") {
36
37
  const rest = path.slice(2).join(".");
37
- return rest ? "Trigger \xB7 " + rest : "Trigger payload";
38
+ return rest ? t("integration.nodeConfig.expression.pill.triggerPath", { rest }) : t("integration.nodeConfig.expression.pill.triggerPayload");
38
39
  }
39
- if (path[0] === "vars") return "Variable \xB7 " + path.slice(1).join(".");
40
+ if (path[0] === "vars") return t("integration.nodeConfig.expression.pill.variable", { path: path.slice(1).join(".") });
40
41
  if (path[0] === "in") {
41
42
  const rest = path.slice(1).join(".");
42
- return rest ? "Input \xB7 " + rest : "Input payload";
43
+ return rest ? t("integration.nodeConfig.expression.pill.inputPath", { rest }) : t("integration.nodeConfig.expression.pill.inputPayload");
43
44
  }
44
45
  return expr;
45
46
  }
@@ -104,9 +105,9 @@ onBeforeUnmount(() => {
104
105
  if (insertHandler?.value === insertToken) insertHandler.value = null;
105
106
  editor.destroy();
106
107
  });
107
- watch(text, (t) => {
108
- if (docToText(editor.getJSON()) !== t) {
109
- editor.commands.setContent(textToDoc(t));
108
+ watch(text, (t2) => {
109
+ if (docToText(editor.getJSON()) !== t2) {
110
+ editor.commands.setContent(textToDoc(t2));
110
111
  }
111
112
  });
112
113
  function insertToken(expr) {
@@ -126,7 +127,7 @@ const sections = computed(() => {
126
127
  label: key,
127
128
  expr: tok("vars." + key)
128
129
  }));
129
- if (varItems.length) out.push({ heading: "VARIABLES", items: varItems });
130
+ if (varItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.variables"), items: varItems });
130
131
  const upstream = props.upstreamOutputs ?? [];
131
132
  const nodeItems = upstream.flatMap((u) => {
132
133
  const base = "nodes." + u.nodeId + ".outputs." + u.output;
@@ -139,13 +140,13 @@ const sections = computed(() => {
139
140
  }
140
141
  return rows;
141
142
  });
142
- if (nodeItems.length) out.push({ heading: "NODE OUTPUTS", items: nodeItems });
143
+ if (nodeItems.length) out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.nodeOutputs"), items: nodeItems });
143
144
  const primary = upstream[0];
144
- const inItems = [{ label: "input payload", expr: tok("in") }];
145
+ const inItems = [{ label: t("integration.nodeConfig.expression.suggestion.inputPayload"), expr: tok("in") }];
145
146
  for (const f of primary?.fields ?? []) {
146
147
  inItems.push({ label: "input." + f, expr: tok("in." + f) });
147
148
  }
148
- out.push({ heading: "INPUT", items: inItems });
149
+ out.push({ heading: t("integration.nodeConfig.expression.suggestion.heading.input"), items: inItems });
149
150
  return out;
150
151
  });
151
152
  const hasSuggestions = computed(() => sections.value.some((s) => s.items.length > 0));
@@ -201,7 +202,7 @@ function insert(expr) {
201
202
  </div>
202
203
  <button
203
204
  type="button"
204
- title="Insert expression"
205
+ :title="t('integration.nodeConfig.expression.insertTitle')"
205
206
  class="flex h-8 shrink-0 items-center rounded-md border border-black/[0.08] bg-white px-1.5 font-mono text-sm text-muted-foreground transition hover:text-foreground hover:bg-muted/30"
206
207
  :class="open ? 'ring-2 ring-emerald-500/40' : ''"
207
208
  @mousedown.prevent="toggle"
@@ -213,28 +214,33 @@ function insert(expr) {
213
214
  <p
214
215
  v-if="preview"
215
216
  class="mt-1 flex min-w-0 items-baseline gap-1 text-[11px]"
216
- :title="preview.misses ? 'Parts of this expression have no sample data yet' : preview.resolved"
217
+ :title="preview.misses ? t('integration.nodeConfig.expression.preview.missTitle') : preview.resolved"
217
218
  >
218
219
  <span class="shrink-0 text-muted-foreground">→</span>
219
220
  <span class="min-w-0 truncate font-mono" :class="preview.misses ? 'text-amber-600' : 'text-emerald-700'">{{ preview.resolved }}</span>
220
221
  </p>
221
222
 
223
+ <!-- Named, because the same expressions are offered twice on this surface —
224
+ here and in the panel beside the form — and a popup nothing can address
225
+ is a popup nothing can test either. -->
222
226
  <div
223
227
  v-if="open && hasSuggestions"
228
+ role="group"
229
+ :aria-label="t('integration.nodeConfig.expression.picker.aria')"
224
230
  class="absolute right-0 top-full z-50 mt-1 flex max-h-72 w-80 flex-col rounded-md border border-black/[0.08] bg-white text-sm shadow-lg"
225
231
  >
226
232
  <div class="shrink-0 border-b border-black/[0.06] p-1">
227
233
  <input
228
234
  v-model="query"
229
- aria-label="Filter available data"
230
- placeholder="Filter…"
235
+ :aria-label="t('integration.nodeConfig.expression.picker.filterAria')"
236
+ :placeholder="t('integration.nodeConfig.expression.picker.filterPlaceholder')"
231
237
  class="w-full rounded px-1.5 py-1 text-sm outline-none placeholder:text-muted-foreground/60 focus:ring-2 focus:ring-primary/40"
232
238
  @mousedown.stop
233
239
  >
234
240
  </div>
235
241
  <div class="min-h-0 flex-1 overflow-y-auto py-1">
236
242
  <p v-if="!filteredSections.length" class="px-2 py-1.5 text-muted-foreground">
237
- Nothing matches “{{ query }}”.
243
+ {{ t("integration.nodeConfig.expression.picker.empty", { query }) }}
238
244
  </p>
239
245
  <template v-for="section in filteredSections" :key="section.heading">
240
246
  <div class="px-2 pb-0.5 pt-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">