@revenexx/studio-integrations 1.2.0 → 1.4.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 +581 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue +53 -0
- package/dist/runtime/components/Integrations/CanvasShortcutsDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +2 -15
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +15 -1
- package/dist/runtime/components/Integrations/NodeConfigField.vue +158 -49
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +15 -1
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/NodeInspector.vue +136 -81
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
- package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +80 -33
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
- package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateCard.vue +3 -6
- package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +0 -4
- package/dist/runtime/components/Integrations/TemplateInstallStep.d.vue.ts +25 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +198 -0
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue.d.ts +25 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +22 -40
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +94 -65
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +200 -54
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +36 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +4 -106
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +6 -2
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
- package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +22 -11
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +34 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +11 -8
- package/dist/runtime/composables/useCredentialTypes.js +3 -9
- package/dist/runtime/composables/useIntegrationsCredentials.js +6 -14
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRuns.js +8 -13
- package/dist/runtime/composables/useIntegrationsSecrets.js +5 -5
- package/dist/runtime/composables/useIntegrationsTemplates.js +10 -8
- package/dist/runtime/composables/useIntegrationsTriggers.js +1 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.js +11 -7
- package/dist/runtime/data/canvasShortcuts.d.ts +208 -0
- package/dist/runtime/data/canvasShortcuts.js +68 -0
- package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -0
- package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
- package/dist/runtime/data/nodeVersionMove.js +65 -0
- package/dist/runtime/i18n/messages/de.json +83 -20
- package/dist/runtime/i18n/messages/en.json +83 -20
- package/dist/runtime/pages/integrations/templates/[slug].vue +22 -132
- package/dist/runtime/pages/integrations/workflows/[id].vue +158 -53
- package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
- package/dist/runtime/types/integrations.d.ts +44 -1
- package/dist/runtime/utils/apiEnvelope.d.ts +27 -0
- package/dist/runtime/utils/apiEnvelope.js +20 -0
- package/dist/runtime/utils/buildSettle.d.ts +68 -0
- package/dist/runtime/utils/buildSettle.js +29 -0
- package/dist/runtime/utils/configDefaults.d.ts +18 -0
- package/dist/runtime/utils/configDefaults.js +7 -0
- package/dist/runtime/utils/errorPathAdvice.d.ts +60 -0
- package/dist/runtime/utils/errorPathAdvice.js +19 -0
- package/dist/runtime/utils/notify.d.ts +2 -2
- package/dist/runtime/utils/runEnvelope.d.ts +12 -0
- package/dist/runtime/utils/runEnvelope.js +2 -1
- package/dist/runtime/utils/selectOptions.d.ts +48 -0
- package/dist/runtime/utils/selectOptions.js +19 -0
- package/dist/runtime/utils/settingCondition.d.ts +48 -0
- package/dist/runtime/utils/settingCondition.js +84 -0
- package/dist/runtime/utils/shortcutLabel.d.ts +40 -0
- package/dist/runtime/utils/shortcutLabel.js +48 -0
- package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
- package/dist/runtime/utils/stateNamespaceName.js +8 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
3
|
import { marked } from "marked";
|
|
4
4
|
import { AltArrowRight as Chevron, CheckCircle as CheckCircle2, Key, RefreshCircle as Loader2, Shield as ShieldAlert, User } from "#solar-icons";
|
|
5
5
|
import { triggerGlyph as glyphForTrigger } from "../../../data/triggerGlyphs";
|
|
@@ -9,27 +9,19 @@ import {
|
|
|
9
9
|
Dialog,
|
|
10
10
|
DialogContent,
|
|
11
11
|
DialogDescription,
|
|
12
|
-
DialogFooter,
|
|
13
12
|
DialogHeader,
|
|
14
|
-
DialogTitle
|
|
15
|
-
Input,
|
|
16
|
-
Label,
|
|
17
|
-
Select,
|
|
18
|
-
SelectContent,
|
|
19
|
-
SelectItem,
|
|
20
|
-
SelectTrigger,
|
|
21
|
-
SelectValue
|
|
13
|
+
DialogTitle
|
|
22
14
|
} from "@revenexx/studio";
|
|
23
15
|
import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPreview.vue";
|
|
16
|
+
import TemplateInstallStep from "../../../components/Integrations/TemplateInstallStep.vue";
|
|
17
|
+
import LoadingAnnouncement from "../../../components/Integrations/LoadingAnnouncement.vue";
|
|
24
18
|
import NodeMark from "../../../components/Integrations/NodeMark.vue";
|
|
25
19
|
import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
|
|
26
20
|
import { describeCron } from "../../../utils/schedule";
|
|
27
|
-
import { errorReason } from "../../../utils/apiError";
|
|
28
|
-
const SELECT_LATER = "__later__";
|
|
29
21
|
const t = useIntegrationsT();
|
|
30
22
|
const route = useRoute();
|
|
31
23
|
const router = useRouter();
|
|
32
|
-
const { get, load, loaded, fetchDetail, requirements
|
|
24
|
+
const { get, load, loaded, fetchDetail, requirements } = useIntegrationsTemplates();
|
|
33
25
|
const { load: loadNodeCatalog, findManifest, resolveLocalized } = useIntegrationsNodes();
|
|
34
26
|
const { keys: secretKeys, load: loadSecrets } = useIntegrationsSecrets();
|
|
35
27
|
const { ofType, load: loadCredentials } = useIntegrationsCredentials();
|
|
@@ -137,39 +129,13 @@ const setupReady = computed(() => missingSecrets.value === 0 && missingCredentia
|
|
|
137
129
|
const packageLabel = computed(() => tpl.value ? templatePackageLabel(tpl.value) : null);
|
|
138
130
|
const categoryLabel = computed(() => templateCategoryLabel(tpl.value?.category ?? ""));
|
|
139
131
|
const installOpen = ref(false);
|
|
140
|
-
const
|
|
141
|
-
const installError = ref(null);
|
|
142
|
-
const name = ref("");
|
|
143
|
-
const bindings = reactive({});
|
|
144
|
-
function nodesFor(usedBy) {
|
|
145
|
-
return [...new Set(usedBy.map((u) => u.nodeId))];
|
|
146
|
-
}
|
|
132
|
+
const stepLoading = ref(false);
|
|
147
133
|
function openInstall() {
|
|
148
134
|
if (!tpl.value) return;
|
|
149
|
-
name.value = tpl.value.name;
|
|
150
|
-
for (const k of Object.keys(bindings)) delete bindings[k];
|
|
151
|
-
installError.value = null;
|
|
152
135
|
installOpen.value = true;
|
|
153
136
|
}
|
|
154
|
-
async function
|
|
155
|
-
|
|
156
|
-
installing.value = true;
|
|
157
|
-
installError.value = null;
|
|
158
|
-
try {
|
|
159
|
-
const credentialBindings = {};
|
|
160
|
-
for (const [nodeId, credId] of Object.entries(bindings)) {
|
|
161
|
-
if (credId && credId !== SELECT_LATER) credentialBindings[nodeId] = credId;
|
|
162
|
-
}
|
|
163
|
-
const id = await instantiate(tpl.value.slug, {
|
|
164
|
-
name: name.value.trim() || void 0,
|
|
165
|
-
credentialBindings
|
|
166
|
-
});
|
|
167
|
-
if (id) await router.push(`/integrations/workflows/${id}`);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
installError.value = errorReason(e, t("integration.templates.detail.install.error.fallback"));
|
|
170
|
-
} finally {
|
|
171
|
-
installing.value = false;
|
|
172
|
-
}
|
|
137
|
+
async function installed(id) {
|
|
138
|
+
await router.push(`/integrations/workflows/${id}`);
|
|
173
139
|
}
|
|
174
140
|
const PUBLISHER_KIND_LABEL_KEY = {
|
|
175
141
|
revenexx: "integration.templates.detail.publisher.kind.revenexx",
|
|
@@ -471,7 +437,13 @@ function levelDot(level) {
|
|
|
471
437
|
{{ t("integration.templates.detail.state.notFound") }}
|
|
472
438
|
</div>
|
|
473
439
|
|
|
474
|
-
|
|
440
|
+
<LoadingAnnouncement
|
|
441
|
+
:label="t('integration.templates.detail.action.useTemplate')"
|
|
442
|
+
:loading="stepLoading"
|
|
443
|
+
:text="t('integration.templates.detail.install.state.loading')"
|
|
444
|
+
/>
|
|
445
|
+
|
|
446
|
+
<!-- Install: the shared step, in this page's dialog ------------------- -->
|
|
475
447
|
<Dialog v-model:open="installOpen">
|
|
476
448
|
<DialogContent class="sm:max-w-lg">
|
|
477
449
|
<DialogHeader>
|
|
@@ -481,95 +453,13 @@ function levelDot(level) {
|
|
|
481
453
|
</DialogDescription>
|
|
482
454
|
</DialogHeader>
|
|
483
455
|
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
<div class="space-y-2">
|
|
492
|
-
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
493
|
-
{{ t("integration.templates.detail.credentials.title") }}
|
|
494
|
-
</div>
|
|
495
|
-
<p v-if="!credentialReqs.length" class="text-sm text-muted-foreground italic">
|
|
496
|
-
{{ t("integration.templates.detail.install.noCredentials") }}
|
|
497
|
-
</p>
|
|
498
|
-
<div v-for="req in credentialReqs" :key="req.slug" class="space-y-2">
|
|
499
|
-
<div class="flex items-center gap-2">
|
|
500
|
-
<span class="text-sm font-medium">{{ req.name ?? req.slug }}</span>
|
|
501
|
-
<Badge v-if="!req.registered" variant="outline" class="bg-rose-500/10 border-rose-500/20 text-rose-700 text-sm">
|
|
502
|
-
{{ t("integration.templates.detail.install.notRegistered") }}
|
|
503
|
-
</Badge>
|
|
504
|
-
<Badge v-else-if="!ofType(req.slug).length" variant="outline" class="bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm">
|
|
505
|
-
{{ t("integration.templates.detail.install.noneYet") }}
|
|
506
|
-
</Badge>
|
|
507
|
-
</div>
|
|
508
|
-
|
|
509
|
-
<p v-if="!ofType(req.slug).length" class="rounded-md border-l-2 border-black/[0.08] bg-muted/30 px-2.5 py-1.5 text-sm text-muted-foreground">
|
|
510
|
-
{{ t("integration.templates.detail.install.noCredentialsOfType") }}
|
|
511
|
-
<NuxtLink to="/integrations/credentials" class="text-primary hover:underline">{{ t("integration.templates.detail.install.createOneLink") }}</NuxtLink>
|
|
512
|
-
{{ t("integration.templates.detail.install.orInstallLater") }}
|
|
513
|
-
</p>
|
|
514
|
-
<div v-for="nodeId in nodesFor(req.usedBy)" v-else :key="nodeId" class="space-y-1">
|
|
515
|
-
<Label :for="`tpl-cred-${nodeId}`" class="text-xs text-muted-foreground font-mono">{{ t("integration.templates.detail.install.nodeLabel", { id: nodeId }) }}</Label>
|
|
516
|
-
<Select
|
|
517
|
-
:model-value="bindings[nodeId] ?? SELECT_LATER"
|
|
518
|
-
@update:model-value="(v2) => {
|
|
519
|
-
bindings[nodeId] = v2 ?? SELECT_LATER;
|
|
520
|
-
}"
|
|
521
|
-
>
|
|
522
|
-
<SelectTrigger :id="`tpl-cred-${nodeId}`" class="bg-white">
|
|
523
|
-
<SelectValue :placeholder="t('integration.templates.detail.install.selectLater')" />
|
|
524
|
-
</SelectTrigger>
|
|
525
|
-
<SelectContent>
|
|
526
|
-
<SelectItem :value="SELECT_LATER">{{ t("integration.templates.detail.install.selectLater") }}</SelectItem>
|
|
527
|
-
<SelectItem v-for="c in ofType(req.slug)" :key="c.id" :value="c.id">
|
|
528
|
-
{{ c.name }}
|
|
529
|
-
</SelectItem>
|
|
530
|
-
</SelectContent>
|
|
531
|
-
</Select>
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
534
|
-
</div>
|
|
535
|
-
|
|
536
|
-
<!-- Secrets -->
|
|
537
|
-
<div v-if="secretChecks.length" class="space-y-2">
|
|
538
|
-
<div class="text-mono text-muted-foreground border-b border-black/[0.06] pb-1.5">
|
|
539
|
-
{{ t("integration.templates.detail.install.secretsHeader") }}
|
|
540
|
-
</div>
|
|
541
|
-
<p class="text-sm text-muted-foreground">
|
|
542
|
-
{{ t("integration.templates.detail.install.secretsDescription") }}
|
|
543
|
-
<NuxtLink to="/integrations/secrets" class="text-primary hover:underline">{{ t("integration.nav.item.secrets") }}</NuxtLink> {{ t("integration.templates.detail.install.secretsDescriptionSuffix") }}
|
|
544
|
-
</p>
|
|
545
|
-
<ul class="space-y-1">
|
|
546
|
-
<li
|
|
547
|
-
v-for="(s, i) in secretChecks"
|
|
548
|
-
:key="s.key ?? `dlg-secret-${i}`"
|
|
549
|
-
class="flex items-center justify-between rounded-md border border-black/[0.06] bg-muted/20 px-2 py-1 text-sm"
|
|
550
|
-
>
|
|
551
|
-
<code class="font-mono">{{ s.label }}</code>
|
|
552
|
-
<Badge variant="outline" :class="s.present ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-700 text-sm' : 'bg-amber-500/10 border-amber-500/20 text-amber-700 text-sm'">
|
|
553
|
-
{{ s.present ? t("integration.templates.detail.install.secretStored") : t("integration.templates.detail.secrets.missing") }}
|
|
554
|
-
</Badge>
|
|
555
|
-
</li>
|
|
556
|
-
</ul>
|
|
557
|
-
</div>
|
|
558
|
-
|
|
559
|
-
<div v-if="installError" class="rounded-md bg-rose-500/10 px-3 py-2 text-sm text-rose-700">
|
|
560
|
-
{{ installError }}
|
|
561
|
-
</div>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
<DialogFooter>
|
|
565
|
-
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
566
|
-
{{ t("integration.common.action.cancel") }}
|
|
567
|
-
</Button>
|
|
568
|
-
<Button :disabled="installing || !name.trim()" @click="confirmInstall">
|
|
569
|
-
<Loader2 v-if="installing" :size="14" class="mr-1 animate-spin" />
|
|
570
|
-
{{ installing ? t("integration.templates.detail.install.submitting") : t("integration.templates.detail.install.submit") }}
|
|
571
|
-
</Button>
|
|
572
|
-
</DialogFooter>
|
|
456
|
+
<TemplateInstallStep :slug="slug" @installed="installed" @update:loading="stepLoading = $event">
|
|
457
|
+
<template #secondary="{ installing }">
|
|
458
|
+
<Button variant="outline" :disabled="installing" @click="installOpen = false">
|
|
459
|
+
{{ t("integration.common.action.cancel") }}
|
|
460
|
+
</Button>
|
|
461
|
+
</template>
|
|
462
|
+
</TemplateInstallStep>
|
|
573
463
|
</DialogContent>
|
|
574
464
|
</Dialog>
|
|
575
465
|
</IntegrationsLayout>
|
|
@@ -42,9 +42,12 @@ import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue"
|
|
|
42
42
|
import { toast } from "vue-sonner";
|
|
43
43
|
import { IntegrationsApiError, apiValidationIssues, errorReason } from "../../../utils/apiError";
|
|
44
44
|
import { createPollBudget, SAFETY_POLL_MS } from "../../../utils/pollBudget";
|
|
45
|
+
import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBuildUnsettled } from "../../../utils/buildSettle";
|
|
45
46
|
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
46
47
|
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
47
48
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
49
|
+
import { unwiredErrorPorts } from "../../../utils/errorPathAdvice";
|
|
50
|
+
import { newestExecutableVersion } from "../../../data/integrationNodeCatalog";
|
|
48
51
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
49
52
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
50
53
|
const t = useIntegrationsT();
|
|
@@ -107,6 +110,27 @@ async function revealNode(nodeId) {
|
|
|
107
110
|
await nextTick();
|
|
108
111
|
editorRef.value?.revealNode(nodeId);
|
|
109
112
|
}
|
|
113
|
+
const errorPathAdvice = computed(() => {
|
|
114
|
+
const blob = shownBlob.value;
|
|
115
|
+
if (!blob) return [];
|
|
116
|
+
const byId = new Map(blob.nodes.map((n) => [n.id, n]));
|
|
117
|
+
return unwiredErrorPorts(blob).flatMap((a) => {
|
|
118
|
+
const node = byId.get(a.nodeId);
|
|
119
|
+
return node ? [{ ...a, label: nodeDisplayLabel(node) }] : [];
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
const adviceExpanded = ref(false);
|
|
123
|
+
const canAttachErrorStop = computed(() => newestExecutableVersion("revenexx:stop-and-error") !== null);
|
|
124
|
+
async function showNodeOnCanvas(nodeId) {
|
|
125
|
+
tab.value = "editor";
|
|
126
|
+
await nextTick();
|
|
127
|
+
editorRef.value?.showNodeOnCanvas(nodeId);
|
|
128
|
+
}
|
|
129
|
+
async function attachErrorStop(nodeId, port) {
|
|
130
|
+
tab.value = "editor";
|
|
131
|
+
await nextTick();
|
|
132
|
+
editorRef.value?.attachErrorStop(nodeId, port);
|
|
133
|
+
}
|
|
110
134
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
111
135
|
onMounted(() => {
|
|
112
136
|
void loadNodeCatalog();
|
|
@@ -124,8 +148,8 @@ const reauthCredentials = computed(() => {
|
|
|
124
148
|
const buildWorking = computed(() => workflow.value?.buildStatus === "building" || workflow.value?.buildStatus === "pending");
|
|
125
149
|
const buildFailed = computed(() => workflow.value?.buildStatus === "failed");
|
|
126
150
|
const waitingForBuild = ref(false);
|
|
127
|
-
const BUILD_WAIT_POLL_MS =
|
|
128
|
-
const BUILD_WAIT_TIMEOUT_MS =
|
|
151
|
+
const BUILD_WAIT_POLL_MS = BUILD_SETTLE_POLL_MS;
|
|
152
|
+
const BUILD_WAIT_TIMEOUT_MS = BUILD_SETTLE_TIMEOUT_MS;
|
|
129
153
|
async function waitForBuildReady() {
|
|
130
154
|
waitingForBuild.value = true;
|
|
131
155
|
try {
|
|
@@ -148,19 +172,14 @@ async function waitForBuildReady() {
|
|
|
148
172
|
waitingForBuild.value = false;
|
|
149
173
|
}
|
|
150
174
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const status = workflow.value.buildStatus;
|
|
158
|
-
if (status !== "pending" && status !== "building") return;
|
|
159
|
-
await new Promise((resolve) => setTimeout(resolve, BUILD_WAIT_POLL_MS));
|
|
160
|
-
if (mine !== buildWatch || !workflow.value) return;
|
|
161
|
-
await get(workflow.value.id);
|
|
162
|
-
}
|
|
175
|
+
const buildSettle = createBuildSettle();
|
|
176
|
+
function settleBuildStatus() {
|
|
177
|
+
void buildSettle.settle({
|
|
178
|
+
unsettled: () => !!workflow.value && isBuildUnsettled(workflow.value.buildStatus),
|
|
179
|
+
read: () => get(workflow.value.id)
|
|
180
|
+
});
|
|
163
181
|
}
|
|
182
|
+
onUnmounted(() => buildSettle.stop());
|
|
164
183
|
const buildNotice = computed(() => {
|
|
165
184
|
switch (workflow.value?.buildStatus) {
|
|
166
185
|
case "building":
|
|
@@ -181,8 +200,9 @@ watch(id, async () => {
|
|
|
181
200
|
if (!w) await leaveTo("/integrations/workflows");
|
|
182
201
|
draft.value = null;
|
|
183
202
|
warningsExpanded.value = false;
|
|
203
|
+
adviceExpanded.value = false;
|
|
184
204
|
if (tab.value === "runs") void loadRuns(1);
|
|
185
|
-
|
|
205
|
+
settleBuildStatus();
|
|
186
206
|
}, { immediate: true });
|
|
187
207
|
function onChange(blob) {
|
|
188
208
|
const d = draftFrom();
|
|
@@ -252,7 +272,7 @@ async function onRevisionRestored() {
|
|
|
252
272
|
if (!workflow.value) return;
|
|
253
273
|
await get(workflow.value.id);
|
|
254
274
|
draft.value = null;
|
|
255
|
-
|
|
275
|
+
settleBuildStatus();
|
|
256
276
|
}
|
|
257
277
|
const { trigger: triggerRun, loadForWorkflow, fetchRun, getRun, retryRun } = useIntegrationsRuns();
|
|
258
278
|
const running = ref(false);
|
|
@@ -373,7 +393,7 @@ async function run(mode) {
|
|
|
373
393
|
const payloadDriven = computed(() => payloadFieldsFromBlob(shownBlob.value ?? { nodes: [] }).length > 0);
|
|
374
394
|
async function runFromEditor() {
|
|
375
395
|
if (!workflow.value) return;
|
|
376
|
-
if (payloadDriven.value) {
|
|
396
|
+
if (payloadDriven.value || workflow.value.execution_mode === "caller_decides") {
|
|
377
397
|
runPayloadOpen.value = true;
|
|
378
398
|
return;
|
|
379
399
|
}
|
|
@@ -630,16 +650,20 @@ function closeRun(target) {
|
|
|
630
650
|
tab.value = target;
|
|
631
651
|
void router.replace({ query: { ...route.query, run: void 0, view: void 0 } });
|
|
632
652
|
}
|
|
633
|
-
const canRetryOpenRun = computed(() =>
|
|
653
|
+
const canRetryOpenRun = computed(() => openRun.value?.status === "failed");
|
|
634
654
|
const retryBusy = ref(false);
|
|
635
655
|
async function retryOpenRun() {
|
|
636
|
-
|
|
656
|
+
const source = openRun.value;
|
|
657
|
+
if (!source || retryBusy.value) return;
|
|
637
658
|
retryBusy.value = true;
|
|
638
659
|
try {
|
|
639
|
-
const res = await retryRun(
|
|
660
|
+
const res = await retryRun(source.workflowId, source.id);
|
|
640
661
|
const newId = res && typeof res === "object" && "id" in res ? String(res.id) : null;
|
|
641
|
-
toast.success(newId && newId !==
|
|
662
|
+
toast.success(newId && newId !== source.id ? t("integration.editor.retry.toast.started", { id: newId }) : t("integration.editor.retry.toast.requested"));
|
|
642
663
|
void loadRuns(1, { silent: true });
|
|
664
|
+
if (newId && newId !== source.id) {
|
|
665
|
+
await router.push({ query: { ...route.query, run: newId, view: void 0 } });
|
|
666
|
+
}
|
|
643
667
|
} catch (e) {
|
|
644
668
|
toast.error(errorReason(e, t("integration.editor.retry.error.generic")));
|
|
645
669
|
} finally {
|
|
@@ -711,7 +735,7 @@ async function save() {
|
|
|
711
735
|
executionMode: d.executionMode
|
|
712
736
|
});
|
|
713
737
|
draft.value = null;
|
|
714
|
-
|
|
738
|
+
settleBuildStatus();
|
|
715
739
|
} catch (e) {
|
|
716
740
|
const issues = apiValidationIssues(e);
|
|
717
741
|
if (issues.length && workflow.value.active) {
|
|
@@ -748,7 +772,7 @@ async function confirmDeactivate() {
|
|
|
748
772
|
draft.value = null;
|
|
749
773
|
deactivationOpen.value = false;
|
|
750
774
|
toast.success(t("integration.editor.deactivate.toast.savedPaused"));
|
|
751
|
-
|
|
775
|
+
settleBuildStatus();
|
|
752
776
|
} catch (e) {
|
|
753
777
|
deactivationOpen.value = false;
|
|
754
778
|
saveError.value = errorReason(e, t("integration.editor.save.error.generic"));
|
|
@@ -774,32 +798,32 @@ function formatDateTime(iso) {
|
|
|
774
798
|
<template v-if="workflow">
|
|
775
799
|
<PageHeader :title="shownName">
|
|
776
800
|
<template #title>
|
|
777
|
-
<span class="
|
|
778
|
-
<NuxtLink to="/integrations/workflows" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
779
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
801
|
+
<span class="flex min-w-0 items-center gap-1.5">
|
|
802
|
+
<NuxtLink to="/integrations/workflows" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary">{{ t("integration.editor.breadcrumb.workflows") }}</NuxtLink>
|
|
803
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
780
804
|
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
781
805
|
back into the workflow surface. -->
|
|
782
806
|
<template v-if="openRunId">
|
|
783
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('editor')">{{ shownName }}</button>
|
|
784
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
785
|
-
<button type="button" class="font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
786
|
-
<Chevron :size="15" weight="Outline" class="text-muted-foreground/50" />
|
|
807
|
+
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" :title="shownName" @click="closeRun('editor')">{{ shownName }}</button>
|
|
808
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
809
|
+
<button type="button" class="shrink-0 font-normal text-muted-foreground/80 transition-colors hover:text-primary" @click="closeRun('runs')">{{ t("integration.editor.breadcrumb.runs") }}</button>
|
|
810
|
+
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
787
811
|
<!-- The Temporal run id IS the run's identity; the internal
|
|
788
812
|
number only fills in until the run is handed off. -->
|
|
789
|
-
<span class="
|
|
813
|
+
<span class="shrink-0 font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
790
814
|
</template>
|
|
791
815
|
<template v-else>
|
|
792
816
|
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
793
817
|
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
794
818
|
</span>
|
|
795
|
-
<span class="truncate">{{ shownName }}</span>
|
|
819
|
+
<span class="truncate" :title="shownName">{{ shownName }}</span>
|
|
796
820
|
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
797
821
|
oversight. A name changes nothing about what happens when Run is
|
|
798
822
|
pressed, so the header may show what the operator just entered. The
|
|
799
823
|
dispatch mode decides what a run does — and a run goes to the
|
|
800
824
|
workflow the platform holds, not to the draft. A badge following the
|
|
801
825
|
draft would read SYNC over a button that fires an async run. -->
|
|
802
|
-
<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) })">
|
|
826
|
+
<Badge variant="outline" class="ml-1.5 shrink-0 border text-xs font-medium uppercase text-muted-foreground" :title="t('integration.editor.header.executionMode.title', { mode: modeLabel(workflow.execution_mode) })">
|
|
803
827
|
{{ modeLabel(workflow.execution_mode) }}
|
|
804
828
|
</Badge>
|
|
805
829
|
</template>
|
|
@@ -816,25 +840,33 @@ function formatDateTime(iso) {
|
|
|
816
840
|
outside it. Leftmost there is as near the rail as the shared header
|
|
817
841
|
allows without a slot of its own.
|
|
818
842
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
843
|
+
The way back out carries its words; the way in does not. An
|
|
844
|
+
icon-only control is one nobody finds, and a folded rail is the one
|
|
845
|
+
state where that is unaffordable — nothing else on the page offers
|
|
846
|
+
the navigation back. Folding it away needs no such help: the rail
|
|
847
|
+
is standing right beside the pictogram, and the words cost 159 px
|
|
848
|
+
of a header row that a long workflow name already fills, where they
|
|
849
|
+
were the widest thing in it for the least important thing in it.
|
|
850
|
+
Icon-only, so it carries the same name as before to a screen reader
|
|
851
|
+
and to a test. Rendered only where the canvas fills the page, the
|
|
852
|
+
one surface it acts on. -->
|
|
823
853
|
<Button
|
|
824
854
|
v-if="canvasFillsPage"
|
|
825
855
|
variant="ghost"
|
|
826
856
|
size="sm"
|
|
827
|
-
class="h-7 shrink-0 gap-1.5
|
|
857
|
+
class="h-7 shrink-0 gap-1.5 text-sm font-normal text-muted-foreground hover:text-primary"
|
|
858
|
+
:class="railFolded ? 'px-2' : 'w-7 px-0'"
|
|
859
|
+
:aria-label="railFolded ? void 0 : t('integration.nav.action.hide')"
|
|
828
860
|
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
829
861
|
@click="toggleRail"
|
|
830
862
|
>
|
|
831
863
|
<RailIcon :size="15" />
|
|
832
|
-
{{
|
|
864
|
+
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
833
865
|
</Button>
|
|
834
866
|
|
|
835
867
|
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
836
868
|
<template v-if="openRunId">
|
|
837
|
-
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" @click="retryOpenRun">
|
|
869
|
+
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
838
870
|
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
839
871
|
<RetryIcon v-else :size="12" />
|
|
840
872
|
{{ t("integration.editor.action.retry") }}
|
|
@@ -847,13 +879,13 @@ function formatDateTime(iso) {
|
|
|
847
879
|
<template v-else>
|
|
848
880
|
<span
|
|
849
881
|
v-if="buildWorking"
|
|
850
|
-
class="inline-flex h-7 items-center gap-1.5 rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
882
|
+
class="inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-sky-500/25 bg-sky-500/10 px-2.5 text-sm text-sky-700"
|
|
851
883
|
:title="buildNotice?.text"
|
|
852
884
|
>
|
|
853
885
|
<Loader2 :size="12" class="animate-spin" />
|
|
854
886
|
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
855
887
|
</span>
|
|
856
|
-
<span v-if="isDirty" class="text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
888
|
+
<span v-if="isDirty" class="shrink-0 whitespace-nowrap text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
857
889
|
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
858
890
|
of it: the buttons beside this menu discard and save an
|
|
859
891
|
afternoon's work, and these two make and unmake the thing that
|
|
@@ -985,6 +1017,62 @@ function formatDateTime(iso) {
|
|
|
985
1017
|
</ul>
|
|
986
1018
|
</div>
|
|
987
1019
|
|
|
1020
|
+
<!-- Error-path advice (PO-443, specs/workflow-advice.md).
|
|
1021
|
+
Slate, NOT amber: amber already means "fix this before it runs" twice
|
|
1022
|
+
over in this editor (the reauth strip, the validation banner) and on
|
|
1023
|
+
the node cards, so a third would leave the colour meaning nothing.
|
|
1024
|
+
This blocks nothing — the activation control stays operable while it
|
|
1025
|
+
shows, which is the point of it not being a finding.
|
|
1026
|
+
|
|
1027
|
+
Editor tab only, unlike the validation banner above: every line here
|
|
1028
|
+
acts on the canvas — reveal a card, attach a step to it — so over the
|
|
1029
|
+
run list or the history preview it offers moves to a canvas that is not
|
|
1030
|
+
on screen, and its node names read as part of whatever is. That is not
|
|
1031
|
+
hypothetical: it put a current node's name over a preview of an
|
|
1032
|
+
archived revision (workflow-history.md AC-3). -->
|
|
1033
|
+
<div
|
|
1034
|
+
v-if="tab === 'editor' && !openRunId && errorPathAdvice.length"
|
|
1035
|
+
class="border-b border-slate-500/20 bg-slate-500/5 px-4 py-2 text-sm text-slate-700"
|
|
1036
|
+
>
|
|
1037
|
+
<p class="font-medium">
|
|
1038
|
+
{{ t("integration.editor.errorPath.advice", errorPathAdvice.length) }}
|
|
1039
|
+
</p>
|
|
1040
|
+
<ul
|
|
1041
|
+
class="mt-1 space-y-0.5"
|
|
1042
|
+
:class="adviceExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
1043
|
+
>
|
|
1044
|
+
<li
|
|
1045
|
+
v-for="a in adviceExpanded ? errorPathAdvice : errorPathAdvice.slice(0, WARN_COLLAPSED)"
|
|
1046
|
+
:key="`${a.nodeId}-${a.port}`"
|
|
1047
|
+
class="flex items-baseline gap-1.5"
|
|
1048
|
+
>
|
|
1049
|
+
<span class="select-none text-slate-400">•</span>
|
|
1050
|
+
<button
|
|
1051
|
+
type="button"
|
|
1052
|
+
class="text-left font-medium underline decoration-transparent underline-offset-2 hover:decoration-slate-700"
|
|
1053
|
+
:aria-label="t('integration.editor.errorPath.ariaReveal', { label: a.label, port: a.port })"
|
|
1054
|
+
@click="showNodeOnCanvas(a.nodeId)"
|
|
1055
|
+
>
|
|
1056
|
+
{{ a.label }}
|
|
1057
|
+
</button>
|
|
1058
|
+
<button
|
|
1059
|
+
v-if="canAttachErrorStop"
|
|
1060
|
+
type="button"
|
|
1061
|
+
class="text-slate-600 underline underline-offset-2 hover:text-slate-900"
|
|
1062
|
+
:aria-label="t('integration.editor.errorPath.ariaFix', { label: a.label, port: a.port })"
|
|
1063
|
+
@click="attachErrorStop(a.nodeId, a.port)"
|
|
1064
|
+
>
|
|
1065
|
+
{{ t("integration.editor.errorPath.action.stop") }}
|
|
1066
|
+
</button>
|
|
1067
|
+
</li>
|
|
1068
|
+
<li v-if="!adviceExpanded && errorPathAdvice.length > WARN_COLLAPSED" class="pl-3">
|
|
1069
|
+
<button type="button" class="text-slate-600 underline underline-offset-2 hover:text-slate-900" @click="adviceExpanded = true">
|
|
1070
|
+
{{ t("integration.editor.errorPath.more", { n: errorPathAdvice.length - WARN_COLLAPSED }) }}
|
|
1071
|
+
</button>
|
|
1072
|
+
</li>
|
|
1073
|
+
</ul>
|
|
1074
|
+
</div>
|
|
1075
|
+
|
|
988
1076
|
<!-- Only the FAILED build state banners (its message matters); the
|
|
989
1077
|
working states live as the header chip — and inside a run the
|
|
990
1078
|
workflow's build state is noise, so nothing shows at all. -->
|
|
@@ -1009,7 +1097,7 @@ function formatDateTime(iso) {
|
|
|
1009
1097
|
</div>
|
|
1010
1098
|
</div>
|
|
1011
1099
|
<div v-else class="min-h-0 flex-1">
|
|
1012
|
-
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId"
|
|
1100
|
+
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId" />
|
|
1013
1101
|
</div>
|
|
1014
1102
|
</template>
|
|
1015
1103
|
|
|
@@ -1037,7 +1125,7 @@ function formatDateTime(iso) {
|
|
|
1037
1125
|
</div>
|
|
1038
1126
|
|
|
1039
1127
|
<!-- Runs — this workflow's full execution history. Manual runs start
|
|
1040
|
-
from the editor's Run
|
|
1128
|
+
from the editor's Run button, through the run dialog. -->
|
|
1041
1129
|
<div v-else-if="tab === 'runs'" class="p-4 space-y-4 overflow-y-auto">
|
|
1042
1130
|
<div v-if="runsError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
1043
1131
|
{{ runsError }}
|
|
@@ -1082,7 +1170,7 @@ function formatDateTime(iso) {
|
|
|
1082
1170
|
</div>
|
|
1083
1171
|
</div>
|
|
1084
1172
|
|
|
1085
|
-
<!--
|
|
1173
|
+
<!-- The run dialog — opened by the editor's Run button. -->
|
|
1086
1174
|
<Dialog v-model:open="runPayloadOpen">
|
|
1087
1175
|
<DialogContent class="sm:max-w-lg">
|
|
1088
1176
|
<DialogHeader>
|
|
@@ -1120,27 +1208,30 @@ function formatDateTime(iso) {
|
|
|
1120
1208
|
</p>
|
|
1121
1209
|
</div>
|
|
1122
1210
|
<div v-if="workflow" class="flex items-center justify-end gap-2">
|
|
1211
|
+
<!-- Both modes, and the waiting one is the offer: the author pressed Run
|
|
1212
|
+
to see what comes back, so sync is the primary button — which puts it
|
|
1213
|
+
on the right, where every primary button in the studio sits. -->
|
|
1123
1214
|
<template v-if="workflow.execution_mode === 'caller_decides'">
|
|
1124
1215
|
<Button
|
|
1125
1216
|
variant="outline"
|
|
1126
1217
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
1127
1218
|
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
1128
1219
|
class="h-8 text-sm"
|
|
1129
|
-
@click="runFromPayloadDialog('
|
|
1220
|
+
@click="runFromPayloadDialog('async')"
|
|
1130
1221
|
>
|
|
1131
|
-
<Loader2 v-if="
|
|
1222
|
+
<Loader2 v-if="running || waitingMode === 'async'" :size="14" class="animate-spin" />
|
|
1132
1223
|
<Play v-else :size="14" />
|
|
1133
|
-
{{ waitingMode === "
|
|
1224
|
+
{{ waitingMode === "async" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.async") }) }}
|
|
1134
1225
|
</Button>
|
|
1135
1226
|
<Button
|
|
1136
1227
|
:disabled="running || syncLoading || waitingForBuild || buildFailed"
|
|
1137
1228
|
:title="buildFailed ? t('integration.editor.build.tooltip.failed') : void 0"
|
|
1138
1229
|
class="h-8 text-sm"
|
|
1139
|
-
@click="runFromPayloadDialog('
|
|
1230
|
+
@click="runFromPayloadDialog('sync')"
|
|
1140
1231
|
>
|
|
1141
|
-
<Loader2 v-if="
|
|
1232
|
+
<Loader2 v-if="syncLoading || waitingMode === 'sync'" :size="14" class="animate-spin" />
|
|
1142
1233
|
<Play v-else :size="14" />
|
|
1143
|
-
{{ waitingMode === "
|
|
1234
|
+
{{ waitingMode === "sync" ? t("integration.editor.run.waitingForBuild") : t("integration.editor.runPayload.action.run", { mode: t("integration.editor.mode.word.sync") }) }}
|
|
1144
1235
|
</Button>
|
|
1145
1236
|
</template>
|
|
1146
1237
|
<Button
|
|
@@ -1195,7 +1286,18 @@ function formatDateTime(iso) {
|
|
|
1195
1286
|
afternoon this dialog exists to protect.
|
|
1196
1287
|
`as-child` so the studio's own Button is that element rather than a second one
|
|
1197
1288
|
beside it. Escape means the same thing, through `update:open`; a click beside
|
|
1198
|
-
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1289
|
+
the dialog does nothing at all, which an alert dialog is entitled to.
|
|
1290
|
+
|
|
1291
|
+
The class list on that Button is the price of `as-child`: `AlertDialogCancel`
|
|
1292
|
+
applies the outline variant to whatever it renders, and every dialog in this
|
|
1293
|
+
module draws the answer on the right as a filled one. So the two variants meet
|
|
1294
|
+
on one element and `tailwind-merge` decides between them per group — which it
|
|
1295
|
+
cannot do where the outline variant names a group the default one does not, and
|
|
1296
|
+
those are the five that have to be said here. Left to itself the merge produced
|
|
1297
|
+
a background from the outline variant and a text colour from the default, which
|
|
1298
|
+
resolve to the same colour in this theme: the label was invisible until hovered
|
|
1299
|
+
(PO-415). Nothing marks this answer as the one holding the keyboard otherwise —
|
|
1300
|
+
`focus-visible` does not match after the click that opened the dialog. -->
|
|
1199
1301
|
<AlertDialog :open="leaveOpen" @update:open="(v) => {
|
|
1200
1302
|
if (!v) answerLeave(false);
|
|
1201
1303
|
}">
|
|
@@ -1211,7 +1313,10 @@ function formatDateTime(iso) {
|
|
|
1211
1313
|
{{ t("integration.editor.leave.dialog.discard") }}
|
|
1212
1314
|
</Button>
|
|
1213
1315
|
<AlertDialogCancel as-child>
|
|
1214
|
-
<Button
|
|
1316
|
+
<Button
|
|
1317
|
+
class="border-transparent bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground dark:border-transparent dark:bg-primary dark:hover:bg-primary/90"
|
|
1318
|
+
@click="answerLeave(false)"
|
|
1319
|
+
>
|
|
1215
1320
|
{{ t("integration.editor.leave.dialog.stay") }}
|
|
1216
1321
|
</Button>
|
|
1217
1322
|
</AlertDialogCancel>
|