diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/attachment_box | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/attachment_box')
4 files changed, 557 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/attachment_box/attachment_box.js b/addons/mail/static/src/components/attachment_box/attachment_box.js new file mode 100644 index 00000000..5bfe7c06 --- /dev/null +++ b/addons/mail/static/src/components/attachment_box/attachment_box.js @@ -0,0 +1,124 @@ +odoo.define('mail/static/src/components/attachment_box/attachment_box.js', function (require) { +'use strict'; + +const components = { + AttachmentList: require('mail/static/src/components/attachment_list/attachment_list.js'), + DropZone: require('mail/static/src/components/drop_zone/drop_zone.js'), + FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'), +}; +const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js'); +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class AttachmentBox extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + this.isDropZoneVisible = useDragVisibleDropZone(); + useShouldUpdateBasedOnProps(); + useStore(props => { + const thread = this.env.models['mail.thread'].get(props.threadLocalId); + return { + thread, + threadAllAttachments: thread ? thread.allAttachments : [], + threadId: thread && thread.id, + threadModel: thread && thread.model, + }; + }, { + compareDepth: { + threadAllAttachments: 1, + }, + }); + /** + * Reference of the file uploader. + * Useful to programmatically prompts the browser file uploader. + */ + this._fileUploaderRef = useRef('fileUploader'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Get an object which is passed to FileUploader component to be used when + * creating attachment. + * + * @returns {Object} + */ + get newAttachmentExtraData() { + return { + originThread: [['link', this.thread]], + }; + } + + /** + * @returns {mail.thread|undefined} + */ + get thread() { + return this.env.models['mail.thread'].get(this.props.threadLocalId); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onAttachmentCreated(ev) { + // FIXME Could be changed by spying attachments count (task-2252858) + this.trigger('o-attachments-changed'); + } + + /** + * @private + * @param {Event} ev + */ + _onAttachmentRemoved(ev) { + // FIXME Could be changed by spying attachments count (task-2252858) + this.trigger('o-attachments-changed'); + } + + /** + * @private + * @param {Event} ev + */ + _onClickAdd(ev) { + ev.preventDefault(); + ev.stopPropagation(); + this._fileUploaderRef.comp.openBrowserFileUploader(); + } + + /** + * @private + * @param {CustomEvent} ev + * @param {Object} ev.detail + * @param {FileList} ev.detail.files + */ + async _onDropZoneFilesDropped(ev) { + ev.stopPropagation(); + await this._fileUploaderRef.comp.uploadFiles(ev.detail.files); + this.isDropZoneVisible.value = false; + } + +} + +Object.assign(AttachmentBox, { + components, + props: { + threadLocalId: String, + }, + template: 'mail.AttachmentBox', +}); + +return AttachmentBox; + +}); diff --git a/addons/mail/static/src/components/attachment_box/attachment_box.scss b/addons/mail/static/src/components/attachment_box/attachment_box.scss new file mode 100644 index 00000000..d51cca9c --- /dev/null +++ b/addons/mail/static/src/components/attachment_box/attachment_box.scss @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_AttachmentBox { + position: relative; +} + +.o_AttachmentBox_buttonAdd { + align-self: center; +} + +.o_AttachmentBox_content { + display: flex; + flex-direction: column; +} + +.o_AttachmentBox_dashedLine { + flex-grow: 1; +} + +.o_AttachmentBox_fileInput { + display: none; +} + +.o_AttachmentBox_title { + display: flex; + align-items: center; +} + +.o_AttachmentBox_titleText { + padding: map-get($spacers, 3); +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_AttachmentBox_dashedLine { + border-style: dashed; + border-color: gray('300'); +} + +.o_AttachmentBox_title { + font-weight: bold; +} diff --git a/addons/mail/static/src/components/attachment_box/attachment_box.xml b/addons/mail/static/src/components/attachment_box/attachment_box.xml new file mode 100644 index 00000000..9cd3e713 --- /dev/null +++ b/addons/mail/static/src/components/attachment_box/attachment_box.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.AttachmentBox" owl="1"> + <div class="o_AttachmentBox"> + <div class="o_AttachmentBox_title"> + <hr class="o_AttachmentBox_dashedLine"/> + <span class="o_AttachmentBox_titleText"> + Attachments + </span> + <hr class="o_AttachmentBox_dashedLine"/> + </div> + <div class="o_AttachmentBox_content"> + <t t-if="isDropZoneVisible.value"> + <DropZone + class="o_AttachmentBox_dropZone" + t-on-o-dropzone-files-dropped="_onDropZoneFilesDropped" + t-ref="dropzone" + /> + </t> + <t t-if="thread and thread.allAttachments.length > 0"> + <AttachmentList + class="o_attachmentBox_attachmentList" + areAttachmentsDownloadable="true" + attachmentLocalIds="thread.allAttachments.map(attachment => attachment.localId)" + attachmentsDetailsMode="'hover'" + attachmentsImageSize="'small'" + showAttachmentsFilenames="true" + t-on-o-attachment-removed="_onAttachmentRemoved" + /> + </t> + <button class="o_AttachmentBox_buttonAdd btn btn-link" type="button" t-on-click="_onClickAdd"> + <i class="fa fa-plus-square"/> + Add attachments + </button> + </div> + <t t-if="thread"> + <FileUploader + attachmentLocalIds="thread.allAttachments.map(attachment => attachment.localId)" + newAttachmentExtraData="newAttachmentExtraData" + uploadModel="thread.model" + uploadId="thread.id" + t-on-o-attachment-created="_onAttachmentCreated" + t-ref="fileUploader" + /> + </t> + </div> + </t> + +</templates> diff --git a/addons/mail/static/src/components/attachment_box/attachment_box_tests.js b/addons/mail/static/src/components/attachment_box/attachment_box_tests.js new file mode 100644 index 00000000..142eb804 --- /dev/null +++ b/addons/mail/static/src/components/attachment_box/attachment_box_tests.js @@ -0,0 +1,337 @@ +odoo.define('mail/static/src/components/attachment_box/attachment_box_tests.js', function (require) { +"use strict"; + +const components = { + AttachmentBox: require('mail/static/src/components/attachment_box/attachment_box.js'), +}; +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + dragenterFiles, + dropFiles, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const { file: { createFile } } = require('web.test_utils'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('attachment_box', {}, function () { +QUnit.module('attachment_box_tests.js', { + beforeEach() { + beforeEach(this); + + this.createAttachmentBoxComponent = async (thread, otherProps) => { + const props = Object.assign({ threadLocalId: thread.localId }, otherProps); + await createRootComponent(this, components.AttachmentBox, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('base empty rendering', async function (assert) { + assert.expect(4); + + await this.start(); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + await this.createAttachmentBoxComponent(thread); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox`).length, + 1, + "should have an attachment box" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox_buttonAdd`).length, + 1, + "should have a button add" + ); + assert.strictEqual( + document.querySelectorAll(`.o_FileUploader_input`).length, + 1, + "should have a file input" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox .o_Attachment`).length, + 0, + "should not have any attachment" + ); +}); + +QUnit.test('base non-empty rendering', async function (assert) { + assert.expect(6); + + this.data['ir.attachment'].records.push( + { + mimetype: 'text/plain', + name: 'Blah.txt', + res_id: 100, + res_model: 'res.partner', + }, + { + mimetype: 'text/plain', + name: 'Blu.txt', + res_id: 100, + res_model: 'res.partner', + } + ); + await this.start({ + async mockRPC(route, args) { + if (route.includes('ir.attachment/search_read')) { + assert.step('ir.attachment/search_read'); + } + return this._super(...arguments); + }, + }); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + await thread.fetchAttachments(); + await this.createAttachmentBoxComponent(thread); + assert.verifySteps( + ['ir.attachment/search_read'], + "should have fetched attachments" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox`).length, + 1, + "should have an attachment box" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox_buttonAdd`).length, + 1, + "should have a button add" + ); + assert.strictEqual( + document.querySelectorAll(`.o_FileUploader_input`).length, + 1, + "should have a file input" + ); + assert.strictEqual( + document.querySelectorAll(`.o_attachmentBox_attachmentList`).length, + 1, + "should have an attachment list" + ); +}); + +QUnit.test('attachment box: drop attachments', async function (assert) { + assert.expect(5); + + await this.start(); + const thread = this.env.models['mail.thread'].create({ + id: 100, + model: 'res.partner', + }); + await thread.fetchAttachments(); + await this.createAttachmentBoxComponent(thread); + const files = [ + await createFile({ + content: 'hello, world', + contentType: 'text/plain', + name: 'text.txt', + }), + ]; + assert.strictEqual( + document.querySelectorAll('.o_AttachmentBox').length, + 1, + "should have an attachment box" + ); + + await afterNextRender(() => + dragenterFiles(document.querySelector('.o_AttachmentBox')) + ); + assert.ok( + document.querySelector('.o_AttachmentBox_dropZone'), + "should have a drop zone" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox .o_Attachment`).length, + 0, + "should have no attachment before files are dropped" + ); + + await afterNextRender(() => + dropFiles( + document.querySelector('.o_AttachmentBox_dropZone'), + files + ) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox .o_Attachment`).length, + 1, + "should have 1 attachment in the box after files dropped" + ); + + await afterNextRender(() => + dragenterFiles(document.querySelector('.o_AttachmentBox')) + ); + const file1 = await createFile({ + content: 'hello, world', + contentType: 'text/plain', + name: 'text2.txt', + }); + const file2 = await createFile({ + content: 'hello, world', + contentType: 'text/plain', + name: 'text3.txt', + }); + await afterNextRender(() => + dropFiles( + document.querySelector('.o_AttachmentBox_dropZone'), + [file1, file2] + ) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBox .o_Attachment`).length, + 3, + "should have 3 attachments in the box after files dropped" + ); +}); + +QUnit.test('view attachments', async function (assert) { + assert.expect(7); + + await this.start({ + hasDialog: true, + }); + const thread = this.env.models['mail.thread'].create({ + attachments: [ + ['insert', { + id: 143, + mimetype: 'text/plain', + name: 'Blah.txt' + }], + ['insert', { + id: 144, + mimetype: 'text/plain', + name: 'Blu.txt' + }] + ], + id: 100, + model: 'res.partner', + }); + const firstAttachment = this.env.models['mail.attachment'].findFromIdentifyingData({ id: 143 }); + await this.createAttachmentBoxComponent(thread); + + await afterNextRender(() => + document.querySelector(` + .o_Attachment[data-attachment-local-id="${firstAttachment.localId}"] + .o_Attachment_image + `).click() + ); + assert.containsOnce( + document.body, + '.o_Dialog', + "a dialog should have been opened once attachment image is clicked", + ); + assert.containsOnce( + document.body, + '.o_AttachmentViewer', + "an attachment viewer should have been opened once attachment image is clicked", + ); + assert.strictEqual( + document.querySelector('.o_AttachmentViewer_name').textContent, + 'Blah.txt', + "attachment viewer iframe should point to clicked attachment", + ); + assert.containsOnce( + document.body, + '.o_AttachmentViewer_buttonNavigationNext', + "attachment viewer should allow to see next attachment", + ); + + await afterNextRender(() => + document.querySelector('.o_AttachmentViewer_buttonNavigationNext').click() + ); + assert.strictEqual( + document.querySelector('.o_AttachmentViewer_name').textContent, + 'Blu.txt', + "attachment viewer iframe should point to next attachment of attachment box", + ); + assert.containsOnce( + document.body, + '.o_AttachmentViewer_buttonNavigationNext', + "attachment viewer should allow to see next attachment", + ); + + await afterNextRender(() => + document.querySelector('.o_AttachmentViewer_buttonNavigationNext').click() + ); + assert.strictEqual( + document.querySelector('.o_AttachmentViewer_name').textContent, + 'Blah.txt', + "attachment viewer iframe should point anew to first attachment", + ); +}); + +QUnit.test('remove attachment should ask for confirmation', async function (assert) { + assert.expect(5); + + await this.start(); + const thread = this.env.models['mail.thread'].create({ + attachments: [ + ['insert', { + id: 143, + mimetype: 'text/plain', + name: 'Blah.txt' + }], + ], + id: 100, + model: 'res.partner', + }); + await this.createAttachmentBoxComponent(thread); + assert.containsOnce( + document.body, + '.o_Attachment', + "should have an attachment", + ); + assert.containsOnce( + document.body, + '.o_Attachment_asideItemUnlink', + "attachment should have a delete button" + ); + + await afterNextRender(() => document.querySelector('.o_Attachment_asideItemUnlink').click()); + assert.containsOnce( + document.body, + '.o_AttachmentDeleteConfirmDialog', + "A confirmation dialog should have been opened" + ); + assert.strictEqual( + document.querySelector('.o_AttachmentDeleteConfirmDialog_mainText').textContent, + `Do you really want to delete "Blah.txt"?`, + "Confirmation dialog should contain the attachment delete confirmation text" + ); + + // Confirm the deletion + await afterNextRender(() => document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click()); + assert.containsNone( + document.body, + '.o_Attachment', + "should no longer have an attachment", + ); +}); + +}); +}); +}); + +}); |
