summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/attachment_box
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/attachment_box
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/attachment_box')
-rw-r--r--addons/mail/static/src/components/attachment_box/attachment_box.js124
-rw-r--r--addons/mail/static/src/components/attachment_box/attachment_box.scss46
-rw-r--r--addons/mail/static/src/components/attachment_box/attachment_box.xml50
-rw-r--r--addons/mail/static/src/components/attachment_box/attachment_box_tests.js337
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",
+ );
+});
+
+});
+});
+});
+
+});