@revenexx/studio-integrations 1.4.1 → 2.1.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 +297 -0
- package/README.md +2 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -6
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +134 -131
- package/dist/runtime/components/Integrations/CredentialLogo.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +1 -1
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +6 -6
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +2 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +90 -11
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +2 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +121 -121
- package/dist/runtime/components/Integrations/NodeConfigField.vue +20 -5
- package/dist/runtime/components/Integrations/NodeInspector.vue +44 -41
- package/dist/runtime/components/Integrations/NodePalette.vue +52 -37
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +6 -5
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +7 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +14 -2
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +7 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +72 -70
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +20 -6
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +226 -277
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +0 -34
- package/dist/runtime/components/Integrations/WorkflowExportDialog.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue +71 -0
- package/dist/runtime/components/Integrations/WorkflowExportDialog.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.d.vue.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue +95 -0
- package/dist/runtime/components/Integrations/WorkflowImportFileStep.vue.d.ts +8 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +5 -4
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +0 -7
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +2 -3
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +14 -8
- package/dist/runtime/composables/useIntegrationsNodes.js +4 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +3 -1
- package/dist/runtime/composables/useIntegrationsWorkflows.js +5 -22
- package/dist/runtime/composables/useNow.d.ts +17 -0
- package/dist/runtime/composables/useNow.js +16 -0
- package/dist/runtime/composables/useWorkflowFile.d.ts +21 -0
- package/dist/runtime/composables/useWorkflowFile.js +63 -0
- package/dist/runtime/data/expressionEditor.d.ts +11 -1
- package/dist/runtime/data/expressionEditor.js +2 -1
- package/dist/runtime/data/nodeIcons.d.ts +14 -0
- package/dist/runtime/data/nodeIcons.js +57 -14
- package/dist/runtime/data/nodeRunData.js +1 -1
- package/dist/runtime/data/payloadForm.js +1 -1
- package/dist/runtime/i18n/messages/de.json +47 -11
- package/dist/runtime/i18n/messages/en.json +47 -11
- package/dist/runtime/pages/integrations/audit.vue +3 -3
- package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +2 -2
- package/dist/runtime/pages/integrations/credentials.vue +17 -5
- package/dist/runtime/pages/integrations/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +24 -13
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -324
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/cockpitSources.d.ts +68 -1
- package/dist/runtime/utils/credentialState.d.ts +15 -7
- package/dist/runtime/utils/credentialState.js +4 -12
- package/dist/runtime/utils/localize.d.ts +12 -0
- package/dist/runtime/utils/localize.js +1 -1
- package/dist/runtime/utils/paletteOrder.d.ts +29 -0
- package/dist/runtime/utils/paletteOrder.js +46 -0
- package/dist/runtime/utils/relativeTime.d.ts +29 -0
- package/dist/runtime/utils/relativeTime.js +23 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +12 -6
- package/dist/runtime/utils/runFeedQuery.js +3 -18
- package/dist/runtime/utils/settingValue.d.ts +33 -0
- package/dist/runtime/utils/settingValue.js +8 -0
- package/dist/runtime/utils/statusStyle.d.ts +8 -8
- package/dist/runtime/utils/statusStyle.js +4 -1
- package/dist/runtime/utils/workflowFile.d.ts +119 -0
- package/dist/runtime/utils/workflowFile.js +192 -0
- package/package.json +10 -5
- package/dist/runtime/utils/errorPathAdvice.d.ts +0 -60
- package/dist/runtime/utils/errorPathAdvice.js +0 -19
|
@@ -22,13 +22,7 @@ import {
|
|
|
22
22
|
DropdownMenuLabel,
|
|
23
23
|
DropdownMenuSeparator,
|
|
24
24
|
DropdownMenuTrigger,
|
|
25
|
-
|
|
26
|
-
SelectContent,
|
|
27
|
-
SelectItem,
|
|
28
|
-
SelectTrigger,
|
|
29
|
-
SelectValue,
|
|
30
|
-
Switch,
|
|
31
|
-
Textarea
|
|
25
|
+
Switch
|
|
32
26
|
} from "@revenexx/studio";
|
|
33
27
|
import { deepClone } from "../../../utils/deepClone";
|
|
34
28
|
import WorkflowEditor from "../../../components/Integrations/WorkflowEditor.vue";
|
|
@@ -37,6 +31,9 @@ import WorkflowRunReplayPanel from "../../../components/Integrations/WorkflowRun
|
|
|
37
31
|
import WorkflowSettingsDialog from "../../../components/Integrations/WorkflowSettingsDialog.vue";
|
|
38
32
|
import WorkflowDeleteDialog from "../../../components/Integrations/WorkflowDeleteDialog.vue";
|
|
39
33
|
import WorkflowDuplicateDialog from "../../../components/Integrations/WorkflowDuplicateDialog.vue";
|
|
34
|
+
import WorkflowExportDialog from "../../../components/Integrations/WorkflowExportDialog.vue";
|
|
35
|
+
import { useWorkflowFile } from "../../../composables/useWorkflowFile";
|
|
36
|
+
import { parseWorkflowFile } from "../../../utils/workflowFile";
|
|
40
37
|
import RunsTable from "../../../components/Integrations/RunsTable.vue";
|
|
41
38
|
import RunDetailPanel from "../../../components/Integrations/RunDetailPanel.vue";
|
|
42
39
|
import { toast } from "vue-sonner";
|
|
@@ -46,8 +43,6 @@ import { BUILD_SETTLE_POLL_MS, BUILD_SETTLE_TIMEOUT_MS, createBuildSettle, isBui
|
|
|
46
43
|
import { splitRunOutputs } from "../../../utils/runOutputs";
|
|
47
44
|
import { isTerminalRun, unwrapRun } from "../../../utils/runEnvelope";
|
|
48
45
|
import { findingsSummary, groupFindingsByNode, nodeDisplayLabel } from "../../../utils/validationFindings";
|
|
49
|
-
import { unwiredErrorPorts } from "../../../utils/errorPathAdvice";
|
|
50
|
-
import { newestExecutableVersion } from "../../../data/integrationNodeCatalog";
|
|
51
46
|
import { payloadFieldsFromBlob } from "../../../data/payloadForm";
|
|
52
47
|
import WorkflowIconView from "../../../components/Integrations/WorkflowIconView.vue";
|
|
53
48
|
const t = useIntegrationsT();
|
|
@@ -110,27 +105,6 @@ async function revealNode(nodeId) {
|
|
|
110
105
|
await nextTick();
|
|
111
106
|
editorRef.value?.revealNode(nodeId);
|
|
112
107
|
}
|
|
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
|
-
}
|
|
134
108
|
const { load: loadNodeCatalog } = useIntegrationsNodes();
|
|
135
109
|
onMounted(() => {
|
|
136
110
|
void loadNodeCatalog();
|
|
@@ -200,7 +174,6 @@ watch(id, async () => {
|
|
|
200
174
|
if (!w) await leaveTo("/integrations/workflows");
|
|
201
175
|
draft.value = null;
|
|
202
176
|
warningsExpanded.value = false;
|
|
203
|
-
adviceExpanded.value = false;
|
|
204
177
|
if (tab.value === "runs") void loadRuns(1);
|
|
205
178
|
settleBuildStatus();
|
|
206
179
|
}, { immediate: true });
|
|
@@ -256,6 +229,41 @@ async function onDuplicated(copy) {
|
|
|
256
229
|
toast.success(t("integration.editor.toast.duplicated", { name: copy.name }));
|
|
257
230
|
await leaveTo(`/integrations/workflows/${copy.id}`);
|
|
258
231
|
}
|
|
232
|
+
const exporter = ref(null);
|
|
233
|
+
const { planOverwrite } = useWorkflowFile();
|
|
234
|
+
const importInput = ref(null);
|
|
235
|
+
const importPlan = ref(null);
|
|
236
|
+
const importOpen = ref(false);
|
|
237
|
+
async function onImportPicked(event) {
|
|
238
|
+
const input = event.target;
|
|
239
|
+
const picked = input.files?.[0];
|
|
240
|
+
input.value = "";
|
|
241
|
+
if (!picked || !workflow.value) return;
|
|
242
|
+
const parsed = parseWorkflowFile(await picked.text());
|
|
243
|
+
if (!parsed.ok) {
|
|
244
|
+
toast.error(t(`integration.workflowFile.refused.${parsed.reason}`));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let plan;
|
|
248
|
+
try {
|
|
249
|
+
plan = await planOverwrite(workflow.value, parsed.file);
|
|
250
|
+
} catch {
|
|
251
|
+
toast.error(t("integration.workflowFile.import.planError"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
importPlan.value = { ...plan, executionMode: parsed.file.workflow.executionMode, active: workflow.value.active, dirty: isDirty.value };
|
|
255
|
+
importOpen.value = true;
|
|
256
|
+
}
|
|
257
|
+
function confirmImport() {
|
|
258
|
+
const plan = importPlan.value;
|
|
259
|
+
importOpen.value = false;
|
|
260
|
+
draft.value = null;
|
|
261
|
+
const base = draftFrom();
|
|
262
|
+
if (!plan || !base) return;
|
|
263
|
+
draft.value = { ...base, executionMode: plan.executionMode, blob: plan.blob };
|
|
264
|
+
editorResetKey.value++;
|
|
265
|
+
toast.success(t("integration.workflowFile.overwrite.toast"));
|
|
266
|
+
}
|
|
259
267
|
const deleteOpen = ref(false);
|
|
260
268
|
const deleteTarget = ref(null);
|
|
261
269
|
function askDelete() {
|
|
@@ -780,14 +788,6 @@ async function confirmDeactivate() {
|
|
|
780
788
|
saving.value = false;
|
|
781
789
|
}
|
|
782
790
|
}
|
|
783
|
-
function formatDateTime(iso) {
|
|
784
|
-
if (!iso) return "\u2014";
|
|
785
|
-
try {
|
|
786
|
-
return new Date(iso).toLocaleString();
|
|
787
|
-
} catch {
|
|
788
|
-
return iso;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
791
|
</script>
|
|
792
792
|
|
|
793
793
|
<template>
|
|
@@ -797,40 +797,40 @@ function formatDateTime(iso) {
|
|
|
797
797
|
<div class="flex h-full flex-col">
|
|
798
798
|
<template v-if="workflow">
|
|
799
799
|
<PageHeader :title="shownName">
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
804
|
-
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
805
|
-
back into the workflow surface. -->
|
|
806
|
-
<template v-if="openRunId">
|
|
807
|
-
<button type="button" class="truncate font-normal text-muted-foreground/80 transition-colors hover:text-primary" :title="shownName" @click="closeRun('editor')">{{ shownName }}</button>
|
|
800
|
+
<template #title>
|
|
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>
|
|
808
803
|
<Chevron :size="15" weight="Outline" class="shrink-0 text-muted-foreground/50" />
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
804
|
+
<!-- Run open: the breadcrumb extends — name and Runs become crumbs
|
|
805
|
+
back into the workflow surface. -->
|
|
806
|
+
<template v-if="openRunId">
|
|
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" />
|
|
811
|
+
<!-- The Temporal run id IS the run's identity; the internal
|
|
812
812
|
number only fills in until the run is handed off. -->
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
813
|
+
<span class="shrink-0 font-mono">{{ openRun?.temporalRunId ?? t("integration.editor.breadcrumb.runNumber", { id: openRunId }) }}</span>
|
|
814
|
+
</template>
|
|
815
|
+
<template v-else>
|
|
816
|
+
<span class="flex size-7 shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-500">
|
|
817
|
+
<WorkflowIconView :icon="shownIcon" :size="16" />
|
|
818
|
+
</span>
|
|
819
|
+
<span class="truncate" :title="shownName">{{ shownName }}</span>
|
|
820
|
+
<!-- The SAVED mode, beside a drafted name, and the difference is not an
|
|
821
821
|
oversight. A name changes nothing about what happens when Run is
|
|
822
822
|
pressed, so the header may show what the operator just entered. The
|
|
823
823
|
dispatch mode decides what a run does — and a run goes to the
|
|
824
824
|
workflow the platform holds, not to the draft. A badge following the
|
|
825
825
|
draft would read SYNC over a button that fires an async run. -->
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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) })">
|
|
827
|
+
{{ modeLabel(workflow.execution_mode) }}
|
|
828
|
+
</Badge>
|
|
829
|
+
</template>
|
|
830
|
+
</span>
|
|
831
|
+
</template>
|
|
832
|
+
<template #actions>
|
|
833
|
+
<!-- Folding the rail away, and getting it back (PO-273). It reads as
|
|
834
834
|
chrome rather than as an action on this workflow, so the natural
|
|
835
835
|
place was ahead of the breadcrumb — but the shell's `#title` slot
|
|
836
836
|
*is* the `<h1>`'s content, and a control inside the heading makes
|
|
@@ -850,43 +850,43 @@ function formatDateTime(iso) {
|
|
|
850
850
|
Icon-only, so it carries the same name as before to a screen reader
|
|
851
851
|
and to a test. Rendered only where the canvas fills the page, the
|
|
852
852
|
one surface it acts on. -->
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
>
|
|
863
|
-
<RailIcon :size="15" />
|
|
864
|
-
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
865
|
-
</Button>
|
|
866
|
-
|
|
867
|
-
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
868
|
-
<template v-if="openRunId">
|
|
869
|
-
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
870
|
-
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
871
|
-
<RetryIcon v-else :size="12" />
|
|
872
|
-
{{ t("integration.editor.action.retry") }}
|
|
873
|
-
</Button>
|
|
874
|
-
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
875
|
-
<ArrowLeft :size="12" />
|
|
876
|
-
{{ t("integration.editor.action.backToWorkflow") }}
|
|
877
|
-
</Button>
|
|
878
|
-
</template>
|
|
879
|
-
<template v-else>
|
|
880
|
-
<span
|
|
881
|
-
v-if="buildWorking"
|
|
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"
|
|
883
|
-
:title="buildNotice?.text"
|
|
853
|
+
<Button
|
|
854
|
+
v-if="canvasFillsPage"
|
|
855
|
+
variant="ghost"
|
|
856
|
+
size="sm"
|
|
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')"
|
|
860
|
+
:title="railFolded ? t('integration.nav.action.show.title') : t('integration.nav.action.hide.title')"
|
|
861
|
+
@click="toggleRail"
|
|
884
862
|
>
|
|
885
|
-
<
|
|
886
|
-
|
|
887
|
-
</
|
|
888
|
-
|
|
889
|
-
<!--
|
|
863
|
+
<RailIcon :size="15" />
|
|
864
|
+
<template v-if="railFolded">{{ t("integration.nav.action.show") }}</template>
|
|
865
|
+
</Button>
|
|
866
|
+
|
|
867
|
+
<!-- Run open: run-scoped actions replace the workflow Save. -->
|
|
868
|
+
<template v-if="openRunId">
|
|
869
|
+
<Button v-if="canRetryOpenRun" variant="outline" class="h-7 text-sm" :disabled="retryBusy" :title="t('integration.editor.action.retry.title')" @click="retryOpenRun">
|
|
870
|
+
<Loader2 v-if="retryBusy" :size="12" class="animate-spin" />
|
|
871
|
+
<RetryIcon v-else :size="12" />
|
|
872
|
+
{{ t("integration.editor.action.retry") }}
|
|
873
|
+
</Button>
|
|
874
|
+
<Button class="h-7 text-sm" @click="closeRun('editor')">
|
|
875
|
+
<ArrowLeft :size="12" />
|
|
876
|
+
{{ t("integration.editor.action.backToWorkflow") }}
|
|
877
|
+
</Button>
|
|
878
|
+
</template>
|
|
879
|
+
<template v-else>
|
|
880
|
+
<span
|
|
881
|
+
v-if="buildWorking"
|
|
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"
|
|
883
|
+
:title="buildNotice?.text"
|
|
884
|
+
>
|
|
885
|
+
<Loader2 :size="12" class="animate-spin" />
|
|
886
|
+
{{ workflow.buildStatus === "building" ? t("integration.editor.build.chip.building") : t("integration.editor.build.chip.queued") }}
|
|
887
|
+
</span>
|
|
888
|
+
<span v-if="isDirty" class="shrink-0 whitespace-nowrap text-sm text-amber-600">{{ t("integration.editor.status.unsavedChanges") }}</span>
|
|
889
|
+
<!-- What is done to the WORKFLOW, as opposed to the draft in front
|
|
890
890
|
of it: the buttons beside this menu discard and save an
|
|
891
891
|
afternoon's work, and these two make and unmake the thing that
|
|
892
892
|
work belongs to. Two reasons they are a menu rather than two more
|
|
@@ -895,33 +895,39 @@ function formatDateTime(iso) {
|
|
|
895
895
|
Duplicate that stayed a button reads as an oversight rather than
|
|
896
896
|
as a grouping — the menu only holds together while it holds both.
|
|
897
897
|
Icon-only, so it carries a name of its own. -->
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
898
|
+
<DropdownMenu>
|
|
899
|
+
<DropdownMenuTrigger as-child>
|
|
900
|
+
<Button variant="outline" size="icon" class="size-7" :disabled="saving" :aria-label="t('integration.editor.action.more.aria')">
|
|
901
|
+
<MoreHorizontal :size="12" />
|
|
902
|
+
</Button>
|
|
903
|
+
</DropdownMenuTrigger>
|
|
904
|
+
<DropdownMenuContent align="end">
|
|
905
|
+
<DropdownMenuLabel>{{ t("integration.editor.action.more.label") }}</DropdownMenuLabel>
|
|
906
|
+
<DropdownMenuSeparator />
|
|
907
|
+
<DropdownMenuItem @select="duplicateOpen = true">
|
|
908
|
+
{{ t("integration.editor.action.duplicate.label") }}
|
|
909
|
+
</DropdownMenuItem>
|
|
910
|
+
<DropdownMenuItem @select="exporter?.start(workflow.id)">
|
|
911
|
+
{{ t("integration.workflowFile.action.export") }}
|
|
912
|
+
</DropdownMenuItem>
|
|
913
|
+
<DropdownMenuItem @select="importInput?.click()">
|
|
914
|
+
{{ t("integration.workflowFile.action.import") }}
|
|
915
|
+
</DropdownMenuItem>
|
|
916
|
+
<DropdownMenuSeparator />
|
|
917
|
+
<DropdownMenuItem class="text-destructive focus:text-destructive" @select="askDelete">
|
|
918
|
+
{{ t("integration.common.action.delete") }}
|
|
919
|
+
</DropdownMenuItem>
|
|
920
|
+
</DropdownMenuContent>
|
|
921
|
+
</DropdownMenu>
|
|
922
|
+
<Button variant="outline" class="h-7 text-sm" :disabled="!isDirty || saving" :title="t('integration.editor.action.discard.title')" @click="discardChanges">
|
|
923
|
+
{{ t("integration.editor.action.discard.label") }}
|
|
924
|
+
</Button>
|
|
925
|
+
<Button class="h-7 text-sm" :disabled="!isDirty || saving" @click="save">
|
|
926
|
+
<Loader2 v-if="saving" :size="12" class="animate-spin" />
|
|
927
|
+
<Save v-else :size="12" />
|
|
928
|
+
{{ saving ? t("integration.editor.action.save.saving") : t("integration.editor.action.save.label") }}
|
|
929
|
+
</Button>
|
|
930
|
+
<!-- Activation is a standing state, not a transport control. The play
|
|
925
931
|
symbol belongs to Run, 600 px away in the canvas's control pane,
|
|
926
932
|
where pressing it makes one thing happen now; arming a workflow's
|
|
927
933
|
triggers looks like nothing happening at all. So this states the
|
|
@@ -933,236 +939,180 @@ function formatDateTime(iso) {
|
|
|
933
939
|
is not disabled, and `aria-describedby` puts the same sentence on
|
|
934
940
|
the switch for anyone who never hovers. Sighted users also have
|
|
935
941
|
the amber banner directly below, which says it at length. -->
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
942
|
+
<div
|
|
943
|
+
class="flex h-7 items-center gap-2 rounded-md border border-black/[0.08] px-2"
|
|
944
|
+
: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')"
|
|
945
|
+
>
|
|
946
|
+
<Switch
|
|
947
|
+
:aria-label="t('integration.editor.action.state.label')"
|
|
948
|
+
:aria-describedby="!workflow.active && isInvalid ? 'wf-activation-blocked' : void 0"
|
|
949
|
+
class="data-[state=checked]:bg-emerald-600 data-[state=unchecked]:bg-slate-300"
|
|
950
|
+
:model-value="workflow.active"
|
|
951
|
+
:disabled="!workflow.active && isInvalid"
|
|
952
|
+
@update:model-value="onToggleActive"
|
|
953
|
+
/>
|
|
954
|
+
<span class="text-sm" :class="workflow.active ? 'font-medium text-emerald-700' : 'text-muted-foreground'">
|
|
955
|
+
{{ workflow.active ? t("integration.workflows.table.status.lifecycle.active") : t("integration.workflows.table.status.lifecycle.paused") }}
|
|
956
|
+
</span>
|
|
957
|
+
<span v-if="!workflow.active && isInvalid" id="wf-activation-blocked" class="sr-only">
|
|
958
|
+
{{ t("integration.editor.action.activate.title.invalid", validationWarnings?.length ?? 0) }}
|
|
959
|
+
</span>
|
|
960
|
+
</div>
|
|
961
|
+
</template>
|
|
955
962
|
</template>
|
|
956
|
-
</template>
|
|
957
963
|
|
|
958
|
-
|
|
964
|
+
</PageHeader>
|
|
959
965
|
|
|
960
|
-
|
|
961
|
-
|
|
966
|
+
<TabBar v-if="!openRunId" v-model="tab" :tabs="tabs" class="shrink-0 px-4" />
|
|
967
|
+
<TabBar v-else v-model="runView" :tabs="runViews" class="shrink-0 px-4" />
|
|
962
968
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
969
|
+
<div v-if="saveError" class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700">
|
|
970
|
+
{{ saveError }}
|
|
971
|
+
</div>
|
|
966
972
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
973
|
+
<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">
|
|
974
|
+
{{ t("integration.editor.reauth.banner", reauthCredentials.length) }}
|
|
975
|
+
({{ reauthCredentials.map((c) => c.name).join(", ") }}) —
|
|
976
|
+
<NuxtLink to="/integrations/credentials" class="font-medium underline">
|
|
977
|
+
{{ t("integration.editor.reauth.action.fix") }}
|
|
978
|
+
</NuxtLink>.
|
|
979
|
+
</div>
|
|
974
980
|
|
|
975
|
-
|
|
981
|
+
<!-- Manifest validation (PO-186): an inactive workflow with warnings can't
|
|
976
982
|
be activated. The banner names what's incomplete; the header's state
|
|
977
983
|
switch stays blocked until the next save recomputes an empty warnings set.
|
|
978
984
|
Amber (not red) — the work is saved fine, it just isn't runnable yet.
|
|
979
985
|
One line per NODE, listing its fields (PO-209): the messages behind
|
|
980
986
|
them ride along as the line's title, and the line leads to the node. -->
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
>
|
|
985
|
-
<p class="font-medium">
|
|
986
|
-
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
987
|
-
</p>
|
|
988
|
-
<ul
|
|
989
|
-
class="mt-1 space-y-0.5"
|
|
990
|
-
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
987
|
+
<div
|
|
988
|
+
v-if="!openRunId && !workflow.active && isInvalid"
|
|
989
|
+
class="border-b border-amber-500/20 bg-amber-500/5 px-4 py-2 text-sm text-amber-800"
|
|
991
990
|
>
|
|
992
|
-
<
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
<!-- Attributed to a node: the whole line jumps there and opens its
|
|
999
|
-
settings. Otherwise there is nowhere to go — state the finding. -->
|
|
1000
|
-
<button
|
|
1001
|
-
v-if="g.nodeId"
|
|
1002
|
-
type="button"
|
|
1003
|
-
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
1004
|
-
:title="g.messages.join('\n')"
|
|
1005
|
-
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
1006
|
-
@click="revealNode(g.nodeId)"
|
|
1007
|
-
>
|
|
1008
|
-
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
1009
|
-
</button>
|
|
1010
|
-
<span v-else>{{ g.messages.join(" ") }}</span>
|
|
1011
|
-
</li>
|
|
1012
|
-
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
1013
|
-
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
1014
|
-
{{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
|
|
1015
|
-
</button>
|
|
1016
|
-
</li>
|
|
1017
|
-
</ul>
|
|
1018
|
-
</div>
|
|
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"
|
|
991
|
+
<p class="font-medium">
|
|
992
|
+
{{ t("integration.editor.validation.banner.title", { summary: warningSummary }) }}
|
|
993
|
+
</p>
|
|
994
|
+
<ul
|
|
995
|
+
class="mt-1 space-y-0.5"
|
|
996
|
+
:class="warningsExpanded ? 'max-h-36 overflow-y-auto pr-1' : ''"
|
|
1048
997
|
>
|
|
1049
|
-
<
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
class="
|
|
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)"
|
|
998
|
+
<li
|
|
999
|
+
v-for="(g, i) in warningsExpanded ? warningGroups : warningGroups.slice(0, WARN_COLLAPSED)"
|
|
1000
|
+
:key="`${g.nodeId ?? 'unattributed'}-${i}`"
|
|
1001
|
+
class="flex gap-1.5"
|
|
1064
1002
|
>
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1003
|
+
<span class="select-none text-amber-500">•</span>
|
|
1004
|
+
<!-- Attributed to a node: the whole line jumps there and opens its
|
|
1005
|
+
settings. Otherwise there is nowhere to go — state the finding. -->
|
|
1006
|
+
<button
|
|
1007
|
+
v-if="g.nodeId"
|
|
1008
|
+
type="button"
|
|
1009
|
+
class="text-left underline decoration-amber-700/30 underline-offset-2 hover:decoration-amber-800"
|
|
1010
|
+
:title="g.messages.join('\n')"
|
|
1011
|
+
:aria-label="t('integration.editor.validation.node.ariaFix', g.fields.length, { label: g.label })"
|
|
1012
|
+
@click="revealNode(g.nodeId)"
|
|
1013
|
+
>
|
|
1014
|
+
<span class="font-medium">{{ g.label }}</span> — {{ g.fields.join(", ") }}
|
|
1015
|
+
</button>
|
|
1016
|
+
<span v-else>{{ g.messages.join(" ") }}</span>
|
|
1017
|
+
</li>
|
|
1018
|
+
<li v-if="!warningsExpanded && warningGroups.length > WARN_COLLAPSED" class="pl-3">
|
|
1019
|
+
<button type="button" class="text-amber-700/90 underline underline-offset-2 hover:text-amber-900" @click="warningsExpanded = true">
|
|
1020
|
+
{{ t("integration.editor.validation.banner.more", { n: warningGroups.length - WARN_COLLAPSED }) }}
|
|
1021
|
+
</button>
|
|
1022
|
+
</li>
|
|
1023
|
+
</ul>
|
|
1024
|
+
</div>
|
|
1075
1025
|
|
|
1076
|
-
|
|
1026
|
+
<!-- Only the FAILED build state banners (its message matters); the
|
|
1077
1027
|
working states live as the header chip — and inside a run the
|
|
1078
1028
|
workflow's build state is noise, so nothing shows at all. -->
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1029
|
+
<div
|
|
1030
|
+
v-if="!openRunId && buildNotice && buildNotice.tone === 'error'"
|
|
1031
|
+
class="border-b border-rose-500/20 bg-rose-500/5 px-4 py-2 text-sm text-rose-700"
|
|
1032
|
+
>
|
|
1033
|
+
{{ buildNotice.text }}
|
|
1034
|
+
</div>
|
|
1085
1035
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1036
|
+
<!-- Nested run detail — overview/timeline panel or Replay canvas. -->
|
|
1037
|
+
<template v-if="openRunId">
|
|
1038
|
+
<div v-if="runView !== 'replay'" class="flex-1 overflow-y-auto p-4">
|
|
1039
|
+
<div class="w-full">
|
|
1040
|
+
<RunDetailPanel v-if="openRun" :run="openRun" :view="runView" :nodes="workflow?.blob?.nodes ?? []" @changed="loadOpenRun" />
|
|
1041
|
+
<div v-else-if="openRunError" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
1042
|
+
{{ openRunError }}
|
|
1043
|
+
</div>
|
|
1044
|
+
<div v-else class="flex items-center gap-2 p-12 text-sm text-muted-foreground">
|
|
1045
|
+
<Loader2 :size="16" class="animate-spin" /> {{ t("integration.editor.run.loading") }}
|
|
1046
|
+
</div>
|
|
1096
1047
|
</div>
|
|
1097
1048
|
</div>
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
</
|
|
1102
|
-
</template>
|
|
1049
|
+
<div v-else class="min-h-0 flex-1">
|
|
1050
|
+
<WorkflowRunReplayPanel :workflow="workflow" :initial-run-id="openRunId" />
|
|
1051
|
+
</div>
|
|
1052
|
+
</template>
|
|
1103
1053
|
|
|
1104
|
-
|
|
1054
|
+
<!-- Editor tab — fills the remaining height inside the panel via
|
|
1105
1055
|
the flex wrapper in the studio layout.
|
|
1106
1056
|
|
|
1107
1057
|
overlay-open: every lightbox this page owns renders as a sibling of
|
|
1108
1058
|
the tabs, so the editor stays mounted underneath one and would keep
|
|
1109
1059
|
its keyboard shortcuts live (Delete deleting the selected node behind
|
|
1110
1060
|
the dialog — PO-208). Add new page-level dialogs to this list. -->
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1061
|
+
<div v-else-if="tab === 'editor'" class="flex-1 min-h-0">
|
|
1062
|
+
<WorkflowEditor
|
|
1063
|
+
:key="editorResetKey"
|
|
1064
|
+
ref="editorRef"
|
|
1065
|
+
:workflow="workflow"
|
|
1066
|
+
:draft="draft?.blob ?? null"
|
|
1067
|
+
:run-busy="running || syncLoading || waitingForBuild"
|
|
1068
|
+
:run-waiting="waitingForBuild"
|
|
1069
|
+
:overlay-open="settingsOpen || duplicateOpen || importOpen || !!exporter?.open || deleteOpen || syncOpen || runPayloadOpen || deactivationOpen || leaveOpen"
|
|
1070
|
+
@change="onChange"
|
|
1071
|
+
@run="runFromEditor"
|
|
1072
|
+
@toggle-active="onToggleActive"
|
|
1073
|
+
@open-settings="settingsOpen = true"
|
|
1074
|
+
/>
|
|
1075
|
+
</div>
|
|
1126
1076
|
|
|
1127
|
-
|
|
1077
|
+
<!-- Runs — this workflow's full execution history. Manual runs start
|
|
1128
1078
|
from the editor's Run button, through the run dialog. -->
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1079
|
+
<div v-else-if="tab === 'runs'" class="p-4 space-y-4 overflow-y-auto">
|
|
1080
|
+
<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">
|
|
1081
|
+
{{ runsError }}
|
|
1082
|
+
</div>
|
|
1133
1083
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1084
|
+
<RunsTable
|
|
1085
|
+
:runs="wfRuns"
|
|
1086
|
+
:loading="runsLoading"
|
|
1087
|
+
:pagination="false"
|
|
1088
|
+
:show-workflow="false"
|
|
1089
|
+
:empty-text="t('integration.editor.runs.empty')"
|
|
1090
|
+
@changed="() => loadRuns()"
|
|
1091
|
+
/>
|
|
1142
1092
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1093
|
+
<!-- Server pagination — the endpoint pages, so the whole history is reachable. -->
|
|
1094
|
+
<div v-if="runsLastPage > 1 || runsTotal > wfRuns.length" class="flex items-center justify-between text-sm text-muted-foreground">
|
|
1095
|
+
<span class="tabular-nums">{{ t("integration.editor.runs.pagination.page", { page: runsPage, lastPage: runsLastPage }) }} · {{ t("integration.editor.runs.pagination.count", runsTotal) }}</span>
|
|
1096
|
+
<div class="flex gap-2">
|
|
1097
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsWanted <= 1 || runsLoading" @click="goRunsPage(runsWanted - 1)">
|
|
1098
|
+
{{ t("integration.editor.runs.pagination.newer") }}
|
|
1099
|
+
</Button>
|
|
1100
|
+
<Button variant="outline" size="sm" class="h-7 text-sm" :disabled="runsWanted >= runsLastPage || runsLoading" @click="goRunsPage(runsWanted + 1)">
|
|
1101
|
+
{{ t("integration.editor.runs.pagination.older") }}
|
|
1102
|
+
</Button>
|
|
1103
|
+
</div>
|
|
1153
1104
|
</div>
|
|
1154
1105
|
</div>
|
|
1155
|
-
</div>
|
|
1156
1106
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1107
|
+
<!-- Dead letters — triggers that never produced a run -->
|
|
1108
|
+
<div v-else-if="tab === 'dead-letters'" class="p-6 overflow-y-auto">
|
|
1109
|
+
<IntegrationsDeadLettersPanel :workflow-id="workflow.id" />
|
|
1110
|
+
</div>
|
|
1161
1111
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1112
|
+
<!-- History — n8n-style: read-only canvas preview + revision list. -->
|
|
1113
|
+
<div v-else-if="tab === 'history'" class="flex-1 min-h-0">
|
|
1114
|
+
<WorkflowHistoryPanel :workflow="workflow" :draft-pending="isDirty" @restored="onRevisionRestored" />
|
|
1115
|
+
</div>
|
|
1166
1116
|
</template>
|
|
1167
1117
|
|
|
1168
1118
|
<div v-else class="p-12 text-center text-sm text-muted-foreground">
|
|
@@ -1277,6 +1227,56 @@ function formatDateTime(iso) {
|
|
|
1277
1227
|
|
|
1278
1228
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
1279
1229
|
|
|
1230
|
+
<WorkflowExportDialog ref="exporter" />
|
|
1231
|
+
|
|
1232
|
+
<!-- The menu item opens the browser's own file picker; this input is only its
|
|
1233
|
+
carrier, and it carries a name so it is not an unnamed control. -->
|
|
1234
|
+
<input
|
|
1235
|
+
ref="importInput"
|
|
1236
|
+
type="file"
|
|
1237
|
+
accept=".json,application/json"
|
|
1238
|
+
class="hidden"
|
|
1239
|
+
:aria-label="t('integration.workflowFile.field.file')"
|
|
1240
|
+
@change="onImportPicked"
|
|
1241
|
+
>
|
|
1242
|
+
|
|
1243
|
+
<!-- Importing over this workflow. Says how much goes, that the credentials have
|
|
1244
|
+
to be chosen again, and — where it applies — that the running version keeps
|
|
1245
|
+
running and that unsaved work is thrown away. -->
|
|
1246
|
+
<AlertDialog :open="importOpen" @update:open="(v) => {
|
|
1247
|
+
if (!v) importOpen = false;
|
|
1248
|
+
}">
|
|
1249
|
+
<AlertDialogContent>
|
|
1250
|
+
<AlertDialogHeader>
|
|
1251
|
+
<AlertDialogTitle>{{ t("integration.workflowFile.overwrite.title") }}</AlertDialogTitle>
|
|
1252
|
+
<AlertDialogDescription v-if="importPlan && workflow">
|
|
1253
|
+
{{ t("integration.workflowFile.overwrite.description", {
|
|
1254
|
+
name: workflow.name,
|
|
1255
|
+
current: t("integration.workflowFile.count.both", {
|
|
1256
|
+
nodes: t("integration.workflowFile.count.nodes", importPlan.replaced.nodes),
|
|
1257
|
+
triggers: t("integration.workflowFile.count.triggers", importPlan.replaced.triggers)
|
|
1258
|
+
}),
|
|
1259
|
+
incoming: t("integration.workflowFile.count.both", {
|
|
1260
|
+
nodes: t("integration.workflowFile.count.nodes", importPlan.incoming.nodes),
|
|
1261
|
+
triggers: t("integration.workflowFile.count.triggers", importPlan.incoming.triggers)
|
|
1262
|
+
})
|
|
1263
|
+
}) }}
|
|
1264
|
+
</AlertDialogDescription>
|
|
1265
|
+
</AlertDialogHeader>
|
|
1266
|
+
<ul class="space-y-1 text-sm text-muted-foreground">
|
|
1267
|
+
<li>{{ t("integration.workflowFile.overwrite.save") }}</li>
|
|
1268
|
+
<li v-if="importPlan?.active" class="text-amber-700">{{ t("integration.workflowFile.overwrite.active") }}</li>
|
|
1269
|
+
<li v-if="importPlan?.dirty" class="text-amber-700">{{ t("integration.workflowFile.overwrite.draft") }}</li>
|
|
1270
|
+
</ul>
|
|
1271
|
+
<AlertDialogFooter>
|
|
1272
|
+
<AlertDialogCancel as-child>
|
|
1273
|
+
<Button variant="outline">{{ t("integration.common.action.cancel") }}</Button>
|
|
1274
|
+
</AlertDialogCancel>
|
|
1275
|
+
<Button @click="confirmImport">{{ t("integration.workflowFile.overwrite.confirm") }}</Button>
|
|
1276
|
+
</AlertDialogFooter>
|
|
1277
|
+
</AlertDialogContent>
|
|
1278
|
+
</AlertDialog>
|
|
1279
|
+
|
|
1280
1280
|
<!-- Leaving the workflow with unsaved work.
|
|
1281
1281
|
Staying is the default in the only sense that decides anything: it holds the
|
|
1282
1282
|
focus when the dialog opens, so Enter or Space keeps the work. That is what
|