[FIX] web_editor: skipped image conversion to attachment in HtmlField

Commit [1] added a check for the closest ".o_dirty" editable element in
order to define which editable is used for obtaining the model and id
reference from its dataset, and also to avoid multiple attachment
creation for the same image.

But such check for the closest ".o-dirty" element evaluates to null when
the Wysiwyg is used by the HtmlField component, as such class is not
added to editable when saving/committing changes. This resulted in
skipping the attachment creation for pasted, dropped or modified images.

After this commit, attachment creation for modified images is not
skipped if an element with the ".o_dirty" class does not exist.

task-3495668

[1]: https://github.com/odoo/odoo/commit/e631bff05a3588f3304b75b6a0018b096c13f68d

Part-of: odoo/odoo#135806
This commit is contained in:
Rodolpho Lima
2023-09-18 23:31:39 +00:00
parent 130f94b691
commit 00701977ad
2 changed files with 118 additions and 3 deletions
@@ -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.
@@ -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: "<p class='test_target'><br></p>",
});
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: `
<form>
<field name="txt" widget="html"/>
</form>`,
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) => {