@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
@@ -1,7 +1,13 @@
1
1
  <script setup>
2
- import { computed, 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";
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, SidebarMinimalistic as RailIcon } from "#solar-icons";
4
4
  import {
5
+ AlertDialog,
6
+ AlertDialogContent,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
5
11
  Badge,
6
12
  Button,
7
13
  Dialog,
@@ -24,28 +30,47 @@ import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDu
24
30
  import RunsTable from "../../../components/Integrations/RunsTable.vue";
25
31
  import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
26
32
  import { toast } from "vue-sonner";
27
- import { IntegrationsApiError } from "../../../utils/apiError";
33
+ import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
34
+ import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
35
+ import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
28
36
  import { payloadFieldsFromBlob } from "../../../data/payloadForm";
29
37
  import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
38
+ const t = useIntegrationsT();
30
39
  const route = useRoute();
31
40
  const router = useRouter();
32
41
  const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
33
- const { autoEnable } = useIntegrationsFocus();
34
- autoEnable();
35
42
  const id = computed(() => Number(route.params.id));
36
43
  const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
37
- 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") }) });
38
45
  const tab = ref("editor");
39
- const tabs = [
40
- { key: "editor", label: "Editor" },
41
- { key: "history", label: "History" },
42
- { key: "runs", label: "Runs" },
43
- { key: "dead-letters", label: "Missed runs" }
44
- ];
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
+ ]);
45
52
  const dirtyBlob = ref(null);
46
53
  const isDirty = computed(() => dirtyBlob.value !== null);
47
54
  const saving = ref(false);
48
55
  const saveError = ref(null);
56
+ const validationWarnings = computed(() => workflow.value?.warnings);
57
+ const isInvalid = computed(() => (validationWarnings.value?.length ?? 0) > 0);
58
+ const WARN_COLLAPSED = 3;
59
+ const warningsExpanded = ref(false);
60
+ const warningGroups = computed(() => groupFindingsByNode(validationWarnings.value ?? [], workflow.value?.blob.nodes ?? []));
61
+ const warningSummary = computed(() => findingsSummary(validationWarnings.value?.length ?? 0, warningGroups.value.filter((g) => g.nodeId).length));
62
+ const editorRef = ref(null);
63
+ async function revealNode(nodeId) {
64
+ tab.value = "editor";
65
+ await nextTick();
66
+ editorRef.value?.revealNode(nodeId);
67
+ }
68
+ const { load: loadNodeCatalog } = useIntegrationsNodes();
69
+ onMounted(() => {
70
+ void loadNodeCatalog();
71
+ });
72
+ const deactivationOpen = ref(false);
73
+ const deactivationIssues = ref([]);
49
74
  const { load: loadCredentials, needsReauth } = useIntegrationsCredentials();
50
75
  onMounted(() => {
51
76
  void loadCredentials();
@@ -70,12 +95,12 @@ async function waitForBuildReady() {
70
95
  const status = fresh?.buildStatus;
71
96
  if (status === "pending" || status === "building") continue;
72
97
  if (status === "failed") {
73
- toast.error("The workflow bundle failed to build \u2014 the run was not started.");
98
+ toast.error(t("integration.editor.build.toast.failed"));
74
99
  return false;
75
100
  }
76
101
  return true;
77
102
  }
78
- 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"));
79
104
  return false;
80
105
  } finally {
81
106
  waitingForBuild.value = false;
@@ -84,13 +109,13 @@ async function waitForBuildReady() {
84
109
  const buildNotice = computed(() => {
85
110
  switch (workflow.value?.buildStatus) {
86
111
  case "building":
87
- 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") };
88
113
  case "pending":
89
- 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") };
90
115
  case "failed":
91
116
  return {
92
117
  tone: "error",
93
- 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")
94
119
  };
95
120
  default:
96
121
  return null;
@@ -100,6 +125,7 @@ watch(id, async () => {
100
125
  const w = await get(id.value);
101
126
  if (!w) await router.push("/integrations/workflows");
102
127
  dirtyBlob.value = null;
128
+ warningsExpanded.value = false;
103
129
  if (tab.value === "runs") void loadRuns(1);
104
130
  }, { immediate: true });
105
131
  function onChange(blob) {
@@ -113,7 +139,7 @@ function discardChanges() {
113
139
  const duplicateOpen = ref(false);
114
140
  const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
115
141
  async function onDuplicated(copy) {
116
- toast.success(`Duplicated as \u201C${copy.name}\u201D`);
142
+ toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
117
143
  await router.push(`/integrations/workflows/${copy.id}`);
118
144
  }
119
145
  async function onRevisionRestored() {
@@ -146,11 +172,34 @@ function viewRunFromModal(id2) {
146
172
  }
147
173
  function syncErrorMessage(e) {
148
174
  if (e instanceof IntegrationsApiError) {
149
- if (e.status === 504) return "Sync run timed out";
150
- if (e.status === 422 && e.body?.status === "failed") return "Sync run failed";
151
- 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
+ }
179
+ return e?.message || t("integration.editor.run.error.syncFailed");
180
+ }
181
+ function refusedForSettings(e) {
182
+ const issues = apiValidationIssues(e);
183
+ if (!issues.length) return null;
184
+ const groups = groupFindingsByNode(issues, workflow.value?.blob.nodes ?? []);
185
+ const named = groups.filter((g) => g.nodeId);
186
+ const summary = findingsSummary(issues.length, named.length);
187
+ const parts = [
188
+ ...named.map((g) => `${g.label} (${g.fields.join(", ")})`),
189
+ ...groups.filter((g) => !g.nodeId).flatMap((g) => g.messages)
190
+ ];
191
+ const where = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
192
+ const line = t("integration.editor.run.error.refusedForSettings", { summary, where });
193
+ return /[.!?]$/.test(line) ? line : `${line}.`;
194
+ }
195
+ async function reportRunFailure(e, fallback) {
196
+ const settings = refusedForSettings(e);
197
+ if (!settings) {
198
+ syncError.value = fallback(e);
199
+ return;
152
200
  }
153
- return e?.message || "Sync run failed";
201
+ syncError.value = settings;
202
+ if (workflow.value) await get(workflow.value.id);
154
203
  }
155
204
  async function run(mode) {
156
205
  if (!workflow.value) return;
@@ -158,7 +207,7 @@ async function run(mode) {
158
207
  try {
159
208
  payload = JSON.parse(payloadText.value || "{}");
160
209
  } catch {
161
- payloadError.value = "Trigger payload is not valid JSON.";
210
+ payloadError.value = t("integration.editor.payload.error.invalidJson");
162
211
  return;
163
212
  }
164
213
  payloadError.value = null;
@@ -179,7 +228,7 @@ async function run(mode) {
179
228
  syncResponse.value = await triggerRun(workflow.value.id, payload, "sync");
180
229
  runModalRunId.value = extractRunId(syncResponse.value);
181
230
  } catch (e) {
182
- syncError.value = syncErrorMessage(e);
231
+ await reportRunFailure(e, syncErrorMessage);
183
232
  if (e instanceof IntegrationsApiError) {
184
233
  runModalRunId.value = extractRunId(e.body?.run);
185
234
  }
@@ -194,7 +243,7 @@ async function run(mode) {
194
243
  const res = await triggerRun(workflow.value.id, payload, "async");
195
244
  runModalRunId.value = extractRunId(res);
196
245
  } catch (e) {
197
- syncError.value = e.message || "Could not start the run.";
246
+ await reportRunFailure(e, (err) => err?.message || t("integration.editor.run.error.couldNotStart"));
198
247
  } finally {
199
248
  running.value = false;
200
249
  syncLoading.value = false;
@@ -217,7 +266,7 @@ const eventLog = typeof useIntegrationsEvents === "function" ? useIntegrationsEv
217
266
  } };
218
267
  const prefillBusy = ref(null);
219
268
  const eventTriggerTopic = computed(() => {
220
- const trigger = (workflow.value?.blob.triggers ?? []).find((t) => t.type === "event");
269
+ const trigger = (workflow.value?.blob.triggers ?? []).find((t2) => t2.type === "event");
221
270
  const topic = trigger?.config?.event_type;
222
271
  return typeof topic === "string" && topic.trim() !== "" ? topic.trim() : null;
223
272
  });
@@ -252,7 +301,7 @@ async function insertLatestEvent() {
252
301
  await eventLog.loadEvents();
253
302
  const match = eventLog.events.value.filter((e) => e.topic === topic).sort((a, b) => (b.time ?? "").localeCompare(a.time ?? ""))[0];
254
303
  if (!match) {
255
- toast.info(`No received ${topic} event in the log yet.`);
304
+ toast.info(t("integration.editor.event.toast.noneReceived", { topic }));
256
305
  return;
257
306
  }
258
307
  payloadText.value = envelope(topic, match.data, {
@@ -278,6 +327,9 @@ const runsError = ref(null);
278
327
  const runsPage = ref(1);
279
328
  const runsTotal = ref(0);
280
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 });
281
333
  let runsPoll = null;
282
334
  function stopRunsPoll() {
283
335
  if (runsPoll) {
@@ -288,12 +340,25 @@ function stopRunsPoll() {
288
340
  function startRunsPoll() {
289
341
  stopRunsPoll();
290
342
  runsPoll = setInterval(() => {
291
- if (runsPage.value === 1) void loadRuns(1, { silent: true });
292
- }, 3e3);
343
+ if (runsPage.value === 1 && runsBudget.due(realtimeDelivering.value)) void loadRuns(1, { silent: true });
344
+ }, RUNS_POLL_MS);
293
345
  }
294
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
+ });
295
359
  async function loadRuns(page = runsPage.value, opts = {}) {
296
360
  if (!workflow.value) return;
361
+ runsBudget.spend();
297
362
  if (!opts.silent) runsLoading.value = true;
298
363
  runsError.value = null;
299
364
  try {
@@ -307,7 +372,7 @@ async function loadRuns(page = runsPage.value, opts = {}) {
307
372
  runsLastPage.value = res.lastPage;
308
373
  runsPage.value = res.page;
309
374
  } catch (e) {
310
- runsError.value = e.message || "Could not load runs.";
375
+ runsError.value = e.message || t("integration.editor.runs.error.load");
311
376
  } finally {
312
377
  if (!opts.silent) runsLoading.value = false;
313
378
  }
@@ -316,20 +381,43 @@ function goRunsPage(p) {
316
381
  const next = Math.min(Math.max(1, p), runsLastPage.value);
317
382
  if (next !== runsPage.value) void loadRuns(next);
318
383
  }
319
- watch(tab, (t) => {
320
- if (t === "runs") {
384
+ watch(tab, (t2) => {
385
+ if (t2 === "runs") {
321
386
  void loadRuns(1);
322
387
  startRunsPoll();
323
388
  } else stopRunsPoll();
324
389
  });
325
390
  const settingsOpen = ref(false);
326
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
+ }
327
415
  const runView = ref("details");
328
- const runViews = [
329
- { key: "details", label: "Details" },
330
- { key: "timeline", label: "Timeline" },
331
- { key: "replay", label: "Replay" }
332
- ];
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
+ ]);
333
421
  const openRun = ref(null);
334
422
  const openRunError = ref(null);
335
423
  async function loadOpenRun() {
@@ -338,7 +426,7 @@ async function loadOpenRun() {
338
426
  try {
339
427
  openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
340
428
  } catch (e) {
341
- openRunError.value = e?.message ?? "Could not load the run.";
429
+ openRunError.value = e?.message ?? t("integration.editor.run.error.loadOpenRun");
342
430
  }
343
431
  }
344
432
  watch(openRunId, () => {
@@ -361,26 +449,26 @@ async function retryOpenRun() {
361
449
  try {
362
450
  const res = await retryRun(openRun.value.workflowId, openRun.value.id);
363
451
  const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
364
- 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"));
365
453
  void loadRuns(1, { silent: true });
366
454
  } catch (e) {
367
- toast.error(e?.message ?? "Could not retry the run.");
455
+ toast.error(e?.message ?? t("integration.editor.retry.error.generic"));
368
456
  } finally {
369
457
  retryBusy.value = false;
370
458
  }
371
459
  }
372
- const EXECUTION_MODES = [
373
- { value: "async_only", label: "Async only", hint: "Runs are always queued; the trigger returns immediately." },
374
- { value: "sync_only", label: "Sync only", hint: "Runs block until they finish and return the result inline." },
375
- { value: "caller_decides", label: "Caller decides", hint: "The caller chooses sync or async per run." }
376
- ];
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
+ ]);
377
465
  const executionModeDraft = ref("async_only");
378
466
  watch(workflow, (w) => {
379
467
  if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
380
468
  }, { immediate: true });
381
469
  const savingMode = ref(false);
382
470
  function modeLabel(m) {
383
- 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");
384
472
  }
385
473
  async function saveExecutionMode(mode) {
386
474
  if (!workflow.value || mode === workflow.value.execution_mode) return;
@@ -399,7 +487,7 @@ async function saveExecutionMode(mode) {
399
487
  await get(workflow.value.id);
400
488
  } catch (e) {
401
489
  executionModeDraft.value = prev;
402
- saveError.value = e.message || "Could not update the execution mode.";
490
+ saveError.value = e.message || t("integration.editor.executionMode.error.generic");
403
491
  } finally {
404
492
  savingMode.value = false;
405
493
  }
@@ -407,7 +495,7 @@ async function saveExecutionMode(mode) {
407
495
  const savingMeta = ref(false);
408
496
  const tagSuggestions = computed(() => {
409
497
  const all = /* @__PURE__ */ new Set();
410
- 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);
411
499
  return [...all].sort();
412
500
  });
413
501
  async function saveWorkflowMeta(meta) {
@@ -432,15 +520,34 @@ async function saveWorkflowMeta(meta) {
432
520
  });
433
521
  await get(workflow.value.id);
434
522
  dirtyBlob.value = null;
435
- toast.success("Appearance saved");
523
+ toast.success(t("integration.editor.appearance.toast.saved"));
436
524
  } catch (e) {
437
- const msg = e.message || "Could not save the appearance.";
525
+ const msg = e.message || t("integration.editor.appearance.error.generic");
438
526
  saveError.value = msg;
439
527
  toast.error(msg);
440
528
  } finally {
441
529
  savingMeta.value = false;
442
530
  }
443
531
  }
532
+ async function onToggleActive() {
533
+ const wf = workflow.value;
534
+ if (!wf) return;
535
+ if (!wf.active && isInvalid.value) {
536
+ toast.error(t("integration.editor.activate.error.needsFixing", validationWarnings.value?.length ?? 0));
537
+ return;
538
+ }
539
+ try {
540
+ await toggleActive(wf.id);
541
+ } catch (e) {
542
+ const issues = apiValidationIssues(e);
543
+ if (issues.length) {
544
+ await get(wf.id);
545
+ toast.error(t("integration.editor.activate.error.validationIssues", issues.length));
546
+ return;
547
+ }
548
+ toast.error(e.message || t("integration.editor.activate.error.generic"));
549
+ }
550
+ }
444
551
  async function save() {
445
552
  if (!workflow.value || !dirtyBlob.value) return;
446
553
  saving.value = true;
@@ -455,7 +562,43 @@ async function save() {
455
562
  });
456
563
  dirtyBlob.value = null;
457
564
  } catch (e) {
458
- saveError.value = e.message || "Save failed.";
565
+ const issues = apiValidationIssues(e);
566
+ if (issues.length && workflow.value.active) {
567
+ const nodes = workflow.value.blob.nodes;
568
+ deactivationIssues.value = issues.map((i) => {
569
+ const m = /^blob\.nodes\.(\d+)\b/.exec(i.path);
570
+ const node = m ? nodes[Number(m[1])] : void 0;
571
+ return { ...i, where: node ? nodeDisplayLabel(node) : null };
572
+ });
573
+ deactivationOpen.value = true;
574
+ } else {
575
+ saveError.value = e.message || t("integration.editor.save.error.generic");
576
+ }
577
+ } finally {
578
+ saving.value = false;
579
+ }
580
+ }
581
+ async function confirmDeactivate() {
582
+ if (!workflow.value || !dirtyBlob.value) {
583
+ deactivationOpen.value = false;
584
+ return;
585
+ }
586
+ saving.value = true;
587
+ try {
588
+ await update(workflow.value.id, {
589
+ name: workflow.value.name,
590
+ description: workflow.value.description,
591
+ blob: dirtyBlob.value,
592
+ active: false,
593
+ executionMode: workflow.value.execution_mode
594
+ });
595
+ await get(workflow.value.id);
596
+ dirtyBlob.value = null;
597
+ deactivationOpen.value = false;
598
+ toast.success(t("integration.editor.deactivate.toast.savedPaused"));
599
+ } catch (e) {
600
+ deactivationOpen.value = false;
601
+ saveError.value = e.message || t("integration.editor.save.error.generic");
459
602
  } finally {
460
603
  saving.value = false;
461
604
  }
@@ -479,41 +622,67 @@ function formatDateTime(iso) {
479
622
  <PageHeader :title="workflow.name">
480
623
  <template #title>
481
624
  <span class="inline-flex min-w-0 items-center gap-1.5">
482
- <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>
483
626
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
484
627
  <!-- Run open: the breadcrumb extends — name and Runs become crumbs
485
628
  back into the workflow surface. -->
486
629
  <template v-if="openRunId">
487
630
  <button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ workflow.name }}</button>
488
631
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
489
- <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>
490
633
  <Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
491
634
  <!-- The Temporal run id IS the run's identity; the internal
492
635
  number only fills in until the run is handed off. -->
493
- <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>
494
637
  </template>
495
638
  <template v-else>
496
639
  <span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
497
640
  <WorkflowIconView :icon="workflow.blob.icon" :size="16" />
498
641
  </span>
499
642
  <span class="truncate">{{ workflow.name }}</span>
500
- <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) })">
501
644
  {{ modeLabel(workflow.execution_mode) }}
502
645
  </Badge>
503
646
  </template>
504
647
  </span>
505
648
  </template>
506
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
+
507
676
  <!-- Run open: run-scoped actions replace the workflow Save. -->
508
677
  <template v-if="openRunId">
509
678
  <Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
510
679
  <Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
511
680
  <RetryIcon v-else :size="12" />
512
- Retry
681
+ {{ t("integration.editor.action.retry") }}
513
682
  </Button>
514
683
  <Button class="h-7 text-sm" @click="closeRun('editor')">
515
684
  <ArrowLeft :size="12" />
516
- Back to Workflow
685
+ {{ t("integration.editor.action.backToWorkflow") }}
517
686
  </Button>
518
687
  </template>
519
688
  <template v-else>
@@ -523,30 +692,31 @@ function formatDateTime(iso) {
523
692
  :title="buildNotice?.text"
524
693
  >
525
694
  <Loader2 :size="12" class="animate-spin" />
526
- {{ 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") }}
527
696
  </span>
528
- <span v-if="isDirty" class="text-sm text-amber-600">Unsaved changes</span>
529
- <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">
530
699
  <CopyIcon :size="12" />
531
- Duplicate
700
+ {{ t("integration.editor.action.duplicate.label") }}
532
701
  </Button>
533
- <Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" title="Discard unsaved changes" @click="discardChanges">
534
- 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") }}
535
704
  </Button>
536
705
  <Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
537
706
  <Loader2 v-if="saving" :size="12" class="animate-spin" />
538
707
  <Save v-else :size="12" />
539
- {{ saving ? "Saving\u2026" : "Save" }}
708
+ {{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
540
709
  </Button>
541
710
  <Button
542
711
  :variant="workflow.active ? 'outline' : 'default'"
543
712
  class="h-7 text-sm"
544
713
  :class="workflow.active ? 'text-amber-700 hover:text-amber-800' : 'bg-emerald-600 text-white hover:bg-emerald-700'"
545
- :title="workflow.active ? 'Pause this workflow' : 'Activate this workflow'"
546
- @click="toggleActive(workflow.id)"
714
+ :disabled="!workflow.active && isInvalid"
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')"
716
+ @click="onToggleActive"
547
717
  >
548
718
  <component :is="workflow.active ? PauseIcon : Play" :size="12" />
549
- {{ workflow.active ? "Pause" : "Activate" }}
719
+ {{ workflow.active ? t("integration.editor.action.pause.label") : t("integration.editor.action.activate.label") }}
550
720
  </Button>
551
721
  </template>
552
722
  </template>
@@ -561,14 +731,58 @@ function formatDateTime(iso) {
561
731
  </div>
562
732
 
563
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">
564
- {{ reauthCredentials.length }} credential{{ reauthCredentials.length === 1 ? "" : "s" }} used here
565
- need{{ reauthCredentials.length === 1 ? "s" : "" }} reconnecting
734
+ {{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
566
735
  ({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
567
736
  <NuxtLink to="/integrations/credentials" class="font-medium underline">
568
- fix in Credentials
737
+ {{ t("integration.editor.reauth.action.fix") }}
569
738
  </NuxtLink>.
570
739
  </div>
571
740
 
741
+ <!-- Manifest validation (PO-186): an inactive workflow with warnings can't
742
+ be activated. The banner names what's incomplete; the Activate button
743
+ stays disabled until the next save recomputes an empty warnings set.
744
+ Amber (not red) — the work is saved fine, it just isn't runnable yet.
745
+ One line per NODE, listing its fields (PO-209): the messages behind
746
+ them ride along as the line's title, and the line leads to the node. -->
747
+ <div
748
+ v-if="!openRunId && !workflow.active && isInvalid"
749
+ class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
750
+ >
751
+ <p class="font-medium">
752
+ {{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
753
+ </p>
754
+ <ul
755
+ class="mt-1 space-y-0.5"
756
+ :class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
757
+ >
758
+ <li
759
+ v-for="(g, i) in warningsExpanded ? warningGroups : warningGroups.slice(0, WARN_COLLAPSED)"
760
+ :key="`${g.nodeId ?? 'unattributed'}-${i}`"
761
+ class="flex gap-1.5"
762
+ >
763
+ <span class="select-none text-amber-500">•</span>
764
+ <!-- Attributed to a node: the whole line jumps there and opens its
765
+ settings. Otherwise there is nowhere to go — state the finding. -->
766
+ <button
767
+ v-if="g.nodeId"
768
+ type="button"
769
+ class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
770
+ :title="g.messages.join('\n')"
771
+ :aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
772
+ @click="revealNode(g.nodeId)"
773
+ >
774
+ <span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
775
+ </button>
776
+ <span v-else>{{ g.messages.join(" ") }}</span>
777
+ </li>
778
+ <li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
779
+ <button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
780
+ {{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
781
+ </button>
782
+ </li>
783
+ </ul>
784
+ </div>
785
+
572
786
  <!-- Only the FAILED build state banners (its message matters); the
573
787
  working states live as the header chip — and inside a run the
574
788
  workflow's build state is noise, so nothing shows at all. -->
@@ -588,7 +802,7 @@ function formatDateTime(iso) {
588
802
  {{ openRunError }}
589
803
  </div>
590
804
  <div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
591
- <Loader2 :size="16" class="animate-spin" /> Loading run…
805
+ <Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
592
806
  </div>
593
807
  </div>
594
808
  </div>
@@ -598,16 +812,23 @@ function formatDateTime(iso) {
598
812
  </template>
599
813
 
600
814
  <!-- Editor tab — fills the remaining height inside the panel via
601
- the flex wrapper in the studio layout. -->
815
+ the flex wrapper in the studio layout.
816
+
817
+ overlay-open: every lightbox this page owns renders as a sibling of
818
+ the tabs, so the editor stays mounted underneath one and would keep
819
+ its keyboard shortcuts live (Delete deleting the selected node behind
820
+ the dialog — PO-208). Add new page-level dialogs to this list. -->
602
821
  <div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
603
822
  <WorkflowEditor
604
823
  :key="editorResetKey"
824
+ ref="editorRef"
605
825
  :workflow="workflow"
606
826
  :run-busy="running || syncLoading || waitingForBuild"
607
827
  :run-waiting="waitingForBuild"
828
+ :overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
608
829
  @change="onChange"
609
830
  @run="runFromEditor"
610
- @toggle-active="toggleActive(workflow.id)"
831
+ @toggle-active="onToggleActive"
611
832
  @open-settings="settingsOpen = true"
612
833
  />
613
834
  </div>
@@ -624,19 +845,19 @@ function formatDateTime(iso) {
624
845
  :loading="runsLoading"
625
846
  :pagination="false"
626
847
  :show-workflow="false"
627
- empty-text="No runs yet — hit Run in the editor."
848
+ :empty-text="t('integration.editor.runs.empty')"
628
849
  @changed="() => loadRuns()"
629
850
  />
630
851
 
631
852
  <!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
632
853
  <div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
633
- <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>
634
855
  <div class="flex gap-2">
635
856
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage <= 1 || runsLoading" @click="goRunsPage(runsPage - 1)">
636
- ← Newer
857
+ {{ t("integration.editor.runs.pagination.newer") }}
637
858
  </Button>
638
859
  <Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage >= runsLastPage || runsLoading" @click="goRunsPage(runsPage + 1)">
639
- Older →
860
+ {{ t("integration.editor.runs.pagination.older") }}
640
861
  </Button>
641
862
  </div>
642
863
  </div>
@@ -654,7 +875,7 @@ function formatDateTime(iso) {
654
875
  </template>
655
876
 
656
877
  <div v-else class="p-12 text-center text-sm text-muted-foreground">
657
- Loading…
878
+ {{ t("integration.editor.state.loading") }}
658
879
  </div>
659
880
  </div>
660
881
 
@@ -662,31 +883,31 @@ function formatDateTime(iso) {
662
883
  <Dialog v-model:open="runPayloadOpen">
663
884
  <DialogContent class="sm:max-w-lg">
664
885
  <DialogHeader>
665
- <DialogTitle>Run with payload</DialogTitle>
886
+ <DialogTitle>{{ t("integration.editor.runPayload.dialog.title") }}</DialogTitle>
666
887
  <DialogDescription class="text-sm">
667
- Starts a manual run with this trigger payload. The result lands on the Runs tab.
888
+ {{ t("integration.editor.runPayload.dialog.description") }}
668
889
  </DialogDescription>
669
890
  </DialogHeader>
670
891
  <div v-if="eventTriggerTopic" class="flex flex-wrap items-center gap-1.5">
671
- <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>
672
893
  <Button
673
894
  variant="outline"
674
895
  class="h-7 text-xs"
675
896
  :disabled="!catalogSample"
676
- :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')"
677
898
  @click="insertSampleEvent"
678
899
  >
679
- Sample event
900
+ {{ t("integration.editor.runPayload.action.sampleEvent") }}
680
901
  </Button>
681
902
  <Button
682
903
  variant="outline"
683
904
  class="h-7 text-xs"
684
905
  :disabled="prefillBusy !== null"
685
- title="Insert the latest received event of this topic"
906
+ :title="t('integration.editor.runPayload.action.latestEvent.title')"
686
907
  @click="insertLatestEvent"
687
908
  >
688
909
  <Loader2 v-if="prefillBusy === 'latest'" :size="12" class="animate-spin" />
689
- Latest real event
910
+ {{ t("integration.editor.runPayload.action.latestEvent") }}
690
911
  </Button>
691
912
  </div>
692
913
  <div class="space-y-1">
@@ -700,35 +921,35 @@ function formatDateTime(iso) {
700
921
  <Button
701
922
  variant="outline"
702
923
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
703
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
924
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
704
925
  class="h-8 text-sm"
705
926
  @click="runFromPayloadDialog('sync')"
706
927
  >
707
928
  <Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
708
929
  <Play v-else :size="14" />
709
- {{ 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") }) }}
710
931
  </Button>
711
932
  <Button
712
933
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
713
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
934
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
714
935
  class="h-8 text-sm"
715
936
  @click="runFromPayloadDialog('async')"
716
937
  >
717
938
  <Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
718
939
  <Play v-else :size="14" />
719
- {{ 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") }) }}
720
941
  </Button>
721
942
  </template>
722
943
  <Button
723
944
  v-else
724
945
  :disabled="running || syncLoading || waitingForBuild || buildFailed"
725
- :title="buildFailed ? 'The workflow bundle failed to build.' : void 0"
946
+ :title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
726
947
  class="h-8 text-sm"
727
948
  @click="runFromPayloadDialog(primaryMode)"
728
949
  >
729
950
  <Loader2 v-if="running || syncLoading || waitingForBuild" :size="14" class="animate-spin" />
730
951
  <Play v-else :size="14" />
731
- {{ 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") }) }}
732
953
  </Button>
733
954
  </div>
734
955
  </DialogContent>
@@ -759,5 +980,37 @@ function formatDateTime(iso) {
759
980
  />
760
981
 
761
982
  <WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
983
+
984
+ <!-- Active-save blocked by manifest validation: keep the work by saving it
985
+ as paused. Cancelling persists nothing (the 422 already rolled back). -->
986
+ <AlertDialog :open="deactivationOpen" @update:open="(v) => {
987
+ if (!v) deactivationOpen = false;
988
+ }">
989
+ <AlertDialogContent>
990
+ <AlertDialogHeader>
991
+ <AlertDialogTitle>{{ t("integration.editor.deactivate.dialog.title") }}</AlertDialogTitle>
992
+ <AlertDialogDescription>
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") }}
996
+ </AlertDialogDescription>
997
+ </AlertDialogHeader>
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">
999
+ <li v-for="(w, i) in deactivationIssues" :key="`${w.path}-${i}`" class="flex gap-1.5">
1000
+ <span class="select-none text-amber-500">•</span>
1001
+ <span><span v-if="w.where" class="font-medium">{{ w.where }}:</span> {{ w.message }}</span>
1002
+ </li>
1003
+ </ul>
1004
+ <AlertDialogFooter>
1005
+ <Button variant="outline" :disabled="saving" @click="deactivationOpen = false">
1006
+ {{ t("integration.editor.deactivate.dialog.cancel") }}
1007
+ </Button>
1008
+ <Button class="bg-emerald-600 text-white hover:bg-emerald-700" :disabled="saving" @click="confirmDeactivate">
1009
+ <Loader2 v-if="saving" :size="12" class="animate-spin" />
1010
+ {{ t("integration.editor.deactivate.dialog.confirm") }}
1011
+ </Button>
1012
+ </AlertDialogFooter>
1013
+ </AlertDialogContent>
1014
+ </AlertDialog>
762
1015
  </IntegrationsLayout>
763
1016
  </template>