diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 6bd58d843d1..35dbebac724 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -1164,7 +1164,8 @@ export class Wysiwyg extends Component { // o_modified_image_to_save by _saveB64Image to request the saving // of the pre-converted webp resizes and all the equivalent jpgs. const b64Proms = [...editableEl.querySelectorAll('.o_b64_image_to_save')].map(async el => { - if (el.closest(".o_dirty") !== editableEl) { + const dirtyEditable = el.closest(".o_dirty"); + if (dirtyEditable && dirtyEditable !== editableEl) { // Do nothing as there is an editable element closer to the // image that will perform the `_saveB64Image()` call with // the correct "resModel" and "resId" parameters. @@ -1173,7 +1174,8 @@ export class Wysiwyg extends Component { await this._saveB64Image(el, resModel, resId); }); const modifiedProms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => { - if (el.closest(".o_dirty") !== editableEl) { + const dirtyEditable = el.closest(".o_dirty"); + if (dirtyEditable && dirtyEditable !== editableEl) { // Do nothing as there is an editable element closer to the // image that will perform the `_saveModifiedImage()` call // with the correct "resModel" and "resId" parameters. diff --git a/addons/web_editor/static/tests/html_field_tests.js b/addons/web_editor/static/tests/html_field_tests.js index 2bbe01f69cd..8097edc83b3 100644 --- a/addons/web_editor/static/tests/html_field_tests.js +++ b/addons/web_editor/static/tests/html_field_tests.js @@ -8,6 +8,7 @@ import { parseHTML } from "@web_editor/js/editor/odoo-editor/src/utils/utils"; import { onRendered } from "@odoo/owl"; import { wysiwygData } from "@web_editor/../tests/test_utils"; import { OdooEditor } from '@web_editor/js/editor/odoo-editor/src/OdooEditor'; +import { Wysiwyg } from "@web_editor/js/wysiwyg/wysiwyg"; // Legacy import legacyEnv from '@web/legacy/js/common_env'; @@ -415,7 +416,6 @@ QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => { // Replace the empty paragraph with a paragrah containing an unsaved // modified image - editor.editable.classList.add("o_dirty"); const imageContainerElement = parseHTML(imageContainerHTML).firstChild; let paragraph = editor.editable.querySelector(".test_target"); editor.editable.replaceChild(imageContainerElement, paragraph); @@ -436,6 +436,119 @@ QUnit.module("WebEditor.HtmlField", ({ beforeEach }) => { await nextTick(); }); + QUnit.test("Pasted/dropped images are converted to attachments on save", async (assert) => { + assert.expect(4); + + // Patch to get a promise to get the htmlField component instance when + // the wysiwyg is instancied. + const htmlFieldPromise = makeDeferred(); + patchWithCleanup(HtmlField.prototype, { + async startWysiwyg() { + await super.startWysiwyg(...arguments); + await nextTick(); + htmlFieldPromise.resolve(this); + } + }); + // Add a partner record + serverData.models.partner.records.push({ + id: 1, + txt: "


", + }); + + const mockRPC = async function (route, args) { + if (route === '/web_editor/attachment/add_data') { + return { + image_src: '/test_image_url.png', + access_token: '1234', + public: false, + } + } + }; + + const pasteImage = async (editor) => { + // Create image file. + const base64ImageData = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" + const binaryImageData = atob(base64ImageData); + const uint8Array = new Uint8Array(binaryImageData.length); + for (let i = 0; i < binaryImageData.length; i++) { + uint8Array[i] = binaryImageData.charCodeAt(i); + } + const file = new File([uint8Array], "test_image.png", { type: 'image/png' }); + + // Create a promise to get the created img elements + const pasteImagePromise = makeDeferred(); + const observer = new MutationObserver(mutations => { + mutations + .filter(mutation => mutation.type === 'childList') + .forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node instanceof HTMLElement) { + pasteImagePromise.resolve(node); + } + }); + }); + }); + observer.observe(editor.editable, { subtree: true, childList: true }); + + // Simulate paste. + editor._onPaste({ + preventDefault() {}, + clipboardData: { + getData() {}, + items: [{ + kind: 'file', + type: 'image/png', + getAsFile: () => file, + }], + }, + }); + + const img = await pasteImagePromise; + observer.disconnect(); + return img; + } + + // Add the ajax service (legacy), because wysiwyg RPCs use it. + patchWithCleanup(legacyEnv, { + services: { + ...legacyEnv.services, + ajax: { + rpc: mockRPC, + }, + } + }); + await makeView({ + type: "form", + resId: 1, + resModel: "partner", + serverData, + arch: ` +
+ + `, + mockRPC: mockRPC, + }); + // Let the htmlField be mounted and recover the Component instance. + const htmlField = await htmlFieldPromise; + const editor = htmlField.wysiwyg.odooEditor; + + const paragraph = editor.editable.querySelector(".test_target"); + Wysiwyg.setRange(paragraph); + + // Paste image. + const img = await pasteImage(editor); + // Test environment replaces 'src' by 'data-src'. + assert.ok(/^data:image\/png;base64,/.test(img.dataset['src'])); + assert.ok(img.classList.contains('o_b64_image_to_save')); + + // Save changes. + // Restore 'src' attribute so that SavePendingImages can do its job. + img.src = img.dataset['src']; + await htmlField.commitChanges(); + assert.equal(img.dataset['src'], '/test_image_url.png?access_token=1234'); + assert.ok(!img.classList.contains('o_b64_image_to_save')); + }); + QUnit.module('Odoo fields synchronisation'); QUnit.test("Synchronise fields when editing.", async (assert) => {