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 | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/attachment')
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", + ); +}); + +}); +}); +}); + +}); |
