@revenexx/studio-integrations 2.0.0 → 2.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 +141 -0
- package/README.md +1 -0
- 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 +28 -9
- 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 +7 -26
- package/dist/runtime/components/Integrations/StickyNoteView.vue +1 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +10 -6
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +224 -225
- 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 +42 -0
- package/dist/runtime/i18n/messages/en.json +42 -0
- 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/index.vue +5 -10
- package/dist/runtime/pages/integrations/schedules.vue +13 -1
- package/dist/runtime/pages/integrations/templates/index.vue +40 -40
- package/dist/runtime/pages/integrations/workflows/[id].vue +324 -244
- package/dist/runtime/types/integrations.d.ts +13 -0
- package/dist/runtime/utils/credentialState.d.ts +0 -7
- package/dist/runtime/utils/credentialState.js +0 -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 +8 -3
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import { RefreshCircle as Loader2 } from "#solar-icons";
|
|
4
|
+
import { Button, Input, Label } from "@revenexx/studio";
|
|
5
|
+
import { useWorkflowFile } from "../../composables/useWorkflowFile";
|
|
6
|
+
import { parseWorkflowFile } from "../../utils/workflowFile";
|
|
7
|
+
import { errorReason } from "../../utils/apiError";
|
|
8
|
+
const emit = defineEmits(["created", "cancel"]);
|
|
9
|
+
const t = useIntegrationsT();
|
|
10
|
+
const { importAsNew } = useWorkflowFile();
|
|
11
|
+
const file = ref(null);
|
|
12
|
+
const name = ref("");
|
|
13
|
+
const error = ref(null);
|
|
14
|
+
const busy = ref(false);
|
|
15
|
+
const summary = computed(() => {
|
|
16
|
+
const b = file.value?.workflow.blob;
|
|
17
|
+
if (!b) return "";
|
|
18
|
+
return t("integration.workflowFile.count.both", {
|
|
19
|
+
nodes: t("integration.workflowFile.count.nodes", b.nodes.length),
|
|
20
|
+
triggers: t("integration.workflowFile.count.triggers", b.triggers?.length ?? 0)
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
async function onPick(event) {
|
|
24
|
+
const input = event.target;
|
|
25
|
+
const picked = input.files?.[0];
|
|
26
|
+
file.value = null;
|
|
27
|
+
error.value = null;
|
|
28
|
+
if (!picked) return;
|
|
29
|
+
const parsed = parseWorkflowFile(await picked.text());
|
|
30
|
+
if (!parsed.ok) {
|
|
31
|
+
error.value = t(`integration.workflowFile.refused.${parsed.reason}`);
|
|
32
|
+
input.value = "";
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
file.value = parsed.file;
|
|
36
|
+
name.value = parsed.file.workflow.name;
|
|
37
|
+
}
|
|
38
|
+
async function submit() {
|
|
39
|
+
if (!file.value || busy.value) return;
|
|
40
|
+
if (!name.value.trim()) {
|
|
41
|
+
error.value = t("integration.workflows.create.error.nameRequired");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
busy.value = true;
|
|
45
|
+
error.value = null;
|
|
46
|
+
try {
|
|
47
|
+
const created = await importAsNew(file.value, name.value);
|
|
48
|
+
emit("created", created.id);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
error.value = errorReason(e, t("integration.workflowFile.import.error"));
|
|
51
|
+
} finally {
|
|
52
|
+
busy.value = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<form class="-m-1 max-h-[65vh] space-y-3 overflow-y-auto p-1" @submit.prevent="submit">
|
|
59
|
+
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
60
|
+
<Label for="wf-import-file">{{ t("integration.workflowFile.field.file") }}</Label>
|
|
61
|
+
<input
|
|
62
|
+
id="wf-import-file"
|
|
63
|
+
type="file"
|
|
64
|
+
accept=".json,application/json"
|
|
65
|
+
:disabled="busy"
|
|
66
|
+
class="text-sm file:mr-3 file:h-8 file:rounded-md file:border file:border-black/[0.08] file:bg-white file:px-3 file:text-sm"
|
|
67
|
+
@change="onPick"
|
|
68
|
+
>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<template v-if="file">
|
|
72
|
+
<div class="grid grid-cols-[130px_1fr] items-center gap-x-4">
|
|
73
|
+
<Label for="wf-import-name">{{ t("integration.workflows.create.field.name.label") }}</Label>
|
|
74
|
+
<Input id="wf-import-name" v-model="name" :disabled="busy" class="bg-white" />
|
|
75
|
+
</div>
|
|
76
|
+
<p class="text-sm text-muted-foreground">
|
|
77
|
+
{{ summary }} · {{ t("integration.workflowFile.import.arrives") }}
|
|
78
|
+
</p>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<div v-if="error" role="alert" class="rounded-md border border-rose-500/20 bg-rose-500/5 px-3 py-2 text-sm text-rose-700">
|
|
82
|
+
{{ error }}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="flex gap-2 pt-1">
|
|
86
|
+
<Button type="submit" :disabled="!file || busy" class="h-8 text-sm">
|
|
87
|
+
<Loader2 v-if="busy" :size="14" class="animate-spin" />
|
|
88
|
+
{{ busy ? t("integration.workflows.create.action.submitting") : t("integration.workflows.create.action.submit") }}
|
|
89
|
+
</Button>
|
|
90
|
+
<Button type="button" variant="outline" class="h-8 text-sm" @click="emit('cancel')">
|
|
91
|
+
{{ t("integration.common.action.cancel") }}
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
|
+
created: (id: number) => any;
|
|
3
|
+
cancel: () => any;
|
|
4
|
+
}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
|
|
5
|
+
onCreated?: ((id: number) => any) | undefined;
|
|
6
|
+
onCancel?: (() => any) | undefined;
|
|
7
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
8
|
+
export default _default;
|
|
@@ -93,10 +93,11 @@ function portSemantic(p) {
|
|
|
93
93
|
}
|
|
94
94
|
function portTone(p) {
|
|
95
95
|
switch (portSemantic(p)) {
|
|
96
|
+
// Switch cases / iterator `item` (`branch`): a taken branch is a normal
|
|
97
|
+
// continuation, so it reads green like any other output rather than a
|
|
98
|
+
// special colour.
|
|
96
99
|
case "success":
|
|
97
100
|
case "branch-true":
|
|
98
|
-
// Switch cases / iterator `item`: a taken branch is a normal continuation,
|
|
99
|
-
// so it reads green like any other output rather than a special colour.
|
|
100
101
|
case "branch":
|
|
101
102
|
return { handle: "!bg-emerald-500", label: "text-emerald-600" };
|
|
102
103
|
// `false` is the neutral "didn't match" route, not a warning — muted slate
|
|
@@ -263,12 +264,12 @@ function remove() {
|
|
|
263
264
|
Everything else lives in the config lightbox. -->
|
|
264
265
|
<div
|
|
265
266
|
class="relative w-[220px] rounded-lg border bg-white transition-all duration-150"
|
|
266
|
-
@mouseenter="onHoverEnter"
|
|
267
|
-
@mouseleave="onHoverLeave"
|
|
268
267
|
:class="[
|
|
269
268
|
selected ? 'border-emerald-500/50 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : invalidFields.length ? 'border-amber-500/70 shadow-[0_0_0_2px_rgba(245,158,11,0.22)] hover:shadow-md' : inSelectedLoop ? 'border-slate-400/80 shadow-[0_0_0_2px_rgba(100,116,139,0.20)]' : 'border-black/[0.08] shadow-sm hover:shadow-md hover:border-black/[0.16]',
|
|
270
269
|
showPortLabels ? 'pb-3.5' : ''
|
|
271
270
|
]"
|
|
271
|
+
@mouseenter="onHoverEnter"
|
|
272
|
+
@mouseleave="onHoverLeave"
|
|
272
273
|
>
|
|
273
274
|
<!-- The card's marks, in one row at the corner the input handles leave free.
|
|
274
275
|
A row rather than two absolutely-placed badges: the amber one's width
|
|
@@ -261,13 +261,6 @@ watch(() => vfNodes.value.map((n) => n.id).join("|"), () => {
|
|
|
261
261
|
});
|
|
262
262
|
const vfEdges = computed(() => {
|
|
263
263
|
const blob = props.workflow.blob;
|
|
264
|
-
const nodePos = (id) => {
|
|
265
|
-
const wn = blob.nodes.find((n) => n.id === id);
|
|
266
|
-
if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
|
|
267
|
-
const ti = blob.triggers.findIndex((t2) => t2.id === id);
|
|
268
|
-
if (ti >= 0) return blob.triggerPositions?.[id] ?? { x: -40, y: 60 + ti * 140 };
|
|
269
|
-
return { x: 0, y: 0 };
|
|
270
|
-
};
|
|
271
264
|
const pickSourceSide = (_sourceId, _targetId) => "bottom";
|
|
272
265
|
const edgeKind = (sourceNodeId, output) => {
|
|
273
266
|
const wn = blob.nodes.find((n) => n.id === sourceNodeId);
|
|
@@ -64,12 +64,12 @@ const OUT_SIDES = ["bottom"];
|
|
|
64
64
|
|
|
65
65
|
<div
|
|
66
66
|
class="w-[200px] rounded-lg border-2 bg-gradient-to-br from-emerald-50 to-white transition-all duration-150"
|
|
67
|
-
@mouseenter="onHoverEnter"
|
|
68
|
-
@mouseleave="onHoverLeave"
|
|
69
67
|
:class="[
|
|
70
68
|
selected ? 'border-emerald-500 shadow-[0_0_0_2px_rgba(16,185,129,0.18)]' : 'border-emerald-400/40 shadow-sm hover:shadow-md hover:border-emerald-500/60',
|
|
71
69
|
data.active === false ? 'opacity-60 grayscale-[0.4]' : ''
|
|
72
70
|
]"
|
|
71
|
+
@mouseenter="onHoverEnter"
|
|
72
|
+
@mouseleave="onHoverLeave"
|
|
73
73
|
>
|
|
74
74
|
<!-- Same type scale as WorkflowNodeView so trigger and node cards read
|
|
75
75
|
identically (icon chip size-8/15, subtitle plain text-sm, mt-0.5). -->
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
SelectTrigger,
|
|
19
19
|
SelectValue
|
|
20
20
|
} from "@revenexx/studio";
|
|
21
|
-
import { toast } from "vue-sonner";
|
|
22
21
|
import { deepClone } from "../../utils/deepClone";
|
|
23
22
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
24
23
|
import { languageKeyRead, languageKeyToWrite, resolveDisplayName } from "../../utils/localize";
|
|
@@ -28,7 +27,7 @@ const props = defineProps({
|
|
|
28
27
|
});
|
|
29
28
|
const emit = defineEmits(["update:open", "update:variables"]);
|
|
30
29
|
const t = useIntegrationsT();
|
|
31
|
-
const KEY_PATTERN = /^[a-z_]
|
|
30
|
+
const KEY_PATTERN = /^[a-z_]\w*$/i;
|
|
32
31
|
const TYPES = ["string", "number", "boolean", "object", "array"];
|
|
33
32
|
const working = ref({});
|
|
34
33
|
const jsonDrafts = reactive({});
|
|
@@ -213,7 +212,7 @@ async function copyUsage(key) {
|
|
|
213
212
|
<SelectValue />
|
|
214
213
|
</SelectTrigger>
|
|
215
214
|
<SelectContent>
|
|
216
|
-
<SelectItem v-for="
|
|
215
|
+
<SelectItem v-for="type in TYPES" :key="type" :value="type">{{ type }}</SelectItem>
|
|
217
216
|
</SelectContent>
|
|
218
217
|
</Select>
|
|
219
218
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { MenuDots as MoreHorizontal } from "#solar-icons";
|
|
2
|
+
import { MenuDots as MoreHorizontal, Layers } from "#solar-icons";
|
|
3
3
|
import { toast } from "vue-sonner";
|
|
4
4
|
import { apiValidationIssues, errorReason } from "../../utils/apiError";
|
|
5
5
|
import WorkflowDeleteDialog from "./WorkflowDeleteDialog.vue";
|
|
6
6
|
import WorkflowDuplicateDialog from "./WorkflowDuplicateDialog.vue";
|
|
7
|
+
import WorkflowExportDialog from "./WorkflowExportDialog.vue";
|
|
7
8
|
import WorkflowIconView from "./WorkflowIconView.vue";
|
|
8
|
-
import { Layers } from "#solar-icons";
|
|
9
9
|
import {
|
|
10
10
|
Badge,
|
|
11
11
|
Button,
|
|
@@ -35,6 +35,7 @@ async function onDuplicated(copy) {
|
|
|
35
35
|
toast.success(t("integration.workflows.table.toast.duplicated", { name: copy.name }));
|
|
36
36
|
await router.push(`/integrations/workflows/${copy.id}`);
|
|
37
37
|
}
|
|
38
|
+
const exporter = ref(null);
|
|
38
39
|
const GROUP_BY_TAGS_KEY = "integrations.workflows.groupByTags";
|
|
39
40
|
const groupByTags = ref(false);
|
|
40
41
|
try {
|
|
@@ -281,10 +282,10 @@ const columns = computed(() => [
|
|
|
281
282
|
<span v-if="wf.description" class="text-sm text-muted-foreground truncate">{{ wf.description }}</span>
|
|
282
283
|
<div v-if="wf.blob.tags?.length" class="mt-0.5 flex flex-wrap gap-1">
|
|
283
284
|
<span
|
|
284
|
-
v-for="
|
|
285
|
-
:key="
|
|
285
|
+
v-for="tag in wf.blob.tags"
|
|
286
|
+
:key="tag"
|
|
286
287
|
class="rounded border border-black/[0.06] bg-muted/50 px-1 text-[10px] leading-4 text-muted-foreground"
|
|
287
|
-
>{{
|
|
288
|
+
>{{ tag }}</span>
|
|
288
289
|
</div>
|
|
289
290
|
</div>
|
|
290
291
|
</div>
|
|
@@ -328,11 +329,11 @@ const columns = computed(() => [
|
|
|
328
329
|
<template #cell-triggers="{ row: wf }">
|
|
329
330
|
<div class="flex min-w-0 flex-col items-start gap-1">
|
|
330
331
|
<span
|
|
331
|
-
v-for="
|
|
332
|
-
:key="
|
|
332
|
+
v-for="trigger in wf.blob.triggers"
|
|
333
|
+
:key="trigger.id"
|
|
333
334
|
class="inline-flex items-center whitespace-nowrap rounded-md border border-black/[0.06] bg-muted/50 px-1.5 py-0.5 text-xs font-medium uppercase text-muted-foreground"
|
|
334
335
|
>
|
|
335
|
-
{{
|
|
336
|
+
{{ trigger.type }}
|
|
336
337
|
</span>
|
|
337
338
|
</div>
|
|
338
339
|
</template>
|
|
@@ -385,6 +386,9 @@ const columns = computed(() => [
|
|
|
385
386
|
<DropdownMenuItem @select="askDuplicate(wf)">
|
|
386
387
|
{{ t("integration.workflows.action.duplicate") }}
|
|
387
388
|
</DropdownMenuItem>
|
|
389
|
+
<DropdownMenuItem @select="exporter?.start(wf.id)">
|
|
390
|
+
{{ t("integration.workflowFile.action.export") }}
|
|
391
|
+
</DropdownMenuItem>
|
|
388
392
|
<DropdownMenuItem @select="onToggleActive(wf)">
|
|
389
393
|
{{ wf.active ? t("integration.workflows.table.menu.action.pause") : t("integration.workflows.table.menu.action.activate") }}
|
|
390
394
|
</DropdownMenuItem>
|
|
@@ -404,4 +408,6 @@ const columns = computed(() => [
|
|
|
404
408
|
<WorkflowDeleteDialog v-model:open="deleteOpen" :workflow="deleteTarget" />
|
|
405
409
|
|
|
406
410
|
<WorkflowDuplicateDialog v-model:open="duplicateOpen" :workflow="duplicateTarget" @duplicated="onDuplicated" />
|
|
411
|
+
|
|
412
|
+
<WorkflowExportDialog ref="exporter" />
|
|
407
413
|
</template>
|
|
@@ -30,7 +30,10 @@ export function useIntegrationsNodes() {
|
|
|
30
30
|
// Registered and built are two different things: a version whose code
|
|
31
31
|
// was never built can be drawn but not executed, so the palette must
|
|
32
32
|
// be able to leave it out (PO-338). Missing field = assume built.
|
|
33
|
-
__hasBundle: n.has_bundle !== false
|
|
33
|
+
__hasBundle: n.has_bundle !== false,
|
|
34
|
+
// The package's own order, which the palette falls back to inside a
|
|
35
|
+
// folder (PO-537). Null and absent both mean "not recorded".
|
|
36
|
+
__packagePosition: typeof n.package_position === "number" ? n.package_position : void 0
|
|
34
37
|
}));
|
|
35
38
|
applyNodeCatalog(manifests);
|
|
36
39
|
loaded.value = true;
|
|
@@ -17,7 +17,9 @@ export declare function useIntegrationsWorkflows(): {
|
|
|
17
17
|
activeCount: import("vue").ComputedRef<any>;
|
|
18
18
|
load: (force?: boolean) => Promise<Workflow[]>;
|
|
19
19
|
list: () => Promise<Workflow[]>;
|
|
20
|
-
get: (id: number
|
|
20
|
+
get: (id: number, { strict }?: {
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
}) => Promise<Workflow | null>;
|
|
21
23
|
create: (input: {
|
|
22
24
|
name: string;
|
|
23
25
|
description?: string | null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useIntegrationsApi } from "./integrationsApi.js";
|
|
3
3
|
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assignTriggerHandles, pausedTriggers } from "../utils/workflowFile.js";
|
|
5
5
|
import { errorReason } from "../utils/apiError.js";
|
|
6
6
|
import { unwrapList, unwrapRecord } from "../utils/apiEnvelope.js";
|
|
7
7
|
import { useLoadGate } from "./useLoadGate.js";
|
|
@@ -135,12 +135,13 @@ export function useIntegrationsWorkflows() {
|
|
|
135
135
|
if (!loaded.value) await load();
|
|
136
136
|
return workflows.value;
|
|
137
137
|
}
|
|
138
|
-
async function get(id) {
|
|
138
|
+
async function get(id, { strict = false } = {}) {
|
|
139
139
|
try {
|
|
140
140
|
const wf = ingest(unwrapRecord(await api.get(`/workflows/${id}`)));
|
|
141
141
|
upsertCache(wf);
|
|
142
142
|
return wf;
|
|
143
|
-
} catch {
|
|
143
|
+
} catch (e) {
|
|
144
|
+
if (strict) throw e;
|
|
144
145
|
return workflows.value.find((w) => w.id === id) ?? null;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
@@ -158,25 +159,7 @@ export function useIntegrationsWorkflows() {
|
|
|
158
159
|
async function duplicate(id, name) {
|
|
159
160
|
const source = await get(id);
|
|
160
161
|
if (!source) throw new Error(t("integration.workflows.error.notFound"));
|
|
161
|
-
const blob =
|
|
162
|
-
const handleMap = /* @__PURE__ */ new Map();
|
|
163
|
-
for (const trigger of blob.triggers ?? []) {
|
|
164
|
-
const fresh = crypto.randomUUID();
|
|
165
|
-
handleMap.set(trigger.id, fresh);
|
|
166
|
-
trigger.id = fresh;
|
|
167
|
-
trigger.active = false;
|
|
168
|
-
}
|
|
169
|
-
for (const edge of blob.edges ?? []) {
|
|
170
|
-
const mapped = handleMap.get(edge.from.nodeId);
|
|
171
|
-
if (mapped) edge.from.nodeId = mapped;
|
|
172
|
-
}
|
|
173
|
-
if (blob.triggerPositions) {
|
|
174
|
-
const rekeyed = {};
|
|
175
|
-
for (const [handle, pos] of Object.entries(blob.triggerPositions)) {
|
|
176
|
-
rekeyed[handleMap.get(handle) ?? handle] = pos;
|
|
177
|
-
}
|
|
178
|
-
blob.triggerPositions = rekeyed;
|
|
179
|
-
}
|
|
162
|
+
const blob = pausedTriggers(assignTriggerHandles(source.blob, new Set((source.blob.triggers ?? []).map((tr) => tr.id))));
|
|
180
163
|
const base = {
|
|
181
164
|
description: source.description,
|
|
182
165
|
blob,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* The current time as a ref that moves every `intervalMs`, for the surfaces that
|
|
4
|
+
* print something measured against it — a running run's elapsed time, and how long
|
|
5
|
+
* ago a moment was (`relativeTime`).
|
|
6
|
+
*
|
|
7
|
+
* `Date.now()` read in a template is not reactive: it is re-read only when the
|
|
8
|
+
* component re-renders for some other reason. A realtime event re-renders the entry
|
|
9
|
+
* it changes and none of the others, so "12s ago" beside an untouched entry would
|
|
10
|
+
* stand still while the one beside it moved on. Passing this ref is what keeps them
|
|
11
|
+
* on one clock (PO-393).
|
|
12
|
+
*
|
|
13
|
+
* Three seconds is the rhythm the runs list already re-timed a running run at. The
|
|
14
|
+
* interval starts on mount and stops on unmount, so a surface that is not shown
|
|
15
|
+
* costs nothing.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useNow(intervalMs?: number): Ref<number>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { onMounted, onUnmounted, ref } from "vue";
|
|
2
|
+
export function useNow(intervalMs = 3e3) {
|
|
3
|
+
const now = ref(Date.now());
|
|
4
|
+
let tick = null;
|
|
5
|
+
onMounted(() => {
|
|
6
|
+
now.value = Date.now();
|
|
7
|
+
tick = setInterval(() => {
|
|
8
|
+
now.value = Date.now();
|
|
9
|
+
}, intervalMs);
|
|
10
|
+
});
|
|
11
|
+
onUnmounted(() => {
|
|
12
|
+
if (tick) clearInterval(tick);
|
|
13
|
+
tick = null;
|
|
14
|
+
});
|
|
15
|
+
return now;
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Workflow } from '../types/integrations.js';
|
|
2
|
+
import type { OverwritePlan, WorkflowFile } from '../utils/workflowFile.js';
|
|
3
|
+
/**
|
|
4
|
+
* Export and Import, wired to the tenant (specs/workflow-file.md).
|
|
5
|
+
*
|
|
6
|
+
* The rules are `utils/workflowFile`; this is what they need from the stores. Each
|
|
7
|
+
* of them is read fresh at the moment it matters rather than trusted from whatever
|
|
8
|
+
* a page happened to load: which settings are credentials comes from the node
|
|
9
|
+
* catalogue, which credentials are this tenant's from the credential list, and which
|
|
10
|
+
* trigger handles are taken from the workflows list — and a stale answer to any of
|
|
11
|
+
* the three is a credential in a file or a webhook address that silently changed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useWorkflowFile(): {
|
|
14
|
+
fileFor: (id: number) => Promise<{
|
|
15
|
+
workflow: Workflow;
|
|
16
|
+
file: WorkflowFile;
|
|
17
|
+
}>;
|
|
18
|
+
download: (file: WorkflowFile) => void;
|
|
19
|
+
importAsNew: (file: WorkflowFile, name: string) => Promise<Workflow>;
|
|
20
|
+
planOverwrite: (target: Workflow, file: WorkflowFile) => Promise<OverwritePlan>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { INTEGRATION_NODE_CATALOG, findManifest } from "../data/integrationNodeCatalog.js";
|
|
2
|
+
import { useIntegrationsCredentials } from "./useIntegrationsCredentials.js";
|
|
3
|
+
import { useIntegrationsNodes } from "./useIntegrationsNodes.js";
|
|
4
|
+
import { useIntegrationsT } from "./useIntegrationsT.js";
|
|
5
|
+
import { useIntegrationsWorkflows } from "./useIntegrationsWorkflows.js";
|
|
6
|
+
import { blobForNewWorkflow, overwritePlan, toWorkflowFile, workflowFileName } from "../utils/workflowFile.js";
|
|
7
|
+
export function useWorkflowFile() {
|
|
8
|
+
const { workflows, error: workflowsError, get, load: loadWorkflows, create } = useIntegrationsWorkflows();
|
|
9
|
+
const { credentials, error: credentialsError, load: loadCredentials } = useIntegrationsCredentials();
|
|
10
|
+
const { loaded: nodesLoaded, load: loadNodes } = useIntegrationsNodes();
|
|
11
|
+
const t = useIntegrationsT();
|
|
12
|
+
async function nodeCatalogue() {
|
|
13
|
+
await loadNodes();
|
|
14
|
+
if (!nodesLoaded.value) throw new Error(t("integration.workflowFile.error.catalogue"));
|
|
15
|
+
}
|
|
16
|
+
const fieldsFor = (node) => {
|
|
17
|
+
const exact = findManifest(node.nodeSlug, node.nodeVersion);
|
|
18
|
+
if (exact && exact.version === node.nodeVersion) return exact.config ?? [];
|
|
19
|
+
const all = INTEGRATION_NODE_CATALOG.filter((m) => m.slug === node.nodeSlug);
|
|
20
|
+
return all.length ? all.flatMap((m) => m.config ?? []) : null;
|
|
21
|
+
};
|
|
22
|
+
async function tenantHandles(except) {
|
|
23
|
+
await loadWorkflows(true);
|
|
24
|
+
if (workflowsError.value) throw new Error(workflowsError.value);
|
|
25
|
+
return new Set(workflows.value.filter((wf) => wf.id !== except).flatMap((wf) => (wf.blob.triggers ?? []).map((tr) => tr.id)));
|
|
26
|
+
}
|
|
27
|
+
async function tenantCredentialIds() {
|
|
28
|
+
await loadCredentials(true);
|
|
29
|
+
if (credentialsError.value) throw new Error(credentialsError.value);
|
|
30
|
+
return new Set(credentials.value.map((c) => c.id));
|
|
31
|
+
}
|
|
32
|
+
async function fileFor(id) {
|
|
33
|
+
const [workflow, ids] = await Promise.all([get(id, { strict: true }), tenantCredentialIds(), nodeCatalogue()]);
|
|
34
|
+
if (!workflow) throw new Error(t("integration.workflowFile.export.error"));
|
|
35
|
+
return { workflow, file: toWorkflowFile(workflow, fieldsFor, ids) };
|
|
36
|
+
}
|
|
37
|
+
function download(file) {
|
|
38
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify(file, null, 2)], { type: "application/json" }));
|
|
39
|
+
const a = document.createElement("a");
|
|
40
|
+
a.href = url;
|
|
41
|
+
a.download = workflowFileName(file.workflow.name);
|
|
42
|
+
document.body.append(a);
|
|
43
|
+
a.click();
|
|
44
|
+
a.remove();
|
|
45
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
46
|
+
}
|
|
47
|
+
async function importAsNew(file, name) {
|
|
48
|
+
const [taken, ids] = await Promise.all([tenantHandles(), tenantCredentialIds(), nodeCatalogue()]);
|
|
49
|
+
const blob = { ...blobForNewWorkflow(file, fieldsFor, taken, ids), name: name.trim() };
|
|
50
|
+
return await create({
|
|
51
|
+
name: name.trim(),
|
|
52
|
+
description: file.workflow.description,
|
|
53
|
+
blob,
|
|
54
|
+
active: false,
|
|
55
|
+
executionMode: file.workflow.executionMode
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function planOverwrite(target, file) {
|
|
59
|
+
const [taken, ids] = await Promise.all([tenantHandles(target.id), tenantCredentialIds(), nodeCatalogue()]);
|
|
60
|
+
return overwritePlan(target.blob, file, fieldsFor, taken, ids);
|
|
61
|
+
}
|
|
62
|
+
return { fileFor, download, importAsNew, planOverwrite };
|
|
63
|
+
}
|
|
@@ -8,8 +8,17 @@ export interface InlineNodeJSON {
|
|
|
8
8
|
text?: string;
|
|
9
9
|
attrs?: Record<string, unknown>;
|
|
10
10
|
content?: InlineNodeJSON[];
|
|
11
|
+
marks?: {
|
|
12
|
+
type: string;
|
|
13
|
+
}[];
|
|
11
14
|
}
|
|
12
15
|
export declare const EXPR_TOKEN_NODE = "exprToken";
|
|
16
|
+
/**
|
|
17
|
+
* A pill the operator has opened to edit: its raw text, marked so it is not
|
|
18
|
+
* turned back into a pill while the caret is in it. Without the mark, the
|
|
19
|
+
* closing `}}` it already carries would re-pill it on the first keystroke.
|
|
20
|
+
*/
|
|
21
|
+
export declare const EXPR_EDITING_MARK = "exprEditing";
|
|
13
22
|
/** Raw text → inline content: text nodes with one atom pill per token. */
|
|
14
23
|
export declare function textToInlineContent(text: string): InlineNodeJSON[];
|
|
15
24
|
/**
|
|
@@ -29,6 +38,7 @@ export declare function textToDoc(text: string, { multiline }?: LineMode): Inlin
|
|
|
29
38
|
* Multiple paragraphs are the lines of a multiline field; in a single-line
|
|
30
39
|
* field they can only have arrived by paste, and collapse to single spaces. */
|
|
31
40
|
export declare function docToText(doc: InlineNodeJSON | null | undefined, { multiline }?: LineMode): string;
|
|
32
|
-
/** True when any TEXT node still carries a complete raw token (needs re-pilling).
|
|
41
|
+
/** True when any TEXT node still carries a complete raw token (needs re-pilling).
|
|
42
|
+
* Text the operator is editing (`EXPR_EDITING_MARK`) is left alone. */
|
|
33
43
|
export declare function hasRawTokenText(doc: InlineNodeJSON | null | undefined): boolean;
|
|
34
44
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listExpressionTokens } from "./nodeRunData.js";
|
|
2
2
|
export const EXPR_TOKEN_NODE = "exprToken";
|
|
3
|
+
export const EXPR_EDITING_MARK = "exprEditing";
|
|
3
4
|
export function textToInlineContent(text) {
|
|
4
5
|
const out = [];
|
|
5
6
|
let cursor = 0;
|
|
@@ -39,7 +40,7 @@ export function hasRawTokenText(doc) {
|
|
|
39
40
|
const stack = [doc];
|
|
40
41
|
while (stack.length) {
|
|
41
42
|
const node = stack.pop();
|
|
42
|
-
if (node.type === "text" && listExpressionTokens(node.text ?? "").length > 0) return true;
|
|
43
|
+
if (node.type === "text" && !node.marks?.some((m) => m.type === EXPR_EDITING_MARK) && listExpressionTokens(node.text ?? "").length > 0) return true;
|
|
43
44
|
for (const child of node.content ?? []) stack.push(child);
|
|
44
45
|
}
|
|
45
46
|
return false;
|
|
@@ -1,2 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node manifest icon → Lucide component.
|
|
3
|
+
*
|
|
4
|
+
* Authors declare icons as `mdi:*` names (the node SDK's convention), but the
|
|
5
|
+
* studio renders Lucide. The naive PascalCase lookup almost never hits a
|
|
6
|
+
* Lucide export, so every node degraded to the generic Box. This curated map
|
|
7
|
+
* covers the registry's actual vocabulary; unmapped names still try the
|
|
8
|
+
* PascalCase lookup before falling back to Box.
|
|
9
|
+
*
|
|
10
|
+
* Exported for its test, which checks every entry against Lucide's exports: a
|
|
11
|
+
* test that found the entries by reading this file's text stopped seeing every
|
|
12
|
+
* unquoted key the day the linter unquoted them.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MDI_TO_LUCIDE: Record<string, string>;
|
|
1
15
|
/** Resolve a manifest icon name to a Lucide component (Box as last resort). */
|
|
2
16
|
export declare function nodeIcon(name?: string | null): unknown;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import * as Lucide from "lucide-vue-next";
|
|
2
|
-
const MDI_TO_LUCIDE = {
|
|
2
|
+
export const MDI_TO_LUCIDE = {
|
|
3
|
+
"account-arrow-down-outline": "Import",
|
|
3
4
|
"account-group-outline": "Users",
|
|
5
|
+
"account-multiple-check-outline": "UserCheck",
|
|
4
6
|
"account-outline": "User",
|
|
5
7
|
"account-plus-outline": "UserPlus",
|
|
6
8
|
"alert-octagon": "OctagonAlert",
|
|
7
|
-
|
|
9
|
+
api: "Webhook",
|
|
8
10
|
"book-open-outline": "BookOpen",
|
|
11
|
+
"bookmark-outline": "Bookmark",
|
|
9
12
|
"bullseye-arrow": "Target",
|
|
10
13
|
"calendar-check-outline": "CalendarCheck",
|
|
11
14
|
"card-account-details-outline": "IdCard",
|
|
12
15
|
"cart-outline": "ShoppingCart",
|
|
13
16
|
"cart-plus": "ShoppingBasket",
|
|
17
|
+
"chart-timeline-variant": "ChartLine",
|
|
18
|
+
"checkbox-marked-circle-plus-outline": "ListTodo",
|
|
14
19
|
"circle-outline": "Circle",
|
|
15
20
|
"clock-outline": "Clock",
|
|
16
21
|
"cloud-download": "CloudDownload",
|
|
@@ -20,20 +25,30 @@ const MDI_TO_LUCIDE = {
|
|
|
20
25
|
"code-json": "Braces",
|
|
21
26
|
"code-tags": "Code",
|
|
22
27
|
"compare-horizontal": "ArrowLeftRight",
|
|
28
|
+
"content-duplicate": "Copy",
|
|
23
29
|
"currency-eur": "Euro",
|
|
24
30
|
"database-cog-outline": "Database",
|
|
25
31
|
"database-export-outline": "HardDriveUpload",
|
|
26
32
|
"database-import-outline": "HardDriveDownload",
|
|
27
|
-
"
|
|
33
|
+
"delete-clock-outline": "Trash2",
|
|
34
|
+
"delete-outline": "Trash2",
|
|
35
|
+
discord: "MessagesSquare",
|
|
36
|
+
domain: "Building2",
|
|
37
|
+
"email-open-outline": "MailOpen",
|
|
38
|
+
"email-remove-outline": "MailX",
|
|
39
|
+
"email-search-outline": "MailSearch",
|
|
28
40
|
"email-send": "Send",
|
|
41
|
+
"file-document-arrow-right": "FileOutput",
|
|
42
|
+
"file-document-outline": "FileText",
|
|
29
43
|
"file-edit-outline": "FilePen",
|
|
30
44
|
"file-export-outline": "FileOutput",
|
|
31
45
|
"file-import-outline": "FileInput",
|
|
32
46
|
"file-move-outline": "FileSymlink",
|
|
33
47
|
"file-plus": "FilePlus",
|
|
48
|
+
"file-plus-outline": "FilePlus",
|
|
34
49
|
"file-question-outline": "FileQuestion",
|
|
35
50
|
"file-remove-outline": "FileX",
|
|
36
|
-
|
|
51
|
+
filter: "Filter",
|
|
37
52
|
"find-replace": "Replace",
|
|
38
53
|
"folder-download-outline": "FolderDown",
|
|
39
54
|
"folder-plus-outline": "FolderPlus",
|
|
@@ -41,25 +56,53 @@ const MDI_TO_LUCIDE = {
|
|
|
41
56
|
"folder-upload-outline": "FolderUp",
|
|
42
57
|
"folder-zip-outline": "FolderArchive",
|
|
43
58
|
"format-list-bulleted-type": "List",
|
|
44
|
-
"
|
|
59
|
+
"format-list-checks": "ListChecks",
|
|
60
|
+
"hand-wave-outline": "Hand",
|
|
61
|
+
handshake: "Handshake",
|
|
45
62
|
"handshake-outline": "Handshake",
|
|
63
|
+
"inbox-arrow-down": "Inbox",
|
|
64
|
+
"inbox-full": "Archive",
|
|
65
|
+
"language-html5": "CodeXml",
|
|
66
|
+
"lightbulb-group-outline": "Lamp",
|
|
67
|
+
"lightbulb-on-outline": "SunMedium",
|
|
68
|
+
"lightbulb-outline": "Lightbulb",
|
|
69
|
+
"lightning-bolt-circle": "Zap",
|
|
46
70
|
"link-lock": "Link2",
|
|
47
|
-
"
|
|
71
|
+
"link-variant": "Link",
|
|
72
|
+
"lock-check": "ShieldCheck",
|
|
73
|
+
magnify: "Search",
|
|
74
|
+
"map-marker-path": "MapPin",
|
|
75
|
+
"map-marker-radius-outline": "MapPinned",
|
|
76
|
+
"message-processing": "MessageSquareMore",
|
|
77
|
+
"microsoft-teams": "Users",
|
|
78
|
+
"note-multiple-outline": "StickyNote",
|
|
79
|
+
"note-text-outline": "NotebookPen",
|
|
48
80
|
"office-building-outline": "Building",
|
|
81
|
+
"package-variant-closed-plus": "PackagePlus",
|
|
82
|
+
"package-variant-remove": "PackageX",
|
|
49
83
|
"pencil-box-outline": "SquarePen",
|
|
50
|
-
|
|
84
|
+
pipe: "Workflow",
|
|
85
|
+
"progress-clock": "Hourglass",
|
|
51
86
|
"receipt-outline": "Receipt",
|
|
52
87
|
"receipt-text-outline": "ReceiptText",
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
regex: "Regex",
|
|
89
|
+
repeat: "Repeat",
|
|
90
|
+
ruler: "Ruler",
|
|
91
|
+
"send-outline": "Send",
|
|
56
92
|
"source-branch": "GitBranch",
|
|
57
93
|
"tag-multiple-outline": "Tags",
|
|
58
|
-
"
|
|
59
|
-
|
|
94
|
+
"tag-plus-outline": "Tag",
|
|
95
|
+
telegram: "Send",
|
|
96
|
+
"timeline-text-outline": "Activity",
|
|
97
|
+
transfer: "ArrowRightLeft",
|
|
98
|
+
translate: "Languages",
|
|
99
|
+
"tray-arrow-up": "Upload",
|
|
100
|
+
"truck-check-outline": "PackageCheck",
|
|
101
|
+
"truck-delivery": "Truck",
|
|
60
102
|
"truck-outline": "Truck",
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
web: "Globe",
|
|
104
|
+
whatsapp: "MessageCircle",
|
|
105
|
+
xml: "CodeXml"
|
|
63
106
|
};
|
|
64
107
|
export function nodeIcon(name) {
|
|
65
108
|
if (!name) return Lucide.Box;
|