summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/attachment
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/components/attachment')
-rw-r--r--addons/mail/static/src/components/attachment/attachment.js204
-rw-r--r--addons/mail/static/src/components/attachment/attachment.scss204
-rw-r--r--addons/mail/static/src/components/attachment/attachment.xml115
-rw-r--r--addons/mail/static/src/components/attachment/attachment_tests.js762
4 files changed, 1285 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/attachment/attachment.js b/addons/mail/static/src/components/attachment/attachment.js
new file mode 100644
index 00000000..a4b7b136
--- /dev/null
+++ b/addons/mail/static/src/components/attachment/attachment.js
@@ -0,0 +1,204 @@
+odoo.define('mail/static/src/components/attachment/attachment.js', function (require) {
+'use strict';
+
+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 components = {
+ AttachmentDeleteConfirmDialog: require('mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js'),
+};
+
+const { Component, useState } = owl;
+
+class Attachment extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useShouldUpdateBasedOnProps({
+ compareDepth: {
+ attachmentLocalIds: 1,
+ },
+ });
+ useStore(props => {
+ const attachment = this.env.models['mail.attachment'].get(props.attachmentLocalId);
+ return {
+ attachment: attachment ? attachment.__state : undefined,
+ };
+ });
+ this.state = useState({
+ hasDeleteConfirmDialog: false,
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.attachment}
+ */
+ get attachment() {
+ return this.env.models['mail.attachment'].get(this.props.attachmentLocalId);
+ }
+
+ /**
+ * Return the url of the attachment. Temporary attachments, a.k.a. uploading
+ * attachments, do not have an url.
+ *
+ * @returns {string}
+ */
+ get attachmentUrl() {
+ if (this.attachment.isTemporary) {
+ return '';
+ }
+ return this.env.session.url('/web/content', {
+ id: this.attachment.id,
+ download: true,
+ });
+ }
+
+ /**
+ * Get the details mode after auto mode is computed
+ *
+ * @returns {string} 'card', 'hover' or 'none'
+ */
+ get detailsMode() {
+ if (this.props.detailsMode !== 'auto') {
+ return this.props.detailsMode;
+ }
+ if (this.attachment.fileType !== 'image') {
+ return 'card';
+ }
+ return 'hover';
+ }
+
+ /**
+ * Get the attachment representation style to be applied
+ *
+ * @returns {string}
+ */
+ get imageStyle() {
+ if (this.attachment.fileType !== 'image') {
+ return '';
+ }
+ if (this.env.isQUnitTest) {
+ // background-image:url is hardly mockable, and attachments in
+ // QUnit tests do not actually exist in DB, so style should not
+ // be fetched at all.
+ return '';
+ }
+ let size;
+ if (this.detailsMode === 'card') {
+ size = '38x38';
+ } else {
+ // The size of background-image depends on the props.imageSize
+ // to sync with width and height of `.o_Attachment_image`.
+ if (this.props.imageSize === "large") {
+ size = '400x400';
+ } else if (this.props.imageSize === "medium") {
+ size = '200x200';
+ } else if (this.props.imageSize === "small") {
+ size = '100x100';
+ }
+ }
+ // background-size set to override value from `o_image` which makes small image stretched
+ return `background-image:url(/web/image/${this.attachment.id}/${size}); background-size: auto;`;
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Download the attachment when clicking on donwload icon.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickDownload(ev) {
+ ev.stopPropagation();
+ window.location = `/web/content/ir.attachment/${this.attachment.id}/datas?download=true`;
+ }
+
+ /**
+ * Open the attachment viewer when clicking on viewable attachment.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickImage(ev) {
+ if (!this.attachment.isViewable) {
+ return;
+ }
+ this.env.models['mail.attachment'].view({
+ attachment: this.attachment,
+ attachments: this.props.attachmentLocalIds.map(
+ attachmentLocalId => this.env.models['mail.attachment'].get(attachmentLocalId)
+ ),
+ });
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickUnlink(ev) {
+ ev.stopPropagation();
+ if (!this.attachment) {
+ return;
+ }
+ if (this.attachment.isLinkedToComposer) {
+ this.attachment.remove();
+ this.trigger('o-attachment-removed', { attachmentLocalId: this.props.attachmentLocalId });
+ } else {
+ this.state.hasDeleteConfirmDialog = true;
+ }
+ }
+
+ /**
+ * @private
+ */
+ _onDeleteConfirmDialogClosed() {
+ this.state.hasDeleteConfirmDialog = false;
+ }
+}
+
+Object.assign(Attachment, {
+ components,
+ defaultProps: {
+ attachmentLocalIds: [],
+ detailsMode: 'auto',
+ imageSize: 'medium',
+ isDownloadable: false,
+ isEditable: true,
+ showExtension: true,
+ showFilename: true,
+ },
+ props: {
+ attachmentLocalId: String,
+ attachmentLocalIds: {
+ type: Array,
+ element: String,
+ },
+ detailsMode: {
+ type: String,
+ validate: prop => ['auto', 'card', 'hover', 'none'].includes(prop),
+ },
+ imageSize: {
+ type: String,
+ validate: prop => ['small', 'medium', 'large'].includes(prop),
+ },
+ isDownloadable: Boolean,
+ isEditable: Boolean,
+ showExtension: Boolean,
+ showFilename: Boolean,
+ },
+ template: 'mail.Attachment',
+});
+
+return Attachment;
+
+});
diff --git a/addons/mail/static/src/components/attachment/attachment.scss b/addons/mail/static/src/components/attachment/attachment.scss
new file mode 100644
index 00000000..583e5703
--- /dev/null
+++ b/addons/mail/static/src/components/attachment/attachment.scss
@@ -0,0 +1,204 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_Attachment {
+ display: flex;
+
+ &:hover .o_Attachment_asideItemUnlink.o-pretty {
+ transform: translateX(0);
+ }
+}
+
+.o_Attachment_action {
+ min-width: 20px;
+}
+
+.o_Attachment_actions {
+ justify-content: space-between;
+ display: flex;
+ flex-direction: column;
+}
+
+.o_Attachment_aside {
+ position: relative;
+ overflow: hidden;
+
+ &:not(.o-has-multiple-action) {
+ min-width: 50px;
+ }
+
+ &.o-has-multiple-action {
+ min-width: 30px;
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.o_Attachment_asideItem {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+}
+
+.o_Attachment_asideItemUnlink.o-pretty {
+ position: absolute;
+ top: 0;
+ transform: translateX(100%);
+}
+
+.o_Attachment_details {
+ display: flex;
+ flex-flow: column;
+ justify-content: center;
+ min-width: 0; /* This allows the text ellipsis in the flex element */
+ /* prevent hover delete button & attachment image to be too close to the text */
+ padding-left : map-get($spacers, 1);
+ padding-right : map-get($spacers, 1);
+}
+
+.o_Attachment_filename {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.o_Attachment_image {
+ flex-shrink: 0;
+ margin: 3px;
+
+ &.o-details-overlay {
+ position: relative;
+ // small, medium and large size styles should be sync with
+ // the size of the background-image and `.o_Attachment_image`.
+ &.o-small {
+ min-width: 100px;
+ min-height: 100px;
+ }
+ &.o-medium {
+ min-width: 200px;
+ min-height: 200px;
+ }
+ &.o-large {
+ min-width: 400px;
+ min-height: 400px;
+ }
+
+ &:hover {
+ .o_Attachment_imageOverlay {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.o_Attachment_imageOverlay {
+ bottom: 0;
+ display:flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ left: 0;
+ padding: 10px;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.o_Attachment_imageOverlayDetails {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ margin: 3px;
+ width: 200px;
+}
+
+
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+
+.o_Attachment {
+ &.o-has-card-details {
+ background-color: gray('300');
+ border-radius: 5px;
+ }
+}
+
+.o_Attachment_action {
+ border-radius: 10px;
+ cursor: pointer;
+ text-align: center;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+}
+
+.o_Attachment_aside {
+ border-radius: 0 5px 5px 0;
+}
+
+.o_Attachment_asideItemDownload {
+ cursor: pointer;
+
+ &:hover {
+ background-color: gray('400');
+ }
+}
+
+.o_Attachment_asideItemUnlink {
+ cursor: pointer;
+
+ &:not(.o-pretty):hover {
+ background-color: gray('400');
+ }
+
+ &.o-pretty {
+ color: white;
+ background-color: $o-brand-primary;
+
+ &:hover {
+ background-color: darken($o-brand-primary, 10%);
+ }
+ }
+
+}
+
+.o_Attachment_asideItemUploaded {
+ color: $o-brand-primary;
+}
+
+.o_Attachment_extension {
+ text-transform: uppercase;
+ font-size: 80%;
+ font-weight: 400;
+}
+
+.o_Attachment_image.o-attachment-viewable {
+ cursor: zoom-in;
+
+ &:not(.o-details-overlay):hover {
+ opacity: 0.7;
+ }
+}
+
+.o_Attachment_imageOverlay {
+ background-image: linear-gradient(180deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.9));
+ border-radius: 5px;
+ color: white;
+ opacity: 0;
+}
+
+// ------------------------------------------------------------------
+// Animation
+// ------------------------------------------------------------------
+
+.o_Attachment_asideItemUnlink.o-pretty {
+ transition: transform 0.3s ease 0s;
+}
+
+.o_Attachment_imageOverlay {
+ transition: all 0.3s ease 0s;
+}
diff --git a/addons/mail/static/src/components/attachment/attachment.xml b/addons/mail/static/src/components/attachment/attachment.xml
new file mode 100644
index 00000000..938ff894
--- /dev/null
+++ b/addons/mail/static/src/components/attachment/attachment.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.Attachment" owl="1">
+ <div class="o_Attachment"
+ t-att-class="{
+ 'o-downloadable': props.isDownloadable,
+ 'o-editable': props.isEditable,
+ 'o-has-card-details': attachment and detailsMode === 'card',
+ 'o-temporary': attachment and attachment.isTemporary,
+ 'o-viewable': attachment and attachment.isViewable,
+ }" t-att-title="attachment ? attachment.displayName : undefined" t-att-data-attachment-local-id="attachment ? attachment.localId : undefined"
+ >
+ <t t-if="attachment">
+ <!-- Image style-->
+ <!-- o_image from mimetype.scss -->
+ <div class="o_Attachment_image o_image" t-on-click="_onClickImage"
+ t-att-class="{
+ 'o-attachment-viewable': attachment.isViewable,
+ 'o-details-overlay': detailsMode !== 'card',
+ 'o-large': props.imageSize === 'large',
+ 'o-medium': props.imageSize === 'medium',
+ 'o-small': props.imageSize === 'small',
+ }" t-att-href="attachmentUrl" t-att-style="imageStyle" t-att-data-mimetype="attachment.mimetype"
+ >
+ <t t-if="(props.showFilename or props.showExtension) and detailsMode === 'hover'">
+ <div class="o_Attachment_imageOverlay">
+ <div class="o_Attachment_details o_Attachment_imageOverlayDetails">
+ <t t-if="props.showFilename">
+ <div class="o_Attachment_filename">
+ <t t-esc="attachment.displayName"/>
+ </div>
+ </t>
+ <t t-if="props.showExtension">
+ <div class="o_Attachment_extension">
+ <t t-esc="attachment.extension"/>
+ </div>
+ </t>
+ </div>
+ <div class="o_Attachment_actions">
+ <!-- Remove button -->
+ <t t-if="props.isEditable" t-key="'unlink'">
+ <div class="o_Attachment_action o_Attachment_actionUnlink"
+ t-att-class="{
+ 'o-pretty': attachment.isLinkedToComposer,
+ }" t-on-click="_onClickUnlink" title="Remove"
+ >
+ <i class="fa fa-times"/>
+ </div>
+ </t>
+ <!-- Download button -->
+ <t t-if="props.isDownloadable and !attachment.isTemporary" t-key="'download'">
+ <div class="o_Attachment_action o_Attachment_actionDownload" t-on-click="_onClickDownload" title="Download">
+ <i class="fa fa-download"/>
+ </div>
+ </t>
+ </div>
+ </div>
+ </t>
+ </div>
+ <!-- Attachment details -->
+ <t t-if="(props.showFilename or props.showExtension) and detailsMode === 'card'">
+ <div class="o_Attachment_details">
+ <t t-if="props.showFilename">
+ <div class="o_Attachment_filename">
+ <t t-esc="attachment.displayName"/>
+ </div>
+ </t>
+ <t t-if="props.showExtension">
+ <div class="o_Attachment_extension">
+ <t t-esc="attachment.extension"/>
+ </div>
+ </t>
+ </div>
+ </t>
+ <!-- Attachment aside -->
+ <t t-if="detailsMode !== 'hover' and (props.isDownloadable or props.isEditable)">
+ <div class="o_Attachment_aside" t-att-class="{ 'o-has-multiple-action': props.isDownloadable and props.isEditable }">
+ <!-- Uploading icon -->
+ <t t-if="attachment.isTemporary and attachment.isLinkedToComposer">
+ <div class="o_Attachment_asideItem o_Attachment_asideItemUploading" title="Uploading">
+ <i class="fa fa-spin fa-spinner"/>
+ </div>
+ </t>
+ <!-- Uploaded icon -->
+ <t t-if="!attachment.isTemporary and attachment.isLinkedToComposer">
+ <div class="o_Attachment_asideItem o_Attachment_asideItemUploaded" title="Uploaded">
+ <i class="fa fa-check"/>
+ </div>
+ </t>
+ <!-- Remove button -->
+ <t t-if="props.isEditable">
+ <div class="o_Attachment_asideItem o_Attachment_asideItemUnlink" t-att-class="{ 'o-pretty': attachment.isLinkedToComposer }" t-on-click="_onClickUnlink" title="Remove">
+ <i class="fa fa-times"/>
+ </div>
+ </t>
+ <!-- Download button -->
+ <t t-if="props.isDownloadable and !attachment.isTemporary">
+ <div class="o_Attachment_asideItem o_Attachment_asideItemDownload" t-on-click="_onClickDownload" title="Download">
+ <i class="fa fa-download"/>
+ </div>
+ </t>
+ </div>
+ </t>
+ <t t-if="state.hasDeleteConfirmDialog">
+ <AttachmentDeleteConfirmDialog
+ attachmentLocalId="props.attachmentLocalId"
+ t-on-dialog-closed="_onDeleteConfirmDialogClosed"
+ />
+ </t>
+ </t>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/components/attachment/attachment_tests.js b/addons/mail/static/src/components/attachment/attachment_tests.js
new file mode 100644
index 00000000..eaeb267d
--- /dev/null
+++ b/addons/mail/static/src/components/attachment/attachment_tests.js
@@ -0,0 +1,762 @@
+odoo.define('mail/static/src/components/attachment/attachment_tests.js', function (require) {
+'use strict';
+
+const components = {
+ Attachment: require('mail/static/src/components/attachment/attachment.js'),
+};
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('attachment', {}, function () {
+QUnit.module('attachment_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createAttachmentComponent = async (attachment, otherProps) => {
+ const props = Object.assign({ attachmentLocalId: attachment.localId }, otherProps);
+ await createRootComponent(this, components.Attachment, {
+ 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('simplest layout', async function (assert) {
+ assert.expect(8);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'none',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: false,
+ showFilename: false,
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Attachment').length,
+ 1,
+ "should have attachment component in DOM"
+ );
+ const attachmentEl = document.querySelector('.o_Attachment');
+ assert.strictEqual(
+ attachmentEl.dataset.attachmentLocalId,
+ this.env.models['mail.attachment'].findFromIdentifyingData({ id: 750 }).localId,
+ "attachment component should be linked to attachment store model"
+ );
+ assert.strictEqual(
+ attachmentEl.title,
+ "test.txt",
+ "attachment should have filename as title attribute"
+ );
+ assert.strictEqual(
+ attachmentEl.querySelectorAll(`:scope .o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ const attachmentImage = document.querySelector(`.o_Attachment_image`);
+ assert.ok(
+ attachmentImage.classList.contains('o_image'),
+ "attachment should have o_image classname (required for mimetype.scss style)"
+ );
+ assert.strictEqual(
+ attachmentImage.dataset.mimetype,
+ 'text/plain',
+ "attachment should have data-mimetype set (required for mimetype.scss style)"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 0,
+ "attachment should not have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 0,
+ "attachment should not have an aside part"
+ );
+});
+
+QUnit.test('simplest layout + deletable', async function (assert) {
+ assert.expect(6);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (route.includes('web/image/750')) {
+ assert.ok(
+ route.includes('/200x200'),
+ "should fetch image with 200x200 pixels ratio");
+ assert.step('fetch_image');
+ }
+ return this._super(...arguments);
+ },
+ });
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'none',
+ isDownloadable: false,
+ isEditable: true,
+ showExtension: false,
+ showFilename: false
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Attachment').length,
+ 1,
+ "should have attachment component in DOM"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 0,
+ "attachment should not have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 1,
+ "attachment should have an aside part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItem`).length,
+ 1,
+ "attachment should have only one aside item"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItemUnlink`).length,
+ 1,
+ "attachment should have a delete button"
+ );
+});
+
+QUnit.test('simplest layout + downloadable', async function (assert) {
+ assert.expect(6);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'none',
+ isDownloadable: true,
+ isEditable: false,
+ showExtension: false,
+ showFilename: false
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Attachment').length,
+ 1,
+ "should have attachment component in DOM"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 0,
+ "attachment should not have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 1,
+ "attachment should have an aside part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItem`).length,
+ 1,
+ "attachment should have only one aside item"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItemDownload`).length,
+ 1,
+ "attachment should have a download button"
+ );
+});
+
+QUnit.test('simplest layout + deletable + downloadable', async function (assert) {
+ assert.expect(8);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'none',
+ isDownloadable: true,
+ isEditable: true,
+ showExtension: false,
+ showFilename: false
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_Attachment').length,
+ 1,
+ "should have attachment component in DOM"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 0,
+ "attachment should not have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 1,
+ "attachment should have an aside part"
+ );
+ assert.ok(
+ document.querySelector(`.o_Attachment_aside`).classList.contains('o-has-multiple-action'),
+ "attachment aside should contain multiple actions"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItem`).length,
+ 2,
+ "attachment should have only two aside items"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItemDownload`).length,
+ 1,
+ "attachment should have a download button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_asideItemUnlink`).length,
+ 1,
+ "attachment should have a delete button"
+ );
+});
+
+QUnit.test('layout with card details', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: false,
+ showFilename: false
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 0,
+ "attachment should not have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 0,
+ "attachment should not have an aside part"
+ );
+});
+
+QUnit.test('layout with card details and filename', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: false,
+ showFilename: true
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 1,
+ "attachment should have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_filename`).length,
+ 1,
+ "attachment should not have its filename shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_extension`).length,
+ 0,
+ "attachment should have its extension shown"
+ );
+});
+
+QUnit.test('layout with card details and extension', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: true,
+ showFilename: false
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 1,
+ "attachment should have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_filename`).length,
+ 0,
+ "attachment should not have its filename shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_extension`).length,
+ 1,
+ "attachment should have its extension shown"
+ );
+});
+
+QUnit.test('layout with card details and filename and extension', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: true,
+ showFilename: true
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_details`).length,
+ 1,
+ "attachment should have a details part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_filename`).length,
+ 1,
+ "attachment should have its filename shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_extension`).length,
+ 1,
+ "attachment should have its extension shown"
+ );
+});
+
+QUnit.test('simplest layout with hover details and filename and extension', async function (assert) {
+ assert.expect(8);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'hover',
+ isDownloadable: true,
+ isEditable: true,
+ showExtension: true,
+ showFilename: true
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Attachment_details:not(.o_Attachment_imageOverlayDetails)
+ `).length,
+ 0,
+ "attachment should not have a details part directly"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_imageOverlayDetails`).length,
+ 1,
+ "attachment should have a details part in the overlay"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_imageOverlay`).length,
+ 1,
+ "attachment should have an image overlay part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_filename`).length,
+ 1,
+ "attachment should have its filename shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_extension`).length,
+ 1,
+ "attachment should have its extension shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_actions`).length,
+ 1,
+ "attachment should have an actions part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 0,
+ "attachment should not have an aside element"
+ );
+});
+
+QUnit.test('auto layout with image', async function (assert) {
+ assert.expect(7);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.png",
+ id: 750,
+ mimetype: 'image/png',
+ name: "test.png",
+ });
+
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'auto',
+ isDownloadable: false,
+ isEditable: false,
+ showExtension: true,
+ showFilename: true
+ });
+ assert.strictEqual(
+ document.querySelectorAll(`
+ .o_Attachment_details:not(.o_Attachment_imageOverlayDetails)
+ `).length,
+ 0,
+ "attachment should not have a details part directly"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_imageOverlayDetails`).length,
+ 1,
+ "attachment should have a details part in the overlay"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_image`).length,
+ 1,
+ "attachment should have an image part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_imageOverlay`).length,
+ 1,
+ "attachment should have an image overlay part"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_filename`).length,
+ 1,
+ "attachment should have its filename shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_extension`).length,
+ 1,
+ "attachment should have its extension shown"
+ );
+ assert.strictEqual(
+ document.querySelectorAll(`.o_Attachment_aside`).length,
+ 0,
+ "attachment should not have an aside element"
+ );
+});
+
+QUnit.test('view attachment', async function (assert) {
+ assert.expect(3);
+
+ await this.start({
+ hasDialog: true,
+ });
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.png",
+ id: 750,
+ mimetype: 'image/png',
+ name: "test.png",
+ });
+
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'hover',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Attachment_image',
+ "attachment should have an image part"
+ );
+ await afterNextRender(() => document.querySelector('.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',
+ );
+});
+
+QUnit.test('close attachment viewer', async function (assert) {
+ assert.expect(3);
+
+ await this.start({ hasDialog: true });
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.png",
+ id: 750,
+ mimetype: 'image/png',
+ name: "test.png",
+ });
+
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'hover',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_Attachment_image',
+ "attachment should have an image part"
+ );
+
+ await afterNextRender(() => document.querySelector('.o_Attachment_image').click());
+ assert.containsOnce(
+ document.body,
+ '.o_AttachmentViewer',
+ "an attachment viewer should have been opened once attachment image is clicked",
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_AttachmentViewer_headerItemButtonClose').click()
+ );
+ assert.containsNone(
+ document.body,
+ '.o_Dialog',
+ "attachment viewer should be closed after clicking on close button"
+ );
+});
+
+QUnit.test('clicking on the delete attachment button multiple times should do the rpc only once', async function (assert) {
+ assert.expect(2);
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === "unlink" && args.model === "ir.attachment") {
+ assert.step('attachment_unlink');
+ return;
+ }
+ return this._super(...arguments);
+ },
+ });
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'hover',
+ });
+ await afterNextRender(() => {
+ document.querySelector('.o_Attachment_actionUnlink').click();
+ });
+
+ await afterNextRender(() => {
+ document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click();
+ document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click();
+ document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click();
+ });
+ assert.verifySteps(
+ ['attachment_unlink'],
+ "The unlink method must be called once"
+ );
+});
+
+QUnit.test('[technical] does not crash when the viewer is closed before image load', async function (assert) {
+ /**
+ * When images are displayed using `src` attribute for the 1st time, it fetches the resource.
+ * In this case, images are actually displayed (fully fetched and rendered on screen) when
+ * `<image>` intercepts `load` event.
+ *
+ * Current code needs to be aware of load state of image, to display spinner when loading
+ * and actual image when loaded. This test asserts no crash from mishandling image becoming
+ * loaded from being viewed for 1st time, but viewer being closed while image is loading.
+ */
+ assert.expect(1);
+
+ await this.start({ hasDialog: true });
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.png",
+ id: 750,
+ mimetype: 'image/png',
+ name: "test.png",
+ });
+ await this.createAttachmentComponent(attachment);
+ await afterNextRender(() => document.querySelector('.o_Attachment_image').click());
+ const imageEl = document.querySelector('.o_AttachmentViewer_viewImage');
+ await afterNextRender(() =>
+ document.querySelector('.o_AttachmentViewer_headerItemButtonClose').click()
+ );
+ // Simulate image becoming loaded.
+ let successfulLoad;
+ try {
+ imageEl.dispatchEvent(new Event('load', { bubbles: true }));
+ successfulLoad = true;
+ } catch (err) {
+ successfulLoad = false;
+ } finally {
+ assert.ok(successfulLoad, 'should not crash when the image is loaded');
+ }
+});
+
+QUnit.test('plain text file is viewable', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.txt",
+ id: 750,
+ mimetype: 'text/plain',
+ name: "test.txt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.hasClass(
+ document.querySelector('.o_Attachment'),
+ 'o-viewable',
+ "should be viewable",
+ );
+});
+
+QUnit.test('HTML file is viewable', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.html",
+ id: 750,
+ mimetype: 'text/html',
+ name: "test.html",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.hasClass(
+ document.querySelector('.o_Attachment'),
+ 'o-viewable',
+ "should be viewable",
+ );
+});
+
+QUnit.test('ODT file is not viewable', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.odt",
+ id: 750,
+ mimetype: 'application/vnd.oasis.opendocument.text',
+ name: "test.odt",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.doesNotHaveClass(
+ document.querySelector('.o_Attachment'),
+ 'o-viewable',
+ "should not be viewable",
+ );
+});
+
+QUnit.test('DOCX file is not viewable', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const attachment = this.env.models['mail.attachment'].create({
+ filename: "test.docx",
+ id: 750,
+ mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ name: "test.docx",
+ });
+ await this.createAttachmentComponent(attachment, {
+ detailsMode: 'card',
+ isDownloadable: false,
+ isEditable: false,
+ });
+ assert.doesNotHaveClass(
+ document.querySelector('.o_Attachment'),
+ 'o-viewable',
+ "should not be viewable",
+ );
+});
+
+});
+});
+});
+
+});