Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions blocks/edit/prose/plugins/imageDrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/nx2/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
45 changes: 45 additions & 0 deletions test/unit/blocks/edit/prose/plugins/imageDrop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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==';
Expand Down
Loading