[IMP] web_editor: improve image optimization workflow in media dialog

Previously, when selecting an image, the user could optimize their
images by clicking a little cog icon in the media dialog. They would
also be prompted to optimize their images when uploading them. In most
cases, it makes a lot more sense to always save the original in the
database, and optimize the original whenever it's dropped in the page
automatically.

A second problem is that when using the optimization feature, optimized
images would be displayed along the originals by default, resulting in a
lot of duplicates while browsing the media dialog.

This commit changes both of these things:

- By default, when uploading an image, the original uploaded image is
  used. The user can still optimize their image manually through the
  media-dialog by using the cog icon. When choosing an image in the
  media-dialog, if it's not already an optimized image, an optimized copy
  is automatically created and used instead.

- Optimized images are now hidden by default, in the media dialog, and
  can be shown by clicking a checkbox at the top of the modal when in
  debug mode

task-2091417
closes odoo/odoo#45174
This commit is contained in:
Samuel Degueldre
2020-03-30 11:06:48 +00:00
parent 25630e4421
commit 49ea7b2b41
9 changed files with 188 additions and 129 deletions
+9
View File
@@ -173,7 +173,16 @@ class Web_Editor(http.Controller):
if attachment.type == 'url':
raise UserError(_("You cannot change the quality, the width or the name of an URL attachment."))
if copy:
original = attachment
attachment = attachment.copy()
attachment.original_id = original
# Uniquify url by adding a path segment with the id before the name
if attachment.url:
url_fragments = attachment.url.split('/')
url_fragments.insert(-1, str(attachment.id))
attachment.url = '/'.join(url_fragments)
elif attachment.original_id:
attachment.datas = attachment.original_id.datas
data = {}
if name:
data['name'] = name
+2 -1
View File
@@ -14,6 +14,7 @@ class IrAttachment(models.Model):
image_src = fields.Char(compute='_compute_image_src')
image_width = fields.Integer(compute='_compute_image_size')
image_height = fields.Integer(compute='_compute_image_size')
original_id = fields.Many2one('ir.attachment', string="Original (unoptimized, unresized) attachment")
def _compute_local_url(self):
for attachment in self:
@@ -47,4 +48,4 @@ class IrAttachment(models.Model):
def _get_media_info(self):
"""Return a dict with the values that we need on the media dialog."""
self.ensure_one()
return self.read(['id', 'name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height'])[0]
return self._read_format(['id', 'name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'])[0]
@@ -171,7 +171,6 @@ var EditorMenuBar = Widget.extend({
if (this.snippetsMenu) {
await this.snippetsMenu.cleanForSave();
}
await this.getParent().saveCroppedImages(this.rte.editable());
await this.rte.save();
@@ -33,20 +33,37 @@ var ImageOptimizeDialog = Dialog.extend({
{text: _t("Keep Original"), close: false, click: this._onKeepOriginalClick.bind(this)}
],
}, options));
this.isExisting = params.isExisting;
this.attachment = params.attachment;
this.params = params;
},
/**
* @override
*/
willStart: async function () {
const _super = this._super.bind(this);
const {isExisting, attachment, optimizedWidth} = this.params;
this.isExisting = isExisting;
this.attachment = attachment;
let original = attachment;
if (attachment.original_id) {
[original] = await this._rpc({
model: 'ir.attachment',
method: 'read',
args: [[attachment.original_id[0]], ['image_width', 'image_height']],
});
}
this.image_width = original.image_width;
this.image_height = original.image_height;
// We do not support resizing and quality for:
// - SVG because it doesn't make sense
// - GIF because our current code is not made to handle all the frames
this.disableResize = ['image/jpeg', 'image/jpe', 'image/jpg', 'image/png'].indexOf(this.attachment.mimetype) === -1;
this.disableQuality = this.disableResize;
this.toggleQuality = this.attachment.mimetype === 'image/png';
this.optimizedWidth = Math.min(params.optimizedWidth || this.attachment.image_width, this.attachment.image_width);
this.optimizedWidth = Math.min(optimizedWidth || this.image_width, this.image_width);
this.defaultQuality = this.isExisting ? 100 : 80;
this.defaultWidth = parseInt(this.isExisting ? this.attachment.image_width : this.optimizedWidth);
this.defaultHeight = parseInt(this.isExisting || !this.attachment.image_width ? this.attachment.image_height :
this.optimizedWidth / this.attachment.image_width * this.attachment.image_height);
this.defaultWidth = parseInt(this.isExisting ? this.image_width : this.optimizedWidth);
this.defaultHeight = parseInt(this.isExisting || !this.image_width ? this.image_height :
this.optimizedWidth / this.image_width * this.image_height);
this.suggestedWidths = [];
this._addSuggestedWidth(128, '128');
@@ -56,11 +73,12 @@ var ImageOptimizeDialog = Dialog.extend({
this._addSuggestedWidth(this.optimizedWidth,
_.str.sprintf(_t("%d (Suggested)"), this.optimizedWidth));
this.suggestedWidths.push({
'width': this.attachment.image_width,
'text': _.str.sprintf(_t("%d (Original)"), this.attachment.image_width),
'width': this.image_width,
'text': _.str.sprintf(_t("%d (Original)"), this.image_width),
});
this.suggestedWidths = _.sortBy(this.suggestedWidths, 'width');
this._updatePreview = _.debounce(this._updatePreview.bind(this), 300);
return _super(...arguments);
},
/**
* @override
@@ -104,7 +122,7 @@ var ImageOptimizeDialog = Dialog.extend({
* @param {string} text
*/
_addSuggestedWidth: function (size, text) {
if (size < this.attachment.image_width) {
if (size < this.image_width) {
this.suggestedWidths.push({
width: size,
text: text,
@@ -154,7 +172,7 @@ var ImageOptimizeDialog = Dialog.extend({
var width = parseInt(this.$widthInput.val() || 0);
var height = parseInt(this.$heightInput.val() || 0);
this.$previewImage.attr('src', _.str.sprintf('/web/image/%d/%dx%d?quality=%d',
this.attachment.id, width, height, this._getAdaptedQuality()));
(this.attachment.original_id || [this.attachment.id])[0], width, height, this._getAdaptedQuality()));
this.$widthPresets.removeClass('active');
_.each(this.$widthPresets, function (button) {
var $button = $(button);
@@ -189,10 +207,10 @@ var ImageOptimizeDialog = Dialog.extend({
if (quality < 0 || quality > 100) {
isValid = false;
}
if (width < 0 || width > this.attachment.image_width) {
if (width < 0 || width > this.image_width) {
isValid = false;
}
if (height < 0 || height > this.attachment.image_height) {
if (height < 0 || height > this.image_height) {
isValid = false;
}
return isValid;
@@ -208,7 +226,7 @@ var ImageOptimizeDialog = Dialog.extend({
_onHeightInput: function () {
var height = parseInt(this.$heightInput.val()) || 0;
this.$widthInput
.val(parseInt(height / this.attachment.image_height * this.attachment.image_width));
.val(parseInt(height / this.image_height * this.image_width));
this._updatePreview();
this._validateForm();
},
@@ -244,7 +262,7 @@ var ImageOptimizeDialog = Dialog.extend({
'width': parseInt(this.$widthInput.val() || 0),
'height': parseInt(this.$heightInput.val() || 0),
};
if (this.isExisting) {
if (this.isExisting && !this.attachment.original_id) {
params['copy'] = true;
}
return this._rpc({
@@ -292,7 +310,7 @@ var ImageOptimizeDialog = Dialog.extend({
_onWidthInput: function () {
var width = parseInt(this.$widthInput.val() || 0);
this.$heightInput
.val(parseInt(width / this.attachment.image_width * this.attachment.image_height));
.val(parseInt(width / this.image_width * this.image_height));
this._updatePreview();
this._validateForm();
},
@@ -118,7 +118,6 @@ var SearchableMediaWidget = MediaWidget.extend({
var FileWidget = SearchableMediaWidget.extend({
events: _.extend({}, SearchableMediaWidget.prototype.events || {}, {
'click .o_upload_media_button': '_onUploadButtonClick',
'click .o_we_quick_upload': '_onQuickUploadClick',
'change .o_file_input': '_onFileInputChange',
'click .o_upload_media_url_button': '_onUploadURLButtonClick',
'input .o_we_url_input': '_onURLInputChange',
@@ -150,7 +149,6 @@ var FileWidget = SearchableMediaWidget.extend({
this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;
this.options = _.extend({
showQuickUpload: config.isDebug(),
mediaWidth: media && media.clientWidth,
}, options || {});
@@ -192,7 +190,7 @@ var FileWidget = SearchableMediaWidget.extend({
await this._renderThumbnails();
if (o.url) {
self._selectAttachement(_.find(self.attachments, function (attachment) {
return attachment.image_src === o.url;
return o.url === attachment.image_src;
}) || o);
}
return def;
@@ -231,7 +229,7 @@ var FileWidget = SearchableMediaWidget.extend({
args: [],
kwargs: {
domain: this._getAttachmentsDomain(this.needle),
fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height'],
fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'],
order: [{name: 'id', asc: false}],
context: this.options.context,
// Try to fetch first record of next page just to know whether there is a next page.
@@ -328,7 +326,8 @@ var FileWidget = SearchableMediaWidget.extend({
var self = this;
this.$('.o_existing_attachment_cell.o_we_attachment_selected').removeClass("o_we_attachment_selected");
_.each(this.selectedAttachments, function (attachment) {
self.$('.o_existing_attachment_cell[data-id=' + attachment.id + ']').addClass("o_we_attachment_selected");
self.$('.o_existing_attachment_cell[data-id=' + attachment.id + ']')
.addClass("o_we_attachment_selected").css('display', '');
});
},
/**
@@ -336,6 +335,7 @@ var FileWidget = SearchableMediaWidget.extend({
* @param {object} attachment
*/
_handleNewAttachment: function (attachment) {
this.attachments = this.attachments.filter(att => att.id !== attachment.id);
this.attachments.unshift(attachment);
this._renderThumbnails();
this._selectAttachement(attachment);
@@ -368,7 +368,7 @@ var FileWidget = SearchableMediaWidget.extend({
* @returns {Promise} resolved with the updated attachment object when the
* optimize dialog is saved. Rejected if the dialog is otherwise closed.
*/
_openImageOptimizeDialog: function (attachment, isExisting) {
_openImageOptimizeDialog: function (attachment, isExisting, $attachmentCell) {
var self = this;
var promise = new Promise(function (resolve, reject) {
self.trigger_up('hide_parent_dialog_request');
@@ -390,7 +390,7 @@ var FileWidget = SearchableMediaWidget.extend({
}
});
});
var always = function () {
var always = () => {
self.trigger_up('show_parent_dialog_request');
};
promise.then(always).guardedCatch(always);
@@ -431,21 +431,23 @@ var FileWidget = SearchableMediaWidget.extend({
* @private
* @returns {Promise}
*/
_save: function () {
var self = this;
_save: async function () {
if (this.options.multiImages) {
return Promise.resolve(this.selectedAttachments);
return this.selectedAttachments;
}
var img = this.selectedAttachments[0];
if (!img || !img.id) {
return Promise.resolve(this.media);
return this.media;
}
// Auto optimize unoptimized images.
if (['image/jpeg', 'image/jpe', 'image/jpg', 'image/png'].includes(img.mimetype) && img.type === 'binary' && !img.original_id) {
img = await this._optimizeAttachment(img);
}
var prom;
if (!img.public && !img.access_token) {
prom = this._rpc({
await this._rpc({
model: 'ir.attachment',
method: 'generate_access_token',
args: [[img.id]]
@@ -454,52 +456,67 @@ var FileWidget = SearchableMediaWidget.extend({
});
}
return Promise.resolve(prom).then(function () {
if (img.image_src) {
var src = img.image_src;
if (!img.public && img.access_token) {
src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!self.$media.is('img')) {
// Note: by default the images receive the bootstrap opt-in
// img-fluid class. We cannot make them all responsive
// by design because of libraries and client databases img.
self.$media = $('<img/>', {class: 'img-fluid o_we_custom_image'});
self.media = self.$media[0];
}
self.$media.attr('src', src);
} else {
if (!self.$media.is('a')) {
$('.note-control-selection').hide();
self.$media = $('<a/>');
self.media = self.$media[0];
}
var href = '/web/content/' + img.id + '?';
if (!img.public && img.access_token) {
href += _.str.sprintf('access_token=%s&', img.access_token);
}
href += 'unique=' + img.checksum + '&download=true';
self.$media.attr('href', href);
self.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
if (img.image_src) {
var src = img.image_src;
if (!img.public && img.access_token) {
src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!this.$media.is('img')) {
self.$media.attr('alt', img.alt);
var style = self.style;
if (style) {
self.$media.css(style);
// Note: by default the images receive the bootstrap opt-in
// img-fluid class. We cannot make them all responsive
// by design because of libraries and client databases img.
this.$media = $('<img/>', {class: 'img-fluid o_we_custom_image'});
this.media = this.$media[0];
}
this.$media.attr('src', src);
} else {
if (!this.$media.is('a')) {
$('.note-control-selection').hide();
this.$media = $('<a/>');
this.media = this.$media[0];
}
var href = '/web/content/' + img.id + '?';
if (!img.public && img.access_token) {
href += _.str.sprintf('access_token=%s&', img.access_token);
}
href += 'unique=' + img.checksum + '&download=true';
this.$media.attr('href', href);
this.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
}
// Remove crop related attributes
if (self.$media.attr('data-aspect-ratio')) {
var attrs = ['aspect-ratio', 'x', 'y', 'width', 'height', 'rotate', 'scale-x', 'scale-y', 'crop:originalSrc'];
self.$media.removeClass('o_cropped_img_to_save');
_.each(attrs, function (attr) {
self.$media.removeData(attr);
self.$media.removeAttr('data-' + attr);
});
}
return self.media;
this.$media.attr('alt', img.alt);
var style = this.style;
if (style) {
this.$media.css(style);
}
// Remove crop related attributes
if (this.$media.attr('data-aspect-ratio')) {
var attrs = ['aspect-ratio', 'x', 'y', 'width', 'height', 'rotate', 'scale-x', 'scale-y', 'crop:originalSrc'];
this.$media.removeClass('o_cropped_img_to_save');
attrs.forEach(attr => {
this.$media.removeData(attr);
this.$media.removeAttr('data-' + attr);
});
}
return this.media;
},
/**
* Creates and returns an optimized copy of an attachment.
*
* @private
* @param {object} attachment
*/
_optimizeAttachment: function (attachment) {
return this._rpc({
route: `/web_editor/attachment/${attachment.id}/update`,
params: {
copy: true,
name: attachment.name,
quality: attachment.mimetype === 'image/png' ? 0 : 80,
width: this._computeOptimizedWidth(),
},
});
},
/**
@@ -562,13 +579,12 @@ var FileWidget = SearchableMediaWidget.extend({
* @private
*/
_onExistingOptimizeClick: function (ev) {
var self = this;
var $a = $(ev.currentTarget).closest('.o_existing_attachment_cell');
var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(this.attachments, {id: id});
ev.stopPropagation();
return this._openImageOptimizeDialog(attachment, true).then(function (newAttachment) {
self._handleNewAttachment(newAttachment);
return this._openImageOptimizeDialog(attachment, true, $a).then(newAttachment => {
this._handleNewAttachment(newAttachment);
});
},
/**
@@ -594,7 +610,6 @@ var FileWidget = SearchableMediaWidget.extend({
_addData: function () {
var self = this;
var uploadMutex = new concurrency.Mutex();
var optimizeMutex = new concurrency.Mutex();
// Upload the smallest file first to block the user the least possible.
var files = _.sortBy(this.$fileInput[0].files, 'size');
@@ -604,54 +619,30 @@ var FileWidget = SearchableMediaWidget.extend({
// limited by bandwidth.
uploadMutex.exec(function () {
return utils.getDataURLFromFile(file).then(function (result) {
var params = {
'name': file.name,
'data': result.split(',')[1],
'res_id': self.options.res_id,
'res_model': self.options.res_model,
};
if (self.quickUpload) {
params['width'] = self._computeOptimizedWidth();
params['quality'] = 80;
} else {
params['width'] = 0;
params['quality'] = 0;
}
return self._rpc({
route: '/web_editor/attachment/add_data',
params: params,
params: {
'name': file.name,
'data': result.split(',')[1],
'res_id': self.options.res_id,
'res_model': self.options.res_model,
'width': 0,
'quality': 0,
},
}).then(function (attachment) {
if (attachment.image_src && !self.quickUpload) {
optimizeMutex.exec(function () {
return self._openImageOptimizeDialog(attachment).then(function (updatedAttachment) {
self._handleNewAttachment(updatedAttachment);
});
});
} else {
self._handleNewAttachment(attachment);
}
self._handleNewAttachment(attachment);
});
});
});
});
return uploadMutex.getUnlockedDef().then(function () {
return optimizeMutex.getUnlockedDef().then(function () {
self.quickUpload = false;
if (!self.options.multiImages && !self.noSave) {
self.trigger_up('save_request');
}
self.noSave = false;
});
if (!self.options.multiImages && !self.noSave) {
self.trigger_up('save_request');
}
self.noSave = false;
});
},
/**
* @private
*/
_onQuickUploadClick: function () {
this.quickUpload = true;
this.$uploadButton.trigger('click');
},
/**
* @private
*/
@@ -671,6 +662,7 @@ var FileWidget = SearchableMediaWidget.extend({
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.attachments = _.without(self.attachments, attachment);
self.attachments.filter(at => at.original_id[0] === attachment.id).forEach(at => delete at.original_id);
if (!self.attachments.length) {
self._renderThumbnails(); //render the message and image if empty
} else {
@@ -760,6 +752,9 @@ var FileWidget = SearchableMediaWidget.extend({
var ImageWidget = FileWidget.extend({
template: 'wysiwyg.widgets.image',
existingAttachmentsTemplate: 'wysiwyg.widgets.image.existing.attachments',
events: Object.assign({}, FileWidget.prototype.events, {
'change input.o_we_show_optimized': '_onShowOptimizedChange',
}),
MIN_ROW_HEIGHT: 128,
/**
@@ -820,6 +815,7 @@ var ImageWidget = FileWidget.extend({
toReplace.replaceWith(el);
}
});
this._toggleOptimized(this.$('input.o_we_show_optimized')[0].checked);
// Placeholders have a 3:2 aspect ratio like most photos.
const placeholderWidth = 3 / 2 * this.MIN_ROW_HEIGHT;
this.$('.o_we_attachment_placeholder').css({
@@ -827,6 +823,23 @@ var ImageWidget = FileWidget.extend({
flexBasis: placeholderWidth,
});
},
/**
* @override
*/
_renderExisting: function (attachments) {
return QWeb.render(this.existingAttachmentsTemplate, {
attachments: attachments,
widget: this,
});
},
/**
* @private
*
* @param {boolean} value whether to toggle optimized attachments on or off
*/
_toggleOptimized: function (value) {
this.$('.o_we_attachment_optimized').each((i, cell) => cell.style.setProperty('display', value ? null : 'none', 'important'));
},
//--------------------------------------------------------------------------
// Handlers
@@ -849,6 +862,12 @@ var ImageWidget = FileWidget.extend({
cell.classList.add('d-flex');
cell.dataset.loaded = 'true';
},
/**
* @override
*/
_onShowOptimizedChange: function (ev) {
this._toggleOptimized(ev.target.checked);
},
});
@@ -269,6 +269,13 @@ body .modal {
box-shadow: 0px 0px 2px 2px $o-brand-primary;
}
.o_we_attachment_optimized .badge {
position: absolute;
bottom: 0;
right: 0;
margin: 2px;
}
.font-icons-icons {
> span {
text-align: center;
+14 -11
View File
@@ -142,7 +142,6 @@
<button type="button" class="btn btn-primary o_upload_media_button">
<t t-esc="uploadText"/>
</button>
<t t-raw="alternateUpload"/>
</div>
<div class="input-group align-items-center ml-2">
@@ -158,7 +157,7 @@
</div>
</div>
</div>
<t t-raw="0"/>
<t t-call="wysiwyg.widgets.media.search"/>
</div>
</t>
@@ -170,12 +169,14 @@
<t t-set="addText">Add URL</t>
<t t-set="searchPlaceholder">Search an image</t>
<t t-set="urlWarningTitle">The URL does not contain any image. The file will be added in the document section.</t>
<t t-if="widget.options.showQuickUpload" t-set="alternateUpload">
<button class="btn btn-primary o_we_quick_upload" type="button" title="Quick Upload">
<i class="fa fa-fast-forward"/>
<span class="sr-only">Quick Upload</span>
</button>
</t>
<div class="d-flex justify-content-end flex-grow-1 pr-3">
<div t-attf-class="custom-control custom-switch #{__debug__ or 'd-none'}">
<input class="o_we_show_optimized ml-2 custom-control-input" type="checkbox" id="o_we_show_optimized_switch"/>
<label class="custom-control-label" for="o_we_show_optimized_switch">
Show optimized images
</label>
</div>
</div>
</t>
</t>
@@ -215,7 +216,7 @@
<div class="input-group-prepend">
<div class="input-group-text">Width</div>
</div>
<input type="number" class="form-control" id="o_we_width" name="width" aria-describedby="sizeHelp" min="1" t-att-max="widget.attachment.image_width" t-att-value="widget.defaultWidth"/>
<input type="number" class="form-control" id="o_we_width" name="width" aria-describedby="sizeHelp" min="1" t-att-max="widget.image_width" t-att-value="widget.defaultWidth"/>
<div class="input-group-append">
<div class="input-group-text">px</div>
</div>
@@ -229,7 +230,7 @@
<div class="input-group-prepend">
<div class="input-group-text">Height</div>
</div>
<input type="number" class="form-control" id="o_we_height" name="height" aria-describedby="sizeHelp" min="1" t-att-max="widget.attachment.image_height" t-att-value="widget.defaultHeight"/>
<input type="number" class="form-control" id="o_we_height" name="height" aria-describedby="sizeHelp" min="1" t-att-max="widget.image_height" t-att-value="widget.defaultHeight"/>
<div class="input-group-append">
<div class="input-group-text">px</div>
</div>
@@ -304,13 +305,15 @@
</t>
<t t-name="wysiwyg.widgets.image.existing.attachment">
<div class="o_existing_attachment_cell position-relative bg-light" t-att-data-id="attachment.id">
<t t-set="isOptimized" t-value="!!attachment.original_id"/>
<div t-attf-class="o_existing_attachment_cell position-relative bg-light #{isOptimized and 'o_we_attachment_optimized d-none'}" t-att-data-id="attachment.id">
<i t-if="attachment.type === 'binary'" class="fa fa-cog o_existing_attachment_optimize p-2" title="Optimize" role="img" aria-label="Optimize"/>
<t t-call="wysiwyg.widgets.file.existing.remove"/>
<div t-attf-class="d-flex align-items-center justify-content-center h-100 w-100">
<!-- 256 is 2 * ImageWidget.MIN_ROW_HEIGHT, hopefully the highest display width of any image -->
<img class="img img-fluid o_we_attachment_highlight" t-attf-src="#{attachment.image_src}?height=256" t-att-alt="attachment.name" t-att-title="attachment.name"/>
</div>
<span t-if="isOptimized" class="badge badge-success">Optimized</span>
</div>
</t>
@@ -284,9 +284,7 @@ QUnit.module('web_editor', {}, function () {
await testUtils.dom.click($('.modal #editor-media-image .o_existing_attachment_cell:first').removeClass('d-none'));
var $editable = form.$('.oe_form_field[name="body"] .note-editable');
assert.strictEqual($editable.data('wysiwyg').getValue(),
'<p>t<img class="img-fluid o_we_custom_image" data-src="/web_editor/static/src/img/transparent.png">oto toto toto</p><p>tata</p>',
assert.ok($editable.find('img')[0].dataset.src.includes('/web_editor/static/src/img/transparent.png'),
"should have the image in the dom");
testUtils.mock.unpatch(MediaDialog);
@@ -259,6 +259,11 @@ function wysiwygData(data) {
string: "image_height",
type: 'integer',
},
original_id: {
string: "original_id",
type: 'many2one',
relation: 'ir.attachment',
},
},
records: [{
id: 1,