From 8019288fd3d1904c15fe7cd5aa0d1ad6e0435a4d Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sun, 15 Feb 2026 22:20:46 +1300 Subject: [PATCH] fix: clear imageRef when user provides new image to prevent stale saves When a user pastes/uploads a new image into a node that already has a saved imageRef, the externalization logic incorrectly assumes the base64 is just hydrated data and discards it. Clear *Ref fields whenever new image data is set so the save logic correctly persists the new image. Co-Authored-By: Claude Opus 4.6 --- src/components/AnnotationModal.tsx | 2 +- src/components/WorkflowCanvas.tsx | 1 + src/components/__tests__/AnnotationNode.test.tsx | 4 ++++ src/components/__tests__/ImageInputNode.test.tsx | 2 ++ src/components/nodes/AnnotationNode.tsx | 4 ++++ src/components/nodes/ImageInputNode.tsx | 2 ++ .../execution/__tests__/simpleNodeExecutors.test.ts | 10 +++++----- src/store/execution/simpleNodeExecutors.ts | 4 ++-- src/store/execution/splitGridExecutor.ts | 1 + 9 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/AnnotationModal.tsx b/src/components/AnnotationModal.tsx index 351ca479..90cdf5c3 100644 --- a/src/components/AnnotationModal.tsx +++ b/src/components/AnnotationModal.tsx @@ -323,7 +323,7 @@ export function AnnotationModal() { const handleDone = useCallback(() => { if (!sourceNodeId) return; const flattenedImage = flattenImage(); - updateNodeData(sourceNodeId, { annotations, outputImage: flattenedImage }); + updateNodeData(sourceNodeId, { annotations, outputImage: flattenedImage, outputImageRef: undefined }); closeModal(); }, [sourceNodeId, annotations, flattenImage, updateNodeData, closeModal]); diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index c80ebf66..0c8da611 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -1143,6 +1143,7 @@ export function WorkflowCanvas() { // Update the selected imageInput node with the pasted image updateNodeData(selectedImageInputNode.id, { image: dataUrl, + imageRef: undefined, filename: `pasted-${Date.now()}.png`, dimensions: { width: img.width, height: img.height }, }); diff --git a/src/components/__tests__/AnnotationNode.test.tsx b/src/components/__tests__/AnnotationNode.test.tsx index e8061bc2..bed5fc1c 100644 --- a/src/components/__tests__/AnnotationNode.test.tsx +++ b/src/components/__tests__/AnnotationNode.test.tsx @@ -347,7 +347,9 @@ describe("AnnotationNode", () => { expect(mockUpdateNodeData).toHaveBeenCalledWith("test-annotation-1", { sourceImage: null, + sourceImageRef: undefined, outputImage: null, + outputImageRef: undefined, annotations: [], }); }); @@ -412,7 +414,9 @@ describe("AnnotationNode", () => { await waitFor(() => { expect(mockUpdateNodeData).toHaveBeenCalledWith("test-annotation-1", { sourceImage: "data:image/png;base64,uploadedImage", + sourceImageRef: undefined, outputImage: null, + outputImageRef: undefined, annotations: [], }); }); diff --git a/src/components/__tests__/ImageInputNode.test.tsx b/src/components/__tests__/ImageInputNode.test.tsx index 07e12a1f..4cbb673c 100644 --- a/src/components/__tests__/ImageInputNode.test.tsx +++ b/src/components/__tests__/ImageInputNode.test.tsx @@ -234,6 +234,7 @@ describe("ImageInputNode", () => { await waitFor(() => { expect(mockUpdateNodeData).toHaveBeenCalledWith("test-image-1", { image: "data:image/png;base64,test123", + imageRef: undefined, filename: "test.png", dimensions: { width: 1024, height: 768 }, }); @@ -406,6 +407,7 @@ describe("ImageInputNode", () => { expect(mockUpdateNodeData).toHaveBeenCalledWith("test-image-1", { image: null, + imageRef: undefined, filename: null, dimensions: null, }); diff --git a/src/components/nodes/AnnotationNode.tsx b/src/components/nodes/AnnotationNode.tsx index 0bcc0891..9a35b075 100644 --- a/src/components/nodes/AnnotationNode.tsx +++ b/src/components/nodes/AnnotationNode.tsx @@ -37,7 +37,9 @@ export function AnnotationNode({ id, data, selected }: NodeProps { updateNodeData(id, { sourceImage: null, + sourceImageRef: undefined, outputImage: null, + outputImageRef: undefined, annotations: [], }); }, [id, updateNodeData]); diff --git a/src/components/nodes/ImageInputNode.tsx b/src/components/nodes/ImageInputNode.tsx index b42d5d73..fb75f778 100644 --- a/src/components/nodes/ImageInputNode.tsx +++ b/src/components/nodes/ImageInputNode.tsx @@ -37,6 +37,7 @@ export function ImageInputNode({ id, data, selected }: NodeProps { updateNodeData(id, { image: base64, + imageRef: undefined, filename: file.name, dimensions: { width: img.width, height: img.height }, }); @@ -74,6 +75,7 @@ export function ImageInputNode({ id, data, selected }: NodeProps { updateNodeData(id, { image: null, + imageRef: undefined, filename: null, dimensions: null, }); diff --git a/src/store/execution/__tests__/simpleNodeExecutors.test.ts b/src/store/execution/__tests__/simpleNodeExecutors.test.ts index 3ba03cea..242ed9bd 100644 --- a/src/store/execution/__tests__/simpleNodeExecutors.test.ts +++ b/src/store/execution/__tests__/simpleNodeExecutors.test.ts @@ -61,7 +61,7 @@ describe("executeAnnotation", () => { await executeAnnotation(ctx); - expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "data:image/png;base64,abc" }); + expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "data:image/png;base64,abc", sourceImageRef: undefined }); }); it("should pass through image as output when no annotations exist", async () => { @@ -79,7 +79,7 @@ describe("executeAnnotation", () => { await executeAnnotation(ctx); - expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { outputImage: "data:image/png;base64,abc" }); + expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { outputImage: "data:image/png;base64,abc", outputImageRef: undefined }); }); it("should not overwrite existing annotated outputImage", async () => { @@ -98,7 +98,7 @@ describe("executeAnnotation", () => { await executeAnnotation(ctx); // Should set sourceImage but NOT overwrite outputImage (it has real annotations) - expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "data:image/png;base64,abc" }); + expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "data:image/png;base64,abc", sourceImageRef: undefined }); const calls = (ctx.updateNodeData as ReturnType).mock.calls; const outputCall = calls.find((c: unknown[]) => (c[1] as Record).outputImage !== undefined); expect(outputCall).toBeUndefined(); @@ -120,8 +120,8 @@ describe("executeAnnotation", () => { await executeAnnotation(ctx); - expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "new-image" }); - expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { outputImage: "new-image" }); + expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { sourceImage: "new-image", sourceImageRef: undefined }); + expect(ctx.updateNodeData).toHaveBeenCalledWith("ann", { outputImage: "new-image", outputImageRef: undefined }); }); it("should do nothing when no images connected", async () => { diff --git a/src/store/execution/simpleNodeExecutors.ts b/src/store/execution/simpleNodeExecutors.ts index 891b9fb1..f782c62a 100644 --- a/src/store/execution/simpleNodeExecutors.ts +++ b/src/store/execution/simpleNodeExecutors.ts @@ -27,11 +27,11 @@ export async function executeAnnotation(ctx: NodeExecutionContext): Promise img.onload = () => { updateNodeData(childSet.imageInput, { image: splitImages[index], + imageRef: undefined, filename: `split-${Math.floor(index / nodeData.gridCols) + 1}-${(index % nodeData.gridCols) + 1}.png`, dimensions: { width: img.width, height: img.height }, });