summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/activity
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/activity
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/activity')
-rw-r--r--addons/mail/static/src/components/activity/activity.js199
-rw-r--r--addons/mail/static/src/components/activity/activity.scss186
-rw-r--r--addons/mail/static/src/components/activity/activity.xml153
-rw-r--r--addons/mail/static/src/components/activity/activity_tests.js1157
4 files changed, 1695 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/activity/activity.js b/addons/mail/static/src/components/activity/activity.js
new file mode 100644
index 00000000..1ee7ecf3
--- /dev/null
+++ b/addons/mail/static/src/components/activity/activity.js
@@ -0,0 +1,199 @@
+odoo.define('mail/static/src/components/activity/activity.js', function (require) {
+'use strict';
+
+const components = {
+ ActivityMarkDonePopover: require('mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js'),
+ FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'),
+ MailTemplate: require('mail/static/src/components/mail_template/mail_template.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 {
+ auto_str_to_date,
+ getLangDateFormat,
+ getLangDatetimeFormat,
+} = require('web.time');
+
+const { Component, useState } = owl;
+const { useRef } = owl.hooks;
+
+class Activity extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useShouldUpdateBasedOnProps();
+ this.state = useState({
+ areDetailsVisible: false,
+ });
+ useStore(props => {
+ const activity = this.env.models['mail.activity'].get(props.activityLocalId);
+ return {
+ activity: activity ? activity.__state : undefined,
+ assigneeNameOrDisplayName: (
+ activity &&
+ activity.assignee &&
+ activity.assignee.nameOrDisplayName
+ ),
+ };
+ });
+ /**
+ * Reference of the file uploader.
+ * Useful to programmatically prompts the browser file uploader.
+ */
+ this._fileUploaderRef = useRef('fileUploader');
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.activity}
+ */
+ get activity() {
+ return this.env.models['mail.activity'].get(this.props.activityLocalId);
+ }
+
+ /**
+ * @returns {string}
+ */
+ get assignedUserText() {
+ return _.str.sprintf(this.env._t("for %s"), this.activity.assignee.nameOrDisplayName);
+ }
+
+ /**
+ * @returns {string}
+ */
+ get delayLabel() {
+ const today = moment().startOf('day');
+ const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
+ // true means no rounding
+ const diff = momentDeadlineDate.diff(today, 'days', true);
+ if (diff === 0) {
+ return this.env._t("Today:");
+ } else if (diff === -1) {
+ return this.env._t("Yesterday:");
+ } else if (diff < 0) {
+ return _.str.sprintf(this.env._t("%d days overdue:"), Math.abs(diff));
+ } else if (diff === 1) {
+ return this.env._t("Tomorrow:");
+ } else {
+ return _.str.sprintf(this.env._t("Due in %d days:"), Math.abs(diff));
+ }
+ }
+
+ /**
+ * @returns {string}
+ */
+ get formattedCreateDatetime() {
+ const momentCreateDate = moment(auto_str_to_date(this.activity.dateCreate));
+ const datetimeFormat = getLangDatetimeFormat();
+ return momentCreateDate.format(datetimeFormat);
+ }
+
+ /**
+ * @returns {string}
+ */
+ get formattedDeadlineDate() {
+ const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
+ const datetimeFormat = getLangDateFormat();
+ return momentDeadlineDate.format(datetimeFormat);
+ }
+
+ /**
+ * @returns {string}
+ */
+ get MARK_DONE() {
+ return this.env._t("Mark Done");
+ }
+
+ /**
+ * @returns {string}
+ */
+ get summary() {
+ return _.str.sprintf(this.env._t("“%s”"), this.activity.summary);
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ * @param {Object} ev.detail
+ * @param {mail.attachment} ev.detail.attachment
+ */
+ _onAttachmentCreated(ev) {
+ this.activity.markAsDone({ attachments: [ev.detail.attachment] });
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick(ev) {
+ if (
+ ev.target.tagName === 'A' &&
+ ev.target.dataset.oeId &&
+ ev.target.dataset.oeModel
+ ) {
+ this.env.messaging.openProfile({
+ id: Number(ev.target.dataset.oeId),
+ model: ev.target.dataset.oeModel,
+ });
+ // avoid following dummy href
+ ev.preventDefault();
+ }
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ async _onClickCancel(ev) {
+ ev.preventDefault();
+ await this.activity.deleteServerRecord();
+ this.trigger('reload', { keepChanges: true });
+ }
+
+ /**
+ * @private
+ */
+ _onClickDetailsButton() {
+ this.state.areDetailsVisible = !this.state.areDetailsVisible;
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickEdit(ev) {
+ this.activity.edit();
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClickUploadDocument(ev) {
+ this._fileUploaderRef.comp.openBrowserFileUploader();
+ }
+
+}
+
+Object.assign(Activity, {
+ components,
+ props: {
+ activityLocalId: String,
+ },
+ template: 'mail.Activity',
+});
+
+return Activity;
+
+});
diff --git a/addons/mail/static/src/components/activity/activity.scss b/addons/mail/static/src/components/activity/activity.scss
new file mode 100644
index 00000000..64a0ceac
--- /dev/null
+++ b/addons/mail/static/src/components/activity/activity.scss
@@ -0,0 +1,186 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_Activity {
+ display: flex;
+ flex: 0 0 auto;
+ padding: map-get($spacers, 2);
+}
+
+.o_Activity_detailsUserAvatar {
+ margin-inline-end: map-get($spacers, 2);
+ object-fit: cover;
+ height: 18px;
+ width: 18px;
+}
+
+.o_Activity_dueDateText, .o_Activity_summary {
+ margin-inline-end: map-get($spacers, 2);
+}
+
+.o_Activity_iconContainer {
+ @include o-position-absolute($top: auto, $left: auto, $bottom: -5px, $right: -5px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 25px;
+ height: 25px;
+ border-width: 2px;
+}
+
+.o_Activity_info {
+ display: flex;
+ align-items: baseline;
+}
+
+.o_Activity_note p {
+ margin-bottom: 0;
+}
+
+.o_Activity_sidebar {
+ display: flex;
+ flex: 0 0 36px;
+ margin-right: map-get($spacers, 3);
+ justify-content: center;
+}
+
+.o_Activity_toolButton {
+ padding-top: map-get($spacers, 0);
+}
+
+.o_Activity_tools {
+ display: flex;
+}
+
+.o_Activity_user {
+ height: 36px;
+ margin-left: map-get($spacers, 2);
+ margin-right: map-get($spacers, 2);
+ position: relative;
+ width: 36px;
+}
+
+.o_Activity_userAvatar {
+ height: map-get($sizes, 100);
+ width: map-get($sizes, 100);
+}
+
+// From python template
+.o_mail_note_title {
+ margin-top: map-get($spacers, 2);
+}
+
+.o_mail_note_title + div p {
+ margin-bottom: 0;
+}
+
+// ------------------------------------------------------------------
+// Style
+// ------------------------------------------------------------------
+
+$o-mail-activity-default-color: gray('300') !default;
+$o-mail-activity-overdue-color: darken(theme-color('danger'), 10%) !default;
+$o-mail-activity-planned-color: darken(theme-color('success'), 10%) !default;
+$o-mail-activity-today-color: darken(theme-color('warning'), 10%) !default;
+
+
+.o_Activity_deadlineDateText {
+ &.o-default {
+ color: $o-mail-activity-default-color;
+ }
+
+ &.o-overdue {
+ color: $o-mail-activity-overdue-color;
+ }
+
+ &.o-planned {
+ color: $o-mail-activity-planned-color;
+ }
+
+ &.o-today {
+ color: $o-mail-activity-today-color;
+ }
+}
+
+.o_Activity_details {
+ color: gray('500');
+}
+
+.o_Activity_detailsCreatorAvatar {
+ margin-inline-start: map-get($spacers, 2);
+}
+
+.o_Activity_detailsUserAvatar {
+ border-radius: 50%;
+}
+
+.o_Activity_dueDateText {
+ font-weight: bolder;
+
+ &.o-default {
+ color: $o-mail-activity-default-color;
+ }
+
+ &.o-overdue {
+ color: $o-mail-activity-overdue-color;
+ }
+
+ &.o-planned {
+ color: $o-mail-activity-planned-color;
+ }
+
+ &.o-today {
+ color: $o-mail-activity-today-color;
+ }
+}
+
+/* Needed specifity to counter default bootstrap style */
+a:not([href]):not([tabindex]).o_Activity_detailsButton {
+ background: transparent;
+ opacity: 0.5;
+ color: gray('500');
+
+ &:hover {
+ opacity: 1;
+ color: gray('600');
+ }
+}
+
+.o_Activity_detailsCreator {
+ font-weight: bold;
+}
+
+.o_Activity_iconContainer {
+ color: white;
+ border-color: white;
+ border-radius: 100%;
+ border-style: solid;
+}
+
+.o_Activity_sidebar {
+ font-size: smaller;
+}
+
+.o_Activity_summary {
+ font-weight: bolder;
+ color: gray('900');
+}
+
+.o_Activity_toolButton {
+ opacity: 0.5;
+ color: gray('500');
+
+ &:hover {
+ opacity: 1;
+ color: gray('600');
+ }
+}
+
+.o_Activity_userAvatar {
+ border-radius: 50%;
+}
+
+.o_Activity_userName {
+ color: gray('500');
+}
diff --git a/addons/mail/static/src/components/activity/activity.xml b/addons/mail/static/src/components/activity/activity.xml
new file mode 100644
index 00000000..e5e9832c
--- /dev/null
+++ b/addons/mail/static/src/components/activity/activity.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.Activity" owl="1">
+ <div class="o_Activity" t-on-click="_onClick">
+ <t t-if="activity">
+ <div class="o_Activity_sidebar">
+ <div class="o_Activity_user">
+ <t t-if="activity.assignee">
+ <img class="o_Activity_userAvatar" t-attf-src="/web/image/res.users/{{ activity.assignee.id }}/image_128" t-att-alt="activity.assignee.nameOrDisplayName"/>
+ </t>
+ <div class="o_Activity_iconContainer"
+ t-att-class="{
+ 'bg-success-full': activity.state === 'planned',
+ 'bg-warning-full': activity.state === 'today',
+ 'bg-danger-full': activity.state === 'overdue',
+ }"
+ >
+ <i class="o_Activity_icon fa" t-attf-class="{{ activity.icon }}"/>
+ </div>
+ </div>
+ </div>
+ <div class="o_Activity_core">
+ <div class="o_Activity_info">
+ <div class="o_Activity_dueDateText"
+ t-att-class="{
+ 'o-default': activity.state === 'default',
+ 'o-overdue': activity.state === 'overdue',
+ 'o-planned': activity.state === 'planned',
+ 'o-today': activity.state === 'today',
+ }"
+ >
+ <t t-esc="delayLabel"/>
+ </div>
+ <t t-if="activity.summary">
+ <div class="o_Activity_summary">
+ <t t-esc="summary"/>
+ </div>
+ </t>
+ <t t-elif="activity.type">
+ <div class="o_Activity_summary o_Activity_type">
+ <t t-esc="activity.type.displayName"/>
+ </div>
+ </t>
+ <t t-if="activity.assignee">
+ <div class="o_Activity_userName">
+ <t t-esc="assignedUserText"/>
+ </div>
+ </t>
+ <a class="o_Activity_detailsButton btn btn-link" t-on-click="_onClickDetailsButton" role="button">
+ <i class="fa fa-info-circle" role="img" title="Info"/>
+ </a>
+ </div>
+
+ <t t-if="state.areDetailsVisible">
+ <div class="o_Activity_details">
+ <dl class="dl-horizontal">
+ <t t-if="activity.type">
+ <dt>Activity type</dt>
+ <dd class="o_Activity_type">
+ <t t-esc="activity.type.displayName"/>
+ </dd>
+ </t>
+ <t t-if="activity.creator">
+ <dt>Created</dt>
+ <dd class="o_Activity_detailsCreation">
+ <t t-esc="formattedCreateDatetime"/>
+ <img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar" t-attf-src="/web/image/res.users/{{ activity.creator.id }}/image_128" t-att-title="activity.creator.nameOrDisplayName" t-att-alt="activity.creator.nameOrDisplayName"/>
+ <span class="o_Activity_detailsCreator">
+ <t t-esc="activity.creator.nameOrDisplayName"/>
+ </span>
+ </dd>
+ </t>
+ <t t-if="activity.assignee">
+ <dt>Assigned to</dt>
+ <dd class="o_Activity_detailsAssignation">
+ <img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar" t-attf-src="/web/image/res.users/{{ activity.assignee.id }}/image_128" t-att-title="activity.assignee.nameOrDisplayName" t-att-alt="activity.assignee.nameOrDisplayName"/>
+ <t t-esc="activity.assignee.nameOrDisplayName"/>
+ </dd>
+ </t>
+ <dt>Due on</dt>
+ <dd class="o_Activity_detailsDueDate">
+ <span class="o_Activity_deadlineDateText"
+ t-att-class="{
+ 'o-default': activity.state === 'default',
+ 'o-overdue': activity.state === 'overdue',
+ 'o-planned': activity.state === 'planned',
+ 'o-today': activity.state === 'today',
+ }"
+ >
+ <t t-esc="formattedDeadlineDate"/>
+ </span>
+ </dd>
+ </dl>
+ </div>
+ </t>
+
+ <t t-if="activity.note">
+ <div class="o_Activity_note">
+ <t t-raw="activity.note"/>
+ </div>
+ </t>
+
+ <t t-if="activity.mailTemplates.length > 0">
+ <div class="o_Activity_mailTemplates">
+ <t t-foreach="activity.mailTemplates" t-as="mailTemplate" t-key="mailTemplate.localId">
+ <MailTemplate
+ class="o_Activity_mailTemplate"
+ activityLocalId="activity.localId"
+ mailTemplateLocalId="mailTemplate.localId"
+ />
+ </t>
+ </div>
+ </t>
+
+ <t t-if="activity.canWrite">
+ <div name="tools" class="o_Activity_tools">
+ <t t-if="activity.category !== 'upload_file'">
+ <Popover position="'right'" title="MARK_DONE">
+ <button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link" t-att-title="MARK_DONE">
+ <i class="fa fa-check"/> Mark Done
+ </button>
+ <t t-set="opened">
+ <ActivityMarkDonePopover activityLocalId="props.activityLocalId"/>
+ </t>
+ </Popover>
+ </t>
+ <t t-else="">
+ <button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link" t-on-click="_onClickUploadDocument">
+ <i class="fa fa-upload"/> Upload Document
+ </button>
+ <FileUploader
+ attachmentLocalIds="activity.attachments.map(attachment => attachment.localId)"
+ uploadId="activity.thread.id"
+ uploadModel="activity.thread.model"
+ t-on-o-attachment-created="_onAttachmentCreated"
+ t-ref="fileUploader"
+ />
+ </t>
+ <button class="o_Activity_toolButton o_Activity_editButton btn btn-link" t-on-click="_onClickEdit">
+ <i class="fa fa-pencil"/> Edit
+ </button>
+ <button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link" t-on-click="_onClickCancel" >
+ <i class="fa fa-times"/> Cancel
+ </button>
+ </div>
+ </t>
+ </div>
+ </t>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/components/activity/activity_tests.js b/addons/mail/static/src/components/activity/activity_tests.js
new file mode 100644
index 00000000..1c260f07
--- /dev/null
+++ b/addons/mail/static/src/components/activity/activity_tests.js
@@ -0,0 +1,1157 @@
+odoo.define('mail/static/src/components/activity/activity_tests.js', function (require) {
+'use strict';
+
+const components = {
+ Activity: require('mail/static/src/components/activity/activity.js'),
+};
+
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+
+const Bus = require('web.Bus');
+const { date_to_str } = require('web.time');
+
+const { Component, tags: { xml } } = owl;
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('activity', {}, function () {
+QUnit.module('activity_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createActivityComponent = async function (activity) {
+ await createRootComponent(this, components.Activity, {
+ props: { activityLocalId: activity.localId },
+ 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('activity simplest layout', async function (assert) {
+ assert.expect(12);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_sidebar').length,
+ 1,
+ "should have activity sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_core').length,
+ 1,
+ "should have activity core"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_user').length,
+ 1,
+ "should have activity user"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_info').length,
+ 1,
+ "should have activity info"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_note').length,
+ 0,
+ "should not have activity note"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details').length,
+ 0,
+ "should not have activity details"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_mailTemplates').length,
+ 0,
+ "should not have activity mail templates"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_editButton').length,
+ 0,
+ "should not have activity Edit button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_cancelButton').length,
+ 0,
+ "should not have activity Cancel button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_markDoneButton').length,
+ 0,
+ "should not have activity Mark as Done button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_uploadButton').length,
+ 0,
+ "should not have activity Upload button"
+ );
+});
+
+QUnit.test('activity with note layout', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ note: 'There is no good or bad note',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_note').length,
+ 1,
+ "should have activity note"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_note').textContent,
+ "There is no good or bad note",
+ "activity note should be 'There is no good or bad note'"
+ );
+});
+
+QUnit.test('activity info layout when planned after tomorrow', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const fiveDaysFromNow = new Date();
+ fiveDaysFromNow.setDate(today.getDate() + 5);
+ const activity = this.env.models['mail.activity'].create({
+ dateDeadline: date_to_str(fiveDaysFromNow),
+ id: 12,
+ state: 'planned',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_dueDateText').length,
+ 1,
+ "should have activity delay"
+ );
+ assert.ok(
+ document.querySelector('.o_Activity_dueDateText').classList.contains('o-planned'),
+ "activity delay should have the right color modifier class (planned)"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_dueDateText').textContent,
+ "Due in 5 days:",
+ "activity delay should have 'Due in 5 days:' as label"
+ );
+});
+
+QUnit.test('activity info layout when planned tomorrow', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ dateDeadline: date_to_str(tomorrow),
+ id: 12,
+ state: 'planned',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_dueDateText').length,
+ 1,
+ "should have activity delay"
+ );
+ assert.ok(
+ document.querySelector('.o_Activity_dueDateText').classList.contains('o-planned'),
+ "activity delay should have the right color modifier class (planned)"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_dueDateText').textContent,
+ 'Tomorrow:',
+ "activity delay should have 'Tomorrow:' as label"
+ );
+});
+
+QUnit.test('activity info layout when planned today', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const activity = this.env.models['mail.activity'].create({
+ dateDeadline: date_to_str(today),
+ id: 12,
+ state: 'today',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_dueDateText').length,
+ 1,
+ "should have activity delay"
+ );
+ assert.ok(
+ document.querySelector('.o_Activity_dueDateText').classList.contains('o-today'),
+ "activity delay should have the right color modifier class (today)"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_dueDateText').textContent,
+ "Today:",
+ "activity delay should have 'Today:' as label"
+ );
+});
+
+QUnit.test('activity info layout when planned yesterday', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(today.getDate() - 1);
+ const activity = this.env.models['mail.activity'].create({
+ dateDeadline: date_to_str(yesterday),
+ id: 12,
+ state: 'overdue',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_dueDateText').length,
+ 1,
+ "should have activity delay"
+ );
+ assert.ok(
+ document.querySelector('.o_Activity_dueDateText').classList.contains('o-overdue'),
+ "activity delay should have the right color modifier class (overdue)"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_dueDateText').textContent,
+ "Yesterday:",
+ "activity delay should have 'Yesterday:' as label"
+ );
+});
+
+QUnit.test('activity info layout when planned before yesterday', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const fiveDaysBeforeNow = new Date();
+ fiveDaysBeforeNow.setDate(today.getDate() - 5);
+ const activity = this.env.models['mail.activity'].create({
+ dateDeadline: date_to_str(fiveDaysBeforeNow),
+ id: 12,
+ state: 'overdue',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_dueDateText').length,
+ 1,
+ "should have activity delay"
+ );
+ assert.ok(
+ document.querySelector('.o_Activity_dueDateText').classList.contains('o-overdue'),
+ "activity delay should have the right color modifier class (overdue)"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_dueDateText').textContent,
+ "5 days overdue:",
+ "activity delay should have '5 days overdue:' as label"
+ );
+});
+
+QUnit.test('activity with a summary layout', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ summary: 'test summary',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_summary').length,
+ 1,
+ "should have activity summary"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_type').length,
+ 0,
+ "should not have the activity type as summary"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_summary').textContent.trim(),
+ "“test summary”",
+ "should have the specific activity summary in activity summary"
+ );
+});
+
+QUnit.test('activity without summary layout', async function (assert) {
+ assert.expect(5);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ type: [['insert', { id: 1, displayName: "Fake type" }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_type').length,
+ 1,
+ "activity details should have an activity type section"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_type').textContent.trim(),
+ "Fake type",
+ "activity details should have the activity type display name in type section"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_summary.o_Activity_type').length,
+ 1,
+ "should have activity type as summary"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_summary:not(.o_Activity_type)').length,
+ 0,
+ "should not have a specific summary"
+ );
+});
+
+QUnit.test('activity details toggle', async function (assert) {
+ assert.expect(5);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ creator: [['insert', { id: 1, display_name: "Admin" }]],
+ dateCreate: date_to_str(today),
+ dateDeadline: date_to_str(tomorrow),
+ id: 12,
+ state: 'planned',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ type: [['insert', { id: 1, displayName: "Fake type" }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details').length,
+ 0,
+ "activity details should not be visible by default"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsButton').length,
+ 1,
+ "activity should have a details button"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Activity_detailsButton').click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details').length,
+ 1,
+ "activity details should be visible after clicking on details button"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Activity_detailsButton').click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details').length,
+ 0,
+ "activity details should no longer be visible after clicking again on details button"
+ );
+});
+
+QUnit.test('activity details layout', async function (assert) {
+ assert.expect(11);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ assignee: [['insert', { id: 10, display_name: "Pauvre pomme" }]],
+ creator: [['insert', { id: 1, display_name: "Admin" }]],
+ dateCreate: date_to_str(today),
+ dateDeadline: date_to_str(tomorrow),
+ id: 12,
+ state: 'planned',
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ type: [['insert', { id: 1, displayName: "Fake type" }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_userAvatar').length,
+ 1,
+ "should have activity user avatar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsButton').length,
+ 1,
+ "activity should have a details button"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Activity_detailsButton').click()
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details').length,
+ 1,
+ "activity details should be visible after clicking on details button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_details .o_Activity_type').length,
+ 1,
+ "activity details should have type"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_details .o_Activity_type').textContent,
+ "Fake type",
+ "activity details type should be 'Fake type'"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsCreation').length,
+ 1,
+ "activity details should have creation date "
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsCreator').length,
+ 1,
+ "activity details should have creator"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsAssignation').length,
+ 1,
+ "activity details should have assignation information"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_Activity_detailsAssignation').textContent.indexOf('Pauvre pomme'),
+ 0,
+ "activity details assignation information should contain creator display name"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_detailsAssignationUserAvatar').length,
+ 1,
+ "activity details should have user avatar"
+ );
+});
+
+QUnit.test('activity with mail template layout', async function (assert) {
+ assert.expect(8);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ mailTemplates: [['insert', { id: 1, name: "Dummy mail template" }]],
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_sidebar').length,
+ 1,
+ "should have activity sidebar"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_mailTemplates').length,
+ 1,
+ "should have activity mail templates"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_mailTemplate').length,
+ 1,
+ "should have activity mail template"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_MailTemplate_name').length,
+ 1,
+ "should have activity mail template name"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_MailTemplate_name').textContent,
+ "Dummy mail template",
+ "should have activity mail template name"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_MailTemplate_preview').length,
+ 1,
+ "should have activity mail template name preview button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_MailTemplate_send').length,
+ 1,
+ "should have activity mail template name send button"
+ );
+});
+
+QUnit.test('activity with mail template: preview mail', async function (assert) {
+ assert.expect(10);
+
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action.context.default_res_id,
+ 42,
+ 'Action should have the activity res id as default res id in context'
+ );
+ assert.strictEqual(
+ payload.action.context.default_model,
+ 'res.partner',
+ 'Action should have the activity res model as default model in context'
+ );
+ assert.ok(
+ payload.action.context.default_use_template,
+ 'Action should have true as default use_template in context'
+ );
+ assert.strictEqual(
+ payload.action.context.default_template_id,
+ 1,
+ 'Action should have the selected mail template id as default template id in context'
+ );
+ assert.strictEqual(
+ payload.action.type,
+ "ir.actions.act_window",
+ 'Action should be of type "ir.actions.act_window"'
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ "mail.compose.message",
+ 'Action should have "mail.compose.message" as res_model'
+ );
+ });
+
+ await this.start({ env: { bus } });
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ mailTemplates: [['insert', {
+ id: 1,
+ name: "Dummy mail template",
+ }]],
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_MailTemplate_preview').length,
+ 1,
+ "should have activity mail template name preview button"
+ );
+
+ document.querySelector('.o_MailTemplate_preview').click();
+ assert.verifySteps(
+ ['do_action'],
+ "should have called 'compose email' action correctly"
+ );
+});
+
+QUnit.test('activity with mail template: send mail', async function (assert) {
+ assert.expect(7);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (args.method === 'activity_send_mail') {
+ assert.step('activity_send_mail');
+ assert.strictEqual(args.args[0].length, 1);
+ assert.strictEqual(args.args[0][0], 42);
+ assert.strictEqual(args.args[1], 1);
+ return;
+ } else {
+ return this._super(...arguments);
+ }
+ },
+ });
+ const activity = this.env.models['mail.activity'].create({
+ id: 12,
+ mailTemplates: [['insert', {
+ id: 1,
+ name: "Dummy mail template",
+ }]],
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_MailTemplate_send').length,
+ 1,
+ "should have activity mail template name send button"
+ );
+
+ document.querySelector('.o_MailTemplate_send').click();
+ assert.verifySteps(
+ ['activity_send_mail'],
+ "should have called activity_send_mail rpc"
+ );
+});
+
+QUnit.test('activity upload document is available', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'upload_file',
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_uploadButton').length,
+ 1,
+ "should have activity upload button"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_FileUploader').length,
+ 1,
+ "should have a file uploader"
+ );
+});
+
+QUnit.test('activity click on mark as done', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'not_upload_file',
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_markDoneButton').length,
+ 1,
+ "should have activity Mark as Done button"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_markDoneButton').click();
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_ActivityMarkDonePopover').length,
+ 1,
+ "should have opened the mark done popover"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_markDoneButton').click();
+ });
+ assert.strictEqual(
+ document.querySelectorAll('.o_ActivityMarkDonePopover').length,
+ 0,
+ "should have closed the mark done popover"
+ );
+});
+
+QUnit.test('activity mark as done popover should focus feedback input on open [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(3);
+
+ await this.start();
+ const today = new Date();
+ const tomorrow = new Date();
+ tomorrow.setDate(today.getDate() + 1);
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'not_upload_file',
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+
+ assert.containsOnce(
+ document.body,
+ '.o_Activity',
+ "should have activity component"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_markDoneButton',
+ "should have activity Mark as Done button"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_markDoneButton').click();
+ });
+ assert.strictEqual(
+ document.querySelector('.o_ActivityMarkDonePopover_feedback'),
+ document.activeElement,
+ "the popover textarea should have the focus"
+ );
+});
+
+QUnit.test('activity click on edit', async function (assert) {
+ assert.expect(9);
+
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action.context.default_res_id,
+ 42,
+ 'Action should have the activity res id as default res id in context'
+ );
+ assert.strictEqual(
+ payload.action.context.default_res_model,
+ 'res.partner',
+ 'Action should have the activity res model as default res model in context'
+ );
+ assert.strictEqual(
+ payload.action.type,
+ "ir.actions.act_window",
+ 'Action should be of type "ir.actions.act_window"'
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ "mail.activity",
+ 'Action should have "mail.activity" as res_model'
+ );
+ assert.strictEqual(
+ payload.action.res_id,
+ 12,
+ 'Action should have activity id as res_id'
+ );
+ });
+
+ await this.start({ env: { bus } });
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ id: 12,
+ mailTemplates: [['insert', { id: 1, name: "Dummy mail template" }]],
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_editButton').length,
+ 1,
+ "should have activity edit button"
+ );
+
+ document.querySelector('.o_Activity_editButton').click();
+ assert.verifySteps(
+ ['do_action'],
+ "should have called 'schedule activity' action correctly"
+ );
+});
+
+QUnit.test('activity edition', async function (assert) {
+ assert.expect(14);
+
+ this.data['mail.activity'].records.push({
+ can_write: true,
+ icon: 'fa-times',
+ id: 12,
+ res_id: 42,
+ res_model: 'res.partner',
+ });
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action.context.default_res_id,
+ 42,
+ 'Action should have the activity res id as default res id in context'
+ );
+ assert.strictEqual(
+ payload.action.context.default_res_model,
+ 'res.partner',
+ 'Action should have the activity res model as default res model in context'
+ );
+ assert.strictEqual(
+ payload.action.type,
+ 'ir.actions.act_window',
+ 'Action should be of type "ir.actions.act_window"'
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ 'mail.activity',
+ 'Action should have "mail.activity" as res_model'
+ );
+ assert.strictEqual(
+ payload.action.res_id,
+ 12,
+ 'Action should have activity id as res_id'
+ );
+ this.data['mail.activity'].records[0].icon = 'fa-check';
+ payload.options.on_close();
+ });
+
+ await this.start({ env: { bus } });
+ const activity = this.env.models['mail.activity'].insert(
+ this.env.models['mail.activity'].convertData(
+ this.data['mail.activity'].records[0]
+ )
+ );
+ await this.createActivityComponent(activity);
+
+ assert.containsOnce(
+ document.body,
+ '.o_Activity',
+ "should have activity component"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_editButton',
+ "should have activity edit button"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_icon',
+ "should have activity icon"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_icon.fa-times',
+ "should have initial activity icon"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_Activity_icon.fa-check',
+ "should not have new activity icon when not edited yet"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_editButton').click();
+ });
+ assert.verifySteps(
+ ['do_action'],
+ "should have called 'schedule activity' action correctly"
+ );
+ assert.containsNone(
+ document.body,
+ '.o_Activity_icon.fa-times',
+ "should no more have initial activity icon once edited"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_icon.fa-check',
+ "should now have new activity icon once edited"
+ );
+});
+
+QUnit.test('activity click on cancel', async function (assert) {
+ assert.expect(7);
+
+ await this.start({
+ async mockRPC(route, args) {
+ if (route === '/web/dataset/call_kw/mail.activity/unlink') {
+ assert.step('unlink');
+ assert.strictEqual(args.args[0].length, 1);
+ assert.strictEqual(args.args[0][0], 12);
+ return;
+ } else {
+ return this._super(...arguments);
+ }
+ },
+ });
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ id: 12,
+ mailTemplates: [['insert', {
+ id: 1,
+ name: "Dummy mail template",
+ }]],
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+
+ // Create a parent component to surround the Activity component in order to be able
+ // to check that activity component has been destroyed
+ class ParentComponent extends Component {
+ constructor(...args) {
+ super(... args);
+ useStore(props => {
+ const activity = this.env.models['mail.activity'].get(props.activityLocalId);
+ return {
+ activity: activity ? activity.__state : undefined,
+ };
+ });
+ }
+
+ /**
+ * @returns {mail.activity}
+ */
+ get activity() {
+ return this.env.models['mail.activity'].get(this.props.activityLocalId);
+ }
+ }
+ ParentComponent.env = this.env;
+ Object.assign(ParentComponent, {
+ components,
+ props: { activityLocalId: String },
+ template: xml`
+ <div>
+ <p>parent</p>
+ <t t-if="activity">
+ <Activity activityLocalId="activity.localId"/>
+ </t>
+ </div>
+ `,
+ });
+ await createRootComponent(this, ParentComponent, {
+ props: { activityLocalId: activity.localId },
+ target: this.widget.el,
+ });
+
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 1,
+ "should have activity component"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity_cancelButton').length,
+ 1,
+ "should have activity cancel button"
+ );
+
+ await afterNextRender(() =>
+ document.querySelector('.o_Activity_cancelButton').click()
+ );
+ assert.verifySteps(
+ ['unlink'],
+ "should have called unlink rpc after clicking on cancel"
+ );
+ assert.strictEqual(
+ document.querySelectorAll('.o_Activity').length,
+ 0,
+ "should no longer display activity after clicking on cancel"
+ );
+});
+
+QUnit.test('activity mark done popover close on ESCAPE', async function (assert) {
+ // This test is not in activity_mark_done_popover_tests.js as it requires the activity mark done
+ // component to have a parent in order to allow testing interactions the popover.
+ assert.expect(2);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'not_upload_file',
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+
+ await this.createActivityComponent(activity);
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_markDoneButton').click();
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ActivityMarkDonePopover',
+ "Popover component should be present"
+ );
+
+ await afterNextRender(() => {
+ const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
+ document.querySelector(`.o_ActivityMarkDonePopover`).dispatchEvent(ev);
+ });
+ assert.containsNone(
+ document.body,
+ '.o_ActivityMarkDonePopover',
+ "ESCAPE pressed should have closed the mark done popover"
+ );
+});
+
+QUnit.test('activity mark done popover click on discard', async function (assert) {
+ // This test is not in activity_mark_done_popover_tests.js as it requires the activity mark done
+ // component to have a parent in order to allow testing interactions the popover.
+ assert.expect(3);
+
+ await this.start();
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'not_upload_file',
+ id: 12,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ await afterNextRender(() => {
+ document.querySelector('.o_Activity_markDoneButton').click();
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ActivityMarkDonePopover',
+ "Popover component should be present"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ActivityMarkDonePopover_discardButton',
+ "Popover component should contain the discard button"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ActivityMarkDonePopover_discardButton').click()
+ );
+ assert.containsNone(
+ document.body,
+ '.o_ActivityMarkDonePopover',
+ "Discard button clicked should have closed the mark done popover"
+ );
+});
+
+QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) {
+ assert.expect(7);
+
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.strictEqual(
+ payload.action.type,
+ 'ir.actions.act_window',
+ "action should open view"
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ 'some.model',
+ "action should open view on 'some.model' model"
+ );
+ assert.strictEqual(
+ payload.action.res_id,
+ 250,
+ "action should open view on 250"
+ );
+ assert.step('do-action:openFormView_some.model_250');
+ });
+ await this.start({ env: { bus } });
+ const activity = this.env.models['mail.activity'].create({
+ canWrite: true,
+ category: 'not_upload_file',
+ id: 12,
+ note: `<p><a href="#" data-oe-id="250" data-oe-model="some.model">some.model_250</a></p>`,
+ thread: [['insert', { id: 42, model: 'res.partner' }]],
+ });
+ await this.createActivityComponent(activity);
+ assert.containsOnce(
+ document.body,
+ '.o_Activity_note',
+ "activity should have a note"
+ );
+ assert.containsOnce(
+ document.querySelector('.o_Activity_note'),
+ 'a',
+ "activity note should have a link"
+ );
+
+ document.querySelector(`.o_Activity_note a`).click();
+ assert.verifySteps(
+ ['do-action:openFormView_some.model_250'],
+ "should have open form view on related record after click on link"
+ );
+});
+
+});
+});
+});
+
+});