diff --git a/addons/web/static/src/legacy/js/fields/relational_fields.js b/addons/web/static/src/legacy/js/fields/relational_fields.js index 0328f650938..f985f187f61 100644 --- a/addons/web/static/src/legacy/js/fields/relational_fields.js +++ b/addons/web/static/src/legacy/js/fields/relational_fields.js @@ -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', diff --git a/addons/web/static/src/legacy/xml/base.xml b/addons/web/static/src/legacy/xml/base.xml index 282924134c1..1ba74e6c8c7 100644 --- a/addons/web/static/src/legacy/xml/base.xml +++ b/addons/web/static/src/legacy/xml/base.xml @@ -1338,7 +1338,7 @@ - +
diff --git a/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml b/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml index 2afe2c5a19c..ed18b060e35 100644 --- a/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml +++ b/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.xml @@ -25,7 +25,7 @@ - +
@@ -46,7 +46,7 @@
-
×
+
×
diff --git a/addons/web/static/tests/legacy/fields/relational_fields_tests.js b/addons/web/static/tests/legacy/fields/relational_fields_tests.js index 2ae838787dc..c98ea14e015 100644 --- a/addons/web/static/tests/legacy/fields/relational_fields_tests.js +++ b/addons/web/static/tests/legacy/fields/relational_fields_tests.js @@ -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); }, diff --git a/addons/web/static/tests/views/fields/many2many_binary_field_tests.js b/addons/web/static/tests/views/fields/many2many_binary_field_tests.js index 796327e478b..cf80b3fcd33 100644 --- a/addons/web/static/tests/views/fields/many2many_binary_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_binary_field_tests.js @@ -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"]); } }, }); diff --git a/addons/web_editor/static/src/components/media_dialog/file_selector.js b/addons/web_editor/static/src/components/media_dialog/file_selector.js index 21a4acc5799..09e67424cfa 100644 --- a/addons/web_editor/static/src/components/media_dialog/file_selector.js +++ b/addons/web_editor/static/src/components/media_dialog/file_selector.js @@ -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) { diff --git a/addons/web_editor/static/src/components/media_dialog/media_dialog.js b/addons/web_editor/static/src/components/media_dialog/media_dialog.js index cb47d0edead..f7b33257a3f 100644 --- a/addons/web_editor/static/src/components/media_dialog/media_dialog.js +++ b/addons/web_editor/static/src/components/media_dialog/media_dialog.js @@ -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, }, }); } diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js index 8a810a0b27b..0a4db994d95 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -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; } diff --git a/addons/web_editor/static/src/js/backend/html_field.js b/addons/web_editor/static/src/js/backend/html_field.js index 16b398c4639..36013ee9615 100644 --- a/addons/web_editor/static/src/js/backend/html_field.js +++ b/addons/web_editor/static/src/js/backend/html_field.js @@ -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 }); } diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js index 8080389feb5..f12fa89e336 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg.js @@ -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; diff --git a/addons/web_editor/static/tests/field_html_tests.js b/addons/web_editor/static/tests/field_html_tests.js index 4afbc222408..1811dd5e64b 100644 --- a/addons/web_editor/static/tests/field_html_tests.js +++ b/addons/web_editor/static/tests/field_html_tests.js @@ -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": "", + "note.note,false,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);