@revenexx/studio-integrations 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +643 -0
- package/README.md +31 -0
- package/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -3
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +32 -28
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +45 -38
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +58 -58
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +10 -9
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +4 -3
- package/dist/runtime/components/Integrations/ExpressionInput.vue +21 -15
- package/dist/runtime/components/Integrations/GroupFrameView.vue +29 -19
- package/dist/runtime/components/Integrations/Layout.vue +28 -3
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +30 -16
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -8
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +3 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +45 -42
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +3 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +315 -221
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +2 -1
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue +37 -10
- package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +2 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +40 -35
- package/dist/runtime/components/Integrations/QueryBuilder.vue +44 -43
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +36 -12
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +14 -13
- package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
- package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
- package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
- package/dist/runtime/components/Integrations/StickyNoteView.vue +82 -34
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
- package/dist/runtime/components/Integrations/SurfaceCard.d.vue.ts +50 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue +55 -0
- package/dist/runtime/components/Integrations/SurfaceCard.vue.d.ts +50 -0
- package/dist/runtime/components/Integrations/TemplateCard.vue +18 -8
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +22 -12
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +41 -39
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +6 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +131 -87
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +27 -24
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +7 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +76 -46
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +55 -43
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +16 -17
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +9 -8
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -13
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +79 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +83 -45
- package/dist/runtime/composables/integrationsApi.js +4 -2
- package/dist/runtime/composables/useCredentialTypes.js +4 -2
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +11 -0
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -6
- package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
- package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
- package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +14 -2
- package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
- package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +3 -1
- package/dist/runtime/composables/useIntegrationsSchedules.d.ts +3 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +4 -2
- package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
- package/dist/runtime/composables/useIntegrationsT.js +9 -0
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
- package/dist/runtime/composables/useIntegrationsTemplates.js +16 -8
- package/dist/runtime/composables/useIntegrationsTriggers.d.ts +23 -3
- package/dist/runtime/composables/useIntegrationsTriggers.js +21 -8
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +10 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +30 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +4 -2
- package/dist/runtime/data/integrationNodeCatalog.d.ts +63 -1
- package/dist/runtime/data/integrationNodeCatalog.js +22 -4
- package/dist/runtime/data/integrationTemplates.d.ts +9 -0
- package/dist/runtime/data/integrationTemplates.js +7 -1
- package/dist/runtime/data/nodeInputData.d.ts +58 -1
- package/dist/runtime/data/nodeInputData.js +30 -5
- package/dist/runtime/data/payloadForm.js +2 -1
- package/dist/runtime/data/triggerGlyphs.d.ts +47 -0
- package/dist/runtime/data/triggerGlyphs.js +11 -0
- package/dist/runtime/data/workflowIconCatalog.js +2 -1
- package/dist/runtime/i18n/messages/de.json +1224 -0
- package/dist/runtime/i18n/messages/en.json +1224 -0
- package/dist/runtime/i18n/translate.d.ts +83 -0
- package/dist/runtime/i18n/translate.js +41 -0
- package/dist/runtime/pages/integrations/audit.vue +52 -34
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +3 -2
- package/dist/runtime/pages/integrations/credentials.vue +73 -52
- package/dist/runtime/pages/integrations/index.vue +66 -73
- package/dist/runtime/pages/integrations/runs.vue +33 -21
- package/dist/runtime/pages/integrations/schedules.vue +49 -28
- package/dist/runtime/pages/integrations/secrets.vue +34 -34
- package/dist/runtime/pages/integrations/templates/[slug].vue +89 -72
- package/dist/runtime/pages/integrations/templates/index.vue +35 -27
- package/dist/runtime/pages/integrations/workflows/[id].vue +351 -197
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -5
- package/dist/runtime/plugins/locale.d.ts +10 -0
- package/dist/runtime/plugins/locale.js +6 -0
- package/dist/runtime/plugins/realtime.client.d.ts +32 -0
- package/dist/runtime/plugins/realtime.client.js +4 -0
- package/dist/runtime/types/integrations.d.ts +25 -1
- package/dist/runtime/utils/cockpitSources.d.ts +22 -3
- package/dist/runtime/utils/credentialState.d.ts +30 -2
- package/dist/runtime/utils/credentialState.js +26 -11
- package/dist/runtime/utils/localize.d.ts +52 -5
- package/dist/runtime/utils/localize.js +47 -9
- package/dist/runtime/utils/pollBudget.d.ts +72 -0
- package/dist/runtime/utils/pollBudget.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +0 -25
- package/dist/runtime/utils/schedule.js +37 -25
- package/dist/runtime/utils/servedPaths.d.ts +22 -0
- package/dist/runtime/utils/servedPaths.js +3 -0
- package/dist/runtime/utils/statusStyle.d.ts +26 -2
- package/dist/runtime/utils/statusStyle.js +9 -1
- package/dist/runtime/utils/validationFindings.d.ts +2 -2
- package/dist/runtime/utils/validationFindings.js +4 -3
- package/dist/types.d.mts +2 -6
- package/package.json +3 -2
|
@@ -1,8 +1,9 @@
|
|
|
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,
|
|
3
|
+
import { AltArrowLeft as ArrowLeft, AltArrowRight as Chevron, Copy as CopyIcon, RefreshCircle as Loader2, Play, Refresh as RetryIcon, Diskette as Save, SidebarMinimalistic as RailIcon } from "#solar-icons";
|
|
4
4
|
import {
|
|
5
5
|
AlertDialog,
|
|
6
|
+
AlertDialogCancel,
|
|
6
7
|
AlertDialogContent,
|
|
7
8
|
AlertDialogDescription,
|
|
8
9
|
AlertDialogFooter,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
SelectItem,
|
|
21
22
|
SelectTrigger,
|
|
22
23
|
SelectValue,
|
|
24
|
+
Switch,
|
|
23
25
|
Textarea
|
|
24
26
|
} from "@revenexx/studio";
|
|
25
27
|
import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
|
|
@@ -31,26 +33,56 @@ import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
|
31
33
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
32
34
|
import { toast } from "vue-sonner";
|
|
33
35
|
import { IntegrationsApiError, apiValidationIssues } from "../../../utils/apiError";
|
|
36
|
+
import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
34
37
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
35
38
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
36
39
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
40
|
+
const t = useIntegrationsT();
|
|
37
41
|
const route = useRoute();
|
|
38
42
|
const router = useRouter();
|
|
39
43
|
const { workflows, get, update, toggleActive } = useIntegrationsWorkflows();
|
|
40
|
-
const { autoEnable } = useIntegrationsFocus();
|
|
41
|
-
autoEnable();
|
|
42
44
|
const id = computed(() => Number(route.params.id));
|
|
43
45
|
const workflow = computed(() => workflows.value.find((w) => w.id === id.value) ?? null);
|
|
44
|
-
useHead({ title: () => `${workflow.value?.name ?? "Workflow"} \xB7 Revenexx Cockpit` });
|
|
45
46
|
const tab = ref("editor");
|
|
46
|
-
const tabs = [
|
|
47
|
-
{ key: "editor", label: "
|
|
48
|
-
{ key: "history", label: "
|
|
49
|
-
{ key: "runs", label: "
|
|
50
|
-
{ key: "dead-letters", label: "
|
|
51
|
-
];
|
|
52
|
-
const
|
|
53
|
-
const isDirty = computed(() =>
|
|
47
|
+
const tabs = computed(() => [
|
|
48
|
+
{ key: "editor", label: t("integration.editor.tab.editor") },
|
|
49
|
+
{ key: "history", label: t("integration.editor.tab.history") },
|
|
50
|
+
{ key: "runs", label: t("integration.editor.tab.runs") },
|
|
51
|
+
{ key: "dead-letters", label: t("integration.editor.tab.deadLetters") }
|
|
52
|
+
]);
|
|
53
|
+
const draft = ref(null);
|
|
54
|
+
const isDirty = computed(() => draft.value !== null);
|
|
55
|
+
function draftFrom() {
|
|
56
|
+
if (draft.value) return draft.value;
|
|
57
|
+
const w = workflow.value;
|
|
58
|
+
if (!w) return null;
|
|
59
|
+
return {
|
|
60
|
+
name: w.name,
|
|
61
|
+
description: w.description ?? null,
|
|
62
|
+
executionMode: w.execution_mode ?? "async_only",
|
|
63
|
+
tags: [...w.blob.tags ?? []],
|
|
64
|
+
icon: w.blob.icon ?? null,
|
|
65
|
+
blob: JSON.parse(JSON.stringify(w.blob))
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function payloadBlob(d) {
|
|
69
|
+
const blob = JSON.parse(JSON.stringify(d.blob));
|
|
70
|
+
blob.name = d.name;
|
|
71
|
+
if (d.description) blob.description = d.description;
|
|
72
|
+
else delete blob.description;
|
|
73
|
+
if (d.tags.length) blob.tags = [...d.tags];
|
|
74
|
+
else delete blob.tags;
|
|
75
|
+
if (d.icon) blob.icon = d.icon;
|
|
76
|
+
else delete blob.icon;
|
|
77
|
+
return blob;
|
|
78
|
+
}
|
|
79
|
+
const shownName = computed(() => draft.value?.name ?? workflow.value?.name ?? "");
|
|
80
|
+
const draftedMode = computed(() => draft.value?.executionMode ?? workflow.value?.execution_mode ?? "async_only");
|
|
81
|
+
const shownIcon = computed(() => draft.value ? draft.value.icon : workflow.value?.blob.icon ?? null);
|
|
82
|
+
const shownTags = computed(() => draft.value ? draft.value.tags : [...workflow.value?.blob.tags ?? []]);
|
|
83
|
+
const shownDescription = computed(() => (draft.value ? draft.value.description : workflow.value?.description) ?? "");
|
|
84
|
+
const shownBlob = computed(() => draft.value?.blob ?? workflow.value?.blob ?? null);
|
|
85
|
+
useHead({ title: () => t("integration.editor.head.title", { name: shownName.value || t("integration.editor.head.untitled") }) });
|
|
54
86
|
const saving = ref(false);
|
|
55
87
|
const saveError = ref(null);
|
|
56
88
|
const validationWarnings = computed(() => workflow.value?.warnings);
|
|
@@ -76,7 +108,7 @@ onMounted(() => {
|
|
|
76
108
|
void loadCredentials();
|
|
77
109
|
});
|
|
78
110
|
const reauthCredentials = computed(() => {
|
|
79
|
-
const blob = JSON.stringify(
|
|
111
|
+
const blob = JSON.stringify(shownBlob.value ?? {});
|
|
80
112
|
return needsReauth.value.filter((c) => blob.includes(c.id));
|
|
81
113
|
});
|
|
82
114
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
@@ -95,12 +127,12 @@ async function waitForBuildReady() {
|
|
|
95
127
|
const status = fresh?.buildStatus;
|
|
96
128
|
if (status === "pending" || status === "building") continue;
|
|
97
129
|
if (status === "failed") {
|
|
98
|
-
toast.error("
|
|
130
|
+
toast.error(t("integration.editor.build.toast.failed"));
|
|
99
131
|
return false;
|
|
100
132
|
}
|
|
101
133
|
return true;
|
|
102
134
|
}
|
|
103
|
-
toast.error("
|
|
135
|
+
toast.error(t("integration.editor.build.toast.timeout"));
|
|
104
136
|
return false;
|
|
105
137
|
} finally {
|
|
106
138
|
waitingForBuild.value = false;
|
|
@@ -109,13 +141,13 @@ async function waitForBuildReady() {
|
|
|
109
141
|
const buildNotice = computed(() => {
|
|
110
142
|
switch (workflow.value?.buildStatus) {
|
|
111
143
|
case "building":
|
|
112
|
-
return { tone: "info", text: "
|
|
144
|
+
return { tone: "info", text: t("integration.editor.build.notice.building") };
|
|
113
145
|
case "pending":
|
|
114
|
-
return { tone: "info", text: "
|
|
146
|
+
return { tone: "info", text: t("integration.editor.build.notice.pending") };
|
|
115
147
|
case "failed":
|
|
116
148
|
return {
|
|
117
149
|
tone: "error",
|
|
118
|
-
text:
|
|
150
|
+
text: workflow.value?.buildError ? t("integration.editor.build.notice.failedWithError", { error: workflow.value.buildError }) : t("integration.editor.build.notice.failed")
|
|
119
151
|
};
|
|
120
152
|
default:
|
|
121
153
|
return null;
|
|
@@ -123,29 +155,67 @@ const buildNotice = computed(() => {
|
|
|
123
155
|
});
|
|
124
156
|
watch(id, async () => {
|
|
125
157
|
const w = await get(id.value);
|
|
126
|
-
if (!w) await
|
|
127
|
-
|
|
158
|
+
if (!w) await leaveTo("/integrations/workflows");
|
|
159
|
+
draft.value = null;
|
|
128
160
|
warningsExpanded.value = false;
|
|
129
161
|
if (tab.value === "runs") void loadRuns(1);
|
|
130
162
|
}, { immediate: true });
|
|
131
163
|
function onChange(blob) {
|
|
132
|
-
|
|
164
|
+
const d = draftFrom();
|
|
165
|
+
if (d) draft.value = { ...d, blob };
|
|
133
166
|
}
|
|
134
167
|
const editorResetKey = ref(0);
|
|
135
168
|
function discardChanges() {
|
|
136
|
-
|
|
169
|
+
draft.value = null;
|
|
137
170
|
editorResetKey.value++;
|
|
138
171
|
}
|
|
172
|
+
const leaveOpen = ref(false);
|
|
173
|
+
let leaveDecision = null;
|
|
174
|
+
function askBeforeLeaving() {
|
|
175
|
+
leaveOpen.value = true;
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
leaveDecision = resolve;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function answerLeave(leave) {
|
|
181
|
+
leaveOpen.value = false;
|
|
182
|
+
leaveDecision?.(leave);
|
|
183
|
+
leaveDecision = null;
|
|
184
|
+
}
|
|
185
|
+
let leavingOnPurpose = false;
|
|
186
|
+
async function leaveTo(path) {
|
|
187
|
+
leavingOnPurpose = true;
|
|
188
|
+
try {
|
|
189
|
+
await router.push(path);
|
|
190
|
+
} finally {
|
|
191
|
+
leavingOnPurpose = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function guardTheDraft(to, from) {
|
|
195
|
+
if (to.path === from.path) return true;
|
|
196
|
+
if (leavingOnPurpose) return true;
|
|
197
|
+
if (!isDirty.value) return true;
|
|
198
|
+
return await askBeforeLeaving();
|
|
199
|
+
}
|
|
200
|
+
onBeforeRouteLeave(guardTheDraft);
|
|
201
|
+
onBeforeRouteUpdate(guardTheDraft);
|
|
202
|
+
function warnBeforeUnload(e) {
|
|
203
|
+
if (!isDirty.value) return;
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
e.returnValue = "unsaved";
|
|
206
|
+
}
|
|
207
|
+
onMounted(() => window.addEventListener("beforeunload", warnBeforeUnload));
|
|
208
|
+
onUnmounted(() => window.removeEventListener("beforeunload", warnBeforeUnload));
|
|
139
209
|
const duplicateOpen = ref(false);
|
|
140
210
|
const duplicateTarget = computed(() => workflow.value ? { id: workflow.value.id, name: workflow.value.name } : null);
|
|
141
211
|
async function onDuplicated(copy) {
|
|
142
|
-
toast.success(
|
|
143
|
-
await
|
|
212
|
+
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
213
|
+
await leaveTo(`/integrations/workflows/${copy.id}`);
|
|
144
214
|
}
|
|
145
215
|
async function onRevisionRestored() {
|
|
146
216
|
if (!workflow.value) return;
|
|
147
217
|
await get(workflow.value.id);
|
|
148
|
-
|
|
218
|
+
draft.value = null;
|
|
149
219
|
}
|
|
150
220
|
const { trigger: triggerRun, loadForWorkflow, fetchRun, retryRun } = useIntegrationsRuns();
|
|
151
221
|
const running = ref(false);
|
|
@@ -172,11 +242,11 @@ function viewRunFromModal(id2) {
|
|
|
172
242
|
}
|
|
173
243
|
function syncErrorMessage(e) {
|
|
174
244
|
if (e instanceof IntegrationsApiError) {
|
|
175
|
-
if (e.status === 504) return "
|
|
176
|
-
if (e.status === 422 && e.body?.status === "failed") return "
|
|
177
|
-
return e.message || "
|
|
245
|
+
if (e.status === 504) return t("integration.editor.run.error.syncTimeout");
|
|
246
|
+
if (e.status === 422 && e.body?.status === "failed") return t("integration.editor.run.error.syncFailed");
|
|
247
|
+
return e.message || t("integration.editor.run.error.syncFailed");
|
|
178
248
|
}
|
|
179
|
-
return e?.message || "
|
|
249
|
+
return e?.message || t("integration.editor.run.error.syncFailed");
|
|
180
250
|
}
|
|
181
251
|
function refusedForSettings(e) {
|
|
182
252
|
const issues = apiValidationIssues(e);
|
|
@@ -189,7 +259,7 @@ function refusedForSettings(e) {
|
|
|
189
259
|
...groups.filter((g) => !g.nodeId).flatMap((g) => g.messages)
|
|
190
260
|
];
|
|
191
261
|
const where = parts.length ? ` \u2014 ${parts.join("; ")}` : "";
|
|
192
|
-
const line =
|
|
262
|
+
const line = t("integration.editor.run.error.refusedForSettings", { summary, where });
|
|
193
263
|
return /[.!?]$/.test(line) ? line : `${line}.`;
|
|
194
264
|
}
|
|
195
265
|
async function reportRunFailure(e, fallback) {
|
|
@@ -207,7 +277,7 @@ async function run(mode) {
|
|
|
207
277
|
try {
|
|
208
278
|
payload = JSON.parse(payloadText.value || "{}");
|
|
209
279
|
} catch {
|
|
210
|
-
payloadError.value = "
|
|
280
|
+
payloadError.value = t("integration.editor.payload.error.invalidJson");
|
|
211
281
|
return;
|
|
212
282
|
}
|
|
213
283
|
payloadError.value = null;
|
|
@@ -243,14 +313,14 @@ async function run(mode) {
|
|
|
243
313
|
const res = await triggerRun(workflow.value.id, payload, "async");
|
|
244
314
|
runModalRunId.value = extractRunId(res);
|
|
245
315
|
} catch (e) {
|
|
246
|
-
await reportRunFailure(e, (err) => err?.message || "
|
|
316
|
+
await reportRunFailure(e, (err) => err?.message || t("integration.editor.run.error.couldNotStart"));
|
|
247
317
|
} finally {
|
|
248
318
|
running.value = false;
|
|
249
319
|
syncLoading.value = false;
|
|
250
320
|
void loadRuns(1);
|
|
251
321
|
}
|
|
252
322
|
}
|
|
253
|
-
const payloadDriven = computed(() => payloadFieldsFromBlob(
|
|
323
|
+
const payloadDriven = computed(() => payloadFieldsFromBlob(shownBlob.value ?? { nodes: [] }).length > 0);
|
|
254
324
|
async function runFromEditor() {
|
|
255
325
|
if (!workflow.value) return;
|
|
256
326
|
if (payloadDriven.value) {
|
|
@@ -266,7 +336,7 @@ const eventLog = typeof useIntegrationsEvents === "function" ? useIntegrationsEv
|
|
|
266
336
|
} };
|
|
267
337
|
const prefillBusy = ref(null);
|
|
268
338
|
const eventTriggerTopic = computed(() => {
|
|
269
|
-
const trigger = (workflow.value?.blob.triggers ?? []).find((
|
|
339
|
+
const trigger = (workflow.value?.blob.triggers ?? []).find((t2) => t2.type === "event");
|
|
270
340
|
const topic = trigger?.config?.event_type;
|
|
271
341
|
return typeof topic === "string" && topic.trim() !== "" ? topic.trim() : null;
|
|
272
342
|
});
|
|
@@ -301,7 +371,7 @@ async function insertLatestEvent() {
|
|
|
301
371
|
await eventLog.loadEvents();
|
|
302
372
|
const match = eventLog.events.value.filter((e) => e.topic === topic).sort((a, b) => (b.time ?? "").localeCompare(a.time ?? ""))[0];
|
|
303
373
|
if (!match) {
|
|
304
|
-
toast.info(
|
|
374
|
+
toast.info(t("integration.editor.event.toast.noneReceived", { topic }));
|
|
305
375
|
return;
|
|
306
376
|
}
|
|
307
377
|
payloadText.value = envelope(topic, match.data, {
|
|
@@ -327,6 +397,9 @@ const runsError = ref(null);
|
|
|
327
397
|
const runsPage = ref(1);
|
|
328
398
|
const runsTotal = ref(0);
|
|
329
399
|
const runsLastPage = ref(1);
|
|
400
|
+
const RUNS_POLL_MS = 3e3;
|
|
401
|
+
const { delivering: realtimeDelivering, runTick, lastRunEvent, buildTick, lastBuildEvent } = useIntegrationsRealtime();
|
|
402
|
+
const runsBudget = createPollBudget({ intervalMs: RUNS_POLL_MS, liveIntervalMs: SAFETY_POLL_MS });
|
|
330
403
|
let runsPoll = null;
|
|
331
404
|
function stopRunsPoll() {
|
|
332
405
|
if (runsPoll) {
|
|
@@ -337,12 +410,25 @@ function stopRunsPoll() {
|
|
|
337
410
|
function startRunsPoll() {
|
|
338
411
|
stopRunsPoll();
|
|
339
412
|
runsPoll = setInterval(() => {
|
|
340
|
-
if (runsPage.value === 1) void loadRuns(1, { silent: true });
|
|
341
|
-
},
|
|
413
|
+
if (runsPage.value === 1 && runsBudget.due(realtimeDelivering.value)) void loadRuns(1, { silent: true });
|
|
414
|
+
}, RUNS_POLL_MS);
|
|
342
415
|
}
|
|
343
416
|
onUnmounted(stopRunsPoll);
|
|
417
|
+
watch(runTick, () => {
|
|
418
|
+
if (tab.value !== "runs" || runsPage.value !== 1) return;
|
|
419
|
+
const eventWorkflow = workflowIdOf(lastRunEvent.value);
|
|
420
|
+
if (eventWorkflow && eventWorkflow !== String(workflow.value?.id ?? "")) return;
|
|
421
|
+
void loadRuns(1, { silent: true });
|
|
422
|
+
});
|
|
423
|
+
watch(buildTick, () => {
|
|
424
|
+
if (!workflow.value) return;
|
|
425
|
+
const eventWorkflow = lastBuildEvent.value?.topicId ?? workflowIdOf(lastBuildEvent.value);
|
|
426
|
+
if (eventWorkflow && eventWorkflow !== String(workflow.value.id)) return;
|
|
427
|
+
void get(workflow.value.id);
|
|
428
|
+
});
|
|
344
429
|
async function loadRuns(page = runsPage.value, opts = {}) {
|
|
345
430
|
if (!workflow.value) return;
|
|
431
|
+
runsBudget.spend();
|
|
346
432
|
if (!opts.silent) runsLoading.value = true;
|
|
347
433
|
runsError.value = null;
|
|
348
434
|
try {
|
|
@@ -356,7 +442,7 @@ async function loadRuns(page = runsPage.value, opts = {}) {
|
|
|
356
442
|
runsLastPage.value = res.lastPage;
|
|
357
443
|
runsPage.value = res.page;
|
|
358
444
|
} catch (e) {
|
|
359
|
-
runsError.value = e.message || "
|
|
445
|
+
runsError.value = e.message || t("integration.editor.runs.error.load");
|
|
360
446
|
} finally {
|
|
361
447
|
if (!opts.silent) runsLoading.value = false;
|
|
362
448
|
}
|
|
@@ -365,20 +451,43 @@ function goRunsPage(p) {
|
|
|
365
451
|
const next = Math.min(Math.max(1, p), runsLastPage.value);
|
|
366
452
|
if (next !== runsPage.value) void loadRuns(next);
|
|
367
453
|
}
|
|
368
|
-
watch(tab, (
|
|
369
|
-
if (
|
|
454
|
+
watch(tab, (t2) => {
|
|
455
|
+
if (t2 === "runs") {
|
|
370
456
|
void loadRuns(1);
|
|
371
457
|
startRunsPoll();
|
|
372
458
|
} else stopRunsPoll();
|
|
373
459
|
});
|
|
374
460
|
const settingsOpen = ref(false);
|
|
375
461
|
const openRunId = computed(() => route.query.run != null ? String(route.query.run) : null);
|
|
462
|
+
const { focused: railFolded, enable, disable } = useIntegrationsFocus();
|
|
463
|
+
const WIDE_CANVAS_KEY = "integrations.editor.wideCanvas";
|
|
464
|
+
const wideCanvasWanted = ref(false);
|
|
465
|
+
try {
|
|
466
|
+
wideCanvasWanted.value = localStorage.getItem(WIDE_CANVAS_KEY) === "1";
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
const canvasFillsPage = computed(() => tab.value === "editor" && !openRunId.value);
|
|
470
|
+
const railShouldFold = computed(() => wideCanvasWanted.value && canvasFillsPage.value);
|
|
471
|
+
function applyRail() {
|
|
472
|
+
if (railShouldFold.value) enable();
|
|
473
|
+
else disable();
|
|
474
|
+
}
|
|
475
|
+
onMounted(() => applyRail());
|
|
476
|
+
watch(railShouldFold, () => applyRail());
|
|
477
|
+
onUnmounted(() => disable());
|
|
478
|
+
function toggleRail() {
|
|
479
|
+
wideCanvasWanted.value = !wideCanvasWanted.value;
|
|
480
|
+
try {
|
|
481
|
+
localStorage.setItem(WIDE_CANVAS_KEY, wideCanvasWanted.value ? "1" : "0");
|
|
482
|
+
} catch {
|
|
483
|
+
}
|
|
484
|
+
}
|
|
376
485
|
const runView = ref("details");
|
|
377
|
-
const runViews = [
|
|
378
|
-
{ key: "details", label: "
|
|
379
|
-
{ key: "timeline", label: "
|
|
380
|
-
{ key: "replay", label: "
|
|
381
|
-
];
|
|
486
|
+
const runViews = computed(() => [
|
|
487
|
+
{ key: "details", label: t("integration.editor.runView.details") },
|
|
488
|
+
{ key: "timeline", label: t("integration.editor.runView.timeline") },
|
|
489
|
+
{ key: "replay", label: t("integration.editor.runView.replay") }
|
|
490
|
+
]);
|
|
382
491
|
const openRun = ref(null);
|
|
383
492
|
const openRunError = ref(null);
|
|
384
493
|
async function loadOpenRun() {
|
|
@@ -387,7 +496,7 @@ async function loadOpenRun() {
|
|
|
387
496
|
try {
|
|
388
497
|
openRun.value = await fetchRun(workflow.value.id, openRunId.value, workflow.value.name);
|
|
389
498
|
} catch (e) {
|
|
390
|
-
openRunError.value = e?.message ?? "
|
|
499
|
+
openRunError.value = e?.message ?? t("integration.editor.run.error.loadOpenRun");
|
|
391
500
|
}
|
|
392
501
|
}
|
|
393
502
|
watch(openRunId, () => {
|
|
@@ -410,91 +519,51 @@ async function retryOpenRun() {
|
|
|
410
519
|
try {
|
|
411
520
|
const res = await retryRun(openRun.value.workflowId, openRun.value.id);
|
|
412
521
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
413
|
-
toast.success(newId && newId !== openRun.value.id ?
|
|
522
|
+
toast.success(newId && newId !== openRun.value.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
|
|
414
523
|
void loadRuns(1, { silent: true });
|
|
415
524
|
} catch (e) {
|
|
416
|
-
toast.error(e?.message ?? "
|
|
525
|
+
toast.error(e?.message ?? t("integration.editor.retry.error.generic"));
|
|
417
526
|
} finally {
|
|
418
527
|
retryBusy.value = false;
|
|
419
528
|
}
|
|
420
529
|
}
|
|
421
|
-
const EXECUTION_MODES = [
|
|
422
|
-
{ value: "async_only", label: "
|
|
423
|
-
{ value: "sync_only", label: "
|
|
424
|
-
{ value: "caller_decides", label: "
|
|
425
|
-
];
|
|
426
|
-
const executionModeDraft = ref("async_only");
|
|
427
|
-
watch(workflow, (w) => {
|
|
428
|
-
if (w) executionModeDraft.value = w.execution_mode ?? "async_only";
|
|
429
|
-
}, { immediate: true });
|
|
430
|
-
const savingMode = ref(false);
|
|
530
|
+
const EXECUTION_MODES = computed(() => [
|
|
531
|
+
{ value: "async_only", label: t("integration.editor.executionMode.async.label"), hint: t("integration.editor.executionMode.async.hint") },
|
|
532
|
+
{ value: "sync_only", label: t("integration.editor.executionMode.sync.label"), hint: t("integration.editor.executionMode.sync.hint") },
|
|
533
|
+
{ value: "caller_decides", label: t("integration.editor.executionMode.callerDecides.label"), hint: t("integration.editor.executionMode.callerDecides.hint") }
|
|
534
|
+
]);
|
|
431
535
|
function modeLabel(m) {
|
|
432
|
-
return m === "sync_only" ? "
|
|
433
|
-
}
|
|
434
|
-
async function saveExecutionMode(mode) {
|
|
435
|
-
if (!workflow.value || mode === workflow.value.execution_mode) return;
|
|
436
|
-
const prev = workflow.value.execution_mode ?? "async_only";
|
|
437
|
-
executionModeDraft.value = mode;
|
|
438
|
-
savingMode.value = true;
|
|
439
|
-
saveError.value = null;
|
|
440
|
-
try {
|
|
441
|
-
await update(workflow.value.id, {
|
|
442
|
-
name: workflow.value.name,
|
|
443
|
-
description: workflow.value.description,
|
|
444
|
-
blob: workflow.value.blob,
|
|
445
|
-
active: workflow.value.active,
|
|
446
|
-
executionMode: mode
|
|
447
|
-
});
|
|
448
|
-
await get(workflow.value.id);
|
|
449
|
-
} catch (e) {
|
|
450
|
-
executionModeDraft.value = prev;
|
|
451
|
-
saveError.value = e.message || "Could not update the execution mode.";
|
|
452
|
-
} finally {
|
|
453
|
-
savingMode.value = false;
|
|
454
|
-
}
|
|
536
|
+
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");
|
|
455
537
|
}
|
|
456
|
-
const savingMeta = ref(false);
|
|
457
538
|
const tagSuggestions = computed(() => {
|
|
458
539
|
const all = /* @__PURE__ */ new Set();
|
|
459
|
-
for (const wf of workflows.value) for (const
|
|
540
|
+
for (const wf of workflows.value) for (const t2 of wf.blob.tags ?? []) all.add(t2);
|
|
460
541
|
return [...all].sort();
|
|
461
542
|
});
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
executionMode: workflow.value.execution_mode
|
|
481
|
-
});
|
|
482
|
-
await get(workflow.value.id);
|
|
483
|
-
dirtyBlob.value = null;
|
|
484
|
-
toast.success("Appearance saved");
|
|
485
|
-
} catch (e) {
|
|
486
|
-
const msg = e.message || "Could not save the appearance.";
|
|
487
|
-
saveError.value = msg;
|
|
488
|
-
toast.error(msg);
|
|
489
|
-
} finally {
|
|
490
|
-
savingMeta.value = false;
|
|
491
|
-
}
|
|
543
|
+
const settingsValues = computed(() => ({
|
|
544
|
+
name: shownName.value,
|
|
545
|
+
description: shownDescription.value,
|
|
546
|
+
tags: [...shownTags.value],
|
|
547
|
+
icon: shownIcon.value,
|
|
548
|
+
executionMode: draftedMode.value
|
|
549
|
+
}));
|
|
550
|
+
function applySettings(values) {
|
|
551
|
+
const d = draftFrom();
|
|
552
|
+
if (!d) return;
|
|
553
|
+
draft.value = {
|
|
554
|
+
...d,
|
|
555
|
+
name: values.name,
|
|
556
|
+
description: values.description || null,
|
|
557
|
+
executionMode: values.executionMode,
|
|
558
|
+
tags: [...values.tags],
|
|
559
|
+
icon: values.icon
|
|
560
|
+
};
|
|
492
561
|
}
|
|
493
562
|
async function onToggleActive() {
|
|
494
563
|
const wf = workflow.value;
|
|
495
564
|
if (!wf) return;
|
|
496
565
|
if (!wf.active && isInvalid.value) {
|
|
497
|
-
toast.error(
|
|
566
|
+
toast.error(t("integration.editor.activate.error.needsFixing", validationWarnings.value?.length ?? 0));
|
|
498
567
|
return;
|
|
499
568
|
}
|
|
500
569
|
try {
|
|
@@ -503,25 +572,26 @@ async function onToggleActive() {
|
|
|
503
572
|
const issues = apiValidationIssues(e);
|
|
504
573
|
if (issues.length) {
|
|
505
574
|
await get(wf.id);
|
|
506
|
-
toast.error(
|
|
575
|
+
toast.error(t("integration.editor.activate.error.validationIssues", issues.length));
|
|
507
576
|
return;
|
|
508
577
|
}
|
|
509
|
-
toast.error(e.message || "
|
|
578
|
+
toast.error(e.message || t("integration.editor.activate.error.generic"));
|
|
510
579
|
}
|
|
511
580
|
}
|
|
512
581
|
async function save() {
|
|
513
|
-
|
|
582
|
+
const d = draft.value;
|
|
583
|
+
if (!workflow.value || !d) return;
|
|
514
584
|
saving.value = true;
|
|
515
585
|
saveError.value = null;
|
|
516
586
|
try {
|
|
517
587
|
await update(workflow.value.id, {
|
|
518
|
-
name:
|
|
519
|
-
description:
|
|
520
|
-
blob:
|
|
588
|
+
name: d.name,
|
|
589
|
+
description: d.description,
|
|
590
|
+
blob: payloadBlob(d),
|
|
521
591
|
active: workflow.value.active,
|
|
522
|
-
executionMode:
|
|
592
|
+
executionMode: d.executionMode
|
|
523
593
|
});
|
|
524
|
-
|
|
594
|
+
draft.value = null;
|
|
525
595
|
} catch (e) {
|
|
526
596
|
const issues = apiValidationIssues(e);
|
|
527
597
|
if (issues.length && workflow.value.active) {
|
|
@@ -533,33 +603,34 @@ async function save() {
|
|
|
533
603
|
});
|
|
534
604
|
deactivationOpen.value = true;
|
|
535
605
|
} else {
|
|
536
|
-
saveError.value = e.message || "
|
|
606
|
+
saveError.value = e.message || t("integration.editor.save.error.generic");
|
|
537
607
|
}
|
|
538
608
|
} finally {
|
|
539
609
|
saving.value = false;
|
|
540
610
|
}
|
|
541
611
|
}
|
|
542
612
|
async function confirmDeactivate() {
|
|
543
|
-
|
|
613
|
+
const d = draft.value;
|
|
614
|
+
if (!workflow.value || !d) {
|
|
544
615
|
deactivationOpen.value = false;
|
|
545
616
|
return;
|
|
546
617
|
}
|
|
547
618
|
saving.value = true;
|
|
548
619
|
try {
|
|
549
620
|
await update(workflow.value.id, {
|
|
550
|
-
name:
|
|
551
|
-
description:
|
|
552
|
-
blob:
|
|
621
|
+
name: d.name,
|
|
622
|
+
description: d.description,
|
|
623
|
+
blob: payloadBlob(d),
|
|
553
624
|
active: false,
|
|
554
|
-
executionMode:
|
|
625
|
+
executionMode: d.executionMode
|
|
555
626
|
});
|
|
556
627
|
await get(workflow.value.id);
|
|
557
|
-
|
|
628
|
+
draft.value = null;
|
|
558
629
|
deactivationOpen.value = false;
|
|
559
|
-
toast.success("
|
|
630
|
+
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
560
631
|
} catch (e) {
|
|
561
632
|
deactivationOpen.value = false;
|
|
562
|
-
saveError.value = e.message || "
|
|
633
|
+
saveError.value = e.message || t("integration.editor.save.error.generic");
|
|
563
634
|
} finally {
|
|
564
635
|
saving.value = false;
|
|
565
636
|
}
|
|
@@ -580,44 +651,76 @@ function formatDateTime(iso) {
|
|
|
580
651
|
remaining height below the page header. -->
|
|
581
652
|
<div class="flex h-full flex-col">
|
|
582
653
|
<template v-if="workflow">
|
|
583
|
-
<PageHeader :title="
|
|
654
|
+
<PageHeader :title="shownName">
|
|
584
655
|
<template #title>
|
|
585
656
|
<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">
|
|
657
|
+
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
587
658
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
588
659
|
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
589
660
|
back into the workflow surface. -->
|
|
590
661
|
<template v-if="openRunId">
|
|
591
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{
|
|
662
|
+
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ shownName }}</button>
|
|
592
663
|
<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')">
|
|
664
|
+
<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
665
|
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
595
666
|
<!-- The Temporal run id IS the run's identity; the internal
|
|
596
667
|
number only fills in until the run is handed off. -->
|
|
597
|
-
<span class="truncate font-mono">{{ openRun?.temporalRunId ??
|
|
668
|
+
<span class="truncate font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
598
669
|
</template>
|
|
599
670
|
<template v-else>
|
|
600
671
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
601
|
-
<WorkflowIconView :icon="
|
|
672
|
+
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
602
673
|
</span>
|
|
603
|
-
<span class="truncate">{{
|
|
604
|
-
|
|
674
|
+
<span class="truncate">{{ shownName }}</span>
|
|
675
|
+
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
676
|
+
oversight. A name changes nothing about what happens when Run is
|
|
677
|
+
pressed, so the header may show what the operator just entered. The
|
|
678
|
+
dispatch mode decides what a run does — and a run goes to the
|
|
679
|
+
workflow the platform holds, not to the draft. A badge following the
|
|
680
|
+
draft would read SYNC over a button that fires an async run. -->
|
|
681
|
+
<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
682
|
{{ modeLabel(workflow.execution_mode) }}
|
|
606
683
|
</Badge>
|
|
607
684
|
</template>
|
|
608
685
|
</span>
|
|
609
686
|
</template>
|
|
610
687
|
<template #actions>
|
|
688
|
+
<!-- Folding the rail away, and getting it back (PO-273). It reads as
|
|
689
|
+
chrome rather than as an action on this workflow, so the natural
|
|
690
|
+
place was ahead of the breadcrumb — but the shell's `#title` slot
|
|
691
|
+
*is* the `<h1>`'s content, and a control inside the heading makes
|
|
692
|
+
the page announce itself as "Hide navigation Workflows › Order
|
|
693
|
+
sync". `accessibility.md` AC-4 promises one heading that names what
|
|
694
|
+
the operator is looking at, so this sits in `#actions`, which is
|
|
695
|
+
outside it. Leftmost there is as near the rail as the shared header
|
|
696
|
+
allows without a slot of its own.
|
|
697
|
+
|
|
698
|
+
It carries its words and not just a pictogram: an icon-only control
|
|
699
|
+
here is one nobody finds, and the way back out of a folded rail is
|
|
700
|
+
the one control that cannot afford to be learnt by chance. Rendered
|
|
701
|
+
only where the canvas fills the page, the one surface it acts on. -->
|
|
702
|
+
<Button
|
|
703
|
+
v-if="canvasFillsPage"
|
|
704
|
+
variant="ghost"
|
|
705
|
+
size="sm"
|
|
706
|
+
class="h-7 shrink-0 gap-1.5 px-2 text-sm font-normal text-muted-foreground hover:text-primary"
|
|
707
|
+
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
708
|
+
@click="toggleRail"
|
|
709
|
+
>
|
|
710
|
+
<RailIcon :size="15" />
|
|
711
|
+
{{ railFolded ? t("integration.nav.action.show") : t("integration.nav.action.hide") }}
|
|
712
|
+
</Button>
|
|
713
|
+
|
|
611
714
|
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
612
715
|
<template v-if="openRunId">
|
|
613
716
|
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
|
|
614
717
|
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
615
718
|
<RetryIcon v-else :size="12" />
|
|
616
|
-
|
|
719
|
+
{{ t("integration.editor.action.retry") }}
|
|
617
720
|
</Button>
|
|
618
721
|
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
619
722
|
<ArrowLeft :size="12" />
|
|
620
|
-
|
|
723
|
+
{{ t("integration.editor.action.backToWorkflow") }}
|
|
621
724
|
</Button>
|
|
622
725
|
</template>
|
|
623
726
|
<template v-else>
|
|
@@ -627,32 +730,52 @@ function formatDateTime(iso) {
|
|
|
627
730
|
:title="buildNotice?.text"
|
|
628
731
|
>
|
|
629
732
|
<Loader2 :size="12" class="animate-spin" />
|
|
630
|
-
{{ workflow.buildStatus === "building" ? "
|
|
733
|
+
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
631
734
|
</span>
|
|
632
|
-
<span v-if="isDirty" class="text-sm text-amber-600">
|
|
633
|
-
<Button variant="outline" class="h-7 text-sm" :disabled="saving" title="
|
|
735
|
+
<span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
736
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="saving" :title="t('integration.editor.action.duplicate.title')" @click="duplicateOpen = true">
|
|
634
737
|
<CopyIcon :size="12" />
|
|
635
|
-
|
|
738
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
636
739
|
</Button>
|
|
637
|
-
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" title="
|
|
638
|
-
|
|
740
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
741
|
+
{{ t("integration.editor.action.discard.label") }}
|
|
639
742
|
</Button>
|
|
640
743
|
<Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
|
|
641
744
|
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
642
745
|
<Save v-else :size="12" />
|
|
643
|
-
{{ saving ? "
|
|
746
|
+
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
644
747
|
</Button>
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
748
|
+
<!-- Activation is a standing state, not a transport control. The play
|
|
749
|
+
symbol belongs to Run, 600 px away in the canvas's control pane,
|
|
750
|
+
where pressing it makes one thing happen now; arming a workflow's
|
|
751
|
+
triggers looks like nothing happening at all. So this states the
|
|
752
|
+
state in the words the workflows list uses — Active, Paused — and
|
|
753
|
+
the switch beside it is the flip (PO-254).
|
|
754
|
+
|
|
755
|
+
The blocked reason has two homes because a disabled control shows
|
|
756
|
+
no native tooltip in Chrome: the title sits on the wrapper, which
|
|
757
|
+
is not disabled, and `aria-describedby` puts the same sentence on
|
|
758
|
+
the switch for anyone who never hovers. Sighted users also have
|
|
759
|
+
the amber banner directly below, which says it at length. -->
|
|
760
|
+
<div
|
|
761
|
+
class="flex h-7 items-center gap-2 rounded-md border border-black/[0.08] px-2"
|
|
762
|
+
: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')"
|
|
652
763
|
>
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
764
|
+
<Switch
|
|
765
|
+
:aria-label="t('integration.editor.action.state.label')"
|
|
766
|
+
:aria-describedby="!workflow.active && isInvalid ? 'wf-activation-blocked' : void 0"
|
|
767
|
+
class="data-[state=checked]:bg-emerald-600 data-[state=unchecked]:bg-slate-300"
|
|
768
|
+
:model-value="workflow.active"
|
|
769
|
+
:disabled="!workflow.active && isInvalid"
|
|
770
|
+
@update:model-value="onToggleActive"
|
|
771
|
+
/>
|
|
772
|
+
<span class="text-sm" :class="workflow.active ? 'font-medium text-emerald-700' : 'text-muted-foreground'">
|
|
773
|
+
{{ workflow.active ? t("integration.workflows.table.status.lifecycle.active") : t("integration.workflows.table.status.lifecycle.paused") }}
|
|
774
|
+
</span>
|
|
775
|
+
<span v-if="!workflow.active && isInvalid" id="wf-activation-blocked" class="sr-only">
|
|
776
|
+
{{ t("integration.editor.action.activate.title.invalid", validationWarnings?.length ?? 0) }}
|
|
777
|
+
</span>
|
|
778
|
+
</div>
|
|
656
779
|
</template>
|
|
657
780
|
</template>
|
|
658
781
|
|
|
@@ -666,17 +789,16 @@ function formatDateTime(iso) {
|
|
|
666
789
|
</div>
|
|
667
790
|
|
|
668
791
|
<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
|
-
{{
|
|
670
|
-
need{{ reauthCredentials.length === 1 ? "s" : "" }} reconnecting
|
|
792
|
+
{{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
|
|
671
793
|
({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
|
|
672
794
|
<NuxtLink to="/integrations/credentials" class="font-medium underline">
|
|
673
|
-
fix
|
|
795
|
+
{{ t("integration.editor.reauth.action.fix") }}
|
|
674
796
|
</NuxtLink>.
|
|
675
797
|
</div>
|
|
676
798
|
|
|
677
799
|
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
678
|
-
be activated. The banner names what's incomplete; the
|
|
679
|
-
stays
|
|
800
|
+
be activated. The banner names what's incomplete; the header's state
|
|
801
|
+
switch stays blocked until the next save recomputes an empty warnings set.
|
|
680
802
|
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
681
803
|
One line per NODE, listing its fields (PO-209): the messages behind
|
|
682
804
|
them ride along as the line's title, and the line leads to the node. -->
|
|
@@ -685,7 +807,7 @@ function formatDateTime(iso) {
|
|
|
685
807
|
class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
|
|
686
808
|
>
|
|
687
809
|
<p class="font-medium">
|
|
688
|
-
|
|
810
|
+
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
689
811
|
</p>
|
|
690
812
|
<ul
|
|
691
813
|
class="mt-1 space-y-0.5"
|
|
@@ -704,7 +826,7 @@ function formatDateTime(iso) {
|
|
|
704
826
|
type="button"
|
|
705
827
|
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
706
828
|
:title="g.messages.join('\n')"
|
|
707
|
-
:aria-label="
|
|
829
|
+
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
708
830
|
@click="revealNode(g.nodeId)"
|
|
709
831
|
>
|
|
710
832
|
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
@@ -713,7 +835,7 @@ function formatDateTime(iso) {
|
|
|
713
835
|
</li>
|
|
714
836
|
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
715
837
|
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
716
|
-
|
|
838
|
+
{{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
|
|
717
839
|
</button>
|
|
718
840
|
</li>
|
|
719
841
|
</ul>
|
|
@@ -738,7 +860,7 @@ function formatDateTime(iso) {
|
|
|
738
860
|
{{ openRunError }}
|
|
739
861
|
</div>
|
|
740
862
|
<div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
|
|
741
|
-
<Loader2 :size="16" class="animate-spin" />
|
|
863
|
+
<Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
|
|
742
864
|
</div>
|
|
743
865
|
</div>
|
|
744
866
|
</div>
|
|
@@ -759,9 +881,10 @@ function formatDateTime(iso) {
|
|
|
759
881
|
:key="editorResetKey"
|
|
760
882
|
ref="editorRef"
|
|
761
883
|
:workflow="workflow"
|
|
884
|
+
:draft="draft?.blob ?? null"
|
|
762
885
|
:run-busy="running || syncLoading || waitingForBuild"
|
|
763
886
|
:run-waiting="waitingForBuild"
|
|
764
|
-
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen"
|
|
887
|
+
:overlay-open="settingsOpen || duplicateOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
765
888
|
@change="onChange"
|
|
766
889
|
@run="runFromEditor"
|
|
767
890
|
@toggle-active="onToggleActive"
|
|
@@ -781,19 +904,19 @@ function formatDateTime(iso) {
|
|
|
781
904
|
:loading="runsLoading"
|
|
782
905
|
:pagination="false"
|
|
783
906
|
:show-workflow="false"
|
|
784
|
-
empty-text="
|
|
907
|
+
:empty-text="t('integration.editor.runs.empty')"
|
|
785
908
|
@changed="() => loadRuns()"
|
|
786
909
|
/>
|
|
787
910
|
|
|
788
911
|
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
789
912
|
<div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
790
|
-
<span class="tabular-nums">
|
|
913
|
+
<span class="tabular-nums">{{ t("integration.editor.runs.pagination.page", { page: runsPage, lastPage: runsLastPage }) }} · {{ t("integration.editor.runs.pagination.count", runsTotal) }}</span>
|
|
791
914
|
<div class="flex gap-2">
|
|
792
915
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage <= 1 || runsLoading" @click="goRunsPage(runsPage - 1)">
|
|
793
|
-
|
|
916
|
+
{{ t("integration.editor.runs.pagination.newer") }}
|
|
794
917
|
</Button>
|
|
795
918
|
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsPage >= runsLastPage || runsLoading" @click="goRunsPage(runsPage + 1)">
|
|
796
|
-
|
|
919
|
+
{{ t("integration.editor.runs.pagination.older") }}
|
|
797
920
|
</Button>
|
|
798
921
|
</div>
|
|
799
922
|
</div>
|
|
@@ -806,12 +929,12 @@ function formatDateTime(iso) {
|
|
|
806
929
|
|
|
807
930
|
<!-- History — n8n-style: read-only canvas preview + revision list. -->
|
|
808
931
|
<div v-else-if="tab === 'history'" class="flex-1 min-h-0">
|
|
809
|
-
<WorkflowHistoryPanel :workflow="workflow" @restored="onRevisionRestored" />
|
|
932
|
+
<WorkflowHistoryPanel :workflow="workflow" :draft-pending="isDirty" @restored="onRevisionRestored" />
|
|
810
933
|
</div>
|
|
811
934
|
</template>
|
|
812
935
|
|
|
813
936
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
814
|
-
|
|
937
|
+
{{ t("integration.editor.state.loading") }}
|
|
815
938
|
</div>
|
|
816
939
|
</div>
|
|
817
940
|
|
|
@@ -819,31 +942,31 @@ function formatDateTime(iso) {
|
|
|
819
942
|
<Dialog v-model:open="runPayloadOpen">
|
|
820
943
|
<DialogContent class="sm:max-w-lg">
|
|
821
944
|
<DialogHeader>
|
|
822
|
-
<DialogTitle>
|
|
945
|
+
<DialogTitle>{{ t("integration.editor.runPayload.dialog.title") }}</DialogTitle>
|
|
823
946
|
<DialogDescription class="text-sm">
|
|
824
|
-
|
|
947
|
+
{{ t("integration.editor.runPayload.dialog.description") }}
|
|
825
948
|
</DialogDescription>
|
|
826
949
|
</DialogHeader>
|
|
827
950
|
<div v-if="eventTriggerTopic" class="flex flex-wrap items-center gap-1.5">
|
|
828
|
-
<span class="text-xs text-muted-foreground">
|
|
951
|
+
<span class="text-xs text-muted-foreground">{{ t("integration.editor.runPayload.prefill.label") }} <code class="font-mono">{{ eventTriggerTopic }}</code>:</span>
|
|
829
952
|
<Button
|
|
830
953
|
variant="outline"
|
|
831
954
|
class="h-7 text-xs"
|
|
832
955
|
:disabled="!catalogSample"
|
|
833
|
-
:title="catalogSample ? '
|
|
956
|
+
:title="catalogSample ? t('integration.editor.runPayload.action.sampleEvent.titleAvailable') : t('integration.editor.runPayload.action.sampleEvent.titleUnavailable')"
|
|
834
957
|
@click="insertSampleEvent"
|
|
835
958
|
>
|
|
836
|
-
|
|
959
|
+
{{ t("integration.editor.runPayload.action.sampleEvent") }}
|
|
837
960
|
</Button>
|
|
838
961
|
<Button
|
|
839
962
|
variant="outline"
|
|
840
963
|
class="h-7 text-xs"
|
|
841
964
|
:disabled="prefillBusy !== null"
|
|
842
|
-
title="
|
|
965
|
+
:title="t('integration.editor.runPayload.action.latestEvent.title')"
|
|
843
966
|
@click="insertLatestEvent"
|
|
844
967
|
>
|
|
845
968
|
<Loader2 v-if="prefillBusy === 'latest'" :size="12" class="animate-spin" />
|
|
846
|
-
|
|
969
|
+
{{ t("integration.editor.runPayload.action.latestEvent") }}
|
|
847
970
|
</Button>
|
|
848
971
|
</div>
|
|
849
972
|
<div class="space-y-1">
|
|
@@ -857,35 +980,35 @@ function formatDateTime(iso) {
|
|
|
857
980
|
<Button
|
|
858
981
|
variant="outline"
|
|
859
982
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
860
|
-
:title="buildFailed ? '
|
|
983
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
861
984
|
class="h-8 text-sm"
|
|
862
985
|
@click="runFromPayloadDialog('sync')"
|
|
863
986
|
>
|
|
864
987
|
<Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
|
|
865
988
|
<Play v-else :size="14" />
|
|
866
|
-
{{ waitingMode === "sync" ? "
|
|
989
|
+
{{ waitingMode === "sync" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.sync") }) }}
|
|
867
990
|
</Button>
|
|
868
991
|
<Button
|
|
869
992
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
870
|
-
:title="buildFailed ? '
|
|
993
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
871
994
|
class="h-8 text-sm"
|
|
872
995
|
@click="runFromPayloadDialog('async')"
|
|
873
996
|
>
|
|
874
997
|
<Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
|
|
875
998
|
<Play v-else :size="14" />
|
|
876
|
-
{{ waitingMode === "async" ? "
|
|
999
|
+
{{ waitingMode === "async" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.async") }) }}
|
|
877
1000
|
</Button>
|
|
878
1001
|
</template>
|
|
879
1002
|
<Button
|
|
880
1003
|
v-else
|
|
881
1004
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
882
|
-
:title="buildFailed ? '
|
|
1005
|
+
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
883
1006
|
class="h-8 text-sm"
|
|
884
1007
|
@click="runFromPayloadDialog(primaryMode)"
|
|
885
1008
|
>
|
|
886
1009
|
<Loader2 v-if="running || syncLoading || waitingForBuild" :size="14" class="animate-spin" />
|
|
887
1010
|
<Play v-else :size="14" />
|
|
888
|
-
{{ waitingForBuild ? "
|
|
1011
|
+
{{ 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
1012
|
</Button>
|
|
890
1013
|
</div>
|
|
891
1014
|
</DialogContent>
|
|
@@ -907,16 +1030,46 @@ function formatDateTime(iso) {
|
|
|
907
1030
|
v-model:open="settingsOpen"
|
|
908
1031
|
:workflow="workflow"
|
|
909
1032
|
:modes="EXECUTION_MODES"
|
|
910
|
-
:
|
|
911
|
-
:saving-mode="savingMode"
|
|
912
|
-
:saving-meta="savingMeta"
|
|
1033
|
+
:current="settingsValues"
|
|
913
1034
|
:tag-suggestions="tagSuggestions"
|
|
914
|
-
@
|
|
915
|
-
@save-meta="saveWorkflowMeta"
|
|
1035
|
+
@apply="applySettings"
|
|
916
1036
|
/>
|
|
917
1037
|
|
|
918
1038
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
919
1039
|
|
|
1040
|
+
<!-- Leaving the workflow with unsaved work.
|
|
1041
|
+
Staying is the default in the only sense that decides anything: it holds the
|
|
1042
|
+
focus when the dialog opens, so Enter or Space keeps the work. That is what
|
|
1043
|
+
`AlertDialogCancel` is for — the dialog focuses its cancel element, and with
|
|
1044
|
+
none declared it focuses the first tabbable button instead, which was
|
|
1045
|
+
"Discard and leave". A breadcrumb click followed by Enter then threw away the
|
|
1046
|
+
afternoon this dialog exists to protect.
|
|
1047
|
+
`as-child` so the studio's own Button is that element rather than a second one
|
|
1048
|
+
beside it. Escape means the same thing, through `update:open`; a click beside
|
|
1049
|
+
the dialog does nothing at all, which an alert dialog is entitled to. -->
|
|
1050
|
+
<AlertDialog :open="leaveOpen" @update:open="(v) => {
|
|
1051
|
+
if (!v) answerLeave(false);
|
|
1052
|
+
}">
|
|
1053
|
+
<AlertDialogContent>
|
|
1054
|
+
<AlertDialogHeader>
|
|
1055
|
+
<AlertDialogTitle>{{ t("integration.editor.leave.dialog.title") }}</AlertDialogTitle>
|
|
1056
|
+
<AlertDialogDescription>
|
|
1057
|
+
{{ t("integration.editor.leave.dialog.description") }}
|
|
1058
|
+
</AlertDialogDescription>
|
|
1059
|
+
</AlertDialogHeader>
|
|
1060
|
+
<AlertDialogFooter>
|
|
1061
|
+
<Button variant="outline" @click="answerLeave(true)">
|
|
1062
|
+
{{ t("integration.editor.leave.dialog.discard") }}
|
|
1063
|
+
</Button>
|
|
1064
|
+
<AlertDialogCancel as-child>
|
|
1065
|
+
<Button @click="answerLeave(false)">
|
|
1066
|
+
{{ t("integration.editor.leave.dialog.stay") }}
|
|
1067
|
+
</Button>
|
|
1068
|
+
</AlertDialogCancel>
|
|
1069
|
+
</AlertDialogFooter>
|
|
1070
|
+
</AlertDialogContent>
|
|
1071
|
+
</AlertDialog>
|
|
1072
|
+
|
|
920
1073
|
<!-- Active-save blocked by manifest validation: keep the work by saving it
|
|
921
1074
|
as paused. Cancelling persists nothing (the 422 already rolled back). -->
|
|
922
1075
|
<AlertDialog :open="deactivationOpen" @update:open="(v) => {
|
|
@@ -924,10 +1077,11 @@ function formatDateTime(iso) {
|
|
|
924
1077
|
}">
|
|
925
1078
|
<AlertDialogContent>
|
|
926
1079
|
<AlertDialogHeader>
|
|
927
|
-
<AlertDialogTitle>
|
|
1080
|
+
<AlertDialogTitle>{{ t("integration.editor.deactivate.dialog.title") }}</AlertDialogTitle>
|
|
928
1081
|
<AlertDialogDescription>
|
|
929
|
-
|
|
930
|
-
<span class="font-medium">paused</span>
|
|
1082
|
+
{{ t("integration.editor.deactivate.dialog.description.pre") }}
|
|
1083
|
+
<span class="font-medium">{{ t("integration.editor.deactivate.dialog.description.paused") }}</span>
|
|
1084
|
+
{{ t("integration.editor.deactivate.dialog.description.post") }}
|
|
931
1085
|
</AlertDialogDescription>
|
|
932
1086
|
</AlertDialogHeader>
|
|
933
1087
|
<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 +1092,11 @@ function formatDateTime(iso) {
|
|
|
938
1092
|
</ul>
|
|
939
1093
|
<AlertDialogFooter>
|
|
940
1094
|
<Button variant="outline" :disabled="saving" @click="deactivationOpen = false">
|
|
941
|
-
|
|
1095
|
+
{{ t("integration.editor.deactivate.dialog.cancel") }}
|
|
942
1096
|
</Button>
|
|
943
1097
|
<Button class="bg-emerald-600 text-white hover:bg-emerald-700" :disabled="saving" @click="confirmDeactivate">
|
|
944
1098
|
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
945
|
-
|
|
1099
|
+
{{ t("integration.editor.deactivate.dialog.confirm") }}
|
|
946
1100
|
</Button>
|
|
947
1101
|
</AlertDialogFooter>
|
|
948
1102
|
</AlertDialogContent>
|