From 23a6c444115ad239fdc3b7f7f42617df6d8128e0 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Thu, 5 Mar 2026 15:59:32 -0300 Subject: [PATCH] fix(image): sync headless image media to Y.Doc for collab persistence handleNodePath (headless/CLI image registration) stored media only in the local editor.storage.image.media object but never wrote to the Y.Doc media map. When a second collab session connected, image nodes synced via XmlFragment but the media binary data was missing. Write directly to ydoc.getMap('media') instead of using editor.commands.addImageToCollaboration, which doesn't dispatch properly inside ProseMirror's appendTransaction callback. SD-2150 --- .../imageHelpers/imageRegistrationPlugin.js | 8 +++++ .../imageRegistrationPlugin.test.js | 35 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js index 1590364e17..99e4dede3e 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.js @@ -216,6 +216,14 @@ export const handleNodePath = (foundImages, editor, state) => { const mediaPath = buildMediaPath(uniqueFileName); mediaStore[mediaPath] = src; + // Sync image data to Y.Doc media map so other collab clients can access it. + // We write directly to the Y.Doc map instead of using editor.commands because + // this runs inside appendTransaction where commands don't dispatch properly. + if (editor.options.ydoc) { + const mediaMap = editor.options.ydoc.getMap('media'); + mediaMap.set(mediaPath, src); + } + const path = mediaPath.startsWith('word/') ? mediaPath.slice(5) : mediaPath; const rId = addImageRelationship({ editor, path }); const inferredSize = hasFinitePositiveSize(node.attrs?.size) ? null : parseSizeFromImageUrl(src); diff --git a/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.test.js b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.test.js index de6e160e55..5b38378790 100644 --- a/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.test.js +++ b/packages/super-editor/src/extensions/image/imageHelpers/imageRegistrationPlugin.test.js @@ -161,6 +161,41 @@ describe('handleNodePath', () => { expect(state.tr.setNodeMarkup).toHaveBeenCalledWith(0, undefined, expect.objectContaining({ size: existingSize })); }); + it('syncs image data to Y.Doc media map when in collaboration mode', () => { + const base64 = `data:image/png;base64,${Buffer.from('test-image').toString('base64')}`; + const foundImages = [{ node: { attrs: { src: base64 } }, pos: 0 }]; + + const state = createStateStub(); + const mediaMapSet = vi.fn(); + const editor = { + ...createEditorStub(), + options: { + mode: 'docx', + ydoc: { getMap: vi.fn(() => ({ set: mediaMapSet })) }, + }, + }; + + handleNodePath(foundImages, editor, state); + + expect(editor.options.ydoc.getMap).toHaveBeenCalledWith('media'); + expect(mediaMapSet).toHaveBeenCalledTimes(1); + expect(mediaMapSet).toHaveBeenCalledWith(expect.stringMatching(/^word\/media\//), base64); + }); + + it('does not write to Y.Doc media map when not in collaboration mode', () => { + const base64 = `data:image/png;base64,${Buffer.from('test-image').toString('base64')}`; + const foundImages = [{ node: { attrs: { src: base64 } }, pos: 0 }]; + + const state = createStateStub(); + const editor = createEditorStub(); // no ydoc + + handleNodePath(foundImages, editor, state); + + // Should not throw — just silently skip collab sync + const mediaEntries = Object.entries(editor.storage.image.media); + expect(mediaEntries).toHaveLength(1); + }); + it('infers size from compact WxH path segment', () => { const sourceUrl = 'https://example.com/images/800x600'; const foundImages = [{ node: { attrs: { src: sourceUrl } }, pos: 0 }];