[FIX] mail, web_editor: dialog file update event
An event was not called on uploading files in a media dialog. This meant the parent of html field didn't know of the new attachments which resulted in attachments being linked to no record at all and not being garbage collected later. This adds a reference inside the media dialog file input so that it can trigger that event on behalf of the html field. The attachments newly uploaded in media dialog are uneditable to prevent users from unlinking attachments that are still used in the body of the composer (causing the same issue) Task-2860761 closes odoo/odoo#101405 X-original-commit: f4dd1eb3f25672e7309759c347e2b552c5652b2f Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -2370,6 +2370,8 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
|
||||
fieldsToFetch: {
|
||||
name: {type: 'char'},
|
||||
mimetype: {type: 'char'},
|
||||
res_id: {type: 'number'},
|
||||
access_token: {type: 'char'},
|
||||
},
|
||||
events: {
|
||||
'click .o_attach': '_onAttach',
|
||||
|
||||
@@ -1338,7 +1338,7 @@
|
||||
<t t-name="FieldBinaryFileUploader.attachment_preview">
|
||||
<t t-set="url" t-value="widget.metadata[file.id] ? widget.metadata[file.id].url : false"/>
|
||||
<t t-if="file.data" t-set="file" t-value="file.data"/>
|
||||
<t t-set="editable" t-value="widget.mode === 'edit'"/>
|
||||
<t t-set="editable" t-value="widget.mode === 'edit' and !(file.res_id === 0 and file.access_token)"/>
|
||||
<t t-if="file.mimetype" t-set="mimetype" t-value="file.mimetype"/>
|
||||
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}" t-att-title="file.name">
|
||||
<div class="o_attachment_wrap">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</t>
|
||||
|
||||
<t t-name="Many2ManyBinaryField.attachment_preview" owl="1">
|
||||
<t t-set="editable" t-value="!props.readonly"/>
|
||||
<t t-set="editable" t-value="!props.readonly and !(file.res_id === 0 and file.access_token)"/>
|
||||
<div t-attf-class="o_attachment o_attachment_many2many #{ editable ? 'o_attachment_editable' : '' } #{upload ? 'o_attachment_uploading' : ''}" t-att-title="file.name">
|
||||
<div class="o_attachment_wrap">
|
||||
<t t-set="ext" t-value="getExtension(file)"/>
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div class="o_attachment_uploaded"><i class="text-success fa fa-check" role="img" aria-label="Uploaded" title="Uploaded"/></div>
|
||||
<div t-if="!props.readonly" class="o_attachment_delete" t-on-click.stop="() => this.onFileRemove(file.id)"><span class="text-white" role="img" aria-label="Delete" title="Delete">×</span></div>
|
||||
<div t-if="editable" class="o_attachment_delete" t-on-click.stop="() => this.onFileRemove(file.id)"><span class="text-white" role="img" aria-label="Delete" title="Delete">×</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -2977,11 +2977,14 @@ QUnit.module('Legacy relational_fields', {
|
||||
fields: {
|
||||
name: {string:"Name", type: "char"},
|
||||
mimetype: {string: "Mimetype", type: "char"},
|
||||
res_id: {type: "number"},
|
||||
access_token: {type: "char"}
|
||||
},
|
||||
records: [{
|
||||
id: 17,
|
||||
name: 'Marley&Me.jpg',
|
||||
mimetype: 'jpg',
|
||||
res_id: 1, //non-zero to avoid transiant model editor attachment protection
|
||||
}],
|
||||
};
|
||||
this.data.turtle.fields.picture_ids = {
|
||||
@@ -3005,7 +3008,7 @@ QUnit.module('Legacy relational_fields', {
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(route);
|
||||
if (route === '/web/dataset/call_kw/ir.attachment/read') {
|
||||
assert.deepEqual(args.args[1], ['name', 'mimetype']);
|
||||
assert.deepEqual(args.args[1], ['name', 'mimetype', 'res_id', 'access_token']);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.step(route);
|
||||
}
|
||||
if (route === "/web/dataset/call_kw/ir.attachment/read") {
|
||||
assert.deepEqual(args.args[1], ["name", "mimetype"]);
|
||||
assert.deepEqual(args.args[1], ["name", "mimetype", "res_id", "access_token"]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -239,6 +239,9 @@ export class FileSelector extends Component {
|
||||
if (!this.props.multiSelect) {
|
||||
await this.props.save();
|
||||
}
|
||||
if (this.props.onAttachmentChange) {
|
||||
this.props.onAttachmentChange(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
onRemoved(attachmentId) {
|
||||
|
||||
@@ -83,6 +83,7 @@ export class MediaDialog extends Component {
|
||||
selectedMedia: this.selectedMedia,
|
||||
selectMedia: (...args) => this.selectMedia(...args, tab.id, additionalProps.multiSelect),
|
||||
save: this.save.bind(this),
|
||||
onAttachmentChange: this.props.onAttachmentChange,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -331,9 +331,10 @@ var FieldHtml = basic_fields.DebouncedField.extend(DynamicPlaceholderFieldMixin)
|
||||
* when closing the wizard.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} attachments
|
||||
* @param {Object} event the event containing attachment data
|
||||
*/
|
||||
_onAttachmentChange: function (attachments) {
|
||||
_onAttachmentChange: function (event) {
|
||||
const attachments = event.data;
|
||||
if (!this.fieldNameAttachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ export class HtmlField extends Component {
|
||||
return {
|
||||
value: this.props.value,
|
||||
autostart: false,
|
||||
onAttachmentChange: this._onAttachmentChange.bind(this),
|
||||
onWysiwygBlur: this._onWysiwygBlur.bind(this),
|
||||
...this.props.wysiwygOptions,
|
||||
recordInfo: {
|
||||
@@ -397,6 +398,15 @@ export class HtmlField extends Component {
|
||||
async _getWysiwygClass() {
|
||||
return getWysiwygClass();
|
||||
}
|
||||
_onAttachmentChange(attachment) {
|
||||
if (!this.props.record.fieldNames.includes('attachment_ids')) {
|
||||
return;
|
||||
}
|
||||
this.props.record.update(_.object(['attachment_ids'], [{
|
||||
operation: 'ADD_M2M',
|
||||
ids: attachment
|
||||
}]));
|
||||
}
|
||||
_onWysiwygBlur() {
|
||||
this.commitChanges({ urgent: true });
|
||||
}
|
||||
|
||||
@@ -1303,6 +1303,7 @@ const Wysiwyg = Widget.extend({
|
||||
node: params.node,
|
||||
restoreSelection: restoreSelection,
|
||||
}),
|
||||
onAttachmentChange: this._onAttachmentChange.bind(this),
|
||||
close: () => restoreSelection(),
|
||||
...this.options.mediaModalParams,
|
||||
...params,
|
||||
@@ -2310,6 +2311,13 @@ const Wysiwyg = Widget.extend({
|
||||
}
|
||||
return new Promise(function () {});
|
||||
},
|
||||
_onAttachmentChange(attachment) {
|
||||
// todo: to remove when removing the legacy field_html
|
||||
this.trigger_up('attachment_changed', attachment);
|
||||
if (this.options.onAttachmentChange) {
|
||||
this.options.onAttachmentChange(attachment);
|
||||
}
|
||||
},
|
||||
_onSelectionChange() {
|
||||
if (this.options.autohideToolbar) {
|
||||
const isVisible = this.linkPopover && this.linkPopover.el.offsetParent;
|
||||
|
||||
@@ -13,6 +13,14 @@ var LinkDialog = require('wysiwyg.widgets.LinkDialog');
|
||||
|
||||
const { legacyExtraNextTick, patchWithCleanup } = require("@web/../tests/helpers/utils");
|
||||
|
||||
const { HtmlField } = require('@web_editor/js/backend/html_field');
|
||||
const { FileSelectorControlPanel } = require('@web_editor/components/media_dialog/file_selector');
|
||||
const { getFixture } = require("@web/../tests/helpers/utils");
|
||||
const Registry = require('@web/core/registry');
|
||||
const UploadService = require('@web_editor/components/upload_progress_toast/upload_service');
|
||||
const UnsplashService = require('@web_unsplash/services/unsplash_service');
|
||||
const webClientHelpers = require("@web/../tests/webclient/helpers");
|
||||
|
||||
const { useEffect } = owl;
|
||||
|
||||
var _t = core._t;
|
||||
@@ -314,6 +322,105 @@ QUnit.module('web_editor', {}, function () {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('media dialog: upload', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const onAttachmentChangeTriggered = testUtils.makeTestPromise();
|
||||
patchWithCleanup(HtmlField.prototype, {
|
||||
'_onAttachmentChange': function (event) {
|
||||
onAttachmentChangeTriggered.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
const defFileSelector = testUtils.makeTestPromise();
|
||||
const onChangeTriggered = testUtils.makeTestPromise();
|
||||
patchWithCleanup(FileSelectorControlPanel.prototype, {
|
||||
setup() {
|
||||
this._super();
|
||||
useEffect(() => {
|
||||
defFileSelector.resolve(true);
|
||||
}, () => []);
|
||||
},
|
||||
async onChangeFileInput() {
|
||||
this._super();
|
||||
onChangeTriggered.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
// create and load form view
|
||||
const serviceRegistry = Registry.registry.category("services");
|
||||
serviceRegistry.add("upload", UploadService.uploadService);
|
||||
serviceRegistry.add("unsplash", UnsplashService.unsplashService);
|
||||
|
||||
const serverData = {
|
||||
models: this.data,
|
||||
};
|
||||
serverData.actions = {
|
||||
1: {
|
||||
id: 1,
|
||||
name: "test",
|
||||
res_model: "note.note",
|
||||
type: "ir.actions.act_window",
|
||||
views: [[false, "form"]],
|
||||
},
|
||||
};
|
||||
serverData.views = {
|
||||
"note.note,false,search": "<search></search>",
|
||||
"note.note,false,form": `
|
||||
<form>
|
||||
<field name="body" type="html"/>
|
||||
</form>`,
|
||||
};
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web_editor/attachment/add_data") {
|
||||
return Promise.resolve({"id": 5, "name": "test.jpg", "description": false, "mimetype": "image/jpeg", "checksum": "7951a43bbfb08fd742224ada280913d1897b89ab",
|
||||
"url": false, "type": "binary", "res_id": 1, "res_model": "note.note", "public": false, "access_token": false,
|
||||
"image_src": "/web/image/1-a0e63e61/test.jpg", "image_width": 1, "image_height": 1, "original_id": false
|
||||
});
|
||||
}
|
||||
else if (route === "/web/dataset/call_kw/ir.attachment/generate_access_token") {
|
||||
return Promise.resolve(["129a52e1-6bf2-470a-830e-8e368b022e13"]);
|
||||
}
|
||||
};
|
||||
const webClient = await webClientHelpers.createWebClient({ serverData, mockRPC });
|
||||
await webClientHelpers.doAction(webClient, 1);
|
||||
|
||||
//trigger wysiwyg mediadialog
|
||||
|
||||
const fixture = getFixture();
|
||||
const formField = fixture.querySelector('.o_field_html[name="body"]');
|
||||
const textInput = formField.querySelector('.note-editable p');
|
||||
textInput.innerText = "test";
|
||||
const pText = $(textInput).contents()[0];
|
||||
Wysiwyg.setRange(pText, 1, pText, 2);
|
||||
await new Promise((resolve) => setTimeout(resolve)); //ensure fully set up
|
||||
|
||||
const wysiwyg = $(textInput.parentElement).data('wysiwyg');
|
||||
|
||||
wysiwyg.openMediaDialog();
|
||||
assert.ok(await Promise.race([defFileSelector, new Promise((res, _) => setTimeout(() => res(false), 400))]), "File Selector did not mount");
|
||||
|
||||
// upload test
|
||||
|
||||
const fileInputs = document.querySelectorAll(".o_select_media_dialog input.d-none.o_file_input");
|
||||
const fileB64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==';
|
||||
const fileBytes = new Uint8Array(atob(fileB64).split('').map(char => char.charCodeAt(0)));
|
||||
// redefine 'files' so we can put mock data in through js
|
||||
fileInputs.forEach((input) => Object.defineProperty(input, 'files', {
|
||||
value: [new File(fileBytes, "test.jpg", { type: 'image/jpeg' })],
|
||||
}));
|
||||
fileInputs.forEach(input => {
|
||||
input.dispatchEvent(new Event('change', {}));
|
||||
});
|
||||
|
||||
assert.ok(await Promise.race([onChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
|
||||
"File change event was not triggered");
|
||||
|
||||
assert.ok(await Promise.race([onAttachmentChangeTriggered, new Promise((res, _) => setTimeout(() => res(false), 400))]),
|
||||
"_onAttachmentChange was not called with the new attachment, necessary for unsused upload cleanup on backend");
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('media dialog: image', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user