@revenexx/studio-integrations 0.1.4 → 0.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/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/CredentialCreateDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +279 -0
- package/dist/runtime/components/Integrations/CredentialCreateDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.d.vue.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +342 -0
- package/dist/runtime/components/Integrations/CredentialDetailDialog.vue.d.ts +20 -0
- package/dist/runtime/components/Integrations/CredentialLogo.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue +37 -0
- package/dist/runtime/components/Integrations/CredentialLogo.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +364 -0
- package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.d.vue.ts +15 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue +71 -0
- package/dist/runtime/components/Integrations/CredentialTypePicker.vue.d.ts +15 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.d.vue.ts +5 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue +151 -0
- package/dist/runtime/components/Integrations/DeadLettersPanel.vue.d.ts +5 -0
- package/dist/runtime/components/Integrations/EditorTip.d.vue.ts +17 -0
- package/dist/runtime/components/Integrations/EditorTip.vue +46 -0
- package/dist/runtime/components/Integrations/EditorTip.vue.d.ts +17 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue +64 -0
- package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +19 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue +236 -0
- package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +19 -0
- package/dist/runtime/components/Integrations/GroupFrameView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue +222 -0
- package/dist/runtime/components/Integrations/GroupFrameView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +184 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +102 -0
- package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +16 -0
- package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +23 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +587 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +23 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +80 -0
- package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +22 -3
- package/dist/runtime/components/Integrations/NodeInspector.vue +1074 -294
- package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +22 -3
- package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue +38 -0
- package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +22 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +411 -90
- package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +22 -1
- package/dist/runtime/components/Integrations/QueryBuilder.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue +239 -0
- package/dist/runtime/components/Integrations/QueryBuilder.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.d.vue.ts → RunActionsMenu.d.vue.ts} +3 -4
- package/dist/runtime/components/Integrations/RunActionsMenu.vue +86 -0
- package/dist/runtime/components/Integrations/{RunDetailDialog.vue.d.ts → RunActionsMenu.vue.d.ts} +3 -4
- package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +777 -0
- package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue +175 -0
- package/dist/runtime/components/Integrations/RunPayloadInput.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +27 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue +84 -0
- package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +27 -0
- package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +21 -0
- package/dist/runtime/components/Integrations/RunsTable.vue +114 -0
- package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +21 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.d.vue.ts +11 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +91 -0
- package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue.d.ts +11 -0
- package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +28 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue +208 -0
- package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +28 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +29 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +201 -0
- package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +29 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +138 -0
- package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +32 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.d.vue.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +322 -0
- package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue.d.ts +9 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.d.vue.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue.d.ts +18 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +68 -6
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +12 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +1424 -144
- package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +417 -0
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +89 -0
- package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.d.vue.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue +35 -0
- package/dist/runtime/components/Integrations/WorkflowIconView.vue.d.ts +10 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +114 -84
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +5 -1
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +654 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +12 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +136 -0
- package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +36 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +63 -0
- package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +35 -15
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.d.vue.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +265 -0
- package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue.d.ts +13 -0
- package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +4 -1
- package/dist/runtime/components/Integrations/WorkflowsTable.vue +122 -39
- package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +4 -1
- package/dist/runtime/components/Integrations/workflowLayout.d.ts +5 -0
- package/dist/runtime/components/Integrations/workflowLayout.js +5 -3
- package/dist/runtime/composables/integrationsApi.d.ts +6 -2
- package/dist/runtime/composables/integrationsApi.js +5 -4
- package/dist/runtime/composables/useCanvasShortcuts.d.ts +37 -0
- package/dist/runtime/composables/useCanvasShortcuts.js +62 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +22 -0
- package/dist/runtime/composables/useIntegrationsDeadLetters.js +50 -0
- package/dist/runtime/composables/useIntegrationsNodes.d.ts +4 -0
- package/dist/runtime/composables/useIntegrationsNodes.js +6 -3
- package/dist/runtime/composables/useIntegrationsRevisions.d.ts +40 -0
- package/dist/runtime/composables/useIntegrationsRevisions.js +51 -0
- package/dist/runtime/composables/useIntegrationsRuns.d.ts +21 -1
- package/dist/runtime/composables/useIntegrationsRuns.js +92 -4
- package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsTemplates.js +32 -4
- package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +1 -0
- package/dist/runtime/composables/useIntegrationsWorkflows.js +57 -1
- package/dist/runtime/composables/useMiniMapLayout.d.ts +34 -0
- package/dist/runtime/composables/useMiniMapLayout.js +119 -0
- package/dist/runtime/composables/useNodeConfigResolve.d.ts +43 -0
- package/dist/runtime/composables/useNodeConfigResolve.js +123 -0
- package/dist/runtime/composables/useNodeDrag.d.ts +22 -0
- package/dist/runtime/composables/useNodeDrag.js +10 -0
- package/dist/runtime/composables/usePaletteLayout.d.ts +41 -0
- package/dist/runtime/composables/usePaletteLayout.js +113 -0
- package/dist/runtime/data/expressionEditor.d.ts +22 -0
- package/dist/runtime/data/expressionEditor.js +40 -0
- package/dist/runtime/data/integrationNodeCatalog.js +8 -8
- package/dist/runtime/data/nodeInputData.d.ts +37 -0
- package/dist/runtime/data/nodeInputData.js +62 -0
- package/dist/runtime/data/nodeRunData.d.ts +56 -0
- package/dist/runtime/data/nodeRunData.js +91 -0
- package/dist/runtime/data/payloadForm.d.ts +44 -0
- package/dist/runtime/data/payloadForm.js +126 -0
- package/dist/runtime/data/workflowIconCatalog.d.ts +34 -0
- package/dist/runtime/data/workflowIconCatalog.js +116 -0
- package/dist/runtime/pages/integrations/audit.vue +25 -35
- package/dist/runtime/pages/integrations/credentials.vue +117 -122
- package/dist/runtime/pages/integrations/index.vue +133 -36
- package/dist/runtime/pages/integrations/runs.vue +125 -75
- package/dist/runtime/pages/integrations/schedules.vue +5 -16
- package/dist/runtime/pages/integrations/secrets.vue +1 -3
- package/dist/runtime/pages/integrations/workflows/[id].vue +599 -194
- package/dist/runtime/pages/integrations/workflows/index.vue +10 -8
- package/dist/runtime/types/integrations.d.ts +190 -4
- package/dist/runtime/utils/credentialState.d.ts +28 -0
- package/dist/runtime/utils/credentialState.js +29 -0
- package/dist/runtime/utils/localize.d.ts +3 -1
- package/dist/runtime/utils/localize.js +1 -2
- package/dist/runtime/utils/statusStyle.d.ts +6 -0
- package/dist/runtime/utils/statusStyle.js +15 -0
- package/package.json +11 -1
- package/dist/runtime/components/Integrations/RunDetailDialog.vue +0 -169
- package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +0 -2
- package/dist/runtime/pages/integrations/workflows/new.vue +0 -153
- package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, markRaw, nextTick, ref, watch } from "vue";
|
|
2
|
+
import { computed, markRaw, nextTick, provide, ref, watch } from "vue";
|
|
3
3
|
import {
|
|
4
4
|
ConnectionLineType,
|
|
5
5
|
MarkerType,
|
|
@@ -9,24 +9,50 @@ import {
|
|
|
9
9
|
useVueFlow
|
|
10
10
|
} from "@vue-flow/core";
|
|
11
11
|
import { Background } from "@vue-flow/background";
|
|
12
|
-
import { Controls } from "@vue-flow/controls";
|
|
13
12
|
import { MiniMap } from "@vue-flow/minimap";
|
|
14
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Widget as LayoutGrid,
|
|
15
|
+
MagicStick as Sparkles,
|
|
16
|
+
Play,
|
|
17
|
+
MinusCircle as ZoomOut,
|
|
18
|
+
AddCircle as ZoomIn,
|
|
19
|
+
RefreshCircle as Loader2
|
|
20
|
+
} from "#solar-icons";
|
|
21
|
+
import {
|
|
22
|
+
Dialog,
|
|
23
|
+
DialogContent,
|
|
24
|
+
DropdownMenu,
|
|
25
|
+
DropdownMenuContent,
|
|
26
|
+
DropdownMenuItem,
|
|
27
|
+
DropdownMenuSeparator,
|
|
28
|
+
DropdownMenuTrigger,
|
|
29
|
+
TooltipProvider
|
|
30
|
+
} from "@revenexx/studio";
|
|
31
|
+
import { Braces, EllipsisVertical, GripVertical, Group as GroupIcon, Hand, Map as MapIcon, Minus, MousePointer2, Plus, Settings2, StickyNote } from "lucide-vue-next";
|
|
15
32
|
import "@vue-flow/core/dist/style.css";
|
|
16
33
|
import "@vue-flow/core/dist/theme-default.css";
|
|
17
|
-
import "@vue-flow/controls/dist/style.css";
|
|
18
34
|
import "@vue-flow/minimap/dist/style.css";
|
|
19
35
|
import NodePalette from "./NodePalette.vue";
|
|
20
36
|
import NodeInspector from "./NodeInspector.vue";
|
|
37
|
+
import NodeDynamicOutputsResolver from "./NodeDynamicOutputsResolver.vue";
|
|
21
38
|
import WorkflowNodeView from "./WorkflowNodeView.vue";
|
|
22
39
|
import WorkflowTriggerView from "./WorkflowTriggerView.vue";
|
|
40
|
+
import StickyNoteView from "./StickyNoteView.vue";
|
|
41
|
+
import GroupFrameView from "./GroupFrameView.vue";
|
|
42
|
+
import WorkflowContextMenu, {} from "./WorkflowContextMenu.vue";
|
|
43
|
+
import WorkflowVariablesDialog from "./WorkflowVariablesDialog.vue";
|
|
44
|
+
import { useCanvasShortcuts } from "../../composables/useCanvasShortcuts";
|
|
45
|
+
import { usePaletteLayout } from "../../composables/usePaletteLayout";
|
|
46
|
+
import { useMiniMapLayout } from "../../composables/useMiniMapLayout";
|
|
47
|
+
import { toast } from "vue-sonner";
|
|
23
48
|
import WorkflowEdgeView from "./WorkflowEdgeView.vue";
|
|
24
49
|
import { layoutWorkflowNodes } from "./workflowLayout";
|
|
25
50
|
import { TRIGGER_TEMPLATES, findManifest } from "../../data/integrationNodeCatalog";
|
|
26
51
|
const props = defineProps({
|
|
27
|
-
workflow: { type: Object, required: true }
|
|
52
|
+
workflow: { type: Object, required: true },
|
|
53
|
+
runBusy: { type: Boolean, required: false }
|
|
28
54
|
});
|
|
29
|
-
const emit = defineEmits(["change"]);
|
|
55
|
+
const emit = defineEmits(["change", "run", "toggleActive", "openSettings"]);
|
|
30
56
|
const { keys: secretKeys } = useIntegrationsSecrets();
|
|
31
57
|
const blob = ref(clone(props.workflow.blob));
|
|
32
58
|
watch(() => props.workflow.id, () => {
|
|
@@ -38,6 +64,12 @@ function clone(v) {
|
|
|
38
64
|
}
|
|
39
65
|
const vfNodes = ref([]);
|
|
40
66
|
const vfEdges = ref([]);
|
|
67
|
+
const flow = useVueFlow();
|
|
68
|
+
const dynamicOutputs = ref({});
|
|
69
|
+
const dynamicOutputNodes = computed(() => blob.value.nodes.filter((n) => {
|
|
70
|
+
const m = findManifest(n.nodeSlug, n.nodeVersion);
|
|
71
|
+
return (m?.outputs ?? []).some((o) => o.resolveOutputs === true);
|
|
72
|
+
}));
|
|
41
73
|
function edgeKindFromPort(sourceNodeId, output) {
|
|
42
74
|
const wn = blob.value.nodes.find((n) => n.id === sourceNodeId);
|
|
43
75
|
if (!wn) return "default";
|
|
@@ -65,40 +97,195 @@ function stripSide(handleId) {
|
|
|
65
97
|
const i = handleId.indexOf("__");
|
|
66
98
|
return i >= 0 ? handleId.slice(0, i) : handleId;
|
|
67
99
|
}
|
|
100
|
+
function triggerPosition(id, index) {
|
|
101
|
+
const saved = blob.value.triggerPositions?.[id];
|
|
102
|
+
if (saved) return { x: saved.x, y: saved.y };
|
|
103
|
+
return { x: -40, y: 60 + index * 140 };
|
|
104
|
+
}
|
|
68
105
|
function nodePosition(id) {
|
|
69
106
|
const wn = blob.value.nodes.find((n) => n.id === id);
|
|
70
107
|
if (wn) return { x: wn.position?.x ?? 0, y: wn.position?.y ?? 0 };
|
|
71
108
|
const ti = blob.value.triggers.findIndex((t) => t.id === id);
|
|
72
|
-
if (ti >= 0) return
|
|
109
|
+
if (ti >= 0) return triggerPosition(id, ti);
|
|
73
110
|
return { x: 0, y: 0 };
|
|
74
111
|
}
|
|
75
|
-
function pickSourceSide(
|
|
76
|
-
|
|
77
|
-
const b = nodePosition(targetId);
|
|
78
|
-
const dx = b.x - a.x;
|
|
79
|
-
const dy = b.y - a.y;
|
|
80
|
-
if (Math.abs(dx) * 1.1 > Math.abs(dy)) return dx >= 0 ? "right" : "left";
|
|
81
|
-
return dy >= 0 ? "bottom" : "top";
|
|
112
|
+
function pickSourceSide(_sourceId, _targetId) {
|
|
113
|
+
return "bottom";
|
|
82
114
|
}
|
|
83
115
|
function oppositeSide(s) {
|
|
84
116
|
return s === "top" ? "bottom" : s === "right" ? "left" : s === "bottom" ? "top" : "right";
|
|
85
117
|
}
|
|
118
|
+
function stickyArea(s) {
|
|
119
|
+
return (s.size?.width ?? 240) * (s.size?.height ?? 160);
|
|
120
|
+
}
|
|
121
|
+
const GROUP_PAD = 24;
|
|
122
|
+
const GROUP_LABEL_ROOM = 26;
|
|
123
|
+
function locText(v) {
|
|
124
|
+
if (!v) return "";
|
|
125
|
+
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
126
|
+
}
|
|
127
|
+
function memberDims(id) {
|
|
128
|
+
const d = flow.findNode(id)?.dimensions;
|
|
129
|
+
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
130
|
+
}
|
|
131
|
+
function groupRect(g) {
|
|
132
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
133
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
134
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
135
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
136
|
+
for (const id of g.nodeIds) {
|
|
137
|
+
const pos = nodePosition(id);
|
|
138
|
+
const dims = memberDims(id);
|
|
139
|
+
minX = Math.min(minX, pos.x);
|
|
140
|
+
minY = Math.min(minY, pos.y);
|
|
141
|
+
maxX = Math.max(maxX, pos.x + dims.width);
|
|
142
|
+
maxY = Math.max(maxY, pos.y + dims.height);
|
|
143
|
+
}
|
|
144
|
+
if (!Number.isFinite(minX)) return g.rect ?? { x: 120, y: 120, width: 220, height: 120 };
|
|
145
|
+
const derived = {
|
|
146
|
+
x: Math.round(minX - GROUP_PAD),
|
|
147
|
+
y: Math.round(minY - GROUP_PAD - GROUP_LABEL_ROOM),
|
|
148
|
+
width: Math.round(maxX - minX + GROUP_PAD * 2),
|
|
149
|
+
height: Math.round(maxY - minY + GROUP_PAD * 2 + GROUP_LABEL_ROOM)
|
|
150
|
+
};
|
|
151
|
+
const r = g.rect;
|
|
152
|
+
if (!r) return derived;
|
|
153
|
+
const x = Math.min(r.x, derived.x);
|
|
154
|
+
const y = Math.min(r.y, derived.y);
|
|
155
|
+
return {
|
|
156
|
+
x,
|
|
157
|
+
y,
|
|
158
|
+
width: Math.max(r.x + r.width, derived.x + derived.width) - x,
|
|
159
|
+
height: Math.max(r.y + r.height, derived.y + derived.height) - y
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function collapsedGroupOf() {
|
|
163
|
+
const m = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const g of blob.value.groups ?? []) {
|
|
165
|
+
if (g.collapsed) for (const id of g.nodeIds) m.set(id, g);
|
|
166
|
+
}
|
|
167
|
+
return m;
|
|
168
|
+
}
|
|
169
|
+
function patchGroup(id, patch) {
|
|
170
|
+
const g = blob.value.groups?.find((x) => x.id === id);
|
|
171
|
+
if (!g) return;
|
|
172
|
+
Object.assign(g, patch);
|
|
173
|
+
syncFromBlob();
|
|
174
|
+
push();
|
|
175
|
+
}
|
|
176
|
+
function ungroup(id) {
|
|
177
|
+
blob.value.groups = (blob.value.groups ?? []).filter((g) => g.id !== id);
|
|
178
|
+
syncFromBlob();
|
|
179
|
+
push();
|
|
180
|
+
}
|
|
181
|
+
function createGroupFromSelection(ids) {
|
|
182
|
+
const valid = ids.filter((id2) => blob.value.nodes.some((n) => n.id === id2) || blob.value.triggers.some((t) => t.id === id2));
|
|
183
|
+
if (valid.length < 1) return;
|
|
184
|
+
for (const g of blob.value.groups ?? []) {
|
|
185
|
+
g.nodeIds = g.nodeIds.filter((x) => !valid.includes(x));
|
|
186
|
+
}
|
|
187
|
+
blob.value.groups = (blob.value.groups ?? []).filter((g) => g.nodeIds.length > 0 || g.rect);
|
|
188
|
+
const id = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
189
|
+
blob.value.groups = [...blob.value.groups ?? [], {
|
|
190
|
+
id,
|
|
191
|
+
label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
|
|
192
|
+
nodeIds: valid
|
|
193
|
+
}];
|
|
194
|
+
syncFromBlob();
|
|
195
|
+
push();
|
|
196
|
+
}
|
|
197
|
+
const groupDragOrigin = ref(null);
|
|
198
|
+
const bornIds = ref(/* @__PURE__ */ new Set());
|
|
199
|
+
function markBorn(...ids) {
|
|
200
|
+
for (const id of ids) bornIds.value.add(id);
|
|
201
|
+
bornIds.value = new Set(bornIds.value);
|
|
202
|
+
window.setTimeout(() => {
|
|
203
|
+
for (const id of ids) bornIds.value.delete(id);
|
|
204
|
+
bornIds.value = new Set(bornIds.value);
|
|
205
|
+
syncFromBlob();
|
|
206
|
+
}, 450);
|
|
207
|
+
}
|
|
208
|
+
function bornClass(id) {
|
|
209
|
+
return bornIds.value.has(id) ? "node-pop-in" : void 0;
|
|
210
|
+
}
|
|
86
211
|
function syncFromBlob() {
|
|
212
|
+
const stickies = [...blob.value.stickies ?? []].sort((a, b) => stickyArea(b) - stickyArea(a));
|
|
213
|
+
const collapsedBy = collapsedGroupOf();
|
|
214
|
+
const groupNodes = (blob.value.groups ?? []).map((g, i) => {
|
|
215
|
+
const rect = groupRect(g);
|
|
216
|
+
return {
|
|
217
|
+
id: g.id,
|
|
218
|
+
type: "group",
|
|
219
|
+
position: { x: rect.x, y: rect.y },
|
|
220
|
+
zIndex: g.collapsed ? 0 : -50 + i,
|
|
221
|
+
class: bornClass(g.id),
|
|
222
|
+
data: {
|
|
223
|
+
group: g,
|
|
224
|
+
rect: { width: rect.width, height: rect.height },
|
|
225
|
+
memberCount: g.nodeIds.length,
|
|
226
|
+
actions: {
|
|
227
|
+
rename: (label) => patchGroup(g.id, { label }),
|
|
228
|
+
setColor: (c) => patchGroup(g.id, { color: c }),
|
|
229
|
+
toggleCollapse: () => patchGroup(g.id, { collapsed: !g.collapsed }),
|
|
230
|
+
ungroup: () => ungroup(g.id),
|
|
231
|
+
resize: (size) => patchGroup(g.id, { rect: { x: rect.x, y: rect.y, width: size.width, height: size.height } })
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
});
|
|
87
236
|
vfNodes.value = [
|
|
88
|
-
...
|
|
237
|
+
...groupNodes,
|
|
238
|
+
// Stickies render BEHIND workflow/trigger nodes (negative zIndex); larger
|
|
239
|
+
// notes sit lower so small ones stacked on top stay reachable.
|
|
240
|
+
...stickies.map((s, i) => ({
|
|
241
|
+
id: s.id,
|
|
242
|
+
type: "sticky",
|
|
243
|
+
position: { x: s.position.x, y: s.position.y },
|
|
244
|
+
zIndex: -100 + i,
|
|
245
|
+
class: bornClass(s.id),
|
|
246
|
+
data: {
|
|
247
|
+
sticky: s,
|
|
248
|
+
onUpdate: (patch) => updateSticky(s.id, patch),
|
|
249
|
+
onRemove: () => removeBlobNode(s.id)
|
|
250
|
+
}
|
|
251
|
+
})),
|
|
252
|
+
...blob.value.triggers.filter((t) => !collapsedBy.has(t.id)).map((t, i) => ({
|
|
89
253
|
id: t.id,
|
|
90
254
|
type: "trigger",
|
|
91
|
-
position:
|
|
255
|
+
position: triggerPosition(t.id, i),
|
|
256
|
+
class: bornClass(t.id),
|
|
92
257
|
data: t
|
|
93
258
|
})),
|
|
94
|
-
...blob.value.nodes.map((n) => ({
|
|
259
|
+
...blob.value.nodes.filter((n) => !collapsedBy.has(n.id)).map((n) => ({
|
|
95
260
|
id: n.id,
|
|
96
261
|
type: "workflow",
|
|
97
262
|
position: n.position ?? { x: 240, y: 80 },
|
|
98
|
-
|
|
263
|
+
class: bornClass(n.id),
|
|
264
|
+
data: { ...n, __resolvedOutputs: dynamicOutputs.value[n.id] }
|
|
99
265
|
}))
|
|
100
266
|
];
|
|
101
|
-
|
|
267
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
268
|
+
const mapped = [];
|
|
269
|
+
for (const e of blob.value.edges) {
|
|
270
|
+
const srcGroup = collapsedBy.get(e.from.nodeId);
|
|
271
|
+
const tgtGroup = collapsedBy.get(e.to.nodeId);
|
|
272
|
+
if (srcGroup && tgtGroup && srcGroup.id === tgtGroup.id) continue;
|
|
273
|
+
const source = srcGroup?.id ?? e.from.nodeId;
|
|
274
|
+
const target = tgtGroup?.id ?? e.to.nodeId;
|
|
275
|
+
const key = `${source}|${srcGroup ? "default" : e.from.output}|${target}`;
|
|
276
|
+
if (seenEdge.has(key)) continue;
|
|
277
|
+
seenEdge.add(key);
|
|
278
|
+
const vf = makeVfEdge(e.id, e.from.nodeId, e.to.nodeId, e.from.output);
|
|
279
|
+
mapped.push({
|
|
280
|
+
...vf,
|
|
281
|
+
source,
|
|
282
|
+
target,
|
|
283
|
+
sourceHandle: srcGroup ? "default__bottom" : vf.sourceHandle,
|
|
284
|
+
targetHandle: tgtGroup ? "__in__top" : vf.targetHandle
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
vfEdges.value = mapped;
|
|
288
|
+
void nextTick(() => flow.updateNodeInternals(vfNodes.value.map((n) => n.id)));
|
|
102
289
|
}
|
|
103
290
|
function makeVfEdge(id, sourceId, targetId, output) {
|
|
104
291
|
const kind = edgeKindFromPort(sourceId, output);
|
|
@@ -143,17 +330,68 @@ function edgeColorByKind(kind) {
|
|
|
143
330
|
}
|
|
144
331
|
}
|
|
145
332
|
syncFromBlob();
|
|
146
|
-
|
|
147
|
-
flow.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
333
|
+
flow.onPaneReady(() => flow.fitView({ padding: 0.2, maxZoom: 1 }));
|
|
334
|
+
flow.onNodeDragStart(({ node }) => {
|
|
335
|
+
if (node.type !== "group") return;
|
|
336
|
+
const g = blob.value.groups?.find((x) => x.id === node.id);
|
|
337
|
+
if (!g) return;
|
|
338
|
+
const members = {};
|
|
339
|
+
for (const id of g.nodeIds) members[id] = nodePosition(id);
|
|
340
|
+
groupDragOrigin.value = { groupId: node.id, frame: { ...node.position }, members };
|
|
341
|
+
});
|
|
342
|
+
flow.onNodeDrag(({ node }) => {
|
|
343
|
+
const origin = groupDragOrigin.value;
|
|
344
|
+
if (!origin || node.type !== "group" || node.id !== origin.groupId) return;
|
|
345
|
+
const dx = node.position.x - origin.frame.x;
|
|
346
|
+
const dy = node.position.y - origin.frame.y;
|
|
347
|
+
for (const [id, pos] of Object.entries(origin.members)) {
|
|
348
|
+
flow.updateNode(id, { position: { x: pos.x + dx, y: pos.y + dy } });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
function persistDraggedNodes(evt) {
|
|
352
|
+
const moved = evt.nodes?.length ? evt.nodes : [evt.node];
|
|
353
|
+
for (const n of moved) {
|
|
354
|
+
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
355
|
+
if (n.type === "group") {
|
|
356
|
+
const origin = groupDragOrigin.value;
|
|
357
|
+
if (origin && origin.groupId === n.id) {
|
|
358
|
+
const dx = Math.round(n.position.x - origin.frame.x);
|
|
359
|
+
const dy = Math.round(n.position.y - origin.frame.y);
|
|
360
|
+
for (const [id, memberPos] of Object.entries(origin.members)) {
|
|
361
|
+
const target = { x: Math.round(memberPos.x + dx), y: Math.round(memberPos.y + dy) };
|
|
362
|
+
if (blob.value.triggers.some((t) => t.id === id)) {
|
|
363
|
+
blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [id]: target };
|
|
364
|
+
} else {
|
|
365
|
+
const wn = blob.value.nodes.find((b) => b.id === id);
|
|
366
|
+
if (wn) wn.position = target;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const g = blob.value.groups?.find((x) => x.id === n.id);
|
|
370
|
+
if (g?.rect) g.rect = { ...g.rect, x: Math.round(g.rect.x + dx), y: Math.round(g.rect.y + dy) };
|
|
371
|
+
groupDragOrigin.value = null;
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (blob.value.triggers.some((t) => t.id === n.id)) {
|
|
376
|
+
blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [n.id]: pos };
|
|
377
|
+
} else if (blob.value.stickies?.some((s) => s.id === n.id)) {
|
|
378
|
+
const st = blob.value.stickies.find((s) => s.id === n.id);
|
|
379
|
+
st.position = pos;
|
|
380
|
+
continue;
|
|
381
|
+
} else {
|
|
382
|
+
const wn = blob.value.nodes.find((b) => b.id === n.id);
|
|
383
|
+
if (wn) wn.position = pos;
|
|
384
|
+
}
|
|
385
|
+
const dims = memberDims(n.id);
|
|
386
|
+
adoptIntoGroupAt(n.id, { x: pos.x + dims.width / 2, y: pos.y + dims.height / 2 });
|
|
387
|
+
}
|
|
153
388
|
syncFromBlob();
|
|
154
389
|
push();
|
|
155
|
-
}
|
|
390
|
+
}
|
|
391
|
+
flow.onNodeDragStop(persistDraggedNodes);
|
|
392
|
+
flow.onSelectionDragStop(persistDraggedNodes);
|
|
156
393
|
flow.onConnect((conn) => {
|
|
394
|
+
connectCompleted = true;
|
|
157
395
|
if (!conn.source || !conn.target) return;
|
|
158
396
|
const id = `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
159
397
|
const output = stripSide(conn.sourceHandle) ?? "default";
|
|
@@ -165,6 +403,36 @@ flow.onConnect((conn) => {
|
|
|
165
403
|
syncFromBlob();
|
|
166
404
|
push();
|
|
167
405
|
});
|
|
406
|
+
const pendingConnect = ref(null);
|
|
407
|
+
let connectStart = null;
|
|
408
|
+
let connectCompleted = false;
|
|
409
|
+
flow.onConnectStart(({ nodeId, handleId, handleType }) => {
|
|
410
|
+
connectCompleted = false;
|
|
411
|
+
connectStart = handleType === "source" && nodeId ? { sourceId: nodeId, output: stripSide(handleId) ?? "default" } : null;
|
|
412
|
+
});
|
|
413
|
+
flow.onConnectEnd((event) => {
|
|
414
|
+
const start = connectStart;
|
|
415
|
+
connectStart = null;
|
|
416
|
+
if (!start || connectCompleted) return;
|
|
417
|
+
if (!(event instanceof MouseEvent)) return;
|
|
418
|
+
const target = event.target;
|
|
419
|
+
if (target?.closest(".vue-flow__node, .vue-flow__handle")) return;
|
|
420
|
+
const pos = flow.screenToFlowCoordinate({ x: event.clientX, y: event.clientY });
|
|
421
|
+
pendingConnect.value = { sourceId: start.sourceId, output: start.output, pos: { x: snap(pos.x), y: snap(pos.y) } };
|
|
422
|
+
pendingSpliceEdgeId.value = null;
|
|
423
|
+
void focusPalette();
|
|
424
|
+
toast.info("Pick a node \u2014 it will be added right there, already connected.");
|
|
425
|
+
});
|
|
426
|
+
flow.onEdgeUpdate(({ edge, connection }) => {
|
|
427
|
+
if (!connection.source || !connection.target) return;
|
|
428
|
+
if (connection.source === connection.target) return;
|
|
429
|
+
const be = blob.value.edges.find((e) => e.id === edge.id);
|
|
430
|
+
if (!be) return;
|
|
431
|
+
be.from = { nodeId: connection.source, output: stripSide(connection.sourceHandle) ?? be.from.output };
|
|
432
|
+
be.to = { nodeId: connection.target, input: be.to.input };
|
|
433
|
+
syncFromBlob();
|
|
434
|
+
push();
|
|
435
|
+
});
|
|
168
436
|
flow.onEdgesChange((changes) => {
|
|
169
437
|
let mutated = false;
|
|
170
438
|
for (const c of changes) {
|
|
@@ -180,10 +448,22 @@ flow.onNodesChange((changes) => {
|
|
|
180
448
|
if (c.type === "remove") removeBlobNode(c.id);
|
|
181
449
|
}
|
|
182
450
|
});
|
|
451
|
+
function stripFromGroups(drop) {
|
|
452
|
+
if (!blob.value.groups?.length) return;
|
|
453
|
+
for (const g of blob.value.groups) g.nodeIds = g.nodeIds.filter((x) => !drop.has(x));
|
|
454
|
+
blob.value.groups = blob.value.groups.filter((g) => (g.nodeIds.length > 0 || g.rect) && !drop.has(g.id));
|
|
455
|
+
}
|
|
183
456
|
function removeBlobNode(id) {
|
|
457
|
+
if (blob.value.groups?.some((g) => g.id === id)) {
|
|
458
|
+
ungroup(id);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
stripFromGroups(/* @__PURE__ */ new Set([id]));
|
|
184
462
|
blob.value.nodes = blob.value.nodes.filter((n) => n.id !== id);
|
|
463
|
+
if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => s.id !== id);
|
|
185
464
|
blob.value.triggers = blob.value.triggers.filter((t) => t.id !== id);
|
|
186
465
|
blob.value.edges = blob.value.edges.filter((e) => e.from.nodeId !== id && e.to.nodeId !== id);
|
|
466
|
+
if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
|
|
187
467
|
if (selectedId.value === id) selectedId.value = null;
|
|
188
468
|
syncFromBlob();
|
|
189
469
|
push();
|
|
@@ -191,6 +471,222 @@ function removeBlobNode(id) {
|
|
|
191
471
|
function push() {
|
|
192
472
|
emit("change", clone(blob.value));
|
|
193
473
|
}
|
|
474
|
+
const STICKY_DEFAULT_SIZE = { width: 240, height: 160 };
|
|
475
|
+
function updateSticky(id, patch) {
|
|
476
|
+
const st = blob.value.stickies?.find((s) => s.id === id);
|
|
477
|
+
if (!st) return;
|
|
478
|
+
Object.assign(st, patch);
|
|
479
|
+
syncFromBlob();
|
|
480
|
+
push();
|
|
481
|
+
}
|
|
482
|
+
const variablesOpen = ref(false);
|
|
483
|
+
const variableCount = computed(() => Object.keys(blob.value.variables ?? {}).length);
|
|
484
|
+
function updateVariables(next) {
|
|
485
|
+
blob.value.variables = Object.keys(next).length ? next : void 0;
|
|
486
|
+
push();
|
|
487
|
+
}
|
|
488
|
+
function addSticky(at) {
|
|
489
|
+
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
490
|
+
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
491
|
+
const sticky = {
|
|
492
|
+
id: `s-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
493
|
+
// n8n-style starter content: teaches the edit gesture and the supported
|
|
494
|
+
// markdown by example.
|
|
495
|
+
content: "## I'm a note\n**Double-click** to edit me. **Bold**, *italic*, `code`, [links](https://revenexx.com) and\n- lists work",
|
|
496
|
+
position: at ? { x: Math.round(at.x), y: Math.round(at.y) } : {
|
|
497
|
+
x: Math.round(center.x - STICKY_DEFAULT_SIZE.width / 2),
|
|
498
|
+
y: Math.round(center.y - STICKY_DEFAULT_SIZE.height / 2)
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
blob.value.stickies = [...blob.value.stickies ?? [], sticky];
|
|
502
|
+
markBorn(sticky.id);
|
|
503
|
+
syncFromBlob();
|
|
504
|
+
push();
|
|
505
|
+
}
|
|
506
|
+
function removeBlobNodes(ids) {
|
|
507
|
+
if (!ids.length) return;
|
|
508
|
+
const drop = new Set(ids);
|
|
509
|
+
stripFromGroups(drop);
|
|
510
|
+
blob.value.nodes = blob.value.nodes.filter((n) => !drop.has(n.id));
|
|
511
|
+
blob.value.triggers = blob.value.triggers.filter((t) => !drop.has(t.id));
|
|
512
|
+
blob.value.edges = blob.value.edges.filter((e) => !drop.has(e.from.nodeId) && !drop.has(e.to.nodeId));
|
|
513
|
+
if (blob.value.stickies?.length) blob.value.stickies = blob.value.stickies.filter((s) => !drop.has(s.id));
|
|
514
|
+
for (const id of ids) {
|
|
515
|
+
if (blob.value.triggerPositions?.[id]) delete blob.value.triggerPositions[id];
|
|
516
|
+
}
|
|
517
|
+
if (selectedId.value && drop.has(selectedId.value)) selectedId.value = null;
|
|
518
|
+
syncFromBlob();
|
|
519
|
+
push();
|
|
520
|
+
}
|
|
521
|
+
function duplicateBlobNodes(ids) {
|
|
522
|
+
const src = blob.value.nodes.filter((n) => ids.includes(n.id));
|
|
523
|
+
if (!src.length) return;
|
|
524
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
525
|
+
const stamp = Date.now();
|
|
526
|
+
src.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
|
|
527
|
+
const offset = 40;
|
|
528
|
+
const copies = src.map((n) => ({
|
|
529
|
+
...clone(n),
|
|
530
|
+
id: idMap.get(n.id),
|
|
531
|
+
position: { x: (n.position?.x ?? 0) + offset, y: (n.position?.y ?? 0) + offset }
|
|
532
|
+
}));
|
|
533
|
+
blob.value.nodes.push(...copies);
|
|
534
|
+
for (const e of blob.value.edges.filter((e2) => idMap.has(e2.from.nodeId) && idMap.has(e2.to.nodeId))) {
|
|
535
|
+
blob.value.edges.push({
|
|
536
|
+
id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
|
537
|
+
from: { nodeId: idMap.get(e.from.nodeId), output: e.from.output },
|
|
538
|
+
to: { nodeId: idMap.get(e.to.nodeId), input: e.to.input }
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
markBorn(...idMap.values());
|
|
542
|
+
syncFromBlob();
|
|
543
|
+
push();
|
|
544
|
+
void nextTick(() => {
|
|
545
|
+
flow.removeSelectedElements();
|
|
546
|
+
flow.addSelectedNodes(flow.getNodes.value.filter((n) => [...idMap.values()].includes(n.id)));
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
provide("workflow-node-actions", {
|
|
550
|
+
duplicate: (id) => duplicateBlobNodes([id]),
|
|
551
|
+
remove: (id) => removeBlobNode(id)
|
|
552
|
+
});
|
|
553
|
+
const CLIP_KIND = "revenexx.workflow.nodes";
|
|
554
|
+
async function copyNodes(ids) {
|
|
555
|
+
const nodes = blob.value.nodes.filter((n) => ids.includes(n.id));
|
|
556
|
+
if (!nodes.length) return;
|
|
557
|
+
const inner = new Set(nodes.map((n) => n.id));
|
|
558
|
+
const edges = blob.value.edges.filter((e) => inner.has(e.from.nodeId) && inner.has(e.to.nodeId));
|
|
559
|
+
try {
|
|
560
|
+
await navigator.clipboard.writeText(JSON.stringify({ kind: CLIP_KIND, version: 1, nodes, edges }, null, 2));
|
|
561
|
+
toast.success(nodes.length === 1 ? "Node copied" : `${nodes.length} nodes copied`);
|
|
562
|
+
} catch {
|
|
563
|
+
toast.error("Could not write to the clipboard");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async function pasteNodes(at) {
|
|
567
|
+
let text = "";
|
|
568
|
+
try {
|
|
569
|
+
text = await navigator.clipboard.readText();
|
|
570
|
+
} catch {
|
|
571
|
+
toast.error("Could not read the clipboard");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
let payload;
|
|
575
|
+
try {
|
|
576
|
+
payload = JSON.parse(text);
|
|
577
|
+
} catch {
|
|
578
|
+
toast.error("Clipboard does not contain nodes");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (payload?.kind !== CLIP_KIND || !Array.isArray(payload.nodes) || !payload.nodes.length) {
|
|
582
|
+
toast.error("Clipboard does not contain nodes");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
586
|
+
const stamp = Date.now();
|
|
587
|
+
payload.nodes.forEach((n, i) => idMap.set(n.id, `n-${stamp}${i}`));
|
|
588
|
+
const minX = Math.min(...payload.nodes.map((n) => n.position?.x ?? 0));
|
|
589
|
+
const minY = Math.min(...payload.nodes.map((n) => n.position?.y ?? 0));
|
|
590
|
+
const dx = at ? Math.round(at.x - minX) : 40;
|
|
591
|
+
const dy = at ? Math.round(at.y - minY) : 40;
|
|
592
|
+
blob.value.nodes.push(...payload.nodes.map((n) => ({
|
|
593
|
+
...clone(n),
|
|
594
|
+
id: idMap.get(n.id),
|
|
595
|
+
position: { x: (n.position?.x ?? 0) + dx, y: (n.position?.y ?? 0) + dy }
|
|
596
|
+
})));
|
|
597
|
+
for (const e of payload.edges ?? []) {
|
|
598
|
+
if (!idMap.has(e.from.nodeId) || !idMap.has(e.to.nodeId)) continue;
|
|
599
|
+
blob.value.edges.push({
|
|
600
|
+
id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
|
601
|
+
from: { nodeId: idMap.get(e.from.nodeId), output: e.from.output },
|
|
602
|
+
to: { nodeId: idMap.get(e.to.nodeId), input: e.to.input }
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
markBorn(...idMap.values());
|
|
606
|
+
syncFromBlob();
|
|
607
|
+
push();
|
|
608
|
+
void nextTick(() => {
|
|
609
|
+
flow.removeSelectedElements();
|
|
610
|
+
flow.addSelectedNodes(flow.getNodes.value.filter((n) => [...idMap.values()].includes(n.id)));
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const menu = ref({ kind: "canvas", targetId: null, flowPos: null });
|
|
614
|
+
function eventFlowPos(event) {
|
|
615
|
+
if (!("clientX" in event)) return null;
|
|
616
|
+
return flow.screenToFlowCoordinate({ x: event.clientX, y: event.clientY });
|
|
617
|
+
}
|
|
618
|
+
flow.onNodeContextMenu(({ event, node }) => {
|
|
619
|
+
const selected = flow.getSelectedNodes.value;
|
|
620
|
+
if (selected.length > 1 && selected.some((n) => n.id === node.id)) {
|
|
621
|
+
menu.value = { kind: "selection", targetId: null, flowPos: eventFlowPos(event) };
|
|
622
|
+
} else {
|
|
623
|
+
flow.removeSelectedElements();
|
|
624
|
+
flow.addSelectedNodes([node]);
|
|
625
|
+
menu.value = { kind: "node", targetId: node.id, flowPos: eventFlowPos(event) };
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
flow.onSelectionContextMenu(({ event }) => {
|
|
629
|
+
menu.value = { kind: "selection", targetId: null, flowPos: eventFlowPos(event) };
|
|
630
|
+
});
|
|
631
|
+
flow.onPaneContextMenu((event) => {
|
|
632
|
+
menu.value = { kind: "canvas", targetId: null, flowPos: eventFlowPos(event) };
|
|
633
|
+
});
|
|
634
|
+
function onMenuAction(id) {
|
|
635
|
+
const selectedIds = flow.getSelectedNodes.value.map((n) => n.id);
|
|
636
|
+
switch (id) {
|
|
637
|
+
case "open":
|
|
638
|
+
if (menu.value.targetId) selectedId.value = menu.value.targetId;
|
|
639
|
+
break;
|
|
640
|
+
case "rename":
|
|
641
|
+
if (menu.value.targetId) {
|
|
642
|
+
renameIntent.value = true;
|
|
643
|
+
selectedId.value = menu.value.targetId;
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
case "copy":
|
|
647
|
+
void copyNodes(menu.value.kind === "node" && menu.value.targetId ? [menu.value.targetId] : selectedIds);
|
|
648
|
+
break;
|
|
649
|
+
case "duplicate":
|
|
650
|
+
duplicateBlobNodes(menu.value.kind === "node" && menu.value.targetId ? [menu.value.targetId] : selectedIds);
|
|
651
|
+
break;
|
|
652
|
+
case "paste":
|
|
653
|
+
void pasteNodes(menu.value.flowPos ?? void 0);
|
|
654
|
+
break;
|
|
655
|
+
case "run":
|
|
656
|
+
emit("run");
|
|
657
|
+
break;
|
|
658
|
+
case "tidy-up":
|
|
659
|
+
void tidyUp();
|
|
660
|
+
break;
|
|
661
|
+
case "fit-view":
|
|
662
|
+
flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 });
|
|
663
|
+
break;
|
|
664
|
+
case "add-node":
|
|
665
|
+
void focusPalette();
|
|
666
|
+
break;
|
|
667
|
+
case "add-sticky":
|
|
668
|
+
addSticky(menu.value.flowPos ?? void 0);
|
|
669
|
+
break;
|
|
670
|
+
case "group":
|
|
671
|
+
createGroupFromSelection(selectedIds);
|
|
672
|
+
break;
|
|
673
|
+
case "select-all":
|
|
674
|
+
flow.addSelectedNodes(flow.getNodes.value);
|
|
675
|
+
break;
|
|
676
|
+
case "clear-selection":
|
|
677
|
+
flow.removeSelectedElements();
|
|
678
|
+
break;
|
|
679
|
+
case "delete":
|
|
680
|
+
if (menu.value.kind === "node" && menu.value.targetId) removeBlobNode(menu.value.targetId);
|
|
681
|
+
else removeBlobNodes(selectedIds);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function onResolvedOutputs(nodeId, outputs) {
|
|
686
|
+
dynamicOutputs.value = { ...dynamicOutputs.value, [nodeId]: outputs };
|
|
687
|
+
syncFromBlob();
|
|
688
|
+
nextTick(() => flow.updateNodeInternals([nodeId]));
|
|
689
|
+
}
|
|
194
690
|
function isValidConnection(conn) {
|
|
195
691
|
if (!conn.source || !conn.target) return false;
|
|
196
692
|
if (conn.source === conn.target) return false;
|
|
@@ -202,22 +698,33 @@ const selection = computed(() => {
|
|
|
202
698
|
return blob.value.nodes.find((n) => n.id === selectedId.value) ?? blob.value.triggers.find((t) => t.id === selectedId.value) ?? null;
|
|
203
699
|
});
|
|
204
700
|
flow.onNodeClick((evt) => {
|
|
701
|
+
if (evt.node.type === "sticky" || evt.node.type === "group") return;
|
|
205
702
|
selectedId.value = evt.node.id;
|
|
206
703
|
});
|
|
207
704
|
flow.onPaneClick(() => {
|
|
208
705
|
selectedId.value = null;
|
|
209
706
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
707
|
+
function localLabel(v) {
|
|
708
|
+
if (!v) return "";
|
|
709
|
+
return typeof v === "string" ? v : v.en ?? Object.values(v)[0] ?? "";
|
|
710
|
+
}
|
|
711
|
+
const selectedUpstreamOutputs = computed(() => {
|
|
712
|
+
const id = selectedId.value;
|
|
713
|
+
if (!id) return [];
|
|
714
|
+
const isNode = blob.value.nodes.some((n) => n.id === id);
|
|
715
|
+
if (!isNode) return [];
|
|
716
|
+
return blob.value.edges.filter((e) => e.to.nodeId === id).flatMap((e) => {
|
|
717
|
+
const source = blob.value.nodes.find((n) => n.id === e.from.nodeId);
|
|
718
|
+
if (!source) return [];
|
|
719
|
+
const portDef = (findManifest(source.nodeSlug, source.nodeVersion)?.outputs ?? []).find((o) => o.name === e.from.output);
|
|
720
|
+
return [{
|
|
721
|
+
nodeId: source.id,
|
|
722
|
+
nodeName: localLabel(source.name) || source.nodeSlug,
|
|
723
|
+
output: e.from.output,
|
|
724
|
+
fields: portDef?.fields
|
|
725
|
+
}];
|
|
726
|
+
});
|
|
213
727
|
});
|
|
214
|
-
watch(
|
|
215
|
-
() => blob.value.nodes.length + blob.value.triggers.length,
|
|
216
|
-
(count, prev) => {
|
|
217
|
-
if (prev === void 0 && count === 0) paletteOpen.value = true;
|
|
218
|
-
},
|
|
219
|
-
{ immediate: true }
|
|
220
|
-
);
|
|
221
728
|
function applyInspectorUpdate(next) {
|
|
222
729
|
const id = next.id;
|
|
223
730
|
const triggerIdx = blob.value.triggers.findIndex((t) => t.id === id);
|
|
@@ -230,21 +737,86 @@ function applyInspectorUpdate(next) {
|
|
|
230
737
|
syncFromBlob();
|
|
231
738
|
push();
|
|
232
739
|
}
|
|
740
|
+
const nodeDrag = useNodeDrag();
|
|
741
|
+
const dropGhost = ref(null);
|
|
742
|
+
const GRID = 20;
|
|
743
|
+
function snap(v) {
|
|
744
|
+
return Math.round(v / GRID) * GRID;
|
|
745
|
+
}
|
|
746
|
+
const dragActive = computed(() => !!nodeDrag.dragging.value);
|
|
747
|
+
watch(dragActive, (on) => {
|
|
748
|
+
if (!on) dropGhost.value = null;
|
|
749
|
+
});
|
|
750
|
+
const ghostRect = computed(() => {
|
|
751
|
+
if (!dropGhost.value) return null;
|
|
752
|
+
const vp = flow.viewport.value;
|
|
753
|
+
const kind = nodeDrag.dragging.value?.kind;
|
|
754
|
+
const size = kind === "group" ? { width: 320, height: 200 } : { width: kind === "trigger" ? 200 : 220, height: 58 };
|
|
755
|
+
return {
|
|
756
|
+
left: dropGhost.value.x * vp.zoom + vp.x,
|
|
757
|
+
top: dropGhost.value.y * vp.zoom + vp.y,
|
|
758
|
+
width: size.width * vp.zoom,
|
|
759
|
+
height: size.height * vp.zoom
|
|
760
|
+
};
|
|
761
|
+
});
|
|
233
762
|
function onDragOver(ev) {
|
|
234
763
|
ev.preventDefault();
|
|
235
764
|
if (ev.dataTransfer) ev.dataTransfer.dropEffect = "move";
|
|
765
|
+
if (!nodeDrag.dragging.value) return;
|
|
766
|
+
const p = flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY });
|
|
767
|
+
dropGhost.value = { x: snap(p.x), y: snap(p.y) };
|
|
236
768
|
}
|
|
237
|
-
function
|
|
238
|
-
ev.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
769
|
+
function onDragLeave(ev) {
|
|
770
|
+
const target = ev.currentTarget;
|
|
771
|
+
if (target && ev.relatedTarget instanceof Node && target.contains(ev.relatedTarget)) return;
|
|
772
|
+
dropGhost.value = null;
|
|
773
|
+
}
|
|
774
|
+
const EMPTY_GROUP_SIZE = { width: 320, height: 200 };
|
|
775
|
+
function groupAtPoint(pos) {
|
|
776
|
+
for (const g of blob.value.groups ?? []) {
|
|
777
|
+
if (g.collapsed) continue;
|
|
778
|
+
const r = groupRect(g);
|
|
779
|
+
if (pos.x >= r.x && pos.x <= r.x + r.width && pos.y >= r.y && pos.y <= r.y + r.height) return g;
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
function addGroupFrame() {
|
|
784
|
+
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
785
|
+
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
786
|
+
addFromPayload({ kind: "group" }, {
|
|
787
|
+
x: center.x - EMPTY_GROUP_SIZE.width / 2,
|
|
788
|
+
y: center.y - EMPTY_GROUP_SIZE.height / 2
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function adoptIntoGroupAt(id, pos) {
|
|
792
|
+
const g = groupAtPoint(pos);
|
|
793
|
+
if (!g || g.nodeIds.includes(id)) return;
|
|
794
|
+
for (const other of blob.value.groups ?? []) other.nodeIds = other.nodeIds.filter((x) => x !== id);
|
|
795
|
+
g.nodeIds.push(id);
|
|
796
|
+
}
|
|
797
|
+
function addFromPayload(payload, pos) {
|
|
798
|
+
const at = { x: snap(pos.x), y: snap(pos.y) };
|
|
799
|
+
if (payload.kind === "group") {
|
|
800
|
+
const id2 = `g-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
801
|
+
blob.value.groups = [...blob.value.groups ?? [], {
|
|
802
|
+
id: id2,
|
|
803
|
+
label: `Group ${(blob.value.groups?.length ?? 0) + 1}`,
|
|
804
|
+
nodeIds: [],
|
|
805
|
+
rect: { x: at.x, y: at.y, ...EMPTY_GROUP_SIZE }
|
|
806
|
+
}];
|
|
807
|
+
markBorn(id2);
|
|
808
|
+
syncFromBlob();
|
|
809
|
+
push();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
243
812
|
if (payload.kind === "trigger") {
|
|
244
813
|
const tpl = TRIGGER_TEMPLATES.find((t) => t.type === payload.type);
|
|
245
814
|
if (!tpl) return;
|
|
246
|
-
const id2 =
|
|
815
|
+
const id2 = crypto.randomUUID();
|
|
247
816
|
blob.value.triggers.push({ id: id2, type: tpl.type, config: { ...tpl.defaultConfig } });
|
|
817
|
+
blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [id2]: at };
|
|
818
|
+
adoptIntoGroupAt(id2, at);
|
|
819
|
+
markBorn(id2);
|
|
248
820
|
syncFromBlob();
|
|
249
821
|
selectedId.value = id2;
|
|
250
822
|
push();
|
|
@@ -258,8 +830,95 @@ function onDrop(ev) {
|
|
|
258
830
|
nodeSlug: manifest.slug,
|
|
259
831
|
nodeVersion: manifest.version,
|
|
260
832
|
config: defaultConfigFor(manifest.config ?? []),
|
|
261
|
-
position:
|
|
833
|
+
position: at
|
|
262
834
|
});
|
|
835
|
+
adoptIntoGroupAt(id, at);
|
|
836
|
+
markBorn(id);
|
|
837
|
+
syncFromBlob();
|
|
838
|
+
selectedId.value = id;
|
|
839
|
+
push();
|
|
840
|
+
}
|
|
841
|
+
function onDrop(ev) {
|
|
842
|
+
ev.preventDefault();
|
|
843
|
+
dropGhost.value = null;
|
|
844
|
+
nodeDrag.end();
|
|
845
|
+
pendingSpliceEdgeId.value = null;
|
|
846
|
+
pendingConnect.value = null;
|
|
847
|
+
const raw = ev.dataTransfer?.getData("application/integration-node");
|
|
848
|
+
if (!raw) return;
|
|
849
|
+
const payload = JSON.parse(raw);
|
|
850
|
+
addFromPayload(payload, flow.screenToFlowCoordinate({ x: ev.clientX, y: ev.clientY }));
|
|
851
|
+
}
|
|
852
|
+
function onPaletteInsert(payload) {
|
|
853
|
+
if (pendingConnect.value && payload.kind === "manifest") {
|
|
854
|
+
const pc = pendingConnect.value;
|
|
855
|
+
pendingConnect.value = null;
|
|
856
|
+
const manifest = findManifest(payload.slug, payload.version);
|
|
857
|
+
if (!manifest) return;
|
|
858
|
+
const id = `n-${Date.now()}`;
|
|
859
|
+
blob.value.nodes.push({
|
|
860
|
+
id,
|
|
861
|
+
nodeSlug: manifest.slug,
|
|
862
|
+
nodeVersion: manifest.version,
|
|
863
|
+
config: defaultConfigFor(manifest.config ?? []),
|
|
864
|
+
position: { x: pc.pos.x, y: pc.pos.y }
|
|
865
|
+
});
|
|
866
|
+
blob.value.edges.push({
|
|
867
|
+
id: `e-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
|
868
|
+
from: { nodeId: pc.sourceId, output: pc.output },
|
|
869
|
+
to: { nodeId: id, input: void 0 }
|
|
870
|
+
});
|
|
871
|
+
markBorn(id);
|
|
872
|
+
syncFromBlob();
|
|
873
|
+
selectedId.value = id;
|
|
874
|
+
push();
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
pendingConnect.value = null;
|
|
878
|
+
if (pendingSpliceEdgeId.value && payload.kind === "manifest") {
|
|
879
|
+
spliceIntoEdge(pendingSpliceEdgeId.value, payload);
|
|
880
|
+
pendingSpliceEdgeId.value = null;
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
pendingSpliceEdgeId.value = null;
|
|
884
|
+
const rect = flow.vueFlowRef.value?.getBoundingClientRect();
|
|
885
|
+
const center = rect ? flow.screenToFlowCoordinate({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }) : { x: 120, y: 120 };
|
|
886
|
+
addFromPayload(payload, center);
|
|
887
|
+
}
|
|
888
|
+
const pendingSpliceEdgeId = ref(null);
|
|
889
|
+
provide("workflow-edge-actions", {
|
|
890
|
+
splice: (edgeId) => {
|
|
891
|
+
if (!blob.value.edges.some((e) => e.id === edgeId)) return;
|
|
892
|
+
pendingSpliceEdgeId.value = edgeId;
|
|
893
|
+
void focusPalette();
|
|
894
|
+
toast.info("Pick a node from the palette \u2014 it will be inserted into the connection.");
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
function primaryOutputName(slug, version) {
|
|
898
|
+
const m = findManifest(slug, version);
|
|
899
|
+
return (m?.outputs ?? []).find((o) => o.name && o.kind !== "error")?.name ?? "default";
|
|
900
|
+
}
|
|
901
|
+
function spliceIntoEdge(edgeId, payload) {
|
|
902
|
+
const edge = blob.value.edges.find((e) => e.id === edgeId);
|
|
903
|
+
const manifest = findManifest(payload.slug, payload.version);
|
|
904
|
+
if (!edge || !manifest) return;
|
|
905
|
+
const a = nodePosition(edge.from.nodeId);
|
|
906
|
+
const b = nodePosition(edge.to.nodeId);
|
|
907
|
+
const id = `n-${Date.now()}`;
|
|
908
|
+
blob.value.nodes.push({
|
|
909
|
+
id,
|
|
910
|
+
nodeSlug: manifest.slug,
|
|
911
|
+
nodeVersion: manifest.version,
|
|
912
|
+
config: defaultConfigFor(manifest.config ?? []),
|
|
913
|
+
position: { x: snap((a.x + b.x) / 2), y: snap((a.y + b.y) / 2) }
|
|
914
|
+
});
|
|
915
|
+
blob.value.edges = blob.value.edges.filter((e) => e.id !== edgeId);
|
|
916
|
+
const stamp = Date.now().toString(36);
|
|
917
|
+
blob.value.edges.push(
|
|
918
|
+
{ id: `e-${stamp}a${Math.random().toString(36).slice(2, 6)}`, from: { nodeId: edge.from.nodeId, output: edge.from.output }, to: { nodeId: id, input: void 0 } },
|
|
919
|
+
{ id: `e-${stamp}b${Math.random().toString(36).slice(2, 6)}`, from: { nodeId: id, output: primaryOutputName(payload.slug, payload.version) }, to: { nodeId: edge.to.nodeId, input: edge.to.input } }
|
|
920
|
+
);
|
|
921
|
+
markBorn(id);
|
|
263
922
|
syncFromBlob();
|
|
264
923
|
selectedId.value = id;
|
|
265
924
|
push();
|
|
@@ -271,22 +930,262 @@ function defaultConfigFor(fields = []) {
|
|
|
271
930
|
}
|
|
272
931
|
return out;
|
|
273
932
|
}
|
|
933
|
+
function nodeDims(id) {
|
|
934
|
+
const d = flow.findNode(id)?.dimensions;
|
|
935
|
+
return d && d.width > 0 ? d : { width: 220, height: 64 };
|
|
936
|
+
}
|
|
274
937
|
async function tidyUp() {
|
|
275
|
-
const
|
|
276
|
-
|
|
938
|
+
const containment = /* @__PURE__ */ new Map();
|
|
939
|
+
for (const s of blob.value.stickies ?? []) {
|
|
940
|
+
const sw = s.size?.width ?? 240;
|
|
941
|
+
const sh = s.size?.height ?? 160;
|
|
942
|
+
const contained = [];
|
|
943
|
+
for (const n of vfNodes.value) {
|
|
944
|
+
if (n.type === "sticky") continue;
|
|
945
|
+
const dims = nodeDims(n.id);
|
|
946
|
+
const cx = n.position.x + dims.width / 2;
|
|
947
|
+
const cy = n.position.y + dims.height / 2;
|
|
948
|
+
if (cx >= s.position.x && cx <= s.position.x + sw && cy >= s.position.y && cy <= s.position.y + sh) {
|
|
949
|
+
contained.push(n.id);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (contained.length) containment.set(s.id, contained);
|
|
953
|
+
}
|
|
954
|
+
const graphNodes = vfNodes.value.filter((n) => n.type !== "sticky" && n.type !== "group");
|
|
955
|
+
const memberToGroup = /* @__PURE__ */ new Map();
|
|
956
|
+
for (const g of blob.value.groups ?? []) for (const id of g.nodeIds) memberToGroup.set(id, g);
|
|
957
|
+
const internal = /* @__PURE__ */ new Map();
|
|
958
|
+
for (const g of blob.value.groups ?? []) {
|
|
959
|
+
if (g.collapsed) continue;
|
|
960
|
+
const members = graphNodes.filter((n) => g.nodeIds.includes(n.id));
|
|
961
|
+
if (!members.length) continue;
|
|
962
|
+
const memberEdges = vfEdges.value.filter((e) => g.nodeIds.includes(e.source) && g.nodeIds.includes(e.target));
|
|
963
|
+
const laidMembers = layoutWorkflowNodes(members, memberEdges, { direction: "TB", nodeWidth: 240, nodeHeight: 90, rankSeparation: 60, nodeSeparation: 30 });
|
|
964
|
+
let maxX = 0;
|
|
965
|
+
let maxY = 0;
|
|
966
|
+
for (const m of laidMembers) {
|
|
967
|
+
const dims = nodeDims(m.id);
|
|
968
|
+
maxX = Math.max(maxX, m.position.x + dims.width);
|
|
969
|
+
maxY = Math.max(maxY, m.position.y + dims.height);
|
|
970
|
+
}
|
|
971
|
+
internal.set(g.id, { nodes: laidMembers, width: maxX + GROUP_PAD * 2, height: maxY + GROUP_PAD * 2 + GROUP_LABEL_ROOM });
|
|
972
|
+
}
|
|
973
|
+
const quotientUnit = (id) => memberToGroup.get(id)?.id ?? id;
|
|
974
|
+
const quotientNodes = [];
|
|
975
|
+
const seenUnit = /* @__PURE__ */ new Set();
|
|
976
|
+
for (const n of graphNodes) {
|
|
977
|
+
const unit = quotientUnit(n.id);
|
|
978
|
+
if (seenUnit.has(unit)) continue;
|
|
979
|
+
seenUnit.add(unit);
|
|
980
|
+
quotientNodes.push(unit === n.id ? n : { id: unit, position: { x: 0, y: 0 } });
|
|
981
|
+
}
|
|
982
|
+
const sizes = {};
|
|
983
|
+
for (const [gid, info] of internal) sizes[gid] = { width: info.width, height: info.height };
|
|
984
|
+
for (const g of blob.value.groups ?? []) {
|
|
985
|
+
if (g.collapsed) sizes[g.id] = { width: 240, height: 90 };
|
|
986
|
+
}
|
|
987
|
+
const seenQe = /* @__PURE__ */ new Set();
|
|
988
|
+
const quotientEdges = [];
|
|
989
|
+
for (const e of vfEdges.value) {
|
|
990
|
+
const su = quotientUnit(e.source);
|
|
991
|
+
const tu = quotientUnit(e.target);
|
|
992
|
+
if (su === tu) continue;
|
|
993
|
+
const key = `${su}|${tu}`;
|
|
994
|
+
if (seenQe.has(key)) continue;
|
|
995
|
+
seenQe.add(key);
|
|
996
|
+
quotientEdges.push({ id: key, source: su, target: tu });
|
|
997
|
+
}
|
|
998
|
+
const laidQuotient = layoutWorkflowNodes(quotientNodes, quotientEdges, { direction: "TB", sizes });
|
|
999
|
+
const unitPos = new Map(laidQuotient.map((n) => [n.id, n.position]));
|
|
1000
|
+
const laid = graphNodes.map((n) => {
|
|
1001
|
+
const g = memberToGroup.get(n.id);
|
|
1002
|
+
if (!g) {
|
|
1003
|
+
const pos = unitPos.get(n.id);
|
|
1004
|
+
return pos ? { ...n, position: pos } : n;
|
|
1005
|
+
}
|
|
1006
|
+
const anchor = unitPos.get(g.id);
|
|
1007
|
+
if (!anchor) return n;
|
|
1008
|
+
if (g.collapsed) {
|
|
1009
|
+
const idx = g.nodeIds.indexOf(n.id);
|
|
1010
|
+
return { ...n, position: { x: anchor.x, y: anchor.y + idx * 30 } };
|
|
1011
|
+
}
|
|
1012
|
+
const inner = internal.get(g.id)?.nodes.find((m) => m.id === n.id);
|
|
1013
|
+
if (!inner) return n;
|
|
1014
|
+
return { ...n, position: { x: anchor.x + GROUP_PAD + inner.position.x, y: anchor.y + GROUP_PAD + GROUP_LABEL_ROOM + inner.position.y } };
|
|
1015
|
+
});
|
|
277
1016
|
for (const n of laid) {
|
|
1017
|
+
const pos = { x: Math.round(n.position.x), y: Math.round(n.position.y) };
|
|
278
1018
|
if (n.type === "workflow") {
|
|
279
1019
|
const wn = blob.value.nodes.find((b) => b.id === n.id);
|
|
280
|
-
if (wn) wn.position =
|
|
1020
|
+
if (wn) wn.position = pos;
|
|
1021
|
+
} else if (n.type === "trigger") {
|
|
1022
|
+
blob.value.triggerPositions = { ...blob.value.triggerPositions ?? {}, [n.id]: pos };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (const g of blob.value.groups ?? []) delete g.rect;
|
|
1026
|
+
const PAD = 24;
|
|
1027
|
+
const LABEL_ROOM = 20;
|
|
1028
|
+
for (const [stickyId, ids] of containment) {
|
|
1029
|
+
const st = blob.value.stickies?.find((x) => x.id === stickyId);
|
|
1030
|
+
if (!st) continue;
|
|
1031
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1032
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1033
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1034
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
1035
|
+
for (const id of ids) {
|
|
1036
|
+
const ln = laid.find((l) => l.id === id);
|
|
1037
|
+
if (!ln) continue;
|
|
1038
|
+
const dims = nodeDims(id);
|
|
1039
|
+
minX = Math.min(minX, ln.position.x);
|
|
1040
|
+
minY = Math.min(minY, ln.position.y);
|
|
1041
|
+
maxX = Math.max(maxX, ln.position.x + dims.width);
|
|
1042
|
+
maxY = Math.max(maxY, ln.position.y + dims.height);
|
|
281
1043
|
}
|
|
1044
|
+
if (!Number.isFinite(minX)) continue;
|
|
1045
|
+
st.position = { x: Math.round(minX - PAD), y: Math.round(minY - PAD - LABEL_ROOM) };
|
|
1046
|
+
st.size = {
|
|
1047
|
+
width: Math.max(150, Math.round(maxX - minX + PAD * 2)),
|
|
1048
|
+
height: Math.max(80, Math.round(maxY - minY + PAD * 2 + LABEL_ROOM))
|
|
1049
|
+
};
|
|
282
1050
|
}
|
|
1051
|
+
syncFromBlob();
|
|
283
1052
|
push();
|
|
284
1053
|
await nextTick();
|
|
285
|
-
flow.fitView({ padding: 0.2, duration: 400 });
|
|
1054
|
+
flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 });
|
|
1055
|
+
}
|
|
1056
|
+
const POINTER_MODE_KEY = "integrations.editor.pointerMode";
|
|
1057
|
+
const pointerMode = ref("select");
|
|
1058
|
+
try {
|
|
1059
|
+
const stored = localStorage.getItem(POINTER_MODE_KEY);
|
|
1060
|
+
if (stored === "hand" || stored === "select") pointerMode.value = stored;
|
|
1061
|
+
} catch {
|
|
286
1062
|
}
|
|
1063
|
+
function setPointerMode(mode) {
|
|
1064
|
+
pointerMode.value = mode;
|
|
1065
|
+
try {
|
|
1066
|
+
localStorage.setItem(POINTER_MODE_KEY, mode);
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const zoomPct = computed(() => `${Math.round((flow.viewport.value?.zoom ?? 1) * 100)}%`);
|
|
1071
|
+
const ZOOM_PRESETS = [50, 75, 100, 150, 200, 300, 400];
|
|
1072
|
+
const MIN_ZOOM_PCT = 25;
|
|
1073
|
+
const MAX_ZOOM_PCT = 400;
|
|
1074
|
+
const zoomMenuOpen = ref(false);
|
|
1075
|
+
const zoomInputText = ref("");
|
|
1076
|
+
function setZoomPct(pct) {
|
|
1077
|
+
const clamped = Math.min(MAX_ZOOM_PCT, Math.max(MIN_ZOOM_PCT, Math.round(pct)));
|
|
1078
|
+
void flow.zoomTo(clamped / 100, { duration: 200 });
|
|
1079
|
+
zoomMenuOpen.value = false;
|
|
1080
|
+
}
|
|
1081
|
+
function applyZoomInput() {
|
|
1082
|
+
const parsed = Number.parseFloat(zoomInputText.value.replace("%", "").trim());
|
|
1083
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return;
|
|
1084
|
+
setZoomPct(parsed);
|
|
1085
|
+
}
|
|
1086
|
+
watch(zoomMenuOpen, (open) => {
|
|
1087
|
+
if (open) zoomInputText.value = String(Math.round((flow.viewport.value?.zoom ?? 1) * 100));
|
|
1088
|
+
});
|
|
1089
|
+
const paletteRef = ref(null);
|
|
1090
|
+
const renameIntent = ref(false);
|
|
1091
|
+
const canvasWrapRef = ref(null);
|
|
1092
|
+
const paletteShellRef = ref(null);
|
|
1093
|
+
const palette = usePaletteLayout(canvasWrapRef);
|
|
1094
|
+
const paletteShellClass = computed(() => {
|
|
1095
|
+
if (palette.mode.value === "docked-left") return "left-3 top-3 bottom-16 origin-top-left";
|
|
1096
|
+
if (palette.mode.value === "docked-right") return "right-3 top-3 bottom-16 origin-top-right";
|
|
1097
|
+
return "h-[min(70vh,34rem)] origin-top-left";
|
|
1098
|
+
});
|
|
1099
|
+
const palettePillClass = computed(() => {
|
|
1100
|
+
if (palette.mode.value === "docked-left") return "left-3 top-3 origin-top-left";
|
|
1101
|
+
if (palette.mode.value === "docked-right") return "right-3 top-3 origin-top-right";
|
|
1102
|
+
return "origin-top-left";
|
|
1103
|
+
});
|
|
1104
|
+
const minimapShellRef = ref(null);
|
|
1105
|
+
const minimapLayout = useMiniMapLayout(canvasWrapRef);
|
|
1106
|
+
const MINIMAP_CORNER_CLASS = {
|
|
1107
|
+
"top-left": "left-3 top-3",
|
|
1108
|
+
"top-right": "right-3 top-3",
|
|
1109
|
+
"bottom-left": "bottom-16 left-3",
|
|
1110
|
+
"bottom-right": "bottom-16 right-3"
|
|
1111
|
+
};
|
|
1112
|
+
const minimapPlacementClass = computed(() => minimapLayout.mode.value === "floating" ? "" : MINIMAP_CORNER_CLASS[minimapLayout.mode.value]);
|
|
1113
|
+
const minimapPreviewClass = computed(() => minimapLayout.dockPreview.value ? MINIMAP_CORNER_CLASS[minimapLayout.dockPreview.value] : "");
|
|
1114
|
+
async function focusPalette() {
|
|
1115
|
+
if (palette.collapsed.value) {
|
|
1116
|
+
palette.setCollapsed(false);
|
|
1117
|
+
await nextTick();
|
|
1118
|
+
}
|
|
1119
|
+
paletteRef.value?.focusSearch();
|
|
1120
|
+
}
|
|
1121
|
+
const lightboxOpen = computed(() => !!selection.value);
|
|
1122
|
+
const singleSelected = computed(() => flow.getSelectedNodes.value.length === 1 ? flow.getSelectedNodes.value[0] : null);
|
|
1123
|
+
useCanvasShortcuts(() => ({
|
|
1124
|
+
"enter": {
|
|
1125
|
+
disabled: () => !singleSelected.value,
|
|
1126
|
+
run: () => {
|
|
1127
|
+
selectedId.value = singleSelected.value.id;
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
"f2|space": {
|
|
1131
|
+
disabled: () => !singleSelected.value,
|
|
1132
|
+
run: () => {
|
|
1133
|
+
renameIntent.value = true;
|
|
1134
|
+
selectedId.value = singleSelected.value.id;
|
|
1135
|
+
}
|
|
1136
|
+
},
|
|
1137
|
+
"mod+a": () => flow.addSelectedNodes(flow.getNodes.value),
|
|
1138
|
+
"mod+c": {
|
|
1139
|
+
disabled: () => !flow.getSelectedNodes.value.length,
|
|
1140
|
+
run: () => {
|
|
1141
|
+
void copyNodes(flow.getSelectedNodes.value.map((n) => n.id));
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
"mod+v": () => {
|
|
1145
|
+
void pasteNodes();
|
|
1146
|
+
},
|
|
1147
|
+
"mod+d": {
|
|
1148
|
+
disabled: () => !flow.getSelectedNodes.value.length,
|
|
1149
|
+
run: () => duplicateBlobNodes(flow.getSelectedNodes.value.map((n) => n.id))
|
|
1150
|
+
},
|
|
1151
|
+
"mod+g": {
|
|
1152
|
+
disabled: () => !flow.getSelectedNodes.value.length,
|
|
1153
|
+
run: () => createGroupFromSelection(flow.getSelectedNodes.value.map((n) => n.id))
|
|
1154
|
+
},
|
|
1155
|
+
"mod+enter": { disabled: () => !!props.runBusy, run: () => emit("run") },
|
|
1156
|
+
"shift+alt+t": () => {
|
|
1157
|
+
void tidyUp();
|
|
1158
|
+
},
|
|
1159
|
+
"shift+s": () => addSticky(),
|
|
1160
|
+
"v": () => setPointerMode("select"),
|
|
1161
|
+
"h": () => setPointerMode("hand"),
|
|
1162
|
+
"shift+a": () => emit("toggleActive"),
|
|
1163
|
+
"shift+v": () => {
|
|
1164
|
+
variablesOpen.value = true;
|
|
1165
|
+
},
|
|
1166
|
+
"shift+alt+s": () => emit("openSettings"),
|
|
1167
|
+
"-": () => {
|
|
1168
|
+
void flow.zoomOut({ duration: 150 });
|
|
1169
|
+
},
|
|
1170
|
+
"=": () => {
|
|
1171
|
+
void flow.zoomIn({ duration: 150 });
|
|
1172
|
+
},
|
|
1173
|
+
"0": () => setZoomPct(100),
|
|
1174
|
+
"1": () => flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 }),
|
|
1175
|
+
"tab": () => {
|
|
1176
|
+
void focusPalette();
|
|
1177
|
+
},
|
|
1178
|
+
"escape": () => {
|
|
1179
|
+
pendingSpliceEdgeId.value = null;
|
|
1180
|
+
pendingConnect.value = null;
|
|
1181
|
+
flow.removeSelectedElements();
|
|
1182
|
+
}
|
|
1183
|
+
}), { disabled: lightboxOpen });
|
|
287
1184
|
const nodeTypes = {
|
|
288
1185
|
workflow: markRaw(WorkflowNodeView),
|
|
289
|
-
trigger: markRaw(WorkflowTriggerView)
|
|
1186
|
+
trigger: markRaw(WorkflowTriggerView),
|
|
1187
|
+
sticky: markRaw(StickyNoteView),
|
|
1188
|
+
group: markRaw(GroupFrameView)
|
|
290
1189
|
};
|
|
291
1190
|
const edgeTypes = {
|
|
292
1191
|
flow: markRaw(WorkflowEdgeView)
|
|
@@ -294,80 +1193,447 @@ const edgeTypes = {
|
|
|
294
1193
|
</script>
|
|
295
1194
|
|
|
296
1195
|
<template>
|
|
297
|
-
<div class="
|
|
298
|
-
<!-- Slide-in node palette: collapses to 0-width unless `paletteOpen`. -->
|
|
1196
|
+
<div class="h-full min-h-0 flex-1 bg-[#f7f6fb] relative">
|
|
299
1197
|
<div
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
<NodePalette />
|
|
304
|
-
</div>
|
|
305
|
-
|
|
306
|
-
<div
|
|
307
|
-
class="relative flex-1 min-w-0"
|
|
1198
|
+
ref="canvasWrapRef"
|
|
1199
|
+
class="relative h-full w-full transition-shadow duration-150"
|
|
1200
|
+
:class="dragActive ? 'canvas-drop-active' : ''"
|
|
308
1201
|
@drop="onDrop"
|
|
309
1202
|
@dragover="onDragOver"
|
|
1203
|
+
@dragleave="onDragLeave"
|
|
310
1204
|
>
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
:
|
|
315
|
-
:
|
|
316
|
-
:
|
|
317
|
-
|
|
318
|
-
:connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
|
|
319
|
-
:is-valid-connection="isValidConnection"
|
|
320
|
-
:snap-to-grid="true"
|
|
321
|
-
:snap-grid="[20, 20]"
|
|
322
|
-
:selection-mode="SelectionMode.Partial"
|
|
323
|
-
:pan-on-scroll-mode="PanOnScrollMode.Free"
|
|
324
|
-
:delete-key-code="['Backspace', 'Delete']"
|
|
325
|
-
:min-zoom="0.25"
|
|
326
|
-
:max-zoom="2"
|
|
327
|
-
fit-view-on-init
|
|
1205
|
+
<WorkflowContextMenu
|
|
1206
|
+
:kind="menu.kind"
|
|
1207
|
+
:selection-count="flow.getSelectedNodes.value.length"
|
|
1208
|
+
:total-nodes="vfNodes.length"
|
|
1209
|
+
:target-is-trigger="!!menu.targetId && blob.triggers.some((t) => t.id === menu.targetId)"
|
|
1210
|
+
:run-busy="runBusy"
|
|
1211
|
+
@action="onMenuAction"
|
|
328
1212
|
>
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
:
|
|
336
|
-
:
|
|
337
|
-
:
|
|
338
|
-
|
|
339
|
-
|
|
1213
|
+
<VueFlow
|
|
1214
|
+
v-model:nodes="vfNodes"
|
|
1215
|
+
v-model:edges="vfEdges"
|
|
1216
|
+
:node-types="nodeTypes"
|
|
1217
|
+
:edge-types="edgeTypes"
|
|
1218
|
+
:default-edge-options="{ type: 'flow' }"
|
|
1219
|
+
:connection-line-type="ConnectionLineType.Step"
|
|
1220
|
+
:connection-line-style="{ stroke: '#94a3b8', strokeWidth: 1.75, strokeDasharray: '4 4' }"
|
|
1221
|
+
:is-valid-connection="isValidConnection"
|
|
1222
|
+
:snap-to-grid="true"
|
|
1223
|
+
:snap-grid="[20, 20]"
|
|
1224
|
+
:selection-mode="SelectionMode.Partial"
|
|
1225
|
+
:selection-key-code="pointerMode === 'select' ? true : 'Shift'"
|
|
1226
|
+
:pan-on-drag="pointerMode === 'hand' ? true : [1]"
|
|
1227
|
+
:pan-on-scroll="true"
|
|
1228
|
+
:pan-on-scroll-mode="PanOnScrollMode.Free"
|
|
1229
|
+
:delete-key-code="['Backspace', 'Delete']"
|
|
1230
|
+
:edges-updatable="true"
|
|
1231
|
+
:min-zoom="0.25"
|
|
1232
|
+
:max-zoom="4"
|
|
1233
|
+
>
|
|
1234
|
+
<Background pattern-color="#cbd5e1" :gap="20" :size="1.2" />
|
|
1235
|
+
</VueFlow>
|
|
1236
|
+
</WorkflowContextMenu>
|
|
1237
|
+
|
|
1238
|
+
<!-- Grid-snapped placement ghost while a palette drag hovers the canvas.
|
|
1239
|
+
Positioned in pane coordinates (flow → screen via the viewport). -->
|
|
1240
|
+
<Transition
|
|
1241
|
+
enter-active-class="transition-all duration-100 ease-out"
|
|
1242
|
+
enter-from-class="opacity-0 scale-90"
|
|
1243
|
+
enter-to-class="opacity-100 scale-100"
|
|
1244
|
+
leave-active-class="transition-opacity duration-100"
|
|
1245
|
+
leave-to-class="opacity-0"
|
|
1246
|
+
>
|
|
1247
|
+
<div
|
|
1248
|
+
v-if="ghostRect"
|
|
1249
|
+
class="pointer-events-none absolute z-10 rounded-lg border-2 border-dashed transition-[left,top] duration-75 ease-out"
|
|
1250
|
+
:class="nodeDrag.dragging.value?.kind === 'trigger' ? 'border-emerald-400/80 bg-emerald-400/[0.08]' : nodeDrag.dragging.value?.kind === 'group' ? 'border-slate-400/80 bg-slate-400/[0.08]' : 'border-violet-400/80 bg-violet-400/[0.08]'"
|
|
1251
|
+
:style="{
|
|
1252
|
+
left: `${ghostRect.left}px`,
|
|
1253
|
+
top: `${ghostRect.top}px`,
|
|
1254
|
+
width: `${ghostRect.width}px`,
|
|
1255
|
+
height: `${ghostRect.height}px`
|
|
1256
|
+
}"
|
|
1257
|
+
>
|
|
1258
|
+
<span
|
|
1259
|
+
class="absolute -top-0.5 left-1/2 -translate-x-1/2 -translate-y-full rounded-md px-1.5 py-0.5 text-xs font-medium text-white shadow-sm"
|
|
1260
|
+
:class="nodeDrag.dragging.value?.kind === 'trigger' ? 'bg-emerald-500' : nodeDrag.dragging.value?.kind === 'group' ? 'bg-slate-500' : 'bg-violet-500'"
|
|
1261
|
+
>
|
|
1262
|
+
Drop to add
|
|
1263
|
+
</span>
|
|
1264
|
+
</div>
|
|
1265
|
+
</Transition>
|
|
1266
|
+
|
|
1267
|
+
<!-- Headless dynamic-output resolvers (PO-143 Phase 3): one per node
|
|
1268
|
+
whose manifest declares a `resolveOutputs` port. They resolve the
|
|
1269
|
+
real ports server-side and feed them into the rendered node without
|
|
1270
|
+
touching the blob. Render nothing. -->
|
|
1271
|
+
<NodeDynamicOutputsResolver
|
|
1272
|
+
v-for="n in dynamicOutputNodes"
|
|
1273
|
+
:key="`dyn-out-${n.id}`"
|
|
1274
|
+
:node-id="n.id"
|
|
1275
|
+
:slug="n.nodeSlug"
|
|
1276
|
+
:version="n.nodeVersion"
|
|
1277
|
+
:config="n.config ?? {}"
|
|
1278
|
+
:static-outputs="findManifest(n.nodeSlug, n.nodeVersion)?.outputs ?? []"
|
|
1279
|
+
@resolved="onResolvedOutputs"
|
|
1280
|
+
/>
|
|
1281
|
+
|
|
1282
|
+
<!-- Dock preview — a ghost of the full-height dock slot that lights up
|
|
1283
|
+
while the dragged palette hovers near the left/right canvas edge. -->
|
|
1284
|
+
<div
|
|
1285
|
+
v-if="palette.dockPreview.value"
|
|
1286
|
+
class="pointer-events-none absolute top-3 bottom-16 z-[9] w-80 rounded-xl border-2 border-dashed border-emerald-400/70 bg-emerald-400/[0.08]"
|
|
1287
|
+
:class="palette.dockPreview.value === 'left' ? 'left-3' : 'right-3'"
|
|
1288
|
+
/>
|
|
340
1289
|
|
|
341
|
-
<!--
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
1290
|
+
<!-- Node palette — dockable left/right, free-floating, or collapsed to
|
|
1291
|
+
a pill. The header (grip + title) drags via pointer events so the
|
|
1292
|
+
node rows keep their HTML5 drag; the ⋮ menu offers the explicit
|
|
1293
|
+
placement actions. -->
|
|
1294
|
+
<Transition
|
|
1295
|
+
enter-active-class="transition duration-150 ease-out"
|
|
1296
|
+
enter-from-class="opacity-0 scale-95"
|
|
1297
|
+
enter-to-class="opacity-100 scale-100"
|
|
1298
|
+
leave-active-class="transition duration-100 ease-in"
|
|
1299
|
+
leave-from-class="opacity-100 scale-100"
|
|
1300
|
+
leave-to-class="opacity-0 scale-95"
|
|
1301
|
+
>
|
|
1302
|
+
<div
|
|
1303
|
+
v-if="palette.collapsed.value"
|
|
1304
|
+
key="palette-pill"
|
|
1305
|
+
class="absolute z-10"
|
|
1306
|
+
:class="palettePillClass"
|
|
1307
|
+
:style="palette.mode.value === 'floating' ? palette.floatingStyle.value : void 0"
|
|
1308
|
+
>
|
|
1309
|
+
<!-- 2-layer frame: outer glass ring + inner white pane (dashboard
|
|
1310
|
+
HeroGreeting pattern) so the pill matches the expanded panel. -->
|
|
1311
|
+
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1312
|
+
<button
|
|
1313
|
+
type="button"
|
|
1314
|
+
class="flex h-9 items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75 hover:cursor-pointer"
|
|
1315
|
+
title="Expand the node palette"
|
|
1316
|
+
@click="void focusPalette()"
|
|
1317
|
+
>
|
|
1318
|
+
<Plus :size="14" class="text-emerald-600" />
|
|
1319
|
+
Add node
|
|
1320
|
+
</button>
|
|
1321
|
+
</div>
|
|
1322
|
+
</div>
|
|
1323
|
+
<div
|
|
1324
|
+
v-else
|
|
1325
|
+
key="palette-panel"
|
|
1326
|
+
ref="paletteShellRef"
|
|
1327
|
+
class="absolute z-10 flex w-80 flex-col rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm"
|
|
1328
|
+
:class="paletteShellClass"
|
|
1329
|
+
:style="palette.mode.value === 'floating' ? palette.floatingStyle.value : void 0"
|
|
349
1330
|
>
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1331
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden border border-white bg-white rounded-lg bg-gradient-to-br from-white via-violet-50/40 to-white relative overflow-hidden">
|
|
1332
|
+
<div
|
|
1333
|
+
class="flex shrink-0 touch-none select-none items-center gap-1 border-b border-black/[0.06] bg-white px-2 py-1.5"
|
|
1334
|
+
:class="palette.dragging.value ? 'cursor-grabbing' : 'cursor-grab'"
|
|
1335
|
+
@pointerdown="palette.startDrag($event, paletteShellRef)"
|
|
1336
|
+
>
|
|
1337
|
+
<GripVertical :size="14" class="shrink-0 text-muted-foreground/60" />
|
|
1338
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">Add node</span>
|
|
1339
|
+
<DropdownMenu>
|
|
1340
|
+
<DropdownMenuTrigger as-child>
|
|
1341
|
+
<button
|
|
1342
|
+
type="button"
|
|
1343
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
1344
|
+
title="Palette placement"
|
|
1345
|
+
>
|
|
1346
|
+
<EllipsisVertical :size="14" />
|
|
1347
|
+
</button>
|
|
1348
|
+
</DropdownMenuTrigger>
|
|
1349
|
+
<DropdownMenuContent align="end" class="w-36 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
|
|
1350
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('left')">Dock left</DropdownMenuItem>
|
|
1351
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="palette.dock('right')">Dock right</DropdownMenuItem>
|
|
1352
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="palette.floatHere(paletteShellRef)">Float here</DropdownMenuItem>
|
|
1353
|
+
<DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
|
|
1354
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="palette.setCollapsed(true)">Collapse</DropdownMenuItem>
|
|
1355
|
+
</DropdownMenuContent>
|
|
1356
|
+
</DropdownMenu>
|
|
1357
|
+
<button
|
|
1358
|
+
type="button"
|
|
1359
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
1360
|
+
title="Collapse the palette"
|
|
1361
|
+
@click="palette.setCollapsed(true)"
|
|
1362
|
+
>
|
|
1363
|
+
<Minus :size="14" />
|
|
1364
|
+
</button>
|
|
1365
|
+
</div>
|
|
1366
|
+
<NodePalette ref="paletteRef" class="min-h-0 flex-1" @insert="onPaletteInsert" />
|
|
1367
|
+
</div>
|
|
1368
|
+
</div>
|
|
1369
|
+
</Transition>
|
|
1370
|
+
|
|
1371
|
+
<!-- Corner preview — lights up while the dragged minimap hovers a
|
|
1372
|
+
snap zone; release drops it into that corner. -->
|
|
1373
|
+
<div
|
|
1374
|
+
v-if="minimapLayout.dockPreview.value"
|
|
1375
|
+
class="pointer-events-none absolute z-[9] h-[190px] w-[200px] rounded-lg border-2 border-dashed border-emerald-400/70 bg-emerald-400/[0.08]"
|
|
1376
|
+
:class="minimapPreviewClass"
|
|
1377
|
+
/>
|
|
1378
|
+
|
|
1379
|
+
<!-- MiniMap — same 2-layer frame as the palette. Lives OUTSIDE the
|
|
1380
|
+
VueFlow tree (the store is provided by this component's
|
|
1381
|
+
useVueFlow), so the shell owns placement: corner-pinned,
|
|
1382
|
+
free-floating via header drag, or collapsed to a pill. -->
|
|
1383
|
+
<Transition
|
|
1384
|
+
enter-active-class="transition duration-150 ease-out"
|
|
1385
|
+
enter-from-class="opacity-0 scale-95"
|
|
1386
|
+
enter-to-class="opacity-100 scale-100"
|
|
1387
|
+
leave-active-class="transition duration-100 ease-in"
|
|
1388
|
+
leave-from-class="opacity-100 scale-100"
|
|
1389
|
+
leave-to-class="opacity-0 scale-95"
|
|
1390
|
+
>
|
|
1391
|
+
<div
|
|
1392
|
+
v-if="minimapLayout.collapsed.value"
|
|
1393
|
+
key="minimap-pill"
|
|
1394
|
+
class="absolute z-10"
|
|
1395
|
+
:class="minimapPlacementClass"
|
|
1396
|
+
:style="minimapLayout.mode.value === 'floating' ? minimapLayout.floatingStyle.value : void 0"
|
|
358
1397
|
>
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
1398
|
+
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1399
|
+
<button
|
|
1400
|
+
type="button"
|
|
1401
|
+
class="flex h-9 items-center gap-1.5 rounded-lg bg-white px-3 text-sm font-medium transition hover:bg-muted/75 hover:cursor-pointer"
|
|
1402
|
+
title="Expand the minimap"
|
|
1403
|
+
@click="minimapLayout.setCollapsed(false)"
|
|
1404
|
+
>
|
|
1405
|
+
<MapIcon :size="14" class="text-emerald-600" />
|
|
1406
|
+
Map
|
|
1407
|
+
</button>
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
<div
|
|
1411
|
+
v-else
|
|
1412
|
+
key="minimap-panel"
|
|
1413
|
+
ref="minimapShellRef"
|
|
1414
|
+
class="absolute z-10 rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm"
|
|
1415
|
+
:class="minimapPlacementClass"
|
|
1416
|
+
:style="minimapLayout.mode.value === 'floating' ? minimapLayout.floatingStyle.value : void 0"
|
|
367
1417
|
>
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
1418
|
+
<div class="flex flex-col overflow-hidden rounded-lg border border-white bg-white bg-gradient-to-br from-white via-violet-50/40 to-white">
|
|
1419
|
+
<div
|
|
1420
|
+
class="flex shrink-0 touch-none select-none items-center gap-1 border-b border-black/[0.06] px-2 py-1"
|
|
1421
|
+
:class="minimapLayout.dragging.value ? 'cursor-grabbing' : 'cursor-grab'"
|
|
1422
|
+
@pointerdown="minimapLayout.startDrag($event, minimapShellRef)"
|
|
1423
|
+
>
|
|
1424
|
+
<GripVertical :size="14" class="shrink-0 text-muted-foreground/60" />
|
|
1425
|
+
<span class="min-w-0 flex-1 truncate text-sm font-medium">Map</span>
|
|
1426
|
+
<DropdownMenu>
|
|
1427
|
+
<DropdownMenuTrigger as-child>
|
|
1428
|
+
<button
|
|
1429
|
+
type="button"
|
|
1430
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
1431
|
+
title="Minimap placement"
|
|
1432
|
+
>
|
|
1433
|
+
<EllipsisVertical :size="14" />
|
|
1434
|
+
</button>
|
|
1435
|
+
</DropdownMenuTrigger>
|
|
1436
|
+
<DropdownMenuContent align="end" class="w-40 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
|
|
1437
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-left')">Top left</DropdownMenuItem>
|
|
1438
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('top-right')">Top right</DropdownMenuItem>
|
|
1439
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-left')">Bottom left</DropdownMenuItem>
|
|
1440
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.dock('bottom-right')">Bottom right</DropdownMenuItem>
|
|
1441
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.floatHere(minimapShellRef)">Float here</DropdownMenuItem>
|
|
1442
|
+
<DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
|
|
1443
|
+
<DropdownMenuItem class="rounded-md text-sm" @select="minimapLayout.setCollapsed(true)">Collapse</DropdownMenuItem>
|
|
1444
|
+
</DropdownMenuContent>
|
|
1445
|
+
</DropdownMenu>
|
|
1446
|
+
<button
|
|
1447
|
+
type="button"
|
|
1448
|
+
class="flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition hover:bg-muted/50 hover:text-foreground"
|
|
1449
|
+
title="Collapse the minimap"
|
|
1450
|
+
@click="minimapLayout.setCollapsed(true)"
|
|
1451
|
+
>
|
|
1452
|
+
<Minus :size="14" />
|
|
1453
|
+
</button>
|
|
1454
|
+
</div>
|
|
1455
|
+
<MiniMap
|
|
1456
|
+
class="rvnxx-minimap-embedded"
|
|
1457
|
+
pannable
|
|
1458
|
+
zoomable
|
|
1459
|
+
:node-stroke-width="3"
|
|
1460
|
+
:node-color="(n2) => n2.type === 'trigger' ? '#10b981' : n2.type === 'sticky' ? '#fcd34d' : '#94a3b8'"
|
|
1461
|
+
:mask-color="'rgba(15, 23, 42, 0.06)'"
|
|
1462
|
+
/>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
</Transition>
|
|
1466
|
+
|
|
1467
|
+
<!-- Floating control pane — bottom center. -->
|
|
1468
|
+
<div class="absolute bottom-3 left-1/2 z-10 -translate-x-1/2">
|
|
1469
|
+
<TooltipProvider :delay-duration="250">
|
|
1470
|
+
<div class="rounded-lg border border-gray-400/10 outline-1 outline-white/30 backdrop-blur-sm">
|
|
1471
|
+
<div class="flex items-center gap-0.5 whitespace-nowrap border border-white bg-white rounded-lg bg-gradient-to-br from-white via-violet-50/40 to-white px-2 py-1">
|
|
1472
|
+
<!-- Smart Run: the page decides what "run" means — workflows that
|
|
1473
|
+
consume trigger.payload open the payload dialog, payload-free
|
|
1474
|
+
ones fire immediately. One button either way. -->
|
|
1475
|
+
<IntegrationsEditorTip label="Run workflow" shortcut="mod+enter">
|
|
1476
|
+
<button
|
|
1477
|
+
type="button"
|
|
1478
|
+
class="relative flex h-8 items-center gap-1.5 rounded-lg bg-gradient-to-b from-emerald-500 to-emerald-600 px-3.5 text-sm font-medium text-white transition after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_1px_0_rgba(255,255,255,0.3),inset_0_-2px_0_rgba(0,0,0,0.25)] hover:from-emerald-600 hover:to-emerald-600 active:translate-y-px disabled:opacity-60"
|
|
1479
|
+
:disabled="runBusy"
|
|
1480
|
+
@click="emit('run')"
|
|
1481
|
+
>
|
|
1482
|
+
<Loader2 v-if="runBusy" :size="14" class="animate-spin" />
|
|
1483
|
+
<Play v-else :size="14" />
|
|
1484
|
+
Run
|
|
1485
|
+
</button>
|
|
1486
|
+
</IntegrationsEditorTip>
|
|
1487
|
+
|
|
1488
|
+
<div class="mx-1 h-5 w-px bg-black/10" />
|
|
1489
|
+
|
|
1490
|
+
<div class="flex items-center gap-0.5 rounded-lg bg-black/[0.05] p-0.5 shadow-[inset_0_1px_2px_rgba(15,23,42,0.12)]">
|
|
1491
|
+
<IntegrationsEditorTip label="Select" shortcut="v">
|
|
1492
|
+
<button
|
|
1493
|
+
type="button"
|
|
1494
|
+
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1495
|
+
:class="pointerMode === 'select' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
|
|
1496
|
+
@click="setPointerMode('select')"
|
|
1497
|
+
>
|
|
1498
|
+
<MousePointer2 :size="14" />
|
|
1499
|
+
</button>
|
|
1500
|
+
</IntegrationsEditorTip>
|
|
1501
|
+
<IntegrationsEditorTip label="Hand (pan)" shortcut="h">
|
|
1502
|
+
<button
|
|
1503
|
+
type="button"
|
|
1504
|
+
class="flex size-7 items-center justify-center rounded-lg transition"
|
|
1505
|
+
:class="pointerMode === 'hand' ? 'bg-gradient-to-b from-white to-gray-50 text-foreground shadow-[0_1px_2px_rgba(15,23,42,0.18),inset_0_1px_0_rgba(255,255,255,0.9)]' : 'text-muted-foreground hover:text-foreground'"
|
|
1506
|
+
@click="setPointerMode('hand')"
|
|
1507
|
+
>
|
|
1508
|
+
<Hand :size="14" />
|
|
1509
|
+
</button>
|
|
1510
|
+
</IntegrationsEditorTip>
|
|
1511
|
+
</div>
|
|
1512
|
+
|
|
1513
|
+
<IntegrationsEditorTip label="Tidy up" shortcut="shift+alt+t">
|
|
1514
|
+
<button
|
|
1515
|
+
type="button"
|
|
1516
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1517
|
+
@click="tidyUp"
|
|
1518
|
+
>
|
|
1519
|
+
<LayoutGrid :size="13" />
|
|
1520
|
+
Tidy up
|
|
1521
|
+
</button>
|
|
1522
|
+
</IntegrationsEditorTip>
|
|
1523
|
+
<IntegrationsEditorTip label="Fit view" shortcut="1">
|
|
1524
|
+
<button
|
|
1525
|
+
type="button"
|
|
1526
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1527
|
+
@click="flow.fitView({ padding: 0.2, duration: 400, maxZoom: 1 })"
|
|
1528
|
+
>
|
|
1529
|
+
<Sparkles :size="13" />
|
|
1530
|
+
Fit
|
|
1531
|
+
</button>
|
|
1532
|
+
</IntegrationsEditorTip>
|
|
1533
|
+
<IntegrationsEditorTip label="Add sticky note" shortcut="shift+s">
|
|
1534
|
+
<button
|
|
1535
|
+
type="button"
|
|
1536
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1537
|
+
@click="addSticky()"
|
|
1538
|
+
>
|
|
1539
|
+
<StickyNote :size="13" />
|
|
1540
|
+
Note
|
|
1541
|
+
</button>
|
|
1542
|
+
</IntegrationsEditorTip>
|
|
1543
|
+
<IntegrationsEditorTip label="Add group frame — nodes dropped inside join it">
|
|
1544
|
+
<button
|
|
1545
|
+
type="button"
|
|
1546
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1547
|
+
@click="addGroupFrame"
|
|
1548
|
+
>
|
|
1549
|
+
<GroupIcon :size="13" />
|
|
1550
|
+
Group
|
|
1551
|
+
</button>
|
|
1552
|
+
</IntegrationsEditorTip>
|
|
1553
|
+
<IntegrationsEditorTip label="Workflow variables" shortcut="shift+v">
|
|
1554
|
+
<button
|
|
1555
|
+
type="button"
|
|
1556
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1557
|
+
@click="variablesOpen = true"
|
|
1558
|
+
>
|
|
1559
|
+
<Braces :size="13" />
|
|
1560
|
+
Variables
|
|
1561
|
+
<span v-if="variableCount" class="rounded-lg bg-muted px-1.5 text-xs tabular-nums text-muted-foreground">{{ variableCount }}</span>
|
|
1562
|
+
</button>
|
|
1563
|
+
</IntegrationsEditorTip>
|
|
1564
|
+
<IntegrationsEditorTip label="Workflow settings" shortcut="shift+alt+s">
|
|
1565
|
+
<button
|
|
1566
|
+
type="button"
|
|
1567
|
+
class="flex h-8 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-foreground transition hover:bg-black/[0.04]"
|
|
1568
|
+
@click="emit('openSettings')"
|
|
1569
|
+
>
|
|
1570
|
+
<Settings2 :size="13" />
|
|
1571
|
+
Settings
|
|
1572
|
+
</button>
|
|
1573
|
+
</IntegrationsEditorTip>
|
|
1574
|
+
|
|
1575
|
+
<div class="mx-1 h-5 w-px bg-black/10" />
|
|
1576
|
+
|
|
1577
|
+
<IntegrationsEditorTip label="Zoom out" shortcut="-">
|
|
1578
|
+
<button
|
|
1579
|
+
type="button"
|
|
1580
|
+
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1581
|
+
@click="flow.zoomOut({ duration: 150 })"
|
|
1582
|
+
>
|
|
1583
|
+
<ZoomOut :size="14" />
|
|
1584
|
+
</button>
|
|
1585
|
+
</IntegrationsEditorTip>
|
|
1586
|
+
<!-- Tooltip sits on a wrapper span so its as-child merge never
|
|
1587
|
+
touches the dropdown trigger (merging both onto one button
|
|
1588
|
+
swallowed the trigger's pointerdown and the menu never opened). -->
|
|
1589
|
+
<IntegrationsEditorTip label="Zoom level, 0 resets to 100%" shortcut="0">
|
|
1590
|
+
<span class="inline-flex">
|
|
1591
|
+
<DropdownMenu v-model:open="zoomMenuOpen">
|
|
1592
|
+
<DropdownMenuTrigger as-child>
|
|
1593
|
+
<button
|
|
1594
|
+
type="button"
|
|
1595
|
+
class="w-12 rounded-lg py-1 text-center text-sm tabular-nums text-muted-foreground transition hover:bg-black/[0.04] hover:text-foreground"
|
|
1596
|
+
>
|
|
1597
|
+
{{ zoomPct }}
|
|
1598
|
+
</button>
|
|
1599
|
+
</DropdownMenuTrigger>
|
|
1600
|
+
<DropdownMenuContent side="top" align="center" class="w-[4.75rem] min-w-0 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
|
|
1601
|
+
<div class="px-0.5 pb-0.5">
|
|
1602
|
+
<input
|
|
1603
|
+
v-model="zoomInputText"
|
|
1604
|
+
type="text"
|
|
1605
|
+
inputmode="numeric"
|
|
1606
|
+
class="h-6 w-full rounded-md border border-black/[0.08] bg-white px-1 text-center text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-primary/40"
|
|
1607
|
+
placeholder="%"
|
|
1608
|
+
@keydown.stop
|
|
1609
|
+
@keydown.enter.prevent="applyZoomInput"
|
|
1610
|
+
>
|
|
1611
|
+
</div>
|
|
1612
|
+
<DropdownMenuSeparator class="my-1 h-px bg-black/[0.06]" />
|
|
1613
|
+
<DropdownMenuItem
|
|
1614
|
+
v-for="p in ZOOM_PRESETS"
|
|
1615
|
+
:key="p"
|
|
1616
|
+
class="justify-center rounded-md px-1 py-0.5 text-sm tabular-nums"
|
|
1617
|
+
@select="setZoomPct(p)"
|
|
1618
|
+
>
|
|
1619
|
+
{{ p }}%
|
|
1620
|
+
</DropdownMenuItem>
|
|
1621
|
+
</DropdownMenuContent>
|
|
1622
|
+
</DropdownMenu>
|
|
1623
|
+
</span>
|
|
1624
|
+
</IntegrationsEditorTip>
|
|
1625
|
+
<IntegrationsEditorTip label="Zoom in" shortcut="=">
|
|
1626
|
+
<button
|
|
1627
|
+
type="button"
|
|
1628
|
+
class="flex size-8 items-center justify-center rounded-lg text-foreground transition hover:bg-black/[0.04]"
|
|
1629
|
+
@click="flow.zoomIn({ duration: 150 })"
|
|
1630
|
+
>
|
|
1631
|
+
<ZoomIn :size="14" />
|
|
1632
|
+
</button>
|
|
1633
|
+
</IntegrationsEditorTip>
|
|
1634
|
+
</div>
|
|
1635
|
+
</div>
|
|
1636
|
+
</TooltipProvider>
|
|
371
1637
|
</div>
|
|
372
1638
|
|
|
373
1639
|
<!-- Empty-state -->
|
|
@@ -375,45 +1641,59 @@ const edgeTypes = {
|
|
|
375
1641
|
v-if="!vfNodes.length"
|
|
376
1642
|
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
377
1643
|
>
|
|
378
|
-
<div class="
|
|
1644
|
+
<div class="rounded-lg border border-dashed border-black/[0.12] bg-white/80 px-5 py-4 text-center text-sm text-muted-foreground shadow-sm backdrop-blur-sm">
|
|
379
1645
|
<div class="font-medium text-foreground mb-1">
|
|
380
1646
|
Empty canvas
|
|
381
1647
|
</div>
|
|
382
|
-
<p
|
|
383
|
-
|
|
1648
|
+
<p>
|
|
1649
|
+
Drag a trigger and an action from the panel on the left.
|
|
384
1650
|
</p>
|
|
385
|
-
<button
|
|
386
|
-
type="button"
|
|
387
|
-
class="inline-flex items-center gap-1 rounded-md bg-emerald-500 px-2 py-1 text-sm font-medium text-white shadow-sm transition hover:bg-emerald-600"
|
|
388
|
-
@click="paletteOpen = true"
|
|
389
|
-
>
|
|
390
|
-
<Plus :size="11" />
|
|
391
|
-
Open palette
|
|
392
|
-
</button>
|
|
393
1651
|
</div>
|
|
394
1652
|
</div>
|
|
395
1653
|
</div>
|
|
396
1654
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
1655
|
+
<!-- Workflow variables lightbox (control-pane entry). -->
|
|
1656
|
+
<WorkflowVariablesDialog
|
|
1657
|
+
v-model:open="variablesOpen"
|
|
1658
|
+
:variables="blob.variables ?? {}"
|
|
1659
|
+
@update:variables="updateVariables"
|
|
1660
|
+
/>
|
|
1661
|
+
|
|
1662
|
+
<!-- Node/trigger configuration — a near-fullscreen workbench lightbox
|
|
1663
|
+
(Input pane / Parameters+Settings tabs / Save+Cancel footer). The
|
|
1664
|
+
inspector edits a draft; closing without Save discards it. -->
|
|
1665
|
+
<Dialog :open="!!selection" @update:open="(v) => {
|
|
1666
|
+
if (!v) {
|
|
1667
|
+
selectedId = null;
|
|
1668
|
+
renameIntent = false;
|
|
1669
|
+
}
|
|
1670
|
+
}">
|
|
1671
|
+
<DialogContent class="flex h-[86vh] w-[min(1280px,94vw)] max-w-none flex-col gap-0 overflow-hidden rounded-lg p-0 sm:max-w-none">
|
|
1672
|
+
<NodeInspector
|
|
1673
|
+
v-if="selection"
|
|
1674
|
+
:selection="selection"
|
|
1675
|
+
:secret-keys="secretKeys"
|
|
1676
|
+
:variables="blob.variables"
|
|
1677
|
+
:upstream-outputs="selectedUpstreamOutputs"
|
|
1678
|
+
:autofocus-name="renameIntent"
|
|
1679
|
+
:nodes="blob.nodes"
|
|
1680
|
+
:edges="blob.edges"
|
|
1681
|
+
:triggers="blob.triggers"
|
|
1682
|
+
:dynamic-outputs="dynamicOutputs"
|
|
1683
|
+
:workflow-id="workflow.id"
|
|
1684
|
+
@update="applyInspectorUpdate"
|
|
1685
|
+
@close="selectedId = null"
|
|
1686
|
+
@remove="selectedId && removeBlobNode(selectedId)"
|
|
1687
|
+
@navigate="(id) => {
|
|
1688
|
+
selectedId = id;
|
|
1689
|
+
renameIntent = false;
|
|
1690
|
+
}"
|
|
1691
|
+
/>
|
|
1692
|
+
</DialogContent>
|
|
1693
|
+
</Dialog>
|
|
414
1694
|
</div>
|
|
415
1695
|
</template>
|
|
416
1696
|
|
|
417
1697
|
<style>
|
|
418
|
-
.vue-flow__attribution{display:none}.vue-flow__connection-path{stroke:#94a3b8;stroke-dasharray:4 4;stroke-width:1.75}.vue-flow__nodesselection-rect,.vue-flow__selection{background:rgba(16,185,129,.06);border:1px dashed rgba(16,185,129,.45)}.vue-flow__handle{z-index:5}.vue-flow__edge.selected .vue-flow__edge-path{filter:drop-shadow(0 0 4px rgba(99,102,241,.5))}.vue-
|
|
1698
|
+
.vue-flow__attribution{display:none}.vue-flow__connection-path{stroke:#94a3b8;stroke-dasharray:4 4;stroke-width:1.75}.vue-flow__nodesselection-rect,.vue-flow__selection{background:rgba(16,185,129,.06);border:1px dashed rgba(16,185,129,.45)}.vue-flow__handle{z-index:5}.vue-flow__edge.selected .vue-flow__edge-path{filter:drop-shadow(0 0 4px rgba(99,102,241,.5))}.vue-flow__node.node-pop-in{animation:rvnxx-node-pop .38s cubic-bezier(.2,.85,.3,1.25) both}@keyframes rvnxx-node-pop{0%{opacity:0;scale:.6}60%{opacity:1;scale:1.06}to{opacity:1;scale:1}}.vue-flow__node-toolbar{z-index:1200!important}.canvas-drop-active{box-shadow:inset 0 0 0 2px rgba(139,92,246,.35)}.rvnxx-minimap-embedded.vue-flow__minimap{background:transparent!important;border:none;border-radius:0;box-shadow:none;margin:0;position:static}
|
|
419
1699
|
</style>
|