@legatoacc3/workflow-builder 0.1.0-alpha.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/README.md +44 -0
- package/dist/components/WorkflowBuilder.vue.d.ts +27 -0
- package/dist/components/WorkflowBuilder.vue.d.ts.map +1 -0
- package/dist/contracts.d.ts +28 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/domain/builder-node-definition.d.ts +786 -0
- package/dist/domain/builder-node-definition.d.ts.map +1 -0
- package/dist/domain/builder-node-definition.js +1275 -0
- package/dist/domain/builder-node-metadata-response.d.ts +61 -0
- package/dist/domain/builder-node-metadata-response.d.ts.map +1 -0
- package/dist/domain/builder-node-metadata-response.js +0 -0
- package/dist/domain/workflow-draft.d.ts +169 -0
- package/dist/domain/workflow-draft.d.ts.map +1 -0
- package/dist/domain/workflow-draft.js +2115 -0
- package/dist/form-schema/form-schema-editor.d.ts +13 -0
- package/dist/form-schema/form-schema-editor.d.ts.map +1 -0
- package/dist/form-schema/form-schema-editor.js +65 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9298 -0
- package/dist/inspector/BuilderNodeSettingsPanel.vue.d.ts +136 -0
- package/dist/inspector/BuilderNodeSettingsPanel.vue.d.ts.map +1 -0
- package/dist/inspector/builder-node-settings-registry.d.ts +5 -0
- package/dist/inspector/builder-node-settings-registry.d.ts.map +1 -0
- package/dist/inspector/builder-node-settings-registry.js +37 -0
- package/dist/session/create-builder-session.d.ts +27 -0
- package/dist/session/create-builder-session.d.ts.map +1 -0
- package/dist/session/create-builder-session.js +670 -0
- package/dist/workflow-builder.css +112 -0
- package/package.json +71 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { defaultWorkflowCanvasConditionalBranches, getWorkflowCanvasNodeTemplate, workflowCanvasContainerStartHandleId, workflowCanvasDefaultOutputHandleId } from "@legatoacc3/workflow-canvas";
|
|
2
|
+
import { createDraftMutation } from "@legatoacc3/workflow-canvas/commands";
|
|
3
|
+
import { createWorkflowGraph } from "@legatoacc3/workflow-canvas/graph";
|
|
4
|
+
import { buildGraphEdgeId, buildGraphNodeId } from "@legatoacc3/workflow-canvas/ids";
|
|
5
|
+
//#region src/session/create-builder-session.ts
|
|
6
|
+
var terminalNodeKinds = ["end", "answer"];
|
|
7
|
+
var explicitWorkflowEndNodeKinds = ["end"];
|
|
8
|
+
var containerNodeKinds = ["iteration", "loop"];
|
|
9
|
+
var containerStartNodeKinds = ["iteration-start", "loop-start"];
|
|
10
|
+
var userInputNodeKinds = ["start"];
|
|
11
|
+
var triggerNodeKinds = ["schedule-trigger", "webhook-trigger"];
|
|
12
|
+
var containerNodeSize = {
|
|
13
|
+
width: 760,
|
|
14
|
+
height: 280
|
|
15
|
+
};
|
|
16
|
+
var noteNodeSize = {
|
|
17
|
+
width: 180,
|
|
18
|
+
height: 120
|
|
19
|
+
};
|
|
20
|
+
var containerStartNodePosition = {
|
|
21
|
+
x: 28,
|
|
22
|
+
y: 144
|
|
23
|
+
};
|
|
24
|
+
var containerChildBounds = {
|
|
25
|
+
minX: 128,
|
|
26
|
+
minY: 100,
|
|
27
|
+
width: 240,
|
|
28
|
+
height: 132,
|
|
29
|
+
gutter: 56
|
|
30
|
+
};
|
|
31
|
+
var defaultIfElseBranches = defaultWorkflowCanvasConditionalBranches;
|
|
32
|
+
var maxUndoStackSize = 50;
|
|
33
|
+
var maxHistoryEntries = 30;
|
|
34
|
+
function cloneDraft(draft) {
|
|
35
|
+
const nextDraft = {
|
|
36
|
+
...structuredClone({
|
|
37
|
+
...draft,
|
|
38
|
+
graphState: void 0
|
|
39
|
+
}),
|
|
40
|
+
graphState: createWorkflowGraph(draft.graphState)
|
|
41
|
+
};
|
|
42
|
+
normalizeAutoGeneratedNodeDescriptions(nextDraft);
|
|
43
|
+
normalizeContainerGraphState(nextDraft);
|
|
44
|
+
return nextDraft;
|
|
45
|
+
}
|
|
46
|
+
function isContainerNodeKind(kind) {
|
|
47
|
+
return kind ? containerNodeKinds.includes(kind) : false;
|
|
48
|
+
}
|
|
49
|
+
function isContainerStartNodeKind(kind) {
|
|
50
|
+
return kind ? containerStartNodeKinds.includes(kind) : false;
|
|
51
|
+
}
|
|
52
|
+
function isUserInputNodeKind(kind) {
|
|
53
|
+
return kind ? userInputNodeKinds.includes(kind) : false;
|
|
54
|
+
}
|
|
55
|
+
function isTriggerNodeKind(kind) {
|
|
56
|
+
return kind ? triggerNodeKinds.includes(kind) : false;
|
|
57
|
+
}
|
|
58
|
+
function isEntryNodeKind(kind) {
|
|
59
|
+
return isUserInputNodeKind(kind) || isTriggerNodeKind(kind);
|
|
60
|
+
}
|
|
61
|
+
function canCreateEntryNode(draft, kind) {
|
|
62
|
+
if (!isEntryNodeKind(kind)) return true;
|
|
63
|
+
const hasUserInputNode = draft.graphState.nodes.some((node) => isUserInputNodeKind(node.data?.kind));
|
|
64
|
+
const hasTriggerNode = draft.graphState.nodes.some((node) => isTriggerNodeKind(node.data?.kind));
|
|
65
|
+
if (isUserInputNodeKind(kind)) return !hasUserInputNode && !hasTriggerNode;
|
|
66
|
+
return !hasUserInputNode;
|
|
67
|
+
}
|
|
68
|
+
function canCreateInsideParent(draft, kind, parentNodeId) {
|
|
69
|
+
if (!parentNodeId) return true;
|
|
70
|
+
const parentNode = draft.graphState.nodes.find((node) => node.id === parentNodeId);
|
|
71
|
+
return Boolean(parentNode && isContainerNodeKind(parentNode.data?.kind) && !explicitWorkflowEndNodeKinds.includes(kind));
|
|
72
|
+
}
|
|
73
|
+
function getContainerStartNodeKind(containerKind) {
|
|
74
|
+
if (containerKind === "iteration") return "iteration-start";
|
|
75
|
+
if (containerKind === "loop") return "loop-start";
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function getNodeParentId(node) {
|
|
79
|
+
return node.parentNodeId ?? node.parentNode;
|
|
80
|
+
}
|
|
81
|
+
function clamp(value, min, max) {
|
|
82
|
+
return Math.min(Math.max(value, min), max);
|
|
83
|
+
}
|
|
84
|
+
function parsePixelDimension(value) {
|
|
85
|
+
if (typeof value === "number") return value;
|
|
86
|
+
if (typeof value === "string") {
|
|
87
|
+
const parsed = Number.parseFloat(value);
|
|
88
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getContainerFrameSize(containerNode) {
|
|
92
|
+
if (!containerNode || !isContainerNodeKind(containerNode.data?.kind)) return containerNodeSize;
|
|
93
|
+
const style = containerNode.style;
|
|
94
|
+
return {
|
|
95
|
+
width: parsePixelDimension(style?.width) ?? containerNodeSize.width,
|
|
96
|
+
height: parsePixelDimension(style?.height) ?? containerNodeSize.height
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function clampContainerChildPosition(position, containerNode) {
|
|
100
|
+
const frameSize = getContainerFrameSize(containerNode);
|
|
101
|
+
return {
|
|
102
|
+
x: clamp(position.x, containerChildBounds.minX, frameSize.width - containerChildBounds.width - containerChildBounds.gutter),
|
|
103
|
+
y: clamp(position.y, containerChildBounds.minY, Math.max(containerChildBounds.minY, frameSize.height - containerChildBounds.height - containerChildBounds.gutter))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getDefaultSourceHandleId(sourceNode, requestedSourceHandleId) {
|
|
107
|
+
if (requestedSourceHandleId) return requestedSourceHandleId;
|
|
108
|
+
if (sourceNode && isContainerNodeKind(sourceNode.data?.kind)) return workflowCanvasDefaultOutputHandleId;
|
|
109
|
+
return requestedSourceHandleId;
|
|
110
|
+
}
|
|
111
|
+
function resolveEdgeSourceHandle(draft, sourceNodeId, targetNodeId, sourceHandleId) {
|
|
112
|
+
const sourceNode = draft.graphState.nodes.find((node) => node.id === sourceNodeId);
|
|
113
|
+
const targetNode = draft.graphState.nodes.find((node) => node.id === targetNodeId);
|
|
114
|
+
if (sourceHandleId === workflowCanvasContainerStartHandleId) return targetNode && getNodeParentId(targetNode) === sourceNodeId ? workflowCanvasContainerStartHandleId : workflowCanvasDefaultOutputHandleId;
|
|
115
|
+
if (!sourceHandleId && isContainerNodeKind(sourceNode?.data?.kind)) return workflowCanvasDefaultOutputHandleId;
|
|
116
|
+
return getDefaultSourceHandleId(sourceNode, sourceHandleId);
|
|
117
|
+
}
|
|
118
|
+
function buildContainerStartNodeId(containerNodeId) {
|
|
119
|
+
return `${containerNodeId}__start`;
|
|
120
|
+
}
|
|
121
|
+
function getContainerStartNode(draft, containerNodeId) {
|
|
122
|
+
return draft.graphState.nodes.find((node) => getNodeParentId(node) === containerNodeId && isContainerStartNodeKind(node.data?.kind));
|
|
123
|
+
}
|
|
124
|
+
function buildContainerStartNode(containerNode) {
|
|
125
|
+
const kind = getContainerStartNodeKind(containerNode.data?.kind);
|
|
126
|
+
if (!kind) return null;
|
|
127
|
+
return {
|
|
128
|
+
id: buildContainerStartNodeId(containerNode.id),
|
|
129
|
+
type: "default",
|
|
130
|
+
position: { ...containerStartNodePosition },
|
|
131
|
+
parentNode: containerNode.id,
|
|
132
|
+
parentNodeId: containerNode.id,
|
|
133
|
+
extent: "parent",
|
|
134
|
+
expandParent: false,
|
|
135
|
+
data: {
|
|
136
|
+
label: kind === "loop-start" ? "Loop Start" : "Iteration Start",
|
|
137
|
+
kind,
|
|
138
|
+
description: "Entry point for this sub-workflow."
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function ensureContainerStartNodes(draft) {
|
|
143
|
+
const nextStartNodes = [];
|
|
144
|
+
for (const node of draft.graphState.nodes) {
|
|
145
|
+
if (!isContainerNodeKind(node.data?.kind) || getContainerStartNode(draft, node.id)) continue;
|
|
146
|
+
const startNode = buildContainerStartNode(node);
|
|
147
|
+
if (startNode) nextStartNodes.push(startNode);
|
|
148
|
+
}
|
|
149
|
+
draft.graphState.nodes.push(...nextStartNodes);
|
|
150
|
+
}
|
|
151
|
+
function normalizeContainerGraphState(draft) {
|
|
152
|
+
ensureContainerStartNodes(draft);
|
|
153
|
+
for (const node of draft.graphState.nodes) {
|
|
154
|
+
const parentNodeId = getNodeParentId(node);
|
|
155
|
+
if (!parentNodeId) continue;
|
|
156
|
+
node.parentNode = parentNodeId;
|
|
157
|
+
node.parentNodeId = parentNodeId;
|
|
158
|
+
node.extent = "parent";
|
|
159
|
+
node.expandParent = false;
|
|
160
|
+
const parentNode = draft.graphState.nodes.find((entry) => entry.id === parentNodeId);
|
|
161
|
+
node.position = isContainerStartNodeKind(node.data?.kind) ? { ...containerStartNodePosition } : clampContainerChildPosition(node.position, parentNode);
|
|
162
|
+
}
|
|
163
|
+
const seenEdgeKeys = /* @__PURE__ */ new Set();
|
|
164
|
+
const nextEdges = [];
|
|
165
|
+
for (const edge of draft.graphState.edges) {
|
|
166
|
+
const targetNode = draft.graphState.nodes.find((node) => node.id === edge.target);
|
|
167
|
+
const legacyContainerStart = edge.sourceHandle === workflowCanvasContainerStartHandleId;
|
|
168
|
+
const sourceNodeId = legacyContainerStart && targetNode && getNodeParentId(targetNode) === edge.source ? getContainerStartNode(draft, edge.source)?.id ?? edge.source : edge.source;
|
|
169
|
+
const sourceHandle = resolveEdgeSourceHandle(draft, sourceNodeId, edge.target, legacyContainerStart ? workflowCanvasDefaultOutputHandleId : edge.sourceHandle);
|
|
170
|
+
const edgeKey = `${sourceNodeId}:${sourceHandle ?? ""}->${edge.target}:${edge.targetHandle ?? ""}`;
|
|
171
|
+
if (seenEdgeKeys.has(edgeKey)) continue;
|
|
172
|
+
seenEdgeKeys.add(edgeKey);
|
|
173
|
+
nextEdges.push({
|
|
174
|
+
...edge,
|
|
175
|
+
id: buildGraphEdgeId(sourceNodeId, edge.target, sourceHandle),
|
|
176
|
+
source: sourceNodeId,
|
|
177
|
+
sourceHandle
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
draft.graphState.edges = nextEdges;
|
|
181
|
+
}
|
|
182
|
+
function buildHistoryId() {
|
|
183
|
+
return `history-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
184
|
+
}
|
|
185
|
+
function formatHistoryTimestamp() {
|
|
186
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
|
187
|
+
hour: "2-digit",
|
|
188
|
+
minute: "2-digit",
|
|
189
|
+
second: "2-digit"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function getMutationHistoryLabel(mutation) {
|
|
193
|
+
switch (mutation.type) {
|
|
194
|
+
case "nodes-repositioned": return "Moved node";
|
|
195
|
+
case "nodes-resized": return "Resized node";
|
|
196
|
+
case "node-remove-requested": return "Deleted node";
|
|
197
|
+
case "node-note-updated": return "Updated note";
|
|
198
|
+
case "node-label-updated": return "Updated node title";
|
|
199
|
+
case "conditional-branch-create-requested": return "Added ELIF case";
|
|
200
|
+
case "conditional-branch-remove-requested": return "Removed ELIF case";
|
|
201
|
+
case "edge-connected": return "Connected nodes";
|
|
202
|
+
case "edge-remove-requested": return "Deleted connection";
|
|
203
|
+
case "node-create-requested": return "Added node";
|
|
204
|
+
default: return "Updated canvas";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function isUndoableMutation(mutation) {
|
|
208
|
+
return !["selection-changed", "viewport-changed"].includes(mutation.type);
|
|
209
|
+
}
|
|
210
|
+
function buildBranchLabel(sourceHandleId) {
|
|
211
|
+
if (!sourceHandleId) return;
|
|
212
|
+
if (sourceHandleId === "if-branch") return "IF";
|
|
213
|
+
if (sourceHandleId === "else-branch") return "ELSE";
|
|
214
|
+
if (sourceHandleId === "elif-branch") return "ELIF 1";
|
|
215
|
+
const dynamicElifMatch = /^elif-branch-(\d+)$/.exec(sourceHandleId);
|
|
216
|
+
return dynamicElifMatch ? `ELIF ${dynamicElifMatch[1]}` : void 0;
|
|
217
|
+
}
|
|
218
|
+
function getElifBranchNumber(branchId) {
|
|
219
|
+
if (branchId === "elif-branch") return 1;
|
|
220
|
+
const dynamicElifMatch = /^elif-branch-(\d+)$/.exec(branchId);
|
|
221
|
+
return dynamicElifMatch ? Number(dynamicElifMatch[1]) : 0;
|
|
222
|
+
}
|
|
223
|
+
function buildNodePresentation(kind) {
|
|
224
|
+
const template = getWorkflowCanvasNodeTemplate(kind);
|
|
225
|
+
const presentation = {
|
|
226
|
+
labelBase: template.labelBase,
|
|
227
|
+
kind: template.kind,
|
|
228
|
+
description: template.description
|
|
229
|
+
};
|
|
230
|
+
if ("noteContent" in template && template.noteContent !== void 0) presentation.noteContent = template.noteContent;
|
|
231
|
+
return presentation;
|
|
232
|
+
}
|
|
233
|
+
function normalizeAutoGeneratedNodeDescriptions(draft) {
|
|
234
|
+
for (const node of draft.graphState.nodes) {
|
|
235
|
+
const kind = node.data?.kind;
|
|
236
|
+
if (!node.data || !kind || kind === "note") continue;
|
|
237
|
+
const generatedDescription = buildNodePresentation(kind).description;
|
|
238
|
+
if (node.data.description === generatedDescription) node.data = {
|
|
239
|
+
...node.data,
|
|
240
|
+
description: void 0
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildWorkflowNode(draft, kind, position, parentNodeId) {
|
|
245
|
+
const kindCount = draft.graphState.nodes.filter((node) => node.data?.kind === kind).length + 1;
|
|
246
|
+
const presentation = buildNodePresentation(kind);
|
|
247
|
+
return {
|
|
248
|
+
id: buildGraphNodeId(kindCount, kind),
|
|
249
|
+
type: kind === "note" ? "default" : isEntryNodeKind(kind) ? "input" : terminalNodeKinds.includes(kind) ? "output" : "default",
|
|
250
|
+
position: parentNodeId ? isContainerStartNodeKind(kind) ? { ...containerStartNodePosition } : { ...position } : position,
|
|
251
|
+
parentNode: parentNodeId,
|
|
252
|
+
parentNodeId,
|
|
253
|
+
extent: parentNodeId ? "parent" : void 0,
|
|
254
|
+
expandParent: false,
|
|
255
|
+
data: {
|
|
256
|
+
label: `${presentation.labelBase} ${kindCount}`,
|
|
257
|
+
kind: presentation.kind,
|
|
258
|
+
description: kind === "note" ? presentation.description : void 0,
|
|
259
|
+
noteContent: "noteContent" in presentation ? presentation.noteContent : void 0,
|
|
260
|
+
conditionalBranches: kind === "if-else" ? structuredClone(defaultIfElseBranches) : void 0,
|
|
261
|
+
toolReferenceId: kind === "tool" ? void 0 : void 0,
|
|
262
|
+
toolReferenceLabel: kind === "tool" ? void 0 : void 0,
|
|
263
|
+
toolReferenceSource: kind === "tool" ? void 0 : void 0,
|
|
264
|
+
toolReferenceVersion: kind === "tool" ? void 0 : void 0
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function buildEdge(sourceNodeId, targetNodeId, sourceHandleId) {
|
|
269
|
+
const branchLabel = buildBranchLabel(sourceHandleId);
|
|
270
|
+
return {
|
|
271
|
+
id: buildGraphEdgeId(sourceNodeId, targetNodeId, sourceHandleId),
|
|
272
|
+
source: sourceNodeId,
|
|
273
|
+
sourceHandle: sourceHandleId,
|
|
274
|
+
target: targetNodeId,
|
|
275
|
+
type: "default",
|
|
276
|
+
animated: false,
|
|
277
|
+
data: branchLabel ? { branchLabel } : void 0
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function addConditionalBranchToDraft(draft, nodeId) {
|
|
281
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === nodeId);
|
|
282
|
+
if (node?.data?.kind !== "if-else") return false;
|
|
283
|
+
const currentBranches = node.data.conditionalBranches?.length ? structuredClone(node.data.conditionalBranches) : structuredClone(defaultIfElseBranches);
|
|
284
|
+
const nextElifNumber = Math.max(0, ...currentBranches.map((branch) => getElifBranchNumber(branch.id))) + 1;
|
|
285
|
+
const nextBranch = {
|
|
286
|
+
id: nextElifNumber === 1 ? "elif-branch" : `elif-branch-${nextElifNumber}`,
|
|
287
|
+
type: "elif",
|
|
288
|
+
label: `ELIF ${nextElifNumber}`,
|
|
289
|
+
description: "Additional condition path"
|
|
290
|
+
};
|
|
291
|
+
const elseIndex = currentBranches.findIndex((branch) => branch.type === "else");
|
|
292
|
+
if (currentBranches.some((branch) => branch.id === nextBranch.id)) return false;
|
|
293
|
+
if (elseIndex === -1) currentBranches.push(nextBranch);
|
|
294
|
+
else currentBranches.splice(elseIndex, 0, nextBranch);
|
|
295
|
+
node.data = {
|
|
296
|
+
...node.data,
|
|
297
|
+
conditionalBranches: currentBranches
|
|
298
|
+
};
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
function removeConditionalBranchFromDraft(draft, nodeId, branchId) {
|
|
302
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === nodeId);
|
|
303
|
+
if (node?.data?.kind !== "if-else") return false;
|
|
304
|
+
const currentBranches = node.data.conditionalBranches?.length ? structuredClone(node.data.conditionalBranches) : structuredClone(defaultIfElseBranches);
|
|
305
|
+
if (currentBranches.find((entry) => entry.id === branchId)?.type !== "elif") return false;
|
|
306
|
+
node.data = {
|
|
307
|
+
...node.data,
|
|
308
|
+
conditionalBranches: currentBranches.filter((entry) => entry.id !== branchId)
|
|
309
|
+
};
|
|
310
|
+
draft.graphState.edges = draft.graphState.edges.filter((edge) => !(edge.source === nodeId && edge.sourceHandle === branchId));
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
function appendStandaloneNode(draft, node) {
|
|
314
|
+
draft.graphState.nodes.push(node);
|
|
315
|
+
const startNode = buildContainerStartNode(node);
|
|
316
|
+
if (startNode) draft.graphState.nodes.push(startNode);
|
|
317
|
+
}
|
|
318
|
+
function buildAppendNodePosition(draft, sourceNode) {
|
|
319
|
+
const parentNodeId = getNodeParentId(sourceNode);
|
|
320
|
+
if (parentNodeId) {
|
|
321
|
+
const parentNode = draft.graphState.nodes.find((entry) => entry.id === parentNodeId);
|
|
322
|
+
if (isContainerStartNodeKind(sourceNode.data?.kind)) return clampContainerChildPosition({
|
|
323
|
+
x: containerChildBounds.minX,
|
|
324
|
+
y: containerChildBounds.minY
|
|
325
|
+
}, parentNode);
|
|
326
|
+
return clampContainerChildPosition({
|
|
327
|
+
x: sourceNode.position.x + containerChildBounds.width + containerChildBounds.gutter,
|
|
328
|
+
y: sourceNode.position.y
|
|
329
|
+
}, parentNode);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
x: sourceNode.position.x + 320,
|
|
333
|
+
y: sourceNode.position.y
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function removeNodeFromDraft(draft, nodeId) {
|
|
337
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === nodeId);
|
|
338
|
+
if (!node || isContainerStartNodeKind(node.data?.kind)) return false;
|
|
339
|
+
const removedNodeIds = new Set([nodeId]);
|
|
340
|
+
let didAddDescendant = true;
|
|
341
|
+
while (didAddDescendant) {
|
|
342
|
+
didAddDescendant = false;
|
|
343
|
+
for (const entry of draft.graphState.nodes) {
|
|
344
|
+
const parentNodeId = getNodeParentId(entry);
|
|
345
|
+
if (parentNodeId && removedNodeIds.has(parentNodeId) && !removedNodeIds.has(entry.id)) {
|
|
346
|
+
removedNodeIds.add(entry.id);
|
|
347
|
+
didAddDescendant = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
draft.graphState.nodes = draft.graphState.nodes.filter((entry) => !removedNodeIds.has(entry.id));
|
|
352
|
+
draft.graphState.edges = draft.graphState.edges.filter((edge) => !removedNodeIds.has(edge.source) && !removedNodeIds.has(edge.target));
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function appendNodeFromSource(draft, node, sourceNodeId, sourceHandleId, options = {}) {
|
|
356
|
+
const sourceNode = draft.graphState.nodes.find((entry) => entry.id === sourceNodeId);
|
|
357
|
+
if (sourceNode && !options.preserveExistingPosition) node.position = buildAppendNodePosition(draft, sourceNode);
|
|
358
|
+
draft.graphState.nodes.push(node);
|
|
359
|
+
const startNode = buildContainerStartNode(node);
|
|
360
|
+
if (startNode) draft.graphState.nodes.push(startNode);
|
|
361
|
+
const resolvedSourceHandleId = resolveEdgeSourceHandle(draft, sourceNodeId, node.id, sourceHandleId);
|
|
362
|
+
draft.graphState.edges = draft.graphState.edges.concat(buildEdge(sourceNodeId, node.id, resolvedSourceHandleId));
|
|
363
|
+
}
|
|
364
|
+
function prependNodeToTarget(draft, node, targetNodeId) {
|
|
365
|
+
const incomingEdges = draft.graphState.edges.filter((edge) => edge.target === targetNodeId);
|
|
366
|
+
if (incomingEdges.length > 1) return false;
|
|
367
|
+
draft.graphState.nodes.push(node);
|
|
368
|
+
const startNode = buildContainerStartNode(node);
|
|
369
|
+
if (startNode) draft.graphState.nodes.push(startNode);
|
|
370
|
+
const workflowNodeSourceHandleId = getDefaultSourceHandleId(node);
|
|
371
|
+
draft.graphState.edges = draft.graphState.edges.filter((edge) => edge.target !== targetNodeId).concat(incomingEdges.map((edge) => buildEdge(edge.source, node.id, resolveEdgeSourceHandle(draft, edge.source, node.id, edge.sourceHandle)))).concat(buildEdge(node.id, targetNodeId, workflowNodeSourceHandleId));
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
function cloneNodeData(data) {
|
|
375
|
+
return data ? structuredClone(data) : void 0;
|
|
376
|
+
}
|
|
377
|
+
function buildNodeDuplicate(draft, sourceNode) {
|
|
378
|
+
const nextKind = sourceNode.data?.kind ?? "approval";
|
|
379
|
+
const nextCount = draft.graphState.nodes.filter((node) => node.data?.kind === nextKind).length + 1;
|
|
380
|
+
const labelBase = buildNodePresentation(nextKind).labelBase;
|
|
381
|
+
const nextData = cloneNodeData(sourceNode.data);
|
|
382
|
+
if (nextData) nextData.label = `${labelBase} ${nextCount}`;
|
|
383
|
+
return {
|
|
384
|
+
...sourceNode,
|
|
385
|
+
id: buildGraphNodeId(nextCount, nextKind),
|
|
386
|
+
position: {
|
|
387
|
+
x: sourceNode.position.x + 48,
|
|
388
|
+
y: sourceNode.position.y + 48
|
|
389
|
+
},
|
|
390
|
+
data: nextData,
|
|
391
|
+
parentNode: void 0,
|
|
392
|
+
parentNodeId: void 0,
|
|
393
|
+
extent: void 0,
|
|
394
|
+
expandParent: false
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function duplicateNodeInDraft(draft, nodeId) {
|
|
398
|
+
const sourceNode = draft.graphState.nodes.find((entry) => entry.id === nodeId);
|
|
399
|
+
if (!sourceNode || isContainerStartNodeKind(sourceNode.data?.kind)) return false;
|
|
400
|
+
appendStandaloneNode(draft, buildNodeDuplicate(draft, sourceNode));
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
function pasteNodeInDraft(draft, sourceNodeId, node, position) {
|
|
404
|
+
const nextKind = node.data?.kind ?? "approval";
|
|
405
|
+
const nextCount = draft.graphState.nodes.filter((entry) => entry.data?.kind === nextKind).length + 1;
|
|
406
|
+
const labelBase = buildNodePresentation(nextKind).labelBase;
|
|
407
|
+
const nextData = cloneNodeData(node.data);
|
|
408
|
+
if (nextData) nextData.label = `${labelBase} ${nextCount}`;
|
|
409
|
+
appendStandaloneNode(draft, {
|
|
410
|
+
...node,
|
|
411
|
+
id: buildGraphNodeId(nextCount, nextKind),
|
|
412
|
+
position,
|
|
413
|
+
data: nextData,
|
|
414
|
+
parentNode: void 0,
|
|
415
|
+
parentNodeId: void 0,
|
|
416
|
+
extent: void 0,
|
|
417
|
+
expandParent: false
|
|
418
|
+
});
|
|
419
|
+
return sourceNodeId !== "";
|
|
420
|
+
}
|
|
421
|
+
function changeNodeKindInDraft(draft, nodeId, nodeKind, toolReference) {
|
|
422
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === nodeId);
|
|
423
|
+
if (!node || isContainerStartNodeKind(node.data?.kind)) return false;
|
|
424
|
+
const presentation = buildNodePresentation(nodeKind);
|
|
425
|
+
node.type = nodeKind === "note" ? "default" : isEntryNodeKind(nodeKind) ? "input" : terminalNodeKinds.includes(nodeKind) ? "output" : "default";
|
|
426
|
+
node.data = {
|
|
427
|
+
label: toolReference?.label ?? presentation.labelBase,
|
|
428
|
+
kind: presentation.kind,
|
|
429
|
+
description: toolReference?.description ?? presentation.description,
|
|
430
|
+
noteContent: "noteContent" in presentation ? presentation.noteContent : void 0,
|
|
431
|
+
conditionalBranches: nodeKind === "if-else" ? structuredClone(defaultIfElseBranches) : void 0,
|
|
432
|
+
toolReferenceId: nodeKind === "tool" ? toolReference?.id : void 0,
|
|
433
|
+
toolReferenceLabel: nodeKind === "tool" ? toolReference?.label : void 0,
|
|
434
|
+
toolReferenceSource: nodeKind === "tool" ? toolReference?.source : void 0,
|
|
435
|
+
toolReferenceVersion: nodeKind === "tool" ? toolReference?.version : void 0
|
|
436
|
+
};
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
function applyDraftGraphMutation(draft, mutation) {
|
|
440
|
+
switch (mutation.type) {
|
|
441
|
+
case "selection-changed": return false;
|
|
442
|
+
case "viewport-changed":
|
|
443
|
+
draft.graphState.viewport = mutation.payload.viewport;
|
|
444
|
+
return true;
|
|
445
|
+
case "nodes-repositioned":
|
|
446
|
+
for (const positionChange of mutation.payload.positions) {
|
|
447
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === positionChange.id);
|
|
448
|
+
if (node) node.position = positionChange.position;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
case "nodes-resized":
|
|
452
|
+
for (const sizeChange of mutation.payload.sizes) {
|
|
453
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === sizeChange.id);
|
|
454
|
+
if (node && isContainerNodeKind(node.data?.kind)) node.style = {
|
|
455
|
+
...node.style ?? {},
|
|
456
|
+
width: `${Math.max(containerNodeSize.width, sizeChange.width)}px`,
|
|
457
|
+
height: `${Math.max(containerNodeSize.height, sizeChange.height)}px`
|
|
458
|
+
};
|
|
459
|
+
else if (node?.data?.kind === "note") node.style = {
|
|
460
|
+
...node.style ?? {},
|
|
461
|
+
width: `${Math.max(noteNodeSize.width, sizeChange.width)}px`,
|
|
462
|
+
height: `${Math.max(noteNodeSize.height, sizeChange.height)}px`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
case "node-remove-requested": return removeNodeFromDraft(draft, mutation.payload.nodeId);
|
|
467
|
+
case "node-duplicate-requested": return duplicateNodeInDraft(draft, mutation.payload.nodeId);
|
|
468
|
+
case "node-paste-requested": return pasteNodeInDraft(draft, mutation.payload.sourceNodeId, mutation.payload.node, mutation.payload.position);
|
|
469
|
+
case "node-kind-change-requested": return changeNodeKindInDraft(draft, mutation.payload.nodeId, mutation.payload.nodeKind, mutation.payload.toolReference);
|
|
470
|
+
case "node-note-updated": {
|
|
471
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === mutation.payload.nodeId);
|
|
472
|
+
if (!node || node.data?.kind !== "note") return false;
|
|
473
|
+
node.data = {
|
|
474
|
+
...node.data,
|
|
475
|
+
label: mutation.payload.label ?? node.data.label,
|
|
476
|
+
description: mutation.payload.noteContent ? node.data.description : "Write your note...",
|
|
477
|
+
noteContent: mutation.payload.noteContent
|
|
478
|
+
};
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
case "node-label-updated": {
|
|
482
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === mutation.payload.nodeId);
|
|
483
|
+
if (!node?.data) return false;
|
|
484
|
+
node.data = {
|
|
485
|
+
...node.data,
|
|
486
|
+
label: mutation.payload.label
|
|
487
|
+
};
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
case "node-description-updated": {
|
|
491
|
+
const node = draft.graphState.nodes.find((entry) => entry.id === mutation.payload.nodeId);
|
|
492
|
+
if (!node?.data) return false;
|
|
493
|
+
node.data = {
|
|
494
|
+
...node.data,
|
|
495
|
+
description: mutation.payload.description
|
|
496
|
+
};
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
case "conditional-branch-create-requested": return addConditionalBranchToDraft(draft, mutation.payload.nodeId);
|
|
500
|
+
case "conditional-branch-remove-requested": return removeConditionalBranchFromDraft(draft, mutation.payload.nodeId, mutation.payload.branchId);
|
|
501
|
+
case "edge-connected": {
|
|
502
|
+
const sourceHandle = resolveEdgeSourceHandle(draft, mutation.payload.edge.source, mutation.payload.edge.target, mutation.payload.edge.sourceHandle);
|
|
503
|
+
const nextEdge = {
|
|
504
|
+
...mutation.payload.edge,
|
|
505
|
+
id: buildGraphEdgeId(mutation.payload.edge.source, mutation.payload.edge.target, sourceHandle),
|
|
506
|
+
sourceHandle,
|
|
507
|
+
data: buildBranchLabel(sourceHandle) ? { branchLabel: buildBranchLabel(sourceHandle) } : mutation.payload.edge.data
|
|
508
|
+
};
|
|
509
|
+
const exists = draft.graphState.edges.some((edge) => edge.source === nextEdge.source && edge.sourceHandle === nextEdge.sourceHandle && edge.target === nextEdge.target && edge.targetHandle === nextEdge.targetHandle);
|
|
510
|
+
if (!exists) draft.graphState.edges.push(nextEdge);
|
|
511
|
+
return !exists;
|
|
512
|
+
}
|
|
513
|
+
case "edge-remove-requested": {
|
|
514
|
+
const nextEdges = draft.graphState.edges.filter((edge) => edge.id !== mutation.payload.edgeId);
|
|
515
|
+
const didRemoveEdge = nextEdges.length !== draft.graphState.edges.length;
|
|
516
|
+
if (didRemoveEdge) draft.graphState.edges = nextEdges;
|
|
517
|
+
return didRemoveEdge;
|
|
518
|
+
}
|
|
519
|
+
case "node-create-requested": {
|
|
520
|
+
const isEntryNodeCreateRequest = isEntryNodeKind(mutation.payload.nodeKind);
|
|
521
|
+
if (!canCreateEntryNode(draft, mutation.payload.nodeKind) || !canCreateInsideParent(draft, mutation.payload.nodeKind, mutation.payload.parentNodeId) || isEntryNodeCreateRequest && (mutation.payload.parentNodeId || mutation.payload.appendFromNodeId || mutation.payload.insertBetween)) return false;
|
|
522
|
+
const workflowNode = buildWorkflowNode(draft, mutation.payload.nodeKind, mutation.payload.position, mutation.payload.parentNodeId);
|
|
523
|
+
if (mutation.payload.nodeKind === "tool" && mutation.payload.toolReference) {
|
|
524
|
+
const nodeData = workflowNode.data;
|
|
525
|
+
if (!nodeData) return false;
|
|
526
|
+
workflowNode.data = {
|
|
527
|
+
...nodeData,
|
|
528
|
+
kind: nodeData.kind,
|
|
529
|
+
label: mutation.payload.toolReference.label,
|
|
530
|
+
description: void 0,
|
|
531
|
+
toolReferenceId: mutation.payload.toolReference.id,
|
|
532
|
+
toolReferenceLabel: mutation.payload.toolReference.label,
|
|
533
|
+
toolReferenceSource: mutation.payload.toolReference.source,
|
|
534
|
+
toolReferenceVersion: mutation.payload.toolReference.version
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (mutation.payload.insertBetween) {
|
|
538
|
+
appendStandaloneNode(draft, workflowNode);
|
|
539
|
+
const insertedSourceHandleId = resolveEdgeSourceHandle(draft, mutation.payload.insertBetween.sourceNodeId, workflowNode.id, mutation.payload.insertBetween.sourceHandleId);
|
|
540
|
+
const workflowNodeSourceHandleId = getDefaultSourceHandleId(workflowNode);
|
|
541
|
+
draft.graphState.edges = draft.graphState.edges.filter((edge) => !(edge.source === mutation.payload.insertBetween?.sourceNodeId && resolveEdgeSourceHandle(draft, edge.source, edge.target, edge.sourceHandle) === insertedSourceHandleId && edge.target === mutation.payload.insertBetween?.targetNodeId)).concat([buildEdge(mutation.payload.insertBetween.sourceNodeId, workflowNode.id, insertedSourceHandleId), buildEdge(workflowNode.id, mutation.payload.insertBetween.targetNodeId, workflowNodeSourceHandleId)]);
|
|
542
|
+
} else if (mutation.payload.prependToNodeId) return prependNodeToTarget(draft, workflowNode, mutation.payload.prependToNodeId);
|
|
543
|
+
else if (mutation.payload.parentNodeId) if (mutation.payload.appendFromNodeId) appendNodeFromSource(draft, workflowNode, mutation.payload.appendFromNodeId, mutation.payload.appendFromHandleId, { preserveExistingPosition: true });
|
|
544
|
+
else appendStandaloneNode(draft, workflowNode);
|
|
545
|
+
else if (mutation.payload.appendFromNodeId) appendNodeFromSource(draft, workflowNode, mutation.payload.appendFromNodeId, mutation.payload.appendFromHandleId);
|
|
546
|
+
else appendStandaloneNode(draft, workflowNode);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
default: return false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function createBuilderSession(inputDraft) {
|
|
553
|
+
const undoStack = [];
|
|
554
|
+
const redoStack = [];
|
|
555
|
+
const state = {
|
|
556
|
+
draft: cloneDraft(inputDraft),
|
|
557
|
+
selectedNodeId: null,
|
|
558
|
+
saveState: "idle",
|
|
559
|
+
history: {
|
|
560
|
+
canUndo: false,
|
|
561
|
+
canRedo: false,
|
|
562
|
+
entries: []
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function refreshHistoryState() {
|
|
566
|
+
state.history = {
|
|
567
|
+
...state.history,
|
|
568
|
+
canUndo: undoStack.length > 0,
|
|
569
|
+
canRedo: redoStack.length > 0
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function prependHistoryEntry(label) {
|
|
573
|
+
state.history = {
|
|
574
|
+
...state.history,
|
|
575
|
+
entries: [{
|
|
576
|
+
id: buildHistoryId(),
|
|
577
|
+
label,
|
|
578
|
+
timestamp: formatHistoryTimestamp()
|
|
579
|
+
}, ...state.history.entries].slice(0, maxHistoryEntries)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function pushUndoSnapshot(previousDraft, mutation) {
|
|
583
|
+
undoStack.push(cloneDraft(previousDraft));
|
|
584
|
+
if (undoStack.length > maxUndoStackSize) undoStack.shift();
|
|
585
|
+
redoStack.length = 0;
|
|
586
|
+
prependHistoryEntry(getMutationHistoryLabel(mutation));
|
|
587
|
+
refreshHistoryState();
|
|
588
|
+
}
|
|
589
|
+
function resetHistory() {
|
|
590
|
+
undoStack.length = 0;
|
|
591
|
+
redoStack.length = 0;
|
|
592
|
+
state.history = {
|
|
593
|
+
canUndo: false,
|
|
594
|
+
canRedo: false,
|
|
595
|
+
entries: []
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
state,
|
|
600
|
+
replaceDraft(nextDraft, options = {}) {
|
|
601
|
+
const selectedNodeId = state.selectedNodeId;
|
|
602
|
+
state.draft = cloneDraft(nextDraft);
|
|
603
|
+
state.selectedNodeId = options.preserveHistory && selectedNodeId && nextDraft.graphState.nodes.some((node) => node.id === selectedNodeId) ? selectedNodeId : null;
|
|
604
|
+
state.saveState = "idle";
|
|
605
|
+
if (!options.preserveHistory) resetHistory();
|
|
606
|
+
else refreshHistoryState();
|
|
607
|
+
},
|
|
608
|
+
updateMetadata(patch) {
|
|
609
|
+
state.draft = {
|
|
610
|
+
...state.draft,
|
|
611
|
+
metadata: {
|
|
612
|
+
...state.draft.metadata,
|
|
613
|
+
...patch
|
|
614
|
+
},
|
|
615
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
applyCanvasMutation(mutation) {
|
|
619
|
+
if (mutation.type === "selection-changed") {
|
|
620
|
+
state.selectedNodeId = mutation.payload.nodeId;
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const previousDraft = cloneDraft(state.draft);
|
|
624
|
+
const nextDraft = cloneDraft(state.draft);
|
|
625
|
+
const didChangeDraft = applyDraftGraphMutation(nextDraft, mutation);
|
|
626
|
+
if (didChangeDraft) {
|
|
627
|
+
normalizeContainerGraphState(nextDraft);
|
|
628
|
+
state.draft = {
|
|
629
|
+
...nextDraft,
|
|
630
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
631
|
+
};
|
|
632
|
+
if (mutation.type === "node-remove-requested" && state.selectedNodeId === mutation.payload.nodeId) state.selectedNodeId = null;
|
|
633
|
+
if (isUndoableMutation(mutation)) pushUndoSnapshot(previousDraft, mutation);
|
|
634
|
+
}
|
|
635
|
+
return didChangeDraft;
|
|
636
|
+
},
|
|
637
|
+
undo() {
|
|
638
|
+
const previousDraft = undoStack.pop();
|
|
639
|
+
if (!previousDraft) {
|
|
640
|
+
refreshHistoryState();
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
redoStack.push(cloneDraft(state.draft));
|
|
644
|
+
state.draft = cloneDraft(previousDraft);
|
|
645
|
+
state.selectedNodeId = null;
|
|
646
|
+
prependHistoryEntry("Undid last change");
|
|
647
|
+
refreshHistoryState();
|
|
648
|
+
return true;
|
|
649
|
+
},
|
|
650
|
+
redo() {
|
|
651
|
+
const nextDraft = redoStack.pop();
|
|
652
|
+
if (!nextDraft) {
|
|
653
|
+
refreshHistoryState();
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
undoStack.push(cloneDraft(state.draft));
|
|
657
|
+
state.draft = cloneDraft(nextDraft);
|
|
658
|
+
state.selectedNodeId = null;
|
|
659
|
+
prependHistoryEntry("Redid last change");
|
|
660
|
+
refreshHistoryState();
|
|
661
|
+
return true;
|
|
662
|
+
},
|
|
663
|
+
clearSelection() {
|
|
664
|
+
state.selectedNodeId = null;
|
|
665
|
+
return createDraftMutation("selection-changed", { nodeId: null });
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
export { createBuilderSession };
|