Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
8019288fd3
  1. 2
      src/components/AnnotationModal.tsx
  2. 1
      src/components/WorkflowCanvas.tsx
  3. 4
      src/components/__tests__/AnnotationNode.test.tsx
  4. 2
      src/components/__tests__/ImageInputNode.test.tsx
  5. 4
      src/components/nodes/AnnotationNode.tsx
  6. 2
      src/components/nodes/ImageInputNode.tsx
  7. 10
      src/store/execution/__tests__/simpleNodeExecutors.test.ts
  8. 4
      src/store/execution/simpleNodeExecutors.ts
  9. 1
      src/store/execution/splitGridExecutor.ts

2
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]);

1
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 },
});

4
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: [],
});
});

2
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,
});

4
src/components/nodes/AnnotationNode.tsx

@ -37,7 +37,9 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
const base64 = event.target?.result as string;
updateNodeData(id, {
sourceImage: base64,
sourceImageRef: undefined,
outputImage: null,
outputImageRef: undefined,
annotations: [],
});
};
@ -81,7 +83,9 @@ export function AnnotationNode({ id, data, selected }: NodeProps<AnnotationNodeT
const handleRemove = useCallback(() => {
updateNodeData(id, {
sourceImage: null,
sourceImageRef: undefined,
outputImage: null,
outputImageRef: undefined,
annotations: [],
});
}, [id, updateNodeData]);

2
src/components/nodes/ImageInputNode.tsx

@ -37,6 +37,7 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT
img.onload = () => {
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<ImageInputNodeT
const handleRemove = useCallback(() => {
updateNodeData(id, {
image: null,
imageRef: undefined,
filename: null,
dimensions: null,
});

10
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<typeof vi.fn>).mock.calls;
const outputCall = calls.find((c: unknown[]) => (c[1] as Record<string, unknown>).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 () => {

4
src/store/execution/simpleNodeExecutors.ts

@ -27,11 +27,11 @@ export async function executeAnnotation(ctx: NodeExecutionContext): Promise<void
const image = images[0] || null;
if (image) {
const nodeData = node.data as AnnotationNodeData;
updateNodeData(node.id, { sourceImage: image });
updateNodeData(node.id, { sourceImage: image, sourceImageRef: undefined });
// Pass through the image if no annotations exist, or if the previous
// output was itself a pass-through of the old source image
if (!nodeData.outputImage || nodeData.outputImage === nodeData.sourceImage) {
updateNodeData(node.id, { outputImage: image });
updateNodeData(node.id, { outputImage: image, outputImageRef: undefined });
}
}
} catch (err) {

1
src/store/execution/splitGridExecutor.ts

@ -55,6 +55,7 @@ export async function executeSplitGrid(ctx: NodeExecutionContext): Promise<void>
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 },
});

Loading…
Cancel
Save