[IMP] website, web_editor, *: save dropped images as attachments

*: mass_mailing

When images are dropped from outside the browser into a website page
text paragraph, they are stored as an inline base64-encoded source.

This commit marks those images to be saved as attachments when it
happens in website. The save happens upon page save similarly to what is
done when images are transformed.
This commit also applies this behavior for pasted images.

task-2928495

closes odoo/odoo#98801

Signed-off-by: Romain Derie (rde) <rde@odoo.com>
This commit is contained in:
Benoit Socias
2022-12-23 19:19:06 +01:00
committed by Romain Derie
parent 7442fb7161
commit 3bbce756c6
7 changed files with 47 additions and 10 deletions
@@ -99,7 +99,7 @@ export class MassMailingHtmlField extends HtmlField {
const $editable = this.wysiwyg.getEditable();
const initialHtml = $editable.html();
await this.wysiwyg.cleanForSave();
await this.wysiwyg.saveModifiedImages(this.$content);
await this.wysiwyg.savePendingImages(this.$content);
await super.commitChanges();
+8
View File
@@ -6,12 +6,14 @@ import logging
import re
import time
import requests
import uuid
import werkzeug.exceptions
import werkzeug.urls
import werkzeug.wrappers
from PIL import Image, ImageFont, ImageDraw
from lxml import etree
from base64 import b64decode, b64encode
from datetime import datetime
from odoo.http import request
from odoo import http, tools, _, SUPERUSER_ID
@@ -208,6 +210,12 @@ class Web_Editor(http.Controller):
mimetype = guess_mimetype(data)
if mimetype not in SUPPORTED_IMAGE_MIMETYPES:
return {'error': format_error_msg}
if not name:
name = '%s-%s%s' % (
datetime.now().strftime('%Y%m%d%H%M%S'),
str(uuid.uuid4())[:6],
SUPPORTED_IMAGE_MIMETYPES[mimetype],
)
except UserError:
# considered as an image by the browser file input, but not
# recognized as such by PIL, eg .webp
@@ -145,7 +145,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
this._setValue(this._getValue());
this._isDirty = this.wysiwyg.isDirty();
await fullClean;
await this.wysiwyg.saveModifiedImages(this.$content);
await this.wysiwyg.savePendingImages(this.$content);
// Update the value to the fully cleaned version.
this._setValue(this._getValue());
_super();
@@ -357,7 +357,7 @@ export class HtmlField extends Component {
if (this.wysiwyg) {
// Avoid listening to changes made during the _toInline process.
this.wysiwyg.odooEditor.observerUnactive('commitChanges');
await this.wysiwyg.saveModifiedImages();
await this.wysiwyg.savePendingImages();
if (this.props.isInlineStyle) {
await this._toInline();
}
@@ -4220,7 +4220,15 @@ export class OdooEditor extends EventTarget {
this.execCommand('insert', fragment);
}
} else if (files.length && targetSupportsHtmlContent) {
this.addImagesFiles(files).then(html => this.execCommand('insert', this._prepareClipboardData(html)));
this.addImagesFiles(files).then(html => {
const imageNodes = this.execCommand('insert', this._prepareClipboardData(html));
if (imageNodes && this.options.dropImageAsAttachment) {
// Mark images as having to be saved as attachments.
for (const imageNode of imageNodes) {
imageNode.classList.add('o_b64_image_to_save');
}
}
});
} else if (clipboardHtml && targetSupportsHtmlContent) {
this.execCommand('insert', this._prepareClipboardData(clipboardHtml));
} else {
@@ -4438,7 +4446,13 @@ export class OdooEditor extends EventTarget {
this.execCommand('insert', this._prepareClipboardData(html));
} else if (fileTransferItems.length) {
this.addImagesFiles(fileTransferItems).then(html => {
this.execCommand('insert', this._prepareClipboardData(html));
const imageNodes = this.execCommand('insert', this._prepareClipboardData(html));
if (imageNodes && this.options.dropImageAsAttachment) {
// Mark images as having to be saved as attachments.
for (const imageNode of imageNodes) {
imageNode.classList.add('o_b64_image_to_save');
}
}
});
} else if (htmlTransferItem) {
htmlTransferItem.getAsString(pastedText => {
@@ -212,6 +212,7 @@ const Wysiwyg = Widget.extend({
direction: options.direction || localization.direction || 'ltr',
collaborationClientAvatarUrl: `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${this.getSession().uid}`,
renderingClasses: ['o_dirty', 'o_transform_removal', 'oe_edited_link'],
dropImageAsAttachment: options.dropImageAsAttachment,
}, editorCollaborationOptions));
this.odooEditor.addEventListener('contentChanged', function () {
@@ -899,7 +900,7 @@ const Wysiwyg = Widget.extend({
await this.cleanForSave();
const editables = this.options.getContentEditableAreas();
await this.saveModifiedImages(editables.length ? $(editables) : this.$editable);
await this.savePendingImages(editables.length ? $(editables) : this.$editable);
await this._saveViewBlocks();
window.removeEventListener('beforeunload', this._onBeforeUnload);
@@ -935,15 +936,28 @@ const Wysiwyg = Widget.extend({
});
},
/**
* Create/Update cropped attachments.
* Create/Update attachments for unsaved images.
* (e.g. modified/cropped images, drag & dropped images, pasted images..)
*
* @param {jQuery} $editable
* @returns {Promise}
*/
saveModifiedImages: function ($editable = this.$editable) {
savePendingImages($editable = this.$editable) {
const defs = _.map($editable, async editableEl => {
const {oeModel: resModel, oeId: resId} = editableEl.dataset;
const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {
const b64Proms = [...editableEl.querySelectorAll('.o_b64_image_to_save')].map(async el => {
const attachment = await this._rpc({
route: '/web_editor/attachment/add_data',
params: {
name: el.dataset.fileName || '',
data: el.getAttribute('src').split(',')[1],
is_image: true,
},
});
el.setAttribute('src', attachment.image_src);
el.classList.remove('o_b64_image_to_save');
});
const modifiedProms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {
const isBackground = !el.matches('img');
el.classList.remove('o_modified_image_to_save');
// Modifying an image always creates a copy of the original, even if
@@ -969,7 +983,7 @@ const Wysiwyg = Widget.extend({
el.setAttribute('src', newAttachmentSrc);
}
});
return Promise.all(proms);
return Promise.all([...b64Proms, ...modifiedProms]);
});
return Promise.all(defs);
},
@@ -51,6 +51,7 @@ const WebsiteWysiwyg = Wysiwyg.extend({
this.options.toolbarHandler = $('#web_editor-top-edit');
// Do not insert a paragraph after each column added by the column commands:
this.options.insertParagraphAfterColumns = false;
this.options.dropImageAsAttachment = true;
const $editableWindow = this.$editable[0].ownerDocument.defaultView;
// Dropdown menu initialization: handle dropdown openings by hand