@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
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
3
- import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, Pause as PauseIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save } from "#solar-icons";
3
+ import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, Pause as PauseIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
4
4
  import {
5
5
  AlertDialog,
6
6
  AlertDialogContent,
@@ -31,24 +31,24 @@ import RunsTable from "../../../components/Integrations/RunsTable.vue";
31
31
  import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
32
32
  import { toast } from "vue-sonner";
33
33
  import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
34
+ import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
34
35
  import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
35
36
  import { payloadFieldsFromBlob } from "../../../data/payloadForm";
36
37
  import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
38
+ const t = useIntegrationsT();
37
39
  const route = useRoute();
38
40
  const router = useRouter();
39
41
  const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
40
- const { autoEnable } = useIntegrationsFocus();
41
- autoEnable();
42
42
  const id = computed(() => Number(route.params.id));
43
43
  const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
44
- useHead({ title: () => `${workflow.value?.name ?? "Workflow"} \xB7 Revenexx Cockpit` });
44
+ useHead({ title: () => t("integration.editor.head.title", { name: workflow.value?.name ?? t("integration.editor.head.untitled") }) });
45
45
  const tab = ref("editor");
46
- const tabs = [
47
- { key: "editor", label: "Editor" },
48
- { key: "history", label: "History" },
49
- { key: "runs", label: "Runs" },
50
- { key: "dead-letters", label: "Missed runs" }
51
- ];
46
+ const tabs = computed(() => [
47
+ { key: "editor", label: t("integration.editor.tab.editor") },
48
+ { key: "history", label: t("integration.editor.tab.history") },
49
+ { key: "runs", label: t("integration.editor.tab.runs") },
50
+ { key: "dead-letters", label: t("integration.editor.tab.deadLetters") }
51
+ ]);
52
52
  const dirtyBlob = ref(null);
53
53
  const isDirty = computed(() => dirtyBlob.value !== null);
54
54
  const saving = ref(false);
@@ -95,12 +95,12 @@ async function waitForBuildReady() {
95
95
  const status = fresh?.buildStatus;
96
96
  if (status === "pending" || status === "building") continue;
97
97
  if (status === "failed") {
98
- toast.error("The workflow bundle failed to build \u2014 the run was not started.");
98
+ toast.error(t("integration.editor.build.toast.failed"));
99
99
  return false;
100
100
  }
101
101
  return true;
102
102
  }
103
- toast.error("The bundle build is taking unusually long \u2014 try the run again once it is ready.");
103
+ toast.error(t("integration.editor.build.toast.timeout"));
104
104
  return false;
105
105
  } finally {
106
106
  waitingForBuild.value = false;
@@ -109,13 +109,13 @@ async function waitForBuildReady() {
109
109
  const buildNotice = computed(() => {
110
110
  switch (workflow.value?.buildStatus) {
111
111
  case "building":
112
- return { tone: "info", text: "The workflow bundle is still building \u2014 runs are unavailable until it is ready." };
112
+ return { tone: "info", text: t("integration.editor.build.notice.building") };
113
113
  case "pending":
114
- return { tone: "info", text: "The workflow bundle build is queued \u2014 runs are unavailable until it is ready." };
114
+ return { tone: "info", text: t("integration.editor.build.notice.pending") };
115
115
  case "failed":
116
116
  return {
117
117
  tone: "error",
118
- text: `The workflow bundle failed to build${workflow.value?.buildError ? `: ${workflow.value.buildError}` : "."} Save the workflow again to retry the build.`
118
+ text: workflow.value?.buildError ? t("integration.editor.build.notice.failedWithError", { error: workflow.value.buildError }) : t("integration.editor.build.notice.failed")
119
119
  };
120
120
  default:
121
121
  return null;
@@ -139,7 +139,7 @@ function discardChanges() {
139
139
  const duplicateOpen = ref(false);
140
140
  const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
141
141
  async function onDuplicated(copy) {
142
- toast.success(`Duplicated as \u201C${copy.name}\u201D`);
142
+ toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
143
143
  await router.push(`/integrations/workflows/${copy.id}`);
144
144
  }
145
145
  async function onRevisionRestored() {
@@ -172,11 +172,11 @@ function viewRunFromModal(id2) {
172
172
  }
173
173
  function syncErrorMessage(e) {
174
174
  if (e instanceof IntegrationsApiError) {
175
- if (e.status === 504) return "Sync run timed out";
176
- if (e.status === 422 && e.body?.status === "failed") return "Sync run failed";
177
- return e.message || "Sync run failed";
175
+ if (e.status === 504) return t("integration.editor.run.error.syncTimeout");
176
+ if (e.status === 422 && e.body?.status === "failed") return t("integration.editor.run.error.syncFailed");
177
+ return e.message || t("integration.editor.run.error.syncFailed");
178
178
  }
179
- return e?.message || "Sync run failed";
179
+ return e?.message || t("integration.editor.run.error.syncFailed");
180
180
  }
181
181
  function refusedForSettings(e) {
182
182
  const issues = apiValidationIssues(e);
@@ -189,7 +189,7 @@ function refusedForSettings(e) {
189
189
  ...groups.filter((g) => !g.nodeId).flatMap((g) => g.messages)
190
190
  ];
191
191
  const where = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
192
- const line = `Can\u2019t run \u2014 ${summary}${where}`;
192
+ const line = t("integration.editor.run.error.refusedForSettings", { summary, where });
193
193
  return /[.!?]$/.test(line) ? line : `${line}.`;
194
194
  }
195
195
  async function reportRunFailure(e, fallback) {
@@ -207,7 +207,7 @@ async function run(mode) {
207
207
  try {
208
208
  payload = JSON.parse(payloadText.value || "{}");
209
209
  } catch {
210
- payloadError.value = "Trigger payload is not valid JSON.";
210
+ payloadError.value = t("integration.editor.payload.error.invalidJson");
211
211
  return;
212
212
  }
213
213
  payloadError.value = null;
@@ -243,7 +243,7 @@ async function run(mode) {
243
243
  const res = await triggerRun(workflow.value.id, payload, "async");
244
244
  runModalRunId.value = extractRunId(res);
245
245
  } catch (e) {
246
- await reportRunFailure(e, (err) => err?.message || "Could not start the run.");
246
+ await reportRunFailure(e, (err) => err?.message || t("integration.editor.run.error.couldNotStart"));
247
247
  } finally {
248
248
  running.value = false;
249
249
  syncLoading.value = false;
@@ -266,7 +266,7 @@ const eventLog = typeof useIntegrationsEvents === "function" ? useIntegrationsEv
266
266
  } };
267
267
  const prefillBusy = ref(null);
268
268
  const eventTriggerTopic = computed(() => {
269
- const trigger = (workflow.value?.blob.triggers ?? []).find((t) => t.type === "event");
269
+ const trigger = (workflow.value?.blob.triggers ?? []).find((t2) => t2.type === "event");
270
270
  const topic = trigger?.config?.event_type;
271
271
  return typeof topic === "string" && topic.trim() !== "" ? topic.trim() : null;
272
272
  });
@@ -301,7 +301,7 @@ async function insertLatestEvent() {
301
301
  await eventLog.loadEvents();
302
302
  const match = eventLog.events.value.filter((e) => e.topic === topic).sort((a, b) => (b.time ?? "").localeCompare(a.time ?? ""))[0];
303
303
  if (!match) {
304
- toast.info(`No received ${topic} event in the log yet.`);
304
+ toast.info(t("integration.editor.event.toast.noneReceived", { topic }));
305
305
  return;
306
306
  }
307
307
  payloadText.value = envelope(topic, match.data, {
@@ -327,6 +327,9 @@ const runsError = ref(null);
327
327
  const runsPage = ref(1);
328
328
  const runsTotal = ref(0);
329
329
  const runsLastPage = ref(1);
330
+ const RUNS_POLL_MS = 3e3;
331
+ const { delivering: realtimeDelivering, runTick, lastRunEvent, buildTick, lastBuildEvent } = useIntegrationsRealtime();
332
+ const runsBudget = createPollBudget({ intervalMs: RUNS_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
330
333
  let runsPoll = null;
331
334
  function stopRunsPoll() {
332
335
  if (runsPoll) {
@@ -337,12 +340,25 @@ function stopRunsPoll() {
337
340
  function startRunsPoll() {
338
341
  stopRunsPoll();
339
342
  runsPoll = setInterval(() => {
340
- if (runsPage.value === 1) void loadRuns(1, { silent: true });
341
- }, 3e3);
343
+ if (runsPage.value === 1 && runsBudget.due(realtimeDelivering.value)) void loadRuns(1, { silent: true });
344
+ }, RUNS_POLL_MS);
342
345
  }
343
346
  onUnmounted(stopRunsPoll);
347
+ watch(runTick, () => {
348
+ if (tab.value !== "runs" || runsPage.value !== 1) return;
349
+ const eventWorkflow = workflowIdOf(lastRunEvent.value);
350
+ if (eventWorkflow && eventWorkflow !== String(workflow.value?.id ?? "")) return;
351
+ void loadRuns(1, { silent: true });
352
+ });
353
+ watch(buildTick, () => {
354
+ if (!workflow.value) return;
355
+ const eventWorkflow = lastBuildEvent.value?.topicId ?? workflowIdOf(lastBuildEvent.value);
356
+ if (eventWorkflow && eventWorkflow !== String(workflow.value.id)) return;
357
+ void get(workflow.value.id);
358
+ });
344
359
  async function loadRuns(page = runsPage.value, opts = {}) {
345
360
  if (!workflow.value) return;
361
+ runsBudget.spend();
346
362
  if (!opts.silent) runsLoading.value = true;
347
363
  runsError.value = null;
348
364
  try {
@@ -356,7 +372,7 @@ async function loadRuns(page = runsPage.value, opts = {}) {
356
372
  runsLastPage.value = res.lastPage;
357
373
  runsPage.value = res.page;
358
374
  } catch (e) {
359
- runsError.value = e.message || "Could not load runs.";
375
+ runsError.value = e.message || t("integration.editor.runs.error.load");
360
376
  } finally {
361
377
  if (!opts.silent) runsLoading.value = false;
362
378
  }
@@ -365,20 +381,43 @@ function goRunsPage(p) {
365
381
  const next = Math.min(Math.max(1, p), runsLastPage.value);
366
382
  if (next !== runsPage.value) void loadRuns(next);
367
383
  }
368
- watch(tab, (t) => {
369
- if (t === "runs") {
384
+ watch(tab, (t2) => {
385
+ if (t2 === "runs") {
370
386
  void loadRuns(1);
371
387
  startRunsPoll();
372
388
  } else stopRunsPoll();
373
389
  });
374
390
  const settingsOpen = ref(false);
375
391
  const openRunId = computed(() => route.query.run != null ? String(route.query.run) : null);
392
+ const { focused: railFolded, enable, disable } = useIntegrationsFocus();
393
+ const WIDE_CANVAS_KEY = "integrations.editor.wideCanvas";
394
+ const wideCanvasWanted = ref(false);
395
+ try {
396
+ wideCanvasWanted.value = localStorage.getItem(WIDE_CANVAS_KEY) === "1";
397
+ } catch {
398
+ }
399
+ const canvasFillsPage = computed(() => tab.value === "editor" && !openRunId.value);
400
+ const railShouldFold = computed(() => wideCanvasWanted.value && canvasFillsPage.value);
401
+ function applyRail() {
402
+ if (railShouldFold.value) enable();
403
+ else disable();
404
+ }
405
+ onMounted(() => applyRail());
406
+ watch(railShouldFold, () => applyRail());
407
+ onUnmounted(() => disable());
408
+ function toggleRail() {
409
+ wideCanvasWanted.value = !wideCanvasWanted.value;
410
+ try {
411
+ localStorage.setItem(WIDE_CANVAS_KEY, wideCanvasWanted.value ? "1" : "0");
412
+ } catch {
413
+ }
414
+ }
376
415
  const runView = ref("details");
377
- const runViews = [
378
- { key: "details", label: "Details" },
379
- { key: "timeline", label: "Timeline" },
380
- { key: "replay", label: "Replay" }
381
- ];
416
+ const runViews = computed(() => [
417
+ { key: "details", label: t("integration.editor.runView.details") },
418
+ { key: "timeline", label: t("integration.editor.runView.timeline") },
419
+ { key: "replay", label: t("integration.editor.runView.replay") }
420
+ ]);
382
421
  const openRun = ref(null);
383
422
  const openRunError = ref(null);
384
423
  async function loadOpenRun() {
@@ -387,7 +426,7 @@ async function loadOpenRun() {
387
426
  try {
388
427
  openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
389
428
  } catch (e) {
390
- openRunError.value = e?.message ?? "Could not load the run.";
429
+ openRunError.value = e?.message ?? t("integration.editor.run.error.loadOpenRun");
391
430
  }
392
431
  }
393
432
  watch(openRunId, () => {
@@ -410,26 +449,26 @@ async function retryOpenRun() {
410
449
  try {
411
450
  const res = await retryRun(openRun.value.workflowId, openRun.value.id);
412
451
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
413
- toast.success(newId && newId !== openRun.value.id ? `Started run #${newId}` : "Retry requested");
452
+ toast.success(newId && newId !== openRun.value.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
414
453
  void loadRuns(1, { silent: true });
415
454
  } catch (e) {
416
- toast.error(e?.message ?? "Could not retry the run.");
455
+ toast.error(e?.message ?? t("integration.editor.retry.error.generic"));
417
456
  } finally {
418
457
  retryBusy.value = false;
419
458
  }
420
459
  }
421
- const EXECUTION_MODES = [
422
- { value: "async_only", label: "Async only", hint: "Runs are always queued; the trigger returns immediately." },
423
- { value: "sync_only", label: "Sync only", hint: "Runs block until they finish and return the result inline." },
424
- { value: "caller_decides", label: "Caller decides", hint: "The caller chooses sync or async per run." }
425
- ];
460
+ const EXECUTION_MODES = computed(() => [
461
+ { value: "async_only", label: t("integration.editor.executionMode.async.label"), hint: t("integration.editor.executionMode.async.hint") },
462
+ { value: "sync_only", label: t("integration.editor.executionMode.sync.label"), hint: t("integration.editor.executionMode.sync.hint") },
463
+ { value: "caller_decides", label: t("integration.editor.executionMode.callerDecides.label"), hint: t("integration.editor.executionMode.callerDecides.hint") }
464
+ ]);
426
465
  const executionModeDraft = ref("async_only");
427
466
  watch(workflow, (w) => {
428
467
  if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
429
468
  }, { immediate: true });
430
469
  const savingMode = ref(false);
431
470
  function modeLabel(m) {
432
- return m === "sync_only" ? "Sync" : m === "caller_decides" ? "Caller decides" : "Async";
471
+ return m === "sync_only" ? t("integration.editor.executionMode.sync.short") : m === "caller_decides" ? t("integration.editor.executionMode.callerDecides.label") : t("integration.editor.executionMode.async.short");
433
472
  }
434
473
  async function saveExecutionMode(mode) {
435
474
  if (!workflow.value || mode === workflow.value.execution_mode) return;
@@ -448,7 +487,7 @@ async function saveExecutionMode(mode) {
448
487
  await get(workflow.value.id);
449
488
  } catch (e) {
450
489
  executionModeDraft.value = prev;
451
- saveError.value = e.message || "Could not update the execution mode.";
490
+ saveError.value = e.message || t("integration.editor.executionMode.error.generic");
452
491
  } finally {
453
492
  savingMode.value = false;
454
493
  }
@@ -456,7 +495,7 @@ async function saveExecutionMode(mode) {
456
495
  const savingMeta = ref(false);
457
496
  const tagSuggestions = computed(() => {
458
497
  const all = /* @__PURE__ */ new Set();
459
- for (const wf of workflows.value) for (const t of wf.blob.tags ?? []) all.add(t);
498
+ for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
460
499
  return [...all].sort();
461
500
  });
462
501
  async function saveWorkflowMeta(meta) {
@@ -481,9 +520,9 @@ async function saveWorkflowMeta(meta) {
481
520
  });
482
521
  await get(workflow.value.id);
483
522
  dirtyBlob.value = null;
484
- toast.success("Appearance saved");
523
+ toast.success(t("integration.editor.appearance.toast.saved"));
485
524
  } catch (e) {
486
- const msg = e.message || "Could not save the appearance.";
525
+ const msg = e.message || t("integration.editor.appearance.error.generic");
487
526
  saveError.value = msg;
488
527
  toast.error(msg);
489
528
  } finally {
@@ -494,7 +533,7 @@ async function onToggleActive() {
494
533
  const wf = workflow.value;
495
534
  if (!wf) return;
496
535
  if (!wf.active && isInvalid.value) {
497
- toast.error(`Can\u2019t activate \u2014 ${validationWarnings.value?.length} setting${validationWarnings.value?.length === 1 ? "" : "s"} still need${validationWarnings.value?.length === 1 ? "s" : ""} fixing (see the banner above).`);
536
+ toast.error(t("integration.editor.activate.error.needsFixing", validationWarnings.value?.length ?? 0));
498
537
  return;
499
538
  }
500
539
  try {
@@ -503,10 +542,10 @@ async function onToggleActive() {
503
542
  const issues = apiValidationIssues(e);
504
543
  if (issues.length) {
505
544
  await get(wf.id);
506
- toast.error(`Can\u2019t activate \u2014 ${issues.length} validation issue${issues.length === 1 ? "" : "s"} to fix.`);
545
+ toast.error(t("integration.editor.activate.error.validationIssues", issues.length));
507
546
  return;
508
547
  }
509
- toast.error(e.message || "Could not change the workflow status.");
548
+ toast.error(e.message || t("integration.editor.activate.error.generic"));
510
549
  }
511
550
  }
512
551
  async function save() {
@@ -533,7 +572,7 @@ async function save() {
533
572
  });
534
573
  deactivationOpen.value = true;
535
574
  } else {
536
- saveError.value = e.message || "Save failed.";
575
+ saveError.value = e.message || t("integration.editor.save.error.generic");
537
576
  }
538
577
  } finally {
539
578
  saving.value = false;
@@ -556,10 +595,10 @@ async function confirmDeactivate() {
556
595
  await get(workflow.value.id);
557
596
  dirtyBlob.value = null;
558
597
  deactivationOpen.value = false;
559
- toast.success("Saved as paused \u2014 fix the highlighted settings, then activate again.");
598
+ toast.success(t("integration.editor.deactivate.toast.savedPaused"));
560
599
  } catch (e) {
561
600
  deactivationOpen.value = false;
562
- saveError.value = e.message || "Save failed.";
601
+ saveError.value = e.message || t("integration.editor.save.error.generic");
563
602
  } finally {
564
603
  saving.value = false;
565
604
  }
@@ -583,41 +622,67 @@ function formatDateTime(iso) {
583
622
  <PageHeader :title="workflow.name">
584
623
  <template #title>
585
624
  <span class="inline-flex min-w-0 items-center gap-1.5">
586
- <NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">Workflows</NuxtLink>
625
+ <NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
587
626
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
588
627
  <!-- Run open: the breadcrumb extends — name and Runs become crumbs
589
628
  back into the workflow surface. -->
590
629
  <template v-if="openRunId">
591
630
  <button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ workflow.name }}</button>
592
631
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
593
- <button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">Runs</button>
632
+ <button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
594
633
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
595
634
  <!-- The Temporal run id IS the run's identity; the internal
596
635
  number only fills in until the run is handed off. -->
597
- <span class="truncate font-mono">{{ openRun?.temporalRunId ?? `Run #${openRunId}` }}</span>
636
+ <span class="truncate font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
598
637
  </template>
599
638
  <template v-else>
600
639
  <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
601
640
  <WorkflowIconView :icon="workflow.blob.icon" :size="16" />
602
641
  </span>
603
642
  <span class="truncate">{{ workflow.name }}</span>
604
- <Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="`Execution mode: ${modeLabel(workflow.execution_mode)}`">
643
+ <Badge variant="outline" class="ml-1.5 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
605
644
  {{ modeLabel(workflow.execution_mode) }}
606
645
  </Badge>
607
646
  </template>
608
647
  </span>
609
648
  </template>
610
649
  <template #actions>
650
+ <!-- Folding the rail away, and getting it back (PO-273). It reads as
651
+ chrome rather than as an action on this workflow, so the natural
652
+ place was ahead of the breadcrumb — but the shell's `#title` slot
653
+ *is* the `<h1>`'s content, and a control inside the heading makes
654
+ the page announce itself as "Hide navigation Workflows › Order
655
+ sync". `accessibility.md` AC-4 promises one heading that names what
656
+ the operator is looking at, so this sits in `#actions`, which is
657
+ outside it. Leftmost there is as near the rail as the shared header
658
+ allows without a slot of its own.
659
+
660
+ It carries its words and not just a pictogram: an icon-only control
661
+ here is one nobody finds, and the way back out of a folded rail is
662
+ the one control that cannot afford to be learnt by chance. Rendered
663
+ only where the canvas fills the page, the one surface it acts on. -->
664
+ <Button
665
+ v-if="canvasFillsPage"
666
+ variant="ghost"
667
+ size="sm"
668
+ class="h-7 shrink-0 gap-1.5 px-2 text-sm font-normal text-muted-foreground hover:text-primary"
669
+ :title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
670
+ @click="toggleRail"
671
+ >
672
+ <RailIcon :size="15" />
673
+ {{ railFolded ? t("integration.nav.action.show") : t("integration.nav.action.hide") }}
674
+ </Button>
675
+
611
676
  <!-- Run open: run-scoped actions replace the workflow Save. -->
612
677
  <template v-if="openRunId">
613
678
  <Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
614
679
  <Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
615
680
  <RetryIcon v-else :size="12" />
616
- Retry
681
+ {{ t("integration.editor.action.retry") }}
617
682
  </Button>
618
683
  <Button class="h-7 text-sm" @click="closeRun('editor')">
619
684
  <ArrowLeft :size="12" />
620
- Back to Workflow
685
+ {{ t("integration.editor.action.backToWorkflow") }}
621
686
  </Button>
622
687
  </template>
623
688
  <template v-else>
@@ -627,31 +692,31 @@ function formatDateTime(iso) {
627
692
  :title="buildNotice?.text"
628
693
  >
629
694
  <Loader2 :size="12" class="animate-spin" />
630
- {{ workflow.buildStatus === "building" ? "Building bundle\u2026" : "Build queued\u2026" }}
695
+ {{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
631
696
  </span>
632
- <span v-if="isDirty" class="text-sm text-amber-600">Unsaved changes</span>
633
- <Button variant="outline" class="h-7 text-sm" :disabled="saving" title="Duplicate this workflow" @click="duplicateOpen = true">
697
+ <span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
698
+ <Button variant="outline" class="h-7 text-sm" :disabled="saving" :title="t('integration.editor.action.duplicate.title')" @click="duplicateOpen = true">
634
699
  <CopyIcon :size="12" />
635
- Duplicate
700
+ {{ t("integration.editor.action.duplicate.label") }}
636
701
  </Button>
637
- <Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" title="Discard unsaved changes" @click="discardChanges">
638
- Cancel
702
+ <Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
703
+ {{ t("integration.editor.action.discard.label") }}
639
704
  </Button>
640
705
  <Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
641
706
  <Loader2 v-if="saving" :size="12" class="animate-spin" />
642
707
  <Save v-else :size="12" />
643
- {{ saving ? "Saving\u2026" : "Save" }}
708
+ {{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
644
709
  </Button>
645
710
  <Button
646
711
  :variant="workflow.active ? 'outline' : 'default'"
647
712
  class="h-7 text-sm"
648
713
  :class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
649
714
  :disabled="!workflow.active && isInvalid"
650
- :title="workflow.active ? 'Pause this workflow' : isInvalid ? `Fix ${validationWarnings?.length} node setting(s) before activating` : 'Activate this workflow'"
715
+ :title="workflow.active ? t('integration.editor.action.pause.title') : isInvalid ? t('integration.editor.action.activate.title.invalid', validationWarnings?.length ?? 0) : t('integration.editor.action.activate.title')"
651
716
  @click="onToggleActive"
652
717
  >
653
718
  <component :is="workflow.active ? PauseIcon : Play" :size="12" />
654
- {{ workflow.active ? "Pause" : "Activate" }}
719
+ {{ workflow.active ? t("integration.editor.action.pause.label") : t("integration.editor.action.activate.label") }}
655
720
  </Button>
656
721
  </template>
657
722
  </template>
@@ -666,11 +731,10 @@ function formatDateTime(iso) {
666
731
  </div>
667
732
 
668
733
  <div v-if="reauthCredentials.length" class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800">
669
- {{ reauthCredentials.length }} credential{{ reauthCredentials.length === 1 ? "" : "s" }} used here
670
- need{{ reauthCredentials.length === 1 ? "s" : "" }} reconnecting
734
+ {{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
671
735
  ({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
672
736
  <NuxtLink to="/integrations/credentials" class="font-medium underline">
673
- fix in Credentials
737
+ {{ t("integration.editor.reauth.action.fix") }}
674
738
  </NuxtLink>.
675
739
  </div>
676
740
 
@@ -685,7 +749,7 @@ function formatDateTime(iso) {
685
749
  class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
686
750
  >
687
751
  <p class="font-medium">
688
- This workflow can’t be activated yet — {{ warningSummary }}:
752
+ {{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
689
753
  </p>
690
754
  <ul
691
755
  class="mt-1 space-y-0.5"
@@ -704,7 +768,7 @@ function formatDateTime(iso) {
704
768
  type="button"
705
769
  class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
706
770
  :title="g.messages.join('\n')"
707
- :aria-label="`Fix ${g.fields.length} setting${g.fields.length === 1 ? '' : 's'} in ${g.label}`"
771
+ :aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
708
772
  @click="revealNode(g.nodeId)"
709
773
  >
710
774
  <span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
@@ -713,7 +777,7 @@ function formatDateTime(iso) {
713
777
  </li>
714
778
  <li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
715
779
  <button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
716
- …and {{ warningGroups.length - WARN_COLLAPSED }} more — open a node to fix its config.
780
+ {{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
717
781
  </button>
718
782
  </li>
719
783
  </ul>
@@ -738,7 +802,7 @@ function formatDateTime(iso) {
738
802
  {{ openRunError }}
739
803
  </div>
740
804
  <div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
741
- <Loader2 :size="16" class="animate-spin" /> Loading run…
805
+ <Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
742
806
  </div>
743
807
  </div>
744
808
  </div>
@@ -781,19 +845,19 @@ function formatDateTime(iso) {
781
845
  :loading="runsLoading"
782
846
  :pagination="false"
783
847
  :show-workflow="false"
784
- empty-text="No runs yet — hit Run in the editor."
848
+ :empty-text="t('integration.editor.runs.empty')"
785
849
  @changed="() => loadRuns()"
786
850
  />
787
851
 
788
852
  <!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
789
853
  <div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
790
- <span class="tabular-nums">Page {{ runsPage }} / {{ runsLastPage }} · {{ runsTotal }} run{{ runsTotal === 1 ? "" : "s" }}</span>
854
+ <span class="tabular-nums">{{ t("integration.editor.runs.pagination.page", { page: runsPage, lastPage: runsLastPage }) }} · {{ t("integration.editor.runs.pagination.count", runsTotal) }}</span>
791
855
  <div class="flex gap-2">
792
856
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage <= 1 || runsLoading" @click="goRunsPage(runsPage - 1)">
793
- ← Newer
857
+ {{ t("integration.editor.runs.pagination.newer") }}
794
858
  </Button>
795
859
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage >= runsLastPage || runsLoading" @click="goRunsPage(runsPage + 1)">
796
- Older →
860
+ {{ t("integration.editor.runs.pagination.older") }}
797
861
  </Button>
798
862
  </div>
799
863
  </div>
@@ -811,7 +875,7 @@ function formatDateTime(iso) {
811
875
  </template>
812
876
 
813
877
  <div v-else class="p-12 text-center text-sm text-muted-foreground">
814
- Loading…
878
+ {{ t("integration.editor.state.loading") }}
815
879
  </div>
816
880
  </div>
817
881
 
@@ -819,31 +883,31 @@ function formatDateTime(iso) {
819
883
  <Dialog v-model:open="runPayloadOpen">
820
884
  <DialogContent class="sm:max-w-lg">
821
885
  <DialogHeader>
822
- <DialogTitle>Run with payload</DialogTitle>
886
+ <DialogTitle>{{ t("integration.editor.runPayload.dialog.title") }}</DialogTitle>
823
887
  <DialogDescription class="text-sm">
824
- Starts a manual run with this trigger payload. The result lands on the Runs tab.
888
+ {{ t("integration.editor.runPayload.dialog.description") }}
825
889
  </DialogDescription>
826
890
  </DialogHeader>
827
891
  <div v-if="eventTriggerTopic" class="flex flex-wrap items-center gap-1.5">
828
- <span class="text-xs text-muted-foreground">Prefill <code class="font-mono">{{ eventTriggerTopic }}</code>:</span>
892
+ <span class="text-xs text-muted-foreground">{{ t("integration.editor.runPayload.prefill.label") }} <code class="font-mono">{{ eventTriggerTopic }}</code>:</span>
829
893
  <Button
830
894
  variant="outline"
831
895
  class="h-7 text-xs"
832
896
  :disabled="!catalogSample"
833
- :title="catalogSample ? 'Insert the catalog sample payload' : 'The event catalog has no sample for this topic.'"
897
+ :title="catalogSample ? t('integration.editor.runPayload.action.sampleEvent.titleAvailable') : t('integration.editor.runPayload.action.sampleEvent.titleUnavailable')"
834
898
  @click="insertSampleEvent"
835
899
  >
836
- Sample event
900
+ {{ t("integration.editor.runPayload.action.sampleEvent") }}
837
901
  </Button>
838
902
  <Button
839
903
  variant="outline"
840
904
  class="h-7 text-xs"
841
905
  :disabled="prefillBusy !== null"
842
- title="Insert the latest received event of this topic"
906
+ :title="t('integration.editor.runPayload.action.latestEvent.title')"
843
907
  @click="insertLatestEvent"
844
908
  >
845
909
  <Loader2 v-if="prefillBusy === 'latest'" :size="12" class="animate-spin" />
846
- Latest real event
910
+ {{ t("integration.editor.runPayload.action.latestEvent") }}
847
911
  </Button>
848
912
  </div>
849
913
  <div class="space-y-1">
@@ -857,35 +921,35 @@ function formatDateTime(iso) {
857
921
  <Button
858
922
  variant="outline"
859
923
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
860
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
924
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
861
925
  class="h-8 text-sm"
862
926
  @click="runFromPayloadDialog('sync')"
863
927
  >
864
928
  <Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
865
929
  <Play v-else :size="14" />
866
- {{ waitingMode === "sync" ? "Waiting for build\u2026" : "Run (sync)" }}
930
+ {{ waitingMode === "sync" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.sync") }) }}
867
931
  </Button>
868
932
  <Button
869
933
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
870
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
934
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
871
935
  class="h-8 text-sm"
872
936
  @click="runFromPayloadDialog('async')"
873
937
  >
874
938
  <Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
875
939
  <Play v-else :size="14" />
876
- {{ waitingMode === "async" ? "Waiting for build\u2026" : "Run (async)" }}
940
+ {{ waitingMode === "async" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.async") }) }}
877
941
  </Button>
878
942
  </template>
879
943
  <Button
880
944
  v-else
881
945
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
882
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
946
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
883
947
  class="h-8 text-sm"
884
948
  @click="runFromPayloadDialog(primaryMode)"
885
949
  >
886
950
  <Loader2 v-if="running || syncLoading || waitingForBuild" :size="14" class="animate-spin" />
887
951
  <Play v-else :size="14" />
888
- {{ waitingForBuild ? "Waiting for build\u2026" : `Run (${primaryMode})` }}
952
+ {{ waitingForBuild ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t(primaryMode === "sync" ? "integration.editor.mode.word.sync" : "integration.editor.mode.word.async") }) }}
889
953
  </Button>
890
954
  </div>
891
955
  </DialogContent>
@@ -924,10 +988,11 @@ function formatDateTime(iso) {
924
988
  }">
925
989
  <AlertDialogContent>
926
990
  <AlertDialogHeader>
927
- <AlertDialogTitle>Can’t keep this workflow active</AlertDialogTitle>
991
+ <AlertDialogTitle>{{ t("integration.editor.deactivate.dialog.title") }}</AlertDialogTitle>
928
992
  <AlertDialogDescription>
929
- These node settings must be valid while the workflow is active. Save it as
930
- <span class="font-medium">paused</span> to keep your changes, then fix them and activate again — or cancel to discard this save.
993
+ {{ t("integration.editor.deactivate.dialog.description.pre") }}
994
+ <span class="font-medium">{{ t("integration.editor.deactivate.dialog.description.paused") }}</span>
995
+ {{ t("integration.editor.deactivate.dialog.description.post") }}
931
996
  </AlertDialogDescription>
932
997
  </AlertDialogHeader>
933
998
  <ul class="max-h-48 space-y-0.5 overflow-y-auto rounded-md border border-amber-500/20 bg-amber-500/5 px-3 py-2 text-sm text-amber-800">
@@ -938,11 +1003,11 @@ function formatDateTime(iso) {
938
1003
  </ul>
939
1004
  <AlertDialogFooter>
940
1005
  <Button variant="outline" :disabled="saving" @click="deactivationOpen = false">
941
- Cancel
1006
+ {{ t("integration.editor.deactivate.dialog.cancel") }}
942
1007
  </Button>
943
1008
  <Button class="bg-emerald-600 text-white hover:bg-emerald-700" :disabled="saving" @click="confirmDeactivate">
944
1009
  <Loader2 v-if="saving" :size="12" class="animate-spin" />
945
- Save as paused
1010
+ {{ t("integration.editor.deactivate.dialog.confirm") }}
946
1011
  </Button>
947
1012
  </AlertDialogFooter>
948
1013
  </AlertDialogContent>