fix: undo breaking Vue node image preview reactivity (#8839)

## Summary
restoreOutputs was assigning the same object reference to both
app.nodeOutputs and the Pinia reactive ref. This caused subsequent
writes via setOutputsByLocatorId to mutate the reactive proxy's target
through the raw reference before the proxy write, making Vue detect no
change and skip reactivity updates permanently.

Shallow-copy the outputs when assigning to the reactive ref so the proxy
target remains a separate object from app.nodeOutputs.

## Screenshots
before


https://github.com/user-attachments/assets/98f2b17c-87b9-41e7-9caa-238e36c3c032


after


https://github.com/user-attachments/assets/cb6e1d25-bd2e-41ed-a536-7b8250f858ec

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8839-fix-undo-breaking-Vue-node-image-preview-reactivity-3056d73d365081d2a1c7d4d9553f30e0)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2026-02-12 15:37:02 -05:00
committed by GitHub
parent 44ce9379eb
commit 0f33444eef
2 changed files with 42 additions and 1 deletions

View File

@@ -87,6 +87,47 @@ describe('imagePreviewStore setNodeOutputsByExecutionId with merge', () => {
})
})
describe('imagePreviewStore restoreOutputs', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
app.nodeOutputs = {}
app.nodePreviewImages = {}
})
it('should keep reactivity after restoreOutputs followed by setNodeOutputsByExecutionId', () => {
const store = useNodeOutputStore()
// Simulate execution: set outputs for node "4" (e.g., PreviewImage)
const executionOutput = createMockOutputs([
{ filename: 'ComfyUI_00001.png', subfolder: '', type: 'temp' }
])
const savedOutputs: Record<string, ExecutedWsMessage['output']> = {
'4': executionOutput
}
// Simulate undo: restoreOutputs makes app.nodeOutputs and the ref
// share the same underlying object if not handled correctly.
store.restoreOutputs(savedOutputs)
expect(store.nodeOutputs['4']).toStrictEqual(executionOutput)
expect(store.nodeOutputs['3']).toBeUndefined()
// Simulate widget callback setting outputs for node "3" (e.g., LoadImage)
const widgetOutput = createMockOutputs([
{ filename: 'example.png', subfolder: '', type: 'input' }
])
store.setNodeOutputsByExecutionId('3', widgetOutput)
// The reactive store must reflect the new output.
// Before the fix, the raw write to app.nodeOutputs would mutate the
// proxy's target before the proxy write, causing Vue to skip the
// reactivity update.
expect(store.nodeOutputs['3']).toStrictEqual(widgetOutput)
expect(app.nodeOutputs['3']).toStrictEqual(widgetOutput)
})
})
describe('imagePreviewStore getPreviewParam', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))

View File

@@ -365,7 +365,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
outputs: Record<string, ExecutedWsMessage['output']>
) {
app.nodeOutputs = outputs
nodeOutputs.value = outputs
nodeOutputs.value = { ...outputs }
}
function updateNodeImages(node: LGraphNode) {