@revenexx/studio-integrations 1.1.1 → 1.2.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 +483 -0
- package/README.md +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +6 -18
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +13 -23
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +7 -6
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +15 -4
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +45 -19
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +5 -4
- package/dist/runtime/components/Integrations/LoadingAnnouncement.d.vue.ts +38 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue +16 -0
- package/dist/runtime/components/Integrations/LoadingAnnouncement.vue.d.ts +38 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +23 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +2 -2
- package/dist/runtime/components/Integrations/NodeInspector.vue +25 -32
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +2 -2
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +2 -1
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +5 -17
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +30 -8
- package/dist/runtime/components/Integrations/RunResponseModal.vue +92 -24
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +30 -8
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +61 -3
- package/dist/runtime/components/Integrations/RunsTable.vue +129 -18
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +61 -3
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +10 -1
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -1
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +18 -3
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue +70 -0
- package/dist/runtime/components/Integrations/WorkflowDeleteDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +2 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -4
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -28
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +17 -6
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +17 -21
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +8 -11
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +6 -7
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +7 -7
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +10 -48
- package/dist/runtime/components/Integrations/canvasSides.d.ts +48 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useCredentialTypes.js +2 -6
- package/dist/runtime/composables/useIntegrationsAudit.js +2 -2
- package/dist/runtime/composables/useIntegrationsCredentials.js +4 -8
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +16 -1
- package/dist/runtime/composables/useIntegrationsNodes.js +2 -2
- package/dist/runtime/composables/useIntegrationsRevisions.js +2 -6
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +4 -15
- package/dist/runtime/composables/useIntegrationsRuns.js +49 -34
- package/dist/runtime/composables/useIntegrationsSchedules.js +2 -2
- package/dist/runtime/composables/useIntegrationsSecrets.js +2 -6
- package/dist/runtime/composables/useIntegrationsTemplates.js +2 -5
- package/dist/runtime/composables/useIntegrationsTriggers.js +2 -2
- package/dist/runtime/composables/useIntegrationsWorkflows.js +4 -7
- package/dist/runtime/composables/useNodeConfigResolve.js +2 -2
- package/dist/runtime/composables/useOAuthConnect.d.ts +42 -0
- package/dist/runtime/composables/useOAuthConnect.js +33 -0
- package/dist/runtime/data/expressionEditor.d.ts +17 -5
- package/dist/runtime/data/expressionEditor.js +11 -5
- package/dist/runtime/data/handleId.d.ts +61 -1
- package/dist/runtime/data/handleId.js +8 -1
- package/dist/runtime/data/payloadForm.js +3 -2
- package/dist/runtime/i18n/messages/de.json +26 -8
- package/dist/runtime/i18n/messages/en.json +26 -8
- package/dist/runtime/pages/integrations/audit.vue +7 -6
- package/dist/runtime/pages/integrations/credentials.vue +9 -21
- package/dist/runtime/pages/integrations/index.vue +55 -8
- package/dist/runtime/pages/integrations/runs.vue +116 -7
- package/dist/runtime/pages/integrations/secrets.vue +2 -1
- package/dist/runtime/pages/integrations/templates/[slug].vue +2 -1
- package/dist/runtime/pages/integrations/templates/index.vue +18 -15
- package/dist/runtime/pages/integrations/workflows/[id].vue +155 -20
- package/dist/runtime/types/integrations.d.ts +5 -0
- package/dist/runtime/utils/apiError.d.ts +21 -1
- package/dist/runtime/utils/apiError.js +1 -1
- package/dist/runtime/utils/clipboard.d.ts +33 -0
- package/dist/runtime/utils/clipboard.js +11 -0
- package/dist/runtime/utils/deepClone.d.ts +41 -0
- package/dist/runtime/utils/deepClone.js +3 -0
- package/dist/runtime/utils/formatDuration.d.ts +44 -0
- package/dist/runtime/utils/formatDuration.js +13 -0
- package/dist/runtime/utils/nodeDisplayName.d.ts +22 -0
- package/dist/runtime/utils/nodeDisplayName.js +6 -0
- package/dist/runtime/utils/notify.d.ts +1 -1
- package/dist/runtime/utils/runEnvelope.d.ts +31 -0
- package/dist/runtime/utils/runEnvelope.js +12 -0
- package/dist/runtime/utils/runFeedQuery.d.ts +135 -0
- package/dist/runtime/utils/runFeedQuery.js +66 -0
- package/dist/runtime/utils/runOutputs.d.ts +33 -0
- package/dist/runtime/utils/runOutputs.js +10 -0
- package/dist/runtime/utils/validationFindings.d.ts +26 -3
- package/dist/runtime/utils/validationFindings.js +18 -3
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { WorkflowRun } from '../../types/integrations.js';
|
|
1
|
+
import type { RunStatus, TriggerType, WorkflowRun } from '../../types/integrations.js';
|
|
2
|
+
import type { RunSort, RunSortDirection } from '../../utils/runFeedQuery.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
runs: WorkflowRun[];
|
|
4
5
|
loading?: boolean;
|
|
@@ -7,14 +8,71 @@ type __VLS_Props = {
|
|
|
7
8
|
pagination?: boolean;
|
|
8
9
|
/** false inside a workflow — the context makes the column redundant. */
|
|
9
10
|
showWorkflow?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The owning list reads its page and its narrowing from the platform.
|
|
13
|
+
*
|
|
14
|
+
* The table then renders the rows it is handed and reports what the operator
|
|
15
|
+
* asked for, instead of narrowing and slicing an array. That is what lets the
|
|
16
|
+
* list reach past the newest hundred runs at all (PO-369).
|
|
17
|
+
*/
|
|
18
|
+
serverMode?: boolean;
|
|
19
|
+
/** How many runs match the narrowing. The pager's "of 7"; server mode only. */
|
|
20
|
+
total?: number;
|
|
21
|
+
/**
|
|
22
|
+
* How many rows a page holds — the SAME number the page asks the platform for.
|
|
23
|
+
*
|
|
24
|
+
* The shared table works its footer's "showing 1–25 of 140" out from its own
|
|
25
|
+
* page size, so a table left on its default while the request asks for another
|
|
26
|
+
* number states a range that is simply wrong: it counted twenty where
|
|
27
|
+
* twenty-five had arrived.
|
|
28
|
+
*/
|
|
29
|
+
perPage?: number;
|
|
30
|
+
/** The narrowing in force. Owned by the page, drawn here. */
|
|
31
|
+
status?: RunStatus[];
|
|
32
|
+
triggerType?: TriggerType | '';
|
|
33
|
+
/** The order the platform is reading the list in. Server mode only. */
|
|
34
|
+
sort?: RunSort;
|
|
35
|
+
direction?: RunSortDirection;
|
|
10
36
|
};
|
|
11
|
-
|
|
37
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
38
|
+
/** 1-based, as the platform pages. The shared table counts from zero. */
|
|
39
|
+
'page'?: number;
|
|
40
|
+
};
|
|
41
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
42
|
+
search: (text: string) => any;
|
|
12
43
|
changed: () => any;
|
|
13
|
-
|
|
44
|
+
narrow: (patch: {
|
|
45
|
+
status?: RunStatus[];
|
|
46
|
+
triggerType?: TriggerType | "";
|
|
47
|
+
}) => any;
|
|
48
|
+
"page-size": (size: number) => any;
|
|
49
|
+
reorder: (order: {
|
|
50
|
+
sort: RunSort;
|
|
51
|
+
direction: RunSortDirection;
|
|
52
|
+
}) => any;
|
|
53
|
+
"update:page": (value: number) => any;
|
|
54
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
55
|
+
onSearch?: ((text: string) => any) | undefined;
|
|
14
56
|
onChanged?: (() => any) | undefined;
|
|
57
|
+
onNarrow?: ((patch: {
|
|
58
|
+
status?: RunStatus[];
|
|
59
|
+
triggerType?: TriggerType | "";
|
|
60
|
+
}) => any) | undefined;
|
|
61
|
+
"onPage-size"?: ((size: number) => any) | undefined;
|
|
62
|
+
onReorder?: ((order: {
|
|
63
|
+
sort: RunSort;
|
|
64
|
+
direction: RunSortDirection;
|
|
65
|
+
}) => any) | undefined;
|
|
66
|
+
"onUpdate:page"?: ((value: number) => any) | undefined;
|
|
15
67
|
}>, {
|
|
68
|
+
sort: RunSort;
|
|
16
69
|
loading: boolean;
|
|
70
|
+
total: number;
|
|
71
|
+
direction: RunSortDirection;
|
|
72
|
+
triggerType: TriggerType | "";
|
|
73
|
+
perPage: number;
|
|
17
74
|
pagination: boolean;
|
|
18
75
|
showWorkflow: boolean;
|
|
76
|
+
serverMode: boolean;
|
|
19
77
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
20
78
|
export default _default;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, ref, watch } from "vue";
|
|
3
3
|
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
4
|
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@revenexx/studio";
|
|
5
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
5
6
|
import { useIntegrationsCredentials } from "../../composables/useIntegrationsCredentials";
|
|
6
7
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
7
8
|
const emit = defineEmits(["created"]);
|
|
@@ -64,7 +65,15 @@ async function importAccount(account) {
|
|
|
64
65
|
</DialogDescription>
|
|
65
66
|
</DialogHeader>
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
<!-- Mounts with the dialog, like the one in the create dialog and with the same
|
|
69
|
+
limit written down there (PO-251). -->
|
|
70
|
+
<LoadingAnnouncement
|
|
71
|
+
:label="t('integration.credentials.sftpImport.title')"
|
|
72
|
+
:loading="sftp.loading.value && !activeAccounts.length"
|
|
73
|
+
:text="t('integration.credentials.sftpImport.state.loading')"
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<div v-if="sftp.loading.value && !activeAccounts.length" aria-hidden="true" class="space-y-2">
|
|
68
77
|
<div v-for="n in 3" :key="`sk-${n}`" class="h-9 rounded-md bg-black/[0.07] animate-pulse" />
|
|
69
78
|
</div>
|
|
70
79
|
<p v-else-if="!activeAccounts.length" class="py-4 text-center text-sm text-muted-foreground">
|
|
@@ -171,7 +171,7 @@ function onViewMouseDown(e) {
|
|
|
171
171
|
v-for="(c, i) in STICKY_COLORS"
|
|
172
172
|
:key="i"
|
|
173
173
|
type="button"
|
|
174
|
-
class="size-
|
|
174
|
+
class="size-6 rounded-full border transition hover:scale-110"
|
|
175
175
|
:class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
|
|
176
176
|
:style="{ backgroundColor: c.bg, borderColor: c.border }"
|
|
177
177
|
:title="t(c.nameKey)"
|
|
@@ -21,10 +21,12 @@ import {
|
|
|
21
21
|
} from "@revenexx/studio";
|
|
22
22
|
import { toast } from "vue-sonner";
|
|
23
23
|
import { TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
|
|
24
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
24
25
|
import TemplateCard from "./TemplateCard.vue";
|
|
25
26
|
import WorkflowIconPicker from "./WorkflowIconPicker.vue";
|
|
26
27
|
import WorkflowTagsInput from "./WorkflowTagsInput.vue";
|
|
27
28
|
import { resolveLocalized } from "../../utils/localize";
|
|
29
|
+
import { errorReason } from "../../utils/apiError";
|
|
28
30
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
29
31
|
const t = useIntegrationsT();
|
|
30
32
|
const router = useRouter();
|
|
@@ -102,7 +104,7 @@ async function submit() {
|
|
|
102
104
|
open.value = false;
|
|
103
105
|
await router.push(`/integrations/workflows/${created.id}`);
|
|
104
106
|
} catch (e) {
|
|
105
|
-
error.value = e
|
|
107
|
+
error.value = errorReason(e, t("integration.workflows.create.error.generic"));
|
|
106
108
|
} finally {
|
|
107
109
|
submitting.value = false;
|
|
108
110
|
}
|
|
@@ -126,7 +128,7 @@ async function useTemplate(slug) {
|
|
|
126
128
|
open.value = false;
|
|
127
129
|
await router.push(`/integrations/workflows/${id}`);
|
|
128
130
|
} catch (e) {
|
|
129
|
-
toast.error(e
|
|
131
|
+
toast.error(errorReason(e, t("integration.workflows.create.toast.templateError")));
|
|
130
132
|
} finally {
|
|
131
133
|
instantiatingSlug.value = null;
|
|
132
134
|
}
|
|
@@ -143,6 +145,19 @@ async function useTemplate(slug) {
|
|
|
143
145
|
<!-- Switcher: the same TabBar the studio surfaces use. -->
|
|
144
146
|
<TabBar v-model="mode" :tabs="modeTabs" class="-mt-1 shrink-0" />
|
|
145
147
|
|
|
148
|
+
<!-- Beside the switcher rather than inside the template half, and that is the
|
|
149
|
+
whole point: the dialog opens on the blank half, so a region placed in the
|
|
150
|
+
other branch would be inserted at the moment the reader switches to it —
|
|
151
|
+
with the catalogue already on its way and its own text already set, which is
|
|
152
|
+
a live region nothing was listening to. Out here it is in the dialog from
|
|
153
|
+
the moment it opens, and the text changes under a reader who is already
|
|
154
|
+
there (PO-251). -->
|
|
155
|
+
<LoadingAnnouncement
|
|
156
|
+
:label="t('integration.workflows.create.tab.template')"
|
|
157
|
+
:loading="mode === 'template' && templatesLoading && !templates.length"
|
|
158
|
+
:text="t('integration.workflows.create.template.state.loading')"
|
|
159
|
+
/>
|
|
160
|
+
|
|
146
161
|
<!-- ── Blank ─────────────────────────────────────────────────────────── -->
|
|
147
162
|
<!-- -m-1/p-1 keeps the selection rings clear of the scroll clipping. -->
|
|
148
163
|
<form v-if="mode === 'blank'" class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
|
|
@@ -261,7 +276,7 @@ async function useTemplate(slug) {
|
|
|
261
276
|
>
|
|
262
277
|
</div>
|
|
263
278
|
|
|
264
|
-
<div v-if="templatesLoading && !templates.length" class="grid gap-2 p-1 sm:grid-cols-2">
|
|
279
|
+
<div v-if="templatesLoading && !templates.length" aria-hidden="true" class="grid gap-2 p-1 sm:grid-cols-2">
|
|
265
280
|
<div v-for="n in 6" :key="`sk-tpl-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-3 space-y-2">
|
|
266
281
|
<div class="flex items-center gap-2">
|
|
267
282
|
<div class="size-7 rounded-md bg-black/[0.07] animate-pulse" />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type DeletableWorkflow = {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
workflow: DeletableWorkflow | null;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
9
|
+
'open'?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
deleted: (workflow: DeletableWorkflow) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, watch } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
Button
|
|
12
|
+
} from "@revenexx/studio";
|
|
13
|
+
import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
|
|
14
|
+
import { errorReason } from "../../utils/apiError";
|
|
15
|
+
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
workflow: { type: [Object, null], required: true }
|
|
18
|
+
});
|
|
19
|
+
const emit = defineEmits(["deleted"]);
|
|
20
|
+
const t = useIntegrationsT();
|
|
21
|
+
const { remove } = useIntegrationsWorkflows();
|
|
22
|
+
const busy = ref(false);
|
|
23
|
+
const error = ref(null);
|
|
24
|
+
watch(open, (o) => {
|
|
25
|
+
if (o) error.value = null;
|
|
26
|
+
});
|
|
27
|
+
async function confirm() {
|
|
28
|
+
const wf = props.workflow;
|
|
29
|
+
if (!wf || busy.value) return;
|
|
30
|
+
busy.value = true;
|
|
31
|
+
error.value = null;
|
|
32
|
+
try {
|
|
33
|
+
await remove(wf.id);
|
|
34
|
+
open.value = false;
|
|
35
|
+
emit("deleted", wf);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
error.value = errorReason(e, t("integration.workflows.delete.error.generic"));
|
|
38
|
+
} finally {
|
|
39
|
+
busy.value = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<AlertDialog :open="open" @update:open="(v) => {
|
|
46
|
+
if (!v && !busy) open = false;
|
|
47
|
+
}">
|
|
48
|
+
<AlertDialogContent>
|
|
49
|
+
<AlertDialogHeader>
|
|
50
|
+
<AlertDialogTitle>{{ t("integration.workflows.delete.dialog.title") }}</AlertDialogTitle>
|
|
51
|
+
<AlertDialogDescription>
|
|
52
|
+
<span v-if="props.workflow">
|
|
53
|
+
<span class="font-medium text-foreground">{{ props.workflow.name }}</span>
|
|
54
|
+
{{ t("integration.workflows.delete.dialog.description") }}
|
|
55
|
+
</span>
|
|
56
|
+
</AlertDialogDescription>
|
|
57
|
+
</AlertDialogHeader>
|
|
58
|
+
<p v-if="error" class="text-sm text-rose-600">{{ error }}</p>
|
|
59
|
+
<AlertDialogFooter>
|
|
60
|
+
<Button variant="outline" :disabled="busy" @click="open = false">
|
|
61
|
+
{{ t("integration.common.action.cancel") }}
|
|
62
|
+
</Button>
|
|
63
|
+
<Button class="bg-destructive text-white hover:bg-destructive/90" :disabled="busy" @click="confirm">
|
|
64
|
+
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
65
|
+
{{ busy ? t("integration.workflows.delete.action.deleting") : t("integration.common.action.delete") }}
|
|
66
|
+
</Button>
|
|
67
|
+
</AlertDialogFooter>
|
|
68
|
+
</AlertDialogContent>
|
|
69
|
+
</AlertDialog>
|
|
70
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type DeletableWorkflow = {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_Props = {
|
|
6
|
+
workflow: DeletableWorkflow | null;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_PublicProps = __VLS_Props & {
|
|
9
|
+
'open'?: boolean;
|
|
10
|
+
};
|
|
11
|
+
declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:open": (value: boolean) => any;
|
|
13
|
+
deleted: (workflow: DeletableWorkflow) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
16
|
+
onDeleted?: ((workflow: DeletableWorkflow) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
export default _default;
|
|
@@ -3,6 +3,7 @@ import { computed, ref, watch } from "vue";
|
|
|
3
3
|
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
4
|
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input } from "@revenexx/studio";
|
|
5
5
|
import { useIntegrationsWorkflows } from "../../composables/useIntegrationsWorkflows";
|
|
6
|
+
import { errorReason } from "../../utils/apiError";
|
|
6
7
|
const open = defineModel("open", { type: Boolean, ...{ default: false } });
|
|
7
8
|
const props = defineProps({
|
|
8
9
|
workflow: { type: [Object, null], required: true }
|
|
@@ -29,7 +30,7 @@ async function submit() {
|
|
|
29
30
|
open.value = false;
|
|
30
31
|
emit("duplicated", copy);
|
|
31
32
|
} catch (e) {
|
|
32
|
-
error.value = e
|
|
33
|
+
error.value = errorReason(e, t("integration.workflows.duplicate.error.generic"));
|
|
33
34
|
} finally {
|
|
34
35
|
busy.value = false;
|
|
35
36
|
}
|
|
@@ -146,20 +146,20 @@ function spliceNode() {
|
|
|
146
146
|
>
|
|
147
147
|
<button
|
|
148
148
|
type="button"
|
|
149
|
-
class="flex size-
|
|
149
|
+
class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-muted/60 hover:text-foreground"
|
|
150
150
|
:title="t('integration.canvas.edge.action.insertNode')"
|
|
151
151
|
@click.stop="spliceNode"
|
|
152
152
|
>
|
|
153
|
-
<Plus :size="
|
|
153
|
+
<Plus :size="13" />
|
|
154
154
|
</button>
|
|
155
155
|
<div class="h-3.5 w-px bg-black/10" />
|
|
156
156
|
<button
|
|
157
157
|
type="button"
|
|
158
|
-
class="flex size-
|
|
158
|
+
class="flex size-6 items-center justify-center rounded text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
|
|
159
159
|
:title="t('integration.canvas.edge.action.delete')"
|
|
160
160
|
@click.stop="removeEdge"
|
|
161
161
|
>
|
|
162
|
-
<TrashBin :size="
|
|
162
|
+
<TrashBin :size="13" />
|
|
163
163
|
</button>
|
|
164
164
|
</div>
|
|
165
165
|
</EdgeLabelRenderer>
|
|
@@ -50,7 +50,10 @@ import { layoutWorkflowNodes } from "./workflowLayout";
|
|
|
50
50
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
51
51
|
import { isTerminalManifest, portsForNode } from "../../data/nodeInputData";
|
|
52
52
|
import { computeLoopBodies, iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
53
|
-
import { portFromHandleId } from "../../data/handleId";
|
|
53
|
+
import { inputHandleId, outputHandleId, portFromHandleId } from "../../data/handleId";
|
|
54
|
+
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
55
|
+
import { deepClone } from "../../utils/deepClone";
|
|
56
|
+
import { copyToClipboard } from "../../utils/clipboard";
|
|
54
57
|
import { groupFindingsByNode } from "../../utils/validationFindings";
|
|
55
58
|
import { reorderBranchSubtrees } from "../../data/branchReorder";
|
|
56
59
|
import { findingsByNodeField } from "../../utils/validationFindings";
|
|
@@ -65,14 +68,11 @@ const props = defineProps({
|
|
|
65
68
|
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
66
69
|
const t = useIntegrationsT();
|
|
67
70
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
68
|
-
const blob = ref(
|
|
71
|
+
const blob = ref(deepClone(props.draft ?? props.workflow.blob));
|
|
69
72
|
watch(() => props.workflow.id, () => {
|
|
70
|
-
blob.value =
|
|
73
|
+
blob.value = deepClone(props.workflow.blob);
|
|
71
74
|
syncFromBlob();
|
|
72
75
|
});
|
|
73
|
-
function clone(v) {
|
|
74
|
-
return JSON.parse(JSON.stringify(v));
|
|
75
|
-
}
|
|
76
76
|
const vfNodes = ref([]);
|
|
77
77
|
const vfEdges = ref([]);
|
|
78
78
|
const flow = useVueFlow();
|
|
@@ -80,7 +80,7 @@ const invalidNodeMessages = computed(() => findingsByNodeField(props.workflow.wa
|
|
|
80
80
|
const dynamicOutputs = ref({});
|
|
81
81
|
const invalidNodeFields = computed(() => {
|
|
82
82
|
const out = {};
|
|
83
|
-
for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "")) {
|
|
83
|
+
for (const g of groupFindingsByNode(props.workflow.warnings ?? [], props.workflow.blob.nodes, () => "", (_n, key) => key)) {
|
|
84
84
|
if (g.nodeId) out[g.nodeId] = g.fields;
|
|
85
85
|
}
|
|
86
86
|
return out;
|
|
@@ -160,9 +160,6 @@ function nodePosition(id) {
|
|
|
160
160
|
function pickSourceSide(_sourceId, _targetId) {
|
|
161
161
|
return "bottom";
|
|
162
162
|
}
|
|
163
|
-
function oppositeSide(s) {
|
|
164
|
-
return s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
165
|
-
}
|
|
166
163
|
function stickyArea(s) {
|
|
167
164
|
return (s.size?.width ?? 240) * (s.size?.height ?? 160);
|
|
168
165
|
}
|
|
@@ -252,6 +249,7 @@ function markBorn(...ids) {
|
|
|
252
249
|
function bornClass(id) {
|
|
253
250
|
return bornIds.value.has(id) ? "node-pop-in" : void 0;
|
|
254
251
|
}
|
|
252
|
+
const NODE_FALLBACK_POSITION = { x: 240, y: 80 };
|
|
255
253
|
function syncFromBlob() {
|
|
256
254
|
const loops = nodeLoopData(blob.value.nodes, blob.value.edges);
|
|
257
255
|
const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
@@ -304,7 +302,7 @@ function syncFromBlob() {
|
|
|
304
302
|
...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
|
|
305
303
|
id: n.id,
|
|
306
304
|
type: "workflow",
|
|
307
|
-
position: n.position ??
|
|
305
|
+
position: n.position ?? NODE_FALLBACK_POSITION,
|
|
308
306
|
class: bornClass(n.id),
|
|
309
307
|
data: {
|
|
310
308
|
...n,
|
|
@@ -330,8 +328,8 @@ function syncFromBlob() {
|
|
|
330
328
|
...vf,
|
|
331
329
|
source,
|
|
332
330
|
target,
|
|
333
|
-
sourceHandle: srcGroup ? "
|
|
334
|
-
targetHandle: tgtGroup ? "
|
|
331
|
+
sourceHandle: srcGroup ? outputHandleId("default", "bottom") : vf.sourceHandle,
|
|
332
|
+
targetHandle: tgtGroup ? inputHandleId("top") : vf.targetHandle
|
|
335
333
|
});
|
|
336
334
|
}
|
|
337
335
|
const bySource = /* @__PURE__ */ new Map();
|
|
@@ -355,15 +353,15 @@ function makeVfEdge(id, sourceId, targetId, output) {
|
|
|
355
353
|
const manifest = wn ? findManifest(wn.nodeSlug, wn.nodeVersion) : null;
|
|
356
354
|
const isBranchPort = (manifest?.outputs ?? []).some((o) => o.sourceFromConfig) || ["true", "false"].includes(output);
|
|
357
355
|
const srcSide = pickSourceSide(sourceId, targetId);
|
|
358
|
-
const tgtSide =
|
|
356
|
+
const tgtSide = OPPOSITE_SIDE[srcSide];
|
|
359
357
|
const order = orderedOutputPorts(sourceId);
|
|
360
358
|
const portIndex = order.indexOf(output);
|
|
361
359
|
return {
|
|
362
360
|
id,
|
|
363
361
|
source: sourceId,
|
|
364
362
|
target: targetId,
|
|
365
|
-
sourceHandle:
|
|
366
|
-
targetHandle:
|
|
363
|
+
sourceHandle: outputHandleId(output, srcSide),
|
|
364
|
+
targetHandle: inputHandleId(tgtSide),
|
|
367
365
|
type: "flow",
|
|
368
366
|
data: {
|
|
369
367
|
kind,
|
|
@@ -561,7 +559,7 @@ function removeBlobNode(id) {
|
|
|
561
559
|
push();
|
|
562
560
|
}
|
|
563
561
|
function push() {
|
|
564
|
-
emit("change",
|
|
562
|
+
emit("change", deepClone(blob.value));
|
|
565
563
|
}
|
|
566
564
|
const STICKY_DEFAULT_SIZE = { width: 240, height: 160 };
|
|
567
565
|
function updateSticky(id, patch) {
|
|
@@ -622,7 +620,7 @@ function duplicateBlobNodes(ids) {
|
|
|
622
620
|
src.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
|
|
623
621
|
const offset = 40;
|
|
624
622
|
const copies = src.map((n) => ({
|
|
625
|
-
...
|
|
623
|
+
...deepClone(n),
|
|
626
624
|
id: idMap.get(n.id),
|
|
627
625
|
position: { x: (n.position?.x ?? 0) + offset, y: (n.position?.y ?? 0) + offset }
|
|
628
626
|
}));
|
|
@@ -661,12 +659,10 @@ async function copyNodes(ids) {
|
|
|
661
659
|
if (!nodes.length) return;
|
|
662
660
|
const inner = new Set(nodes.map((n) => n.id));
|
|
663
661
|
const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
toast.error(t("integration.editor.clipboard.error.write"));
|
|
669
|
-
}
|
|
662
|
+
await copyToClipboard(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2), {
|
|
663
|
+
copied: t("integration.editor.clipboard.toast.copied", nodes.length),
|
|
664
|
+
failed: t("integration.editor.clipboard.error.write")
|
|
665
|
+
});
|
|
670
666
|
}
|
|
671
667
|
async function pasteNodes(at) {
|
|
672
668
|
let text = "";
|
|
@@ -695,7 +691,7 @@ async function pasteNodes(at) {
|
|
|
695
691
|
const dx = at ? Math.round(at.x - minX) : 40;
|
|
696
692
|
const dy = at ? Math.round(at.y - minY) : 40;
|
|
697
693
|
blob.value.nodes.push(...payload.nodes.map((n) => ({
|
|
698
|
-
...
|
|
694
|
+
...deepClone(n),
|
|
699
695
|
id: idMap.get(n.id),
|
|
700
696
|
position: { x: (n.position?.x ?? 0) + dx, y: (n.position?.y ?? 0) + dy }
|
|
701
697
|
})));
|
|
@@ -906,10 +902,10 @@ function groupAtPoint(pos) {
|
|
|
906
902
|
function addGroupFrame() {
|
|
907
903
|
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
908
904
|
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
909
|
-
addFromPayload({ kind: "group" }, {
|
|
905
|
+
addFromPayload({ kind: "group" }, freeSpotNear({
|
|
910
906
|
x: center.x - EMPTY_GROUP_SIZE.width / 2,
|
|
911
907
|
y: center.y - EMPTY_GROUP_SIZE.height / 2
|
|
912
|
-
});
|
|
908
|
+
}));
|
|
913
909
|
}
|
|
914
910
|
function adoptIntoGroupAt(id, pos) {
|
|
915
911
|
const g = groupAtPoint(pos);
|
|
@@ -970,6 +966,21 @@ function onDrop(ev) {
|
|
|
970
966
|
const payload = JSON.parse(raw);
|
|
971
967
|
addFromPayload(payload, flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY }));
|
|
972
968
|
}
|
|
969
|
+
const CASCADE_STEP = 2 * GRID;
|
|
970
|
+
function freeSpotNear(at) {
|
|
971
|
+
const taken = [
|
|
972
|
+
...vfNodes.value.map((n) => n.position),
|
|
973
|
+
...blob.value.nodes.map((n) => n.position ?? NODE_FALLBACK_POSITION),
|
|
974
|
+
...Object.values(blob.value.triggerPositions ?? {})
|
|
975
|
+
];
|
|
976
|
+
const occupied = (p) => taken.some((q) => Math.abs(q.x - p.x) < GRID && Math.abs(q.y - p.y) < GRID);
|
|
977
|
+
const spot = { x: snap(at.x), y: snap(at.y) };
|
|
978
|
+
for (let i = 0; i <= taken.length && occupied(spot); i++) {
|
|
979
|
+
spot.x += CASCADE_STEP;
|
|
980
|
+
spot.y += CASCADE_STEP;
|
|
981
|
+
}
|
|
982
|
+
return spot;
|
|
983
|
+
}
|
|
973
984
|
function onPaletteInsert(payload) {
|
|
974
985
|
if (pendingConnect.value && payload.kind === "manifest") {
|
|
975
986
|
const pc = pendingConnect.value;
|
|
@@ -1003,7 +1014,7 @@ function onPaletteInsert(payload) {
|
|
|
1003
1014
|
pendingSpliceEdgeId.value = null;
|
|
1004
1015
|
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
1005
1016
|
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
1006
|
-
addFromPayload(payload, center);
|
|
1017
|
+
addFromPayload(payload, freeSpotNear(center));
|
|
1007
1018
|
}
|
|
1008
1019
|
const pendingSpliceEdgeId = ref(null);
|
|
1009
1020
|
provide("workflow-edge-actions", {
|
|
@@ -11,6 +11,7 @@ import { Badge, Button } from "@revenexx/studio";
|
|
|
11
11
|
import { toast } from "vue-sonner";
|
|
12
12
|
import "@vue-flow/core/dist/style.css";
|
|
13
13
|
import "@vue-flow/core/dist/theme-default.css";
|
|
14
|
+
import LoadingAnnouncement from "./LoadingAnnouncement.vue";
|
|
14
15
|
import WorkflowNodeView from "./WorkflowNodeView.vue";
|
|
15
16
|
import WorkflowTriggerView from "./WorkflowTriggerView.vue";
|
|
16
17
|
import StickyNoteView from "./StickyNoteView.vue";
|
|
@@ -18,8 +19,11 @@ import GroupFrameView from "./GroupFrameView.vue";
|
|
|
18
19
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
19
20
|
import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
|
|
20
21
|
import { findManifest } from "../../data/integrationNodeCatalog";
|
|
22
|
+
import { OPPOSITE_SIDE } from "./canvasSides";
|
|
23
|
+
import { inputHandleId, outputHandleId } from "../../data/handleId";
|
|
21
24
|
import { iteratorNodeIds, nodeLoopData } from "../../data/loopBody";
|
|
22
25
|
import { useIntegrationsRevisions } from "../../composables/useIntegrationsRevisions";
|
|
26
|
+
import { errorReason } from "../../utils/apiError";
|
|
23
27
|
const props = defineProps({
|
|
24
28
|
workflow: { type: Object, required: true },
|
|
25
29
|
draftPending: { type: Boolean, required: false }
|
|
@@ -118,7 +122,6 @@ function buildPreview(blob) {
|
|
|
118
122
|
return { x: 0, y: 0 };
|
|
119
123
|
};
|
|
120
124
|
const pickSourceSide = (_sourceId, _targetId) => "bottom";
|
|
121
|
-
const opposite = (s) => s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
122
125
|
const edgeKind = (sourceNodeId, output) => {
|
|
123
126
|
const wn = blob.nodes.find((n) => n.id === sourceNodeId);
|
|
124
127
|
if (!wn) return "default";
|
|
@@ -182,8 +185,8 @@ function buildPreview(blob) {
|
|
|
182
185
|
id: e.id,
|
|
183
186
|
source: e.from.nodeId,
|
|
184
187
|
target: e.to.nodeId,
|
|
185
|
-
sourceHandle:
|
|
186
|
-
targetHandle:
|
|
188
|
+
sourceHandle: outputHandleId(e.from.output, srcSide),
|
|
189
|
+
targetHandle: inputHandleId(OPPOSITE_SIDE[srcSide]),
|
|
187
190
|
type: "flow",
|
|
188
191
|
selectable: false,
|
|
189
192
|
data: { kind, label: isBranchPort ? e.from.output : void 0 },
|
|
@@ -222,7 +225,7 @@ async function select(revision) {
|
|
|
222
225
|
buildPreview(detail.blob);
|
|
223
226
|
}
|
|
224
227
|
} catch (e) {
|
|
225
|
-
toast.error(e
|
|
228
|
+
toast.error(errorReason(e, t("integration.history.toast.loadRevisionFailed")));
|
|
226
229
|
} finally {
|
|
227
230
|
detailLoading.value = false;
|
|
228
231
|
}
|
|
@@ -239,7 +242,7 @@ async function restoreRevision(rev) {
|
|
|
239
242
|
await loadList(true);
|
|
240
243
|
void select(null);
|
|
241
244
|
} catch (e) {
|
|
242
|
-
toast.error(e
|
|
245
|
+
toast.error(errorReason(e, t("integration.history.toast.restoreFailed")));
|
|
243
246
|
} finally {
|
|
244
247
|
restoring.value = null;
|
|
245
248
|
}
|
|
@@ -328,6 +331,14 @@ onMounted(() => {
|
|
|
328
331
|
</p>
|
|
329
332
|
</div>
|
|
330
333
|
|
|
334
|
+
<!-- In the aside's chrome rather than in the scrolling list, so it is on the
|
|
335
|
+
page while the revisions are on their way (PO-251). -->
|
|
336
|
+
<LoadingAnnouncement
|
|
337
|
+
:label="t('integration.history.heading.title')"
|
|
338
|
+
:loading="listLoading && !entries.length"
|
|
339
|
+
:text="t('integration.history.state.loading')"
|
|
340
|
+
/>
|
|
341
|
+
|
|
331
342
|
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
|
332
343
|
<!-- Current (live) version -->
|
|
333
344
|
<button
|
|
@@ -385,7 +396,7 @@ onMounted(() => {
|
|
|
385
396
|
{{ revisionsApi.error.value }}
|
|
386
397
|
</p>
|
|
387
398
|
|
|
388
|
-
<div v-if="listLoading && !entries.length" class="space-y-2 px-2 py-2">
|
|
399
|
+
<div v-if="listLoading && !entries.length" aria-hidden="true" class="space-y-2 px-2 py-2">
|
|
389
400
|
<div v-for="n in 5" :key="`sk-rev-${n}`" class="flex items-center gap-3 py-1.5">
|
|
390
401
|
<div class="size-7 shrink-0 rounded-md bg-black/[0.07] animate-pulse" />
|
|
391
402
|
<div class="min-w-0 flex-1 space-y-1.5">
|