diff --git a/blocks/edit/prose/plugins/imageDrop.js b/blocks/edit/prose/plugins/imageDrop.js index c0afa3ebb..cd649277e 100644 --- a/blocks/edit/prose/plugins/imageDrop.js +++ b/blocks/edit/prose/plugins/imageDrop.js @@ -23,7 +23,7 @@ export async function uploadImageFile(view, file) { view.dispatch(view.state.tr.replaceSelectionWith(fpo)); const { source } = await getNx2Api(); - const resp = await source.save(path, { body: file }); + const resp = await source.uploadMedia(path, { body: file }); if (!resp.ok) { // eslint-disable-next-line no-console console.error(`Failed to upload image "${file.name}": ${resp.status} ${resp.statusText}`); @@ -45,23 +45,33 @@ export async function uploadImageFile(view, file) { return; } const json = await resp.json(); + const imgSrc = json.source.contentUrl; - // Create a doc image to pre-download the image before showing it. - const docImg = document.createElement('img'); - docImg.addEventListener('load', () => { + let replaced = false; + function injectImage() { // Find the placeholder by its unique src rather than a stale position so // concurrent uploads and collab updates cannot cause the wrong node to be // replaced. - let replaced = false; view.state.doc.descendants((node, pos) => { if (!replaced && node.type.name === 'image' && node.attrs.src === fpoSrc) { replaced = true; - const img = schema.nodes.image.create({ src: json.source.contentUrl }); + const img = schema.nodes.image.create({ src: imgSrc }); view.dispatch(view.state.tr.replaceWith(pos, pos + node.nodeSize, img)); } }); - }); - docImg.src = json.source.contentUrl; + } + + // Create a doc image to pre-download the image before showing it. + const docImg = document.createElement('img'); + docImg.src = imgSrc; + + if (imgSrc.startsWith('./media_')) { + // for relative media images, always replace the placeholder (for now) + injectImage(); + } else { + // otherwise, wait until the image was loaded + docImg.addEventListener('load', injectImage); + } } export default function imageDrop() { diff --git a/test/fixtures/nx2/utils/api.js b/test/fixtures/nx2/utils/api.js index f46c2a3f2..fee1868a0 100644 --- a/test/fixtures/nx2/utils/api.js +++ b/test/fixtures/nx2/utils/api.js @@ -242,6 +242,11 @@ export const source = { return daFetch({ url, opts }); }), + uploadMedia: withArgs(async ({ org, site, path, body }) => { + const url = await getDaApiPath(SOURCE, org, site, path); + return daFetch({ url, opts: { method: 'POST', body } }); + }), + // HEAD request — the value is in the response headers (doc-id, last-modified, etc.). getMetadata: withArgs(async ({ org, site, path }) => { const url = await getDaApiPath(SOURCE, org, site, path); diff --git a/test/unit/blocks/edit/prose/plugins/imageDrop.test.js b/test/unit/blocks/edit/prose/plugins/imageDrop.test.js index 08e5bf8ed..eb71fc94f 100644 --- a/test/unit/blocks/edit/prose/plugins/imageDrop.test.js +++ b/test/unit/blocks/edit/prose/plugins/imageDrop.test.js @@ -168,6 +168,51 @@ describe('imageDrop plugin', () => { } }); + it('uploadImageFile uploads via source.uploadMedia (not source.save)', async () => { + const savedFetch = window.fetch; + let requestUrl = null; + let requestMethod = null; + window.fetch = (url, opts) => { + requestUrl = url; + requestMethod = opts?.method; + return Promise.resolve(new Response( + JSON.stringify({ source: { contentUrl: '/path/uploaded.png' } }), + { status: 200 }, + )); + }; + try { + const file = new File(['x'], 'pic.png', { type: 'image/png' }); + await uploadImageFile(editor.view, file); + expect(requestUrl).to.be.a('string'); + expect(requestUrl).to.include('/pic.png'); + expect(requestMethod).to.equal('POST'); + } finally { + window.fetch = savedFetch; + } + }); + + it('uploadImageFile replaces the FPO immediately for relative media URLs, without waiting for load', async () => { + const mediaUrl = './media_abc123.png?width=750&format=webply'; + const savedFetch = window.fetch; + window.fetch = () => Promise.resolve(new Response( + JSON.stringify({ source: { contentUrl: mediaUrl } }), + { status: 200 }, + )); + try { + const file = new File(['x'], 'pic.png', { type: 'image/png' }); + await uploadImageFile(editor.view, file); + // No extra wait for the img "load" event — the relative media src + // should already have replaced the FPO by the time upload resolves. + let finalSrc = null; + editor.view.state.doc.descendants((node) => { + if (node.type.name === 'image') finalSrc = node.attrs.src; + }); + expect(finalSrc).to.equal(mediaUrl); + } finally { + window.fetch = savedFetch; + } + }); + it('uploadImageFile replaces FPO with the real image URL after upload completes', async () => { // Use a data URL so the browser fires the img load event in the test environment. const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';