Skip to content

Commit f1cc838

Browse files
committed
feat: enhance project workflow loading and streamline auto-save functionality
1 parent b7594a8 commit f1cc838

File tree

1 file changed

+135
-116
lines changed

1 file changed

+135
-116
lines changed

frontend/src/components/canvas-view.tsx

Lines changed: 135 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,25 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
7070
if (project.workflow) {
7171
try {
7272
const flow = JSON.parse(project.workflow);
73-
if (flow.nodes) setNodes(flow.nodes);
73+
if (flow.nodes)
74+
setNodes(
75+
flow.nodes.map((n: any) => ({
76+
...n,
77+
data: {
78+
...n.data,
79+
processing: false,
80+
error: undefined,
81+
runTrigger: undefined,
82+
},
83+
}))
84+
);
7485
if (flow.edges) setEdges(flow.edges);
7586

7687
// Update ID counter based on highest ID found
7788
let maxId = 0;
7889
if (Array.isArray(flow.nodes)) {
7990
flow.nodes.forEach((n: Node) => {
80-
const idNum = parseInt(n.id.replace(/[^0-9]/g, ''));
91+
const idNum = parseInt(n.id.replace(/[^0-9]/g, ""));
8192
if (!isNaN(idNum) && idNum > maxId) maxId = idNum;
8293
});
8394
}
@@ -94,46 +105,34 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
94105
const saveProject = useCallback(async () => {
95106
if (!project.id) return;
96107

108+
const nodesToSave = nodes.map((n: any) => ({
109+
...n,
110+
data: {
111+
...n.data,
112+
processing: false,
113+
error: undefined,
114+
runTrigger: undefined,
115+
},
116+
}));
117+
97118
const workflow = JSON.stringify({
98-
nodes,
119+
nodes: nodesToSave,
99120
edges,
100121
});
101122

102123
try {
103-
await SaveProject(new database.Project({
104-
...project,
105-
name: name,
106-
workflow,
107-
}));
124+
await SaveProject(
125+
new database.Project({
126+
...project,
127+
name: name,
128+
workflow,
129+
})
130+
);
108131
} catch (err) {
109132
console.error("Auto-save failed:", err);
110133
}
111134
}, [nodes, edges, name, project]);
112135

113-
// Auto-save
114-
useEffect(() => {
115-
if (!isInitialized) return;
116-
117-
const save = async () => {
118-
const workflow = JSON.stringify({
119-
nodes,
120-
edges,
121-
});
122-
123-
try {
124-
await SaveProject(new database.Project({
125-
...project,
126-
name: name,
127-
workflow,
128-
}));
129-
} catch (err) {
130-
console.error("Auto-save failed:", err);
131-
}
132-
};
133-
134-
save();
135-
}, [nodes, edges, name, isInitialized, saveProject]);
136-
137136
// 拖拽开始 - 暂停自动保存
138137
const onNodeDragStart = useCallback((_: React.MouseEvent, node: Node) => {
139138
isDragging.current = true;
@@ -143,12 +142,13 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
143142
}, []);
144143

145144
// 拖拽结束 - 立即保存
146-
const onNodeDragStop = useCallback((_: React.MouseEvent, node: Node) => {
147-
isDragging.current = false;
148-
saveProject();
149-
}, [saveProject]);
150-
151-
145+
const onNodeDragStop = useCallback(
146+
(_: React.MouseEvent, node: Node) => {
147+
isDragging.current = false;
148+
saveProject();
149+
},
150+
[saveProject]
151+
);
152152

153153
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
154154

@@ -164,17 +164,22 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
164164
[]
165165
);
166166

167-
const onSelectionChange = useCallback(({ nodes: selectedNodes }: { nodes: Node[] }) => {
168-
if (selectedNodes.some((node) => node.type === "group")) {
169-
return;
170-
}
171-
setSelectedNodes(selectedNodes.map((node) => node.id));
172-
}, []);
167+
const onSelectionChange = useCallback(
168+
({ nodes: selectedNodes }: { nodes: Node[] }) => {
169+
if (selectedNodes.some((node) => node.type === "group")) {
170+
return;
171+
}
172+
setSelectedNodes(selectedNodes.map((node) => node.id));
173+
},
174+
[]
175+
);
173176

174177
const createGroup = useCallback(() => {
175178
if (selectedNodes.length === 0) return;
176179

177-
const selectedNodeObjects = nodes.filter((n) => selectedNodes.includes(n.id) && n.type !== "group");
180+
const selectedNodeObjects = nodes.filter(
181+
(n) => selectedNodes.includes(n.id) && n.type !== "group"
182+
);
178183
if (selectedNodeObjects.length === 0) return;
179184

180185
// Calculate bounding box
@@ -241,14 +246,15 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
241246
y: Math.random() * 400 + 100,
242247
},
243248
data: {
244-
label: `${type === "text"
245-
? "文本"
246-
: type === "image"
249+
label: `${
250+
type === "text"
251+
? "文本"
252+
: type === "image"
247253
? "图片"
248254
: type === "video"
249-
? "视频"
250-
: "音频"
251-
}节点 ${nodeIdCounter}`,
255+
? "视频"
256+
: "音频"
257+
}节点 ${nodeIdCounter}`,
252258
type: type,
253259
},
254260
};
@@ -259,13 +265,19 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
259265
);
260266

261267
const { getIntersectingNodes } = useReactFlow();
262-
const dragRef = useRef<{ id: string; position: { x: number; y: number } } | null>(null);
263-
268+
const dragRef = useRef<{
269+
id: string;
270+
position: { x: number; y: number };
271+
} | null>(null);
264272

265273
// group 子节点跟随拖动
266274
const onNodeDrag = useCallback(
267275
(_: React.MouseEvent, node: Node) => {
268-
if (node.type === "group" && dragRef.current && dragRef.current.id === node.id) {
276+
if (
277+
node.type === "group" &&
278+
dragRef.current &&
279+
dragRef.current.id === node.id
280+
) {
269281
const dx = node.position.x - dragRef.current.position.x;
270282
const dy = node.position.y - dragRef.current.position.y;
271283

@@ -283,7 +295,9 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
283295
if (intersectingNodes.length > 0) {
284296
setNodes((nds) =>
285297
nds.map((n: Node) => {
286-
if (intersectingNodes.some((inNode: Node) => inNode.id === n.id)) {
298+
if (
299+
intersectingNodes.some((inNode: Node) => inNode.id === n.id)
300+
) {
287301
return {
288302
...n,
289303
position: {
@@ -303,17 +317,22 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
303317
[getIntersectingNodes, setNodes]
304318
);
305319

306-
const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
307-
if (node.type === "group") {
308-
// Manual selection toggle for group nodes since they are not selectable by box
309-
setNodes((nds) => nds.map(n => {
310-
if (n.id === node.id) {
311-
return { ...n, selected: !n.selected };
312-
}
313-
return n;
314-
}));
315-
}
316-
}, [setNodes]);
320+
const onNodeClick = useCallback(
321+
(_: React.MouseEvent, node: Node) => {
322+
if (node.type === "group") {
323+
// Manual selection toggle for group nodes since they are not selectable by box
324+
setNodes((nds) =>
325+
nds.map((n) => {
326+
if (n.id === node.id) {
327+
return { ...n, selected: !n.selected };
328+
}
329+
return n;
330+
})
331+
);
332+
}
333+
},
334+
[setNodes]
335+
);
317336

318337
const { getNodes } = useReactFlow();
319338

@@ -325,17 +344,26 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
325344
if (nodesBounds.width > 0 && nodesBounds.height > 0) {
326345
const imageWidth = 800; // Efficient size for cover
327346
const imageHeight = 450;
328-
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2, 0);
347+
const viewport = getViewportForBounds(
348+
nodesBounds,
349+
imageWidth,
350+
imageHeight,
351+
0.5,
352+
2,
353+
0
354+
);
329355

330-
const viewportElement = document.querySelector('.react-flow__viewport') as HTMLElement;
356+
const viewportElement = document.querySelector(
357+
".react-flow__viewport"
358+
) as HTMLElement;
331359
if (viewportElement) {
332360
const dataUrl = await toPng(viewportElement, {
333-
backgroundColor: 'transparent',
361+
backgroundColor: "transparent",
334362
width: imageWidth,
335363
height: imageHeight,
336364
filter: (node) => {
337365
// Exclude video elements to prevent SecurityError: The operation is insecure.
338-
if (node.tagName === 'VIDEO') return false;
366+
if (node.tagName === "VIDEO") return false;
339367
return true;
340368
},
341369
style: {
@@ -354,20 +382,7 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
354382
}
355383

356384
// 3. Save Project (explicit save to ensure cover image is persisted)
357-
try {
358-
const workflow = JSON.stringify({
359-
nodes,
360-
edges,
361-
});
362-
await SaveProject(new database.Project({
363-
...project,
364-
name: name,
365-
workflow,
366-
coverImage: project.coverImage // Ensure this is sent
367-
}));
368-
} catch (err) {
369-
console.error("Failed to save project on exit:", err);
370-
}
385+
saveProject();
371386

372387
// 4. Navigate back
373388
onBack();
@@ -379,7 +394,8 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
379394
edges,
380395
};
381396
const jsonString = JSON.stringify(data, null, 2);
382-
navigator.clipboard.writeText(jsonString)
397+
navigator.clipboard
398+
.writeText(jsonString)
383399
.then(() => toast.success("Project JSON copied to clipboard"))
384400
.catch((err) => {
385401
console.error("Failed to copy:", err);
@@ -393,37 +409,40 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
393409
fileInputRef.current?.click();
394410
};
395411

396-
const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
397-
const file = event.target.files?.[0];
398-
if (!file) return;
399-
400-
const reader = new FileReader();
401-
reader.onload = (e) => {
402-
try {
403-
const content = e.target?.result as string;
404-
const data = JSON.parse(content);
405-
if (data.nodes && data.edges) {
406-
setNodes(data.nodes);
407-
setEdges(data.edges);
408-
// Update ID counter based on highest ID found to avoid collisions
409-
let maxId = 0;
410-
data.nodes.forEach((n: Node) => {
411-
const idNum = parseInt(n.id.replace(/[^0-9]/g, ''));
412-
if (!isNaN(idNum) && idNum > maxId) maxId = idNum;
413-
});
414-
setNodeIdCounter(maxId + 1);
415-
} else {
416-
alert("Invalid project file format");
412+
const handleFileChange = useCallback(
413+
(event: React.ChangeEvent<HTMLInputElement>) => {
414+
const file = event.target.files?.[0];
415+
if (!file) return;
416+
417+
const reader = new FileReader();
418+
reader.onload = (e) => {
419+
try {
420+
const content = e.target?.result as string;
421+
const data = JSON.parse(content);
422+
if (data.nodes && data.edges) {
423+
setNodes(data.nodes);
424+
setEdges(data.edges);
425+
// Update ID counter based on highest ID found to avoid collisions
426+
let maxId = 0;
427+
data.nodes.forEach((n: Node) => {
428+
const idNum = parseInt(n.id.replace(/[^0-9]/g, ""));
429+
if (!isNaN(idNum) && idNum > maxId) maxId = idNum;
430+
});
431+
setNodeIdCounter(maxId + 1);
432+
} else {
433+
alert("Invalid project file format");
434+
}
435+
} catch (err) {
436+
console.error("Import failed:", err);
437+
alert("Failed to parse project file");
417438
}
418-
} catch (err) {
419-
console.error("Import failed:", err);
420-
alert("Failed to parse project file");
421-
}
422-
};
423-
reader.readAsText(file);
424-
// Reset input so same file can be selected again
425-
event.target.value = "";
426-
}, [setNodes, setEdges, setNodeIdCounter]);
439+
};
440+
reader.readAsText(file);
441+
// Reset input so same file can be selected again
442+
event.target.value = "";
443+
},
444+
[setNodes, setEdges, setNodeIdCounter]
445+
);
427446

428447
return (
429448
<div className="flex h-full relative">
@@ -479,7 +498,7 @@ function CanvasEditor({ project, onBack }: CanvasViewProps) {
479498

480499
{/* 左侧悬浮工具栏 */}
481500
<div className="absolute left-4 top-1/2 -translate-y-1/2 z-10 flex flex-col gap-2">
482-
<Card className="p-2 shadow-lg">
501+
<Card className="p-2 shadow-lg rounded-full">
483502
<div className="flex flex-col gap-2">
484503
<Button
485504
variant="ghost"

0 commit comments

Comments
 (0)