');
- const date = moment(this.record.data.closest_deadline).toDate();
- // To remove year only if current year
- if (moment().year() === moment(date).year()) {
- $date.text(date.toLocaleDateString(moment().locale(), {
- day: 'numeric', month: 'short'
- }));
- } else {
- $date.text(moment(date).format('ll'));
- }
- this.$('a').html($date);
- if (this.record.data.activity_ids.res_ids.length > 1) {
- this.$('a').append($('
', {
- class: 'badge bg-light rounded-pill border-0 ' + this.record.data.activity_state,
- text: this.record.data.activity_ids.res_ids.length,
- }));
- }
- if (this.$el.hasClass('show')) {
- // note: this part of the rendering might be asynchronous
- this._renderDropdown();
- }
- }
-});
-
-export default ActivityCell;
diff --git a/addons/mail/static/src/js/views/activity/activity_controller.js b/addons/mail/static/src/js/views/activity/activity_controller.js
index 80de260afc0..fb14b6c3e09 100644
--- a/addons/mail/static/src/js/views/activity/activity_controller.js
+++ b/addons/mail/static/src/js/views/activity/activity_controller.js
@@ -4,11 +4,9 @@ import '@mail/js/activity';
import BasicController from 'web.BasicController';
import core from 'web.core';
-import field_registry from 'web.field_registry';
import { sprintf } from '@web/core/utils/strings';
import ViewDialogs from 'web.view_dialogs';
-var KanbanActivity = field_registry.get('kanban_activity');
var _t = core._t;
var ActivityController = BasicController.extend({
@@ -57,7 +55,6 @@ var ActivityController = BasicController.extend({
* @private
*/
_onScheduleActivity: function () {
- var self = this;
var state = this.model.get(this.handle);
new ViewDialogs.SelectCreateDialog(this, {
res_model: state.model,
@@ -67,10 +64,11 @@ var ActivityController = BasicController.extend({
no_create: !this.activeActions.create,
disable_multiple_selection: true,
context: state.context,
- on_selected: function (record) {
- var fakeRecord = state.getKanbanActivityData({}, record[0].id);
- var widget = new KanbanActivity(self, 'activity_ids', fakeRecord, {});
- widget.scheduleActivity();
+ on_selected: async records => {
+ const messaging = await owl.Component.env.services.messaging.get();
+ const thread = messaging.models['Thread'].insert({ id: records[0].id, model: this.model.modelName });
+ await messaging.openActivityForm({ thread });
+ this.trigger_up('reload');
},
}).open();
},
diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js
index a74048b57d4..01e0d622c8a 100644
--- a/addons/mail/static/src/js/views/activity/activity_renderer.js
+++ b/addons/mail/static/src/js/views/activity/activity_renderer.js
@@ -1,10 +1,10 @@
/** @odoo-module **/
-import ActivityCell from '@mail/js/views/activity/activity_cell';
import ActivityRecord from '@mail/js/views/activity/activity_record';
import AbstractRendererOwl from 'web.AbstractRendererOwl';
-import core from 'web.core';
+import core from 'web.core';
+import field_registry from 'web.field_registry';
import KanbanColumnProgressBar from 'web.KanbanColumnProgressBar';
import { ComponentAdapter } from 'web.OwlCompatibility';
import QWeb from 'web.QWeb';
@@ -13,6 +13,7 @@ import utils from 'web.utils';
const { useState } = owl;
const _t = core._t;
+const KanbanActivity = field_registry.get('kanban_activity');
/**
* Owl Component Adapter for ActivityRecord which is KanbanRecord (Odoo Widget)
@@ -31,8 +32,8 @@ class ActivityRecordAdapter extends ComponentAdapter {
}
/**
- * Owl Component Adapter for ActivityCell which is KanbanActivity (AbstractField)
- * TODO: Remove this adapter when ActivityCell is a Component
+ * Owl Component Adapter for KanbanActivity.
+ * TODO: Remove this adapter when KanbanActivity is a Component.
*/
class ActivityCellAdapter extends ComponentAdapter {
renderWidget() {
@@ -93,7 +94,7 @@ class ActivityRenderer extends AbstractRendererOwl {
});
this.widgetComponents = {
ActivityRecord,
- ActivityCell,
+ KanbanActivity,
KanbanColumnProgressBar,
};
}
diff --git a/addons/mail/static/src/models/activity.js b/addons/mail/static/src/models/activity.js
index ed6057ca8a5..4b484c87c25 100644
--- a/addons/mail/static/src/models/activity.js
+++ b/addons/mail/static/src/models/activity.js
@@ -70,7 +70,7 @@ registerModel({
if ('mail_template_ids' in data) {
data2.mailTemplates = insert(data.mail_template_ids);
}
- if ('res_id' in data && 'res_model' in data) {
+ if (data.res_id && data.res_model) {
data2.thread = insert({
id: data.res_id,
model: data.res_model,
@@ -121,28 +121,11 @@ registerModel({
*
* @return {Promise} promise that is fulfilled when the form has been closed
*/
- edit() {
- const action = {
- type: 'ir.actions.act_window',
- name: this.env._t("Schedule Activity"),
- res_model: 'mail.activity',
- view_mode: 'form',
- views: [[false, 'form']],
- target: 'new',
- context: {
- default_res_id: this.thread.id,
- default_res_model: this.thread.model,
- },
- res_id: this.id,
- };
- return new Promise(resolve => {
- this.env.services.action.doAction(action, {
- onClose: () => {
- resolve();
- this.fetchAndUpdate();
- },
- })
- });
+ async edit() {
+ await this.messaging.openActivityForm({ activity: this });
+ if (this.exists()) {
+ this.fetchAndUpdate();
+ }
},
async fetchAndUpdate() {
const [data] = await this.messaging.rpc({
@@ -151,7 +134,7 @@ registerModel({
args: [this.id],
}, { shadow: true }).catch(e => {
const errorName = e.message && e.message.data && e.message.data.name;
- if (errorName === 'odoo.exceptions.MissingError') {
+ if ([errorName, e.exceptionName].includes('odoo.exceptions.MissingError')) {
return [];
} else {
throw e;
diff --git a/addons/mail/static/src/models/activity_button_view.js b/addons/mail/static/src/models/activity_button_view.js
new file mode 100644
index 00000000000..f4f23ec6427
--- /dev/null
+++ b/addons/mail/static/src/models/activity_button_view.js
@@ -0,0 +1,99 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+import { clear } from '@mail/model/model_field_command';
+
+registerModel({
+ name: 'ActivityButtonView',
+ identifyingMode: 'xor',
+ recordMethods: {
+ onClick(ev) {
+ if (!this.exists()) {
+ return;
+ }
+ if (this.listFieldActivityViewOwner) {
+ ev.stopPropagation(); // prevents list view click that opens form view. TODO use special_click instead?
+ }
+ this.update({ activityListPopoverView: this.activityListPopoverView ? clear() : {} });
+ },
+ },
+ fields: {
+ activityListPopoverView: one('PopoverView', {
+ inverse: 'activityButtonViewOwnerAsActivityList',
+ }),
+ buttonClass: attr({
+ compute() {
+ if (!this.thread) {
+ return clear();
+ }
+ const classes = [];
+ switch (this.webRecord.data.activity_state) {
+ case 'overdue':
+ classes.push('text-danger');
+ break;
+ case 'today':
+ classes.push('text-warning');
+ break;
+ case 'planned':
+ classes.push('text-success');
+ break;
+ default:
+ classes.push('text-muted');
+ break;
+ }
+ switch (this.webRecord.data.activity_exception_decoration) {
+ case 'warning':
+ classes.push('text-warning');
+ classes.push(this.webRecord.data.activity_exception_icon);
+ break;
+ case 'danger':
+ classes.push('text-danger');
+ classes.push(this.webRecord.data.activity_exception_icon);
+ break;
+ default:
+ if (this.webRecord.data.activity_type_icon) {
+ classes.push(this.webRecord.data.activity_type_icon);
+ break;
+ }
+ classes.push('fa-clock-o');
+ break;
+ }
+ return classes.join(' ');
+ },
+ }),
+ buttonRef: attr(),
+ kanbanFieldActivityViewOwner: one('KanbanFieldActivityView', {
+ identifying: true,
+ inverse: 'activityButtonView',
+ }),
+ listFieldActivityViewOwner: one('ListFieldActivityView', {
+ identifying: true,
+ inverse: 'activityButtonView',
+ }),
+ thread: one('Thread', {
+ compute() {
+ if (this.kanbanFieldActivityViewOwner) {
+ return this.kanbanFieldActivityViewOwner.thread;
+ }
+ if (this.listFieldActivityViewOwner) {
+ return this.listFieldActivityViewOwner.thread;
+ }
+ return clear();
+ },
+ required: true,
+ }),
+ webRecord: attr({
+ compute() {
+ if (this.kanbanFieldActivityViewOwner) {
+ return this.kanbanFieldActivityViewOwner.webRecord;
+ }
+ if (this.listFieldActivityViewOwner) {
+ return this.listFieldActivityViewOwner.webRecord;
+ }
+ return clear();
+ },
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/activity_list_view.js b/addons/mail/static/src/models/activity_list_view.js
new file mode 100644
index 00000000000..34f30575724
--- /dev/null
+++ b/addons/mail/static/src/models/activity_list_view.js
@@ -0,0 +1,89 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, many, one } from '@mail/model/model_field';
+
+import session from 'web.session';
+
+registerModel({
+ name: 'ActivityListView',
+ lifecycleHooks: {
+ async _created() {
+ if (this.activities.length === 0) {
+ return;
+ }
+ const messaging = this.messaging;
+ const activitiesData = await this.messaging.rpc({
+ model: 'mail.activity',
+ method: 'activity_format',
+ args: [this.activities.map(activity => activity.id)],
+ kwargs: { context: session.user_context },
+ }, { shadow: true });
+ if (!messaging.exists()) {
+ return;
+ }
+ messaging.models['Activity'].insert(
+ activitiesData.map(activityData => messaging.models['Activity'].convertData(activityData))
+ );
+ },
+ },
+ recordMethods: {
+ onClickAddActivityButton() {
+ const thread = this.thread;
+ const webRecord = this.webRecord;
+ this.messaging.openActivityForm({
+ thread,
+ }).then(() => {
+ thread.fetchData(['activities']);
+ webRecord.model.load({ resId: thread.id });
+ });
+ this.popoverViewOwner.delete();
+ },
+ },
+ fields: {
+ activities: many('Activity', {
+ compute() {
+ return this.thread && this.thread.activities;
+ },
+ sort: [
+ ['truthy-first', 'dateDeadline'],
+ ['case-insensitive-asc', 'dateDeadline'],
+ ],
+ }),
+ activityListViewItems: many('ActivityListViewItem', {
+ compute() {
+ return this.activities.map(activity => {
+ return {
+ activity,
+ };
+ });
+ },
+ inverse: 'activityListViewOwner',
+ }),
+ overdueActivityListViewItems: many('ActivityListViewItem', {
+ inverse: 'activityListViewOwnerAsOverdue',
+ }),
+ plannedActivityListViewItems: many('ActivityListViewItem', {
+ inverse: 'activityListViewOwnerAsPlanned',
+ }),
+ popoverViewOwner: one('PopoverView', {
+ identifying: true,
+ inverse: 'activityListView',
+ }),
+ thread: one('Thread', {
+ compute() {
+ return this.popoverViewOwner.activityButtonViewOwnerAsActivityList.thread;
+ },
+ required: true,
+ }),
+ todayActivityListViewItems: many('ActivityListViewItem', {
+ inverse: 'activityListViewOwnerAsToday',
+ }),
+ webRecord: attr({
+ compute() {
+ return this.popoverViewOwner.activityButtonViewOwnerAsActivityList.webRecord;
+ },
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/activity_list_view_item.js b/addons/mail/static/src/models/activity_list_view_item.js
new file mode 100644
index 00000000000..0ef83f5424d
--- /dev/null
+++ b/addons/mail/static/src/models/activity_list_view_item.js
@@ -0,0 +1,112 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+import { clear } from '@mail/model/model_field_command';
+
+import { auto_str_to_date } from 'web.time';
+import { sprintf } from '@web/core/utils/strings';
+
+registerModel({
+ name: 'ActivityListViewItem',
+ recordMethods: {
+ onClickEditActivityButton() {
+ const popoverViewOwner = this.activityListViewOwner.popoverViewOwner;
+ const webRecord = this.webRecord;
+ const thread = this.activity.thread;
+ this.activity.edit().then(() => {
+ webRecord.model.load({ resId: thread.id });
+ });
+ popoverViewOwner.delete();
+ },
+ onClickMarkAsDone() {
+ this.update({ markDonePopoverView: this.markDonePopoverView ? clear() : {} });
+ },
+ /**
+ * Handles the click on the upload document button. This open the file
+ * explorer for upload.
+ */
+ onClickUploadDocument() {
+ this.fileUploader.openBrowserFileUploader();
+ },
+ },
+ fields: {
+ activity: one('Activity', {
+ identifying: true,
+ }),
+ activityListViewOwner: one('ActivityListView', {
+ identifying: true,
+ inverse: 'activityListViewItems',
+ }),
+ activityListViewOwnerAsOverdue: one('ActivityListView', {
+ compute() {
+ return this.activity.state === 'overdue' ? this.activityListViewOwner : clear();
+ },
+ inverse: 'overdueActivityListViewItems',
+ }),
+ activityListViewOwnerAsPlanned: one('ActivityListView', {
+ compute() {
+ return this.activity.state === 'planned' ? this.activityListViewOwner : clear();
+ },
+ inverse: 'plannedActivityListViewItems',
+ }),
+ activityListViewOwnerAsToday: one('ActivityListView', {
+ compute() {
+ return this.activity.state === 'today' ? this.activityListViewOwner : clear();
+ },
+ inverse: 'todayActivityListViewItems',
+ }),
+ clockWatcher: one('ClockWatcher', {
+ default: {
+ clock: {
+ frequency: 60 * 1000,
+ },
+ },
+ inverse: 'activityListViewItemOwner',
+ }),
+ /**
+ * Compute the label for "when" the activity is due.
+ */
+ delayLabel: attr({
+ compute() {
+ if (!this.activity.dateDeadline) {
+ return clear();
+ }
+ if (!this.clockWatcher.clock.date) {
+ return clear();
+ }
+ const today = moment(this.clockWatcher.clock.date.getTime()).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 sprintf(this.env._t("%s days overdue"), Math.round(Math.abs(diff)));
+ } else if (diff === 1) {
+ return this.env._t("Tomorrow");
+ } else {
+ return sprintf(this.env._t("Due in %s days"), Math.round(Math.abs(diff)));
+ }
+ },
+ }),
+ fileUploader: one('FileUploader', {
+ compute() {
+ return this.activity.category === 'upload_file' ? {} : clear();
+ },
+ inverse: 'activityListViewItemOwner',
+ }),
+ markDoneButtonRef: attr(),
+ markDonePopoverView: one('PopoverView', {
+ inverse: 'activityListViewItemOwnerAsMarkDone',
+ }),
+ webRecord: attr({
+ compute() {
+ return this.activityListViewOwner.webRecord;
+ },
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/activity_mark_done_popover_content_view.js b/addons/mail/static/src/models/activity_mark_done_popover_content_view.js
index c265bbe06f9..af5d47c5d6c 100644
--- a/addons/mail/static/src/models/activity_mark_done_popover_content_view.js
+++ b/addons/mail/static/src/models/activity_mark_done_popover_content_view.js
@@ -26,27 +26,39 @@ registerModel({
* Handles click on this "Done" button.
*/
async onClickDone() {
- const { chatter } = this.activityViewOwner.activityBoxView;
- await this.activityViewOwner.activity.markAsDone({
+ const chatter = this.activityViewOwner && this.activityViewOwner.activityBoxView.chatter;
+ const webRecord = this.webRecord;
+ const thread = this.activity.thread;
+ await this.activity.markAsDone({
feedback: this.feedbackTextareaRef.el.value,
});
- if (!chatter.exists() || !chatter.component) {
- return;
+ if (chatter && chatter.exists() && chatter.component) {
+ chatter.reloadParentView();
+ }
+ if (webRecord) {
+ webRecord.model.load({ resId: thread.id });
}
- chatter.reloadParentView();
},
/**
* Handles click on this "Done & Schedule Next" button.
*/
async onClickDoneAndScheduleNext() {
- const { chatter } = this.activityViewOwner.activityBoxView;
- await this.activityViewOwner.activity.markAsDoneAndScheduleNext({
+ const chatter = this.activityViewOwner && this.activityViewOwner.activityBoxView.chatter;
+ const webRecord = this.webRecord;
+ const thread = this.activity.thread;
+ const activityListViewOwner = this.activityListViewItemOwner && this.activityListViewItemOwner.activityListViewOwner;
+ await this.activity.markAsDoneAndScheduleNext({
feedback: this.feedbackTextareaRef.el.value,
});
- if (!chatter.exists() || !chatter.component) {
- return;
+ if (chatter && chatter.exists() && chatter.component) {
+ chatter.reloadParentView();
+ }
+ if (webRecord) {
+ webRecord.model.load({ resId: thread.id });
+ }
+ if (activityListViewOwner && activityListViewOwner.exists()) {
+ activityListViewOwner.popoverViewOwner.delete();
}
- chatter.reloadParentView();
},
/**
* Handles keydown on this activity mark done.
@@ -60,7 +72,7 @@ registerModel({
* @private
*/
_backupFeedback() {
- this.activityViewOwner.activity.update({
+ this.activity.update({
feedbackBackup: this.feedbackTextareaRef.el.value,
});
},
@@ -69,10 +81,37 @@ registerModel({
*/
_close() {
this._backupFeedback();
- this.activityViewOwner.update({ markDonePopoverView: clear() });
+ if (this.activityViewOwner) {
+ this.activityViewOwner.update({ markDonePopoverView: clear() });
+ return;
+ }
+ if (this.activityListViewItemOwner) {
+ this.activityListViewItemOwner.markDonePopoverView.delete();
+ return;
+ }
},
},
fields: {
+ activity: one('Activity', {
+ compute() {
+ if (this.activityListViewItemOwner) {
+ return this.activityListViewItemOwner.activity;
+ }
+ if (this.activityViewOwner) {
+ return this.activityViewOwner.activity;
+ }
+ return clear();
+ },
+ required: true,
+ }),
+ activityListViewItemOwner: one('ActivityListViewItem', {
+ compute() {
+ if (this.popoverViewOwner.activityListViewItemOwnerAsMarkDone) {
+ return this.popoverViewOwner.activityListViewItemOwnerAsMarkDone;
+ }
+ return clear();
+ },
+ }),
activityViewOwner: one('ActivityView', {
compute() {
if (this.popoverViewOwner.activityViewOwnerAsMarkDone) {
@@ -88,13 +127,20 @@ registerModel({
if (this.activityViewOwner) {
return this.activityViewOwner.markDoneText;
}
- return clear();
+ return this.env._t("Mark Done");
},
- default: '',
}),
popoverViewOwner: one('PopoverView', {
identifying: true,
inverse: 'activityMarkDonePopoverContentView',
}),
+ webRecord: attr({
+ compute() {
+ if (this.activityListViewItemOwner) {
+ return this.activityListViewItemOwner.webRecord;
+ }
+ return clear();
+ },
+ }),
},
});
diff --git a/addons/mail/static/src/models/chatter.js b/addons/mail/static/src/models/chatter.js
index b0afd94380c..d3d2ab706ce 100644
--- a/addons/mail/static/src/models/chatter.js
+++ b/addons/mail/static/src/models/chatter.js
@@ -71,26 +71,11 @@ registerModel({
*
* @param {MouseEvent} ev
*/
- onClickScheduleActivity(ev) {
- const action = {
- type: 'ir.actions.act_window',
- name: this.env._t("Schedule Activity"),
- res_model: 'mail.activity',
- view_mode: 'form',
- views: [[false, 'form']],
- target: 'new',
- context: {
- default_res_id: this.thread.id,
- default_res_model: this.thread.model,
- },
- res_id: false,
- };
- return this.env.services.action.doAction(
- action,
- {
- onClose: () => this.reloadParentView(),
- }
- );
+ async onClickScheduleActivity(ev) {
+ await this.messaging.openActivityForm({ thread: this.thread });
+ if (this.exists()) {
+ this.reloadParentView();
+ }
},
/**
* Handles click on "send message" button.
diff --git a/addons/mail/static/src/models/clock_watcher.js b/addons/mail/static/src/models/clock_watcher.js
index bdbe4d77265..70a46636c6b 100644
--- a/addons/mail/static/src/models/clock_watcher.js
+++ b/addons/mail/static/src/models/clock_watcher.js
@@ -10,6 +10,10 @@ registerModel({
name: 'ClockWatcher',
identifyingMode: 'xor',
fields: {
+ activityListViewItemOwner: one('ActivityListViewItem', {
+ identifying: true,
+ inverse: 'clockWatcher',
+ }),
activityViewOwner: one('ActivityView', {
identifying: true,
inverse: 'clockWatcher',
diff --git a/addons/mail/static/src/models/file_uploader.js b/addons/mail/static/src/models/file_uploader.js
index 7012c547007..650826e7674 100644
--- a/addons/mail/static/src/models/file_uploader.js
+++ b/addons/mail/static/src/models/file_uploader.js
@@ -94,6 +94,7 @@ registerModel({
* @returns {Promise}
*/
async _performUpload({ files }) {
+ const webRecord = this.activityListViewItemOwner && this.activityListViewItemOwner.webRecord;
const composer = this.composerView && this.composerView.composer; // save before async
const thread = this.thread; // save before async
const chatter = (
@@ -101,7 +102,10 @@ registerModel({
(this.attachmentBoxView && this.attachmentBoxView.chatter) ||
(this.activityView && this.activityView.activityBoxView.chatter)
); // save before async
- const activity = this.activityView && this.activityView.activity; // save before async
+ const activity = (
+ this.activityView && this.activityView.activity ||
+ this.activityListViewItemOwner && this.activityListViewItemOwner.activity
+ ); // save before async
const uploadingAttachments = new Map();
for (const file of files) {
uploadingAttachments.set(file, this.messaging.models['Attachment'].insert({
@@ -145,6 +149,9 @@ registerModel({
}
}
}
+ if (webRecord) {
+ webRecord.model.load({ resId: thread.id });
+ }
if (chatter && chatter.exists() && chatter.hasParentReloadOnAttachmentsChanged) {
chatter.reloadParentView();
}
@@ -154,6 +161,10 @@ registerModel({
},
},
fields: {
+ activityListViewItemOwner: one('ActivityListViewItem', {
+ identifying: true,
+ inverse: 'fileUploader',
+ }),
activityView: one('ActivityView', {
identifying: true,
inverse: 'fileUploader',
@@ -189,6 +200,9 @@ registerModel({
if (this.activityView) {
return this.activityView.activity.thread;
}
+ if (this.activityListViewItemOwner) {
+ return this.activityListViewItemOwner.activity.thread;
+ }
if (this.attachmentBoxView) {
return this.attachmentBoxView.chatter.thread;
}
diff --git a/addons/mail/static/src/models/kanban_field_activity_view.js b/addons/mail/static/src/models/kanban_field_activity_view.js
new file mode 100644
index 00000000000..d53dd20140c
--- /dev/null
+++ b/addons/mail/static/src/models/kanban_field_activity_view.js
@@ -0,0 +1,24 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+
+registerModel({
+ name: 'KanbanFieldActivityView',
+ fields: {
+ activityButtonView: one('ActivityButtonView', {
+ default: {},
+ inverse: 'kanbanFieldActivityViewOwner',
+ required: true,
+ }),
+ id: attr({
+ identifying: true,
+ }),
+ thread: one('Thread', {
+ required: true,
+ }),
+ webRecord: attr({
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/list_field_activity_view.js b/addons/mail/static/src/models/list_field_activity_view.js
new file mode 100644
index 00000000000..5dbdac6eb15
--- /dev/null
+++ b/addons/mail/static/src/models/list_field_activity_view.js
@@ -0,0 +1,43 @@
+/** @odoo-module **/
+
+import { registerModel } from '@mail/model/model_core';
+import { attr, one } from '@mail/model/model_field';
+import { clear } from '@mail/model/model_field_command';
+
+registerModel({
+ name: 'ListFieldActivityView',
+ fields: {
+ activityButtonView: one('ActivityButtonView', {
+ default: {},
+ inverse: 'listFieldActivityViewOwner',
+ required: true,
+ }),
+ id: attr({
+ identifying: true,
+ }),
+ summaryText: attr({
+ compute() {
+ if (this.thread.activities.length === 0) {
+ return clear();
+ }
+ if (this.webRecord.data.activity_exception_decoration) {
+ return this.env._t("Warning");
+ }
+ if (this.webRecord.data.activity_summary) {
+ return this.webRecord.data.activity_summary;
+ }
+ if (this.webRecord.data.activity_type_id) {
+ return this.webRecord.data.activity_type_id[1]; // 1 = display_name
+ }
+ return clear();
+ },
+ default: '',
+ }),
+ thread: one('Thread', {
+ required: true,
+ }),
+ webRecord: attr({
+ required: true,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/messaging.js b/addons/mail/static/src/models/messaging.js
index be742a404ed..7a173a7bd6a 100644
--- a/addons/mail/static/src/models/messaging.js
+++ b/addons/mail/static/src/models/messaging.js
@@ -93,6 +93,42 @@ addRecordMethods('Messaging', {
const { message, ...options } = params;
return this.env.services.notification.add(message, options);
},
+ /**
+ * Opens the activity form view for creating an activity on the given
+ * thread (if no activity is specified) or to update an existing
+ * activity (if specified).
+ *
+ * @param {Object} param0
+ * @param {Activity} [param0.activity]
+ * @param {integer} [param0.defaultActivityTypeId]
+ * @param {Thread} [param0.thread]
+ * @returns {Promise} resolved when the form is closed
+ */
+ async openActivityForm({ activity, defaultActivityTypeId, thread }) {
+ const targetThread = activity && activity.thread || thread;
+ const context = {
+ default_res_id: targetThread.id,
+ default_res_model: targetThread.model,
+ };
+ if (defaultActivityTypeId !== undefined) {
+ context.default_activity_type_id = defaultActivityTypeId;
+ }
+ const action = {
+ type: 'ir.actions.act_window',
+ name: this.env._t("Schedule Activity"),
+ res_model: 'mail.activity',
+ view_mode: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context,
+ res_id: activity ? activity.id : false,
+ };
+ return new Promise(resolve => {
+ this.env.services.action.doAction(action, {
+ onClose: resolve,
+ });
+ });
+ },
/**
* Opens a chat with the provided person and returns it.
*
diff --git a/addons/mail/static/src/models/popover_view.js b/addons/mail/static/src/models/popover_view.js
index 00969d62ec8..21b76f0452c 100644
--- a/addons/mail/static/src/models/popover_view.js
+++ b/addons/mail/static/src/models/popover_view.js
@@ -42,13 +42,34 @@ registerModel({
if (this.component.root.el.contains(ev.target)) {
return;
}
+ if (this.activityListView) {
+ for (const activityListViewItem of this.activityListView.activityListViewItems) {
+ if (activityListViewItem.markDonePopoverView && activityListViewItem.markDonePopoverView.contains(ev.target)) {
+ return;
+ }
+ }
+ }
this.delete();
},
},
fields: {
+ activityButtonViewOwnerAsActivityList: one('ActivityButtonView', {
+ identifying: true,
+ inverse: 'activityListPopoverView',
+ }),
+ activityListView: one('ActivityListView', {
+ compute() {
+ return this.activityButtonViewOwnerAsActivityList ? {} : clear();
+ },
+ inverse: 'popoverViewOwner',
+ }),
+ activityListViewItemOwnerAsMarkDone: one('ActivityListViewItem', {
+ identifying: true,
+ inverse: 'markDonePopoverView',
+ }),
activityMarkDonePopoverContentView: one('ActivityMarkDonePopoverContentView', {
compute() {
- if (this.activityViewOwnerAsMarkDone) {
+ if (this.activityViewOwnerAsMarkDone || this.activityListViewItemOwnerAsMarkDone) {
return {};
}
return clear();
@@ -64,6 +85,9 @@ registerModel({
*/
anchorRef: attr({
compute() {
+ if (this.activityListViewItemOwnerAsMarkDone) {
+ return this.activityListViewItemOwnerAsMarkDone.markDoneButtonRef;
+ }
if (this.activityViewOwnerAsMarkDone) {
return this.activityViewOwnerAsMarkDone.markDoneButtonRef;
}
@@ -79,6 +103,9 @@ registerModel({
if (this.composerViewOwnerAsEmoji) {
return this.composerViewOwnerAsEmoji.buttonEmojisRef;
}
+ if (this.activityButtonViewOwnerAsActivityList) {
+ return this.activityButtonViewOwnerAsActivityList.buttonRef;
+ }
if (this.messageActionViewOwnerAsReaction) {
return this.messageActionViewOwnerAsReaction.actionRef;
}
@@ -158,6 +185,9 @@ registerModel({
if (this.emojiPickerView) {
return this.emojiPickerView;
}
+ if (this.activityButtonViewOwnerAsActivityList) {
+ return this.activityListView;
+ }
if (this.messageNotificationPopoverContentView) {
return this.messageNotificationPopoverContentView;
}
@@ -201,6 +231,9 @@ registerModel({
if (this.emojiPickerView) {
return 'EmojiPickerView';
}
+ if (this.activityButtonViewOwnerAsActivityList) {
+ return 'ActivityListView';
+ }
if (this.messageNotificationPopoverContentView) {
return 'MessageNotificationPopoverContent';
}
@@ -259,6 +292,9 @@ registerModel({
*/
position: attr({
compute() {
+ if (this.activityListViewItemOwnerAsMarkDone) {
+ return 'right';
+ }
if (this.activityViewOwnerAsMarkDone) {
return 'right';
}
@@ -274,6 +310,9 @@ registerModel({
if (this.composerViewOwnerAsEmoji) {
return 'top';
}
+ if (this.activityButtonViewOwnerAsActivityList) {
+ return 'bottom-start';
+ }
if (this.messageActionViewOwnerAsReaction) {
return 'top';
}
diff --git a/addons/mail/static/src/models/thread.js b/addons/mail/static/src/models/thread.js
index a7d0d612af5..419b1fd7645 100644
--- a/addons/mail/static/src/models/thread.js
+++ b/addons/mail/static/src/models/thread.js
@@ -1088,6 +1088,20 @@ registerModel({
activities: many('Activity', {
inverse: 'thread',
}),
+ activity_state: attr({
+ compute() {
+ if (this.overdueActivities.length > 0) {
+ return 'overdue';
+ }
+ if (this.todayActivities.length > 0) {
+ return 'today';
+ }
+ if (this.futureActivities.length > 0) {
+ return 'planned';
+ }
+ return clear();
+ },
+ }),
allAttachments: many('Attachment', {
compute() {
const allAttachments = [...new Set(this.originThreadAttachments.concat(this.attachments))]
diff --git a/addons/mail/static/src/xml/activity_view.xml b/addons/mail/static/src/xml/activity_view.xml
index c3622fb6408..c72658b202f 100644
--- a/addons/mail/static/src/xml/activity_view.xml
+++ b/addons/mail/static/src/xml/activity_view.xml
@@ -61,8 +61,8 @@
-
+
|
',
};
- const { openView } = await start({
+ const { click, openView } = await start({
mockRPC: function (route, args) {
if (
args.method !== 'get_views' &&
@@ -197,7 +197,7 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
this.props.selectRecord = (...args) => {
assert.step(`select_record ${JSON.stringify(args)}`);
return selectRecord(...args);
- }
+ };
}
});
@@ -206,29 +206,21 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
views: [[false, 'list']],
});
- assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Call with Al');
+ assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al');
- // click on the first record to open it, to ensure that the 'switch_view'
- // assertion is relevant (it won't be opened as there is no action manager,
- // but we'll log the 'switch_view' event)
- await testUtils.dom.click(document.querySelector('.o_data_cell'));
+ await click('.o_ActivityButtonView'); // open the popover
+ await click('.o_ActivityListViewItem_markAsDone'); // mark the first activity as done
+ await click('.o_ActivityMarkDonePopoverContent_doneButton'); // confirm
- // from this point, no 'switch_view' event should be triggered, as we
- // interact with the activity widget
- assert.step('open dropdown');
- await testUtils.dom.click(document.querySelector('.o_activity_btn span')); // open the popover
- await testUtils.dom.click(document.querySelector('.o_mark_as_done')); // mark the first activity as done
- await testUtils.dom.click(document.querySelector('.o_activity_popover_done')); // confirm
-
- assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Meet FP');
+ assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Meet FP');
assert.verifySteps([
'web_search_read',
- 'select_record [2,{"activeIds":[2]}]',
- 'open dropdown',
'activity_format',
'action_feedback',
- 'read',
+ '/mail/thread/messages',
+ '/mail/thread/data',
+ 'web_search_read',
]);
});
@@ -287,108 +279,6 @@ QUnit.test('list activity exception widget with activity', async function (asser
assert.containsOnce(document.querySelectorAll('.o_data_row .o_activity_exception_cell')[1], '.o_ActivityException', "there is an exception on a record");
});
-QUnit.test('list activity widget: done the activity with "ENTER" keyboard shortcut', async function (assert) {
- assert.expect(1);
-
- const pyEnv = await startServer();
- const mailActivityTypeId1 = pyEnv['mail.activity.type'].create({});
- const mailActivityId1 = pyEnv['mail.activity'].create([
- {
- display_name: "Call with Al",
- date_deadline: moment().format("YYYY-MM-DD"), // now
- can_write: true,
- state: "today",
- user_id: pyEnv.currentUserId,
- create_uid: pyEnv.currentUserId,
- activity_type_id: mailActivityTypeId1,
- }
- ]);
- pyEnv['res.users'].write([pyEnv.currentUserId], {
- activity_ids: [mailActivityId1],
- activity_state: 'today',
- activity_summary: 'Call with Al',
- activity_type_id: mailActivityTypeId1,
- });
- const views = {
- 'res.users,false,list':
- `
-
- `,
- };
- const { openView } = await start({
- serverData: { views },
- });
- await openView({
- res_model: 'res.users',
- views: [[false, 'list']],
- });
-
- await testUtils.dom.click(document.querySelector('.o_activity_btn span'));
- await testUtils.dom.click(document.querySelector('.o_mark_as_done'));
- pyEnv['res.users'].write([pyEnv.currentUserId], {
- activity_state: '',
- });
- await testUtils.dom.triggerEvent(
- document.querySelector('.o_activity_popover_done'),
- 'keydown',
- { key: 'Enter' },
- );
-
- assert.containsOnce(document.body, '.o_mail_activity .o_activity_color_default');
-});
-
-QUnit.test('list activity widget: done and schedule the next activity with "ENTER" keyboard shortcut', async function (assert) {
- assert.expect(1);
-
- const pyEnv = await startServer();
- const mailActivityTypeId1 = pyEnv['mail.activity.type'].create({});
- const mailActivityId1 = pyEnv['mail.activity'].create([
- {
- display_name: "Call with Al",
- date_deadline: moment().format("YYYY-MM-DD"), // now
- can_write: true,
- state: "today",
- user_id: pyEnv.currentUserId,
- create_uid: pyEnv.currentUserId,
- activity_type_id: mailActivityTypeId1,
- }
- ]);
- pyEnv['res.users'].write([pyEnv.currentUserId], {
- activity_ids: [mailActivityId1],
- activity_state: 'today',
- activity_summary: 'Call with Al',
- activity_type_id: mailActivityTypeId1,
- });
-
- const views = {
- 'res.users,false,list':
- `
-
- `,
- };
- const { env, openView } = await start({
- serverData: { views },
- });
- await openView({
- res_model: 'res.users',
- views: [[false, 'list']],
- });
- patchWithCleanup(env.services.action, {
- doAction(action) {
- assert.strictEqual(action.name, "Schedule an Activity", 'should do a do_action with correct parameters');
- return this._super(...arguments);
- }
- });
-
- await testUtils.dom.click(document.querySelector('.o_activity_btn span'));
- await testUtils.dom.click(document.querySelector('.o_mark_as_done'));
- await testUtils.dom.triggerEvent(
- document.querySelector('.o_activity_popover_done_next'),
- 'keydown',
- { key: 'Enter' },
- );
-});
-
QUnit.module('FieldMany2ManyTagsEmail');
QUnit.test('fieldmany2many tags email (edition)', async function (assert) {
diff --git a/addons/test_mail/static/tests/activity_tests.js b/addons/test_mail/static/tests/activity_tests.js
index cfa59417337..b8d9d479777 100644
--- a/addons/test_mail/static/tests/activity_tests.js
+++ b/addons/test_mail/static/tests/activity_tests.js
@@ -387,7 +387,7 @@ QUnit.test('activity view: search more to schedule an activity for a record of a
var $modal = $('.modal-lg');
assert.strictEqual($modal.find('.o_data_row').length, 3, "all mail.test.activity should be available to select");
// select a record to schedule an activity for it (this triggers a do_action)
- testUtils.dom.click($modal.find('.o_data_row:last'));
+ await testUtils.dom.click($modal.find('.o_data_row:last'));
assert.verifySteps(['doAction']);
});
|