From 7621343501259e709c658fbb22c3b7a2106d8f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Fri, 9 Sep 2022 14:43:20 +0000 Subject: [PATCH] [IMP] mail: create kanban and list activity field components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes odoo/odoo#101330 X-original-commit: 069220ac94b3e33201991aa04506f1aa4227be1d Related: odoo/enterprise#31869 Signed-off-by: Alexandre Kühn (aku) Signed-off-by: Sébastien Theys (seb) --- addons/crm/static/src/js/tours/crm.js | 4 +- .../activity_button_view.js | 26 ++++ .../activity_button_view.xml | 10 ++ .../activity_list_view/activity_list_view.js | 20 +++ .../activity_list_view.scss | 3 + .../activity_list_view/activity_list_view.xml | 43 ++++++ .../activity_list_view_item.js | 25 +++ .../activity_list_view_item.xml | 32 ++++ .../kanban_field_activity_view.js | 20 +++ .../kanban_field_activity_view.xml | 8 + .../kanban_field_activity_view_container.js | 98 ++++++++++++ .../kanban_field_activity_view_container.xml | 8 + .../list_field_activity_view.js | 20 +++ .../list_field_activity_view.xml | 9 ++ .../list_field_activity_view_container.js | 98 ++++++++++++ .../list_field_activity_view_container.xml | 8 + .../activity_mark_done_popover_content.js | 4 +- .../activity_mark_done_popover_content.xml | 2 +- addons/mail/static/src/js/activity.js | 107 +++++++------ .../src/js/views/activity/activity_cell.js | 53 ------- .../js/views/activity/activity_controller.js | 12 +- .../js/views/activity/activity_renderer.js | 11 +- addons/mail/static/src/models/activity.js | 31 +--- .../static/src/models/activity_button_view.js | 99 ++++++++++++ .../static/src/models/activity_list_view.js | 89 +++++++++++ .../src/models/activity_list_view_item.js | 112 ++++++++++++++ ...activity_mark_done_popover_content_view.js | 74 +++++++-- addons/mail/static/src/models/chatter.js | 25 +-- .../mail/static/src/models/clock_watcher.js | 4 + .../mail/static/src/models/file_uploader.js | 16 +- .../src/models/kanban_field_activity_view.js | 24 +++ .../src/models/list_field_activity_view.js | 43 ++++++ addons/mail/static/src/models/messaging.js | 36 +++++ addons/mail/static/src/models/popover_view.js | 41 ++++- addons/mail/static/src/models/thread.js | 14 ++ addons/mail/static/src/xml/activity_view.xml | 4 +- .../mail/static/tests/helpers/mock_server.js | 2 +- .../tests/qunit_suite_tests/chatter_tests.js | 146 +++--------------- .../test_mail/static/tests/activity_tests.js | 2 +- 39 files changed, 1070 insertions(+), 313 deletions(-) create mode 100644 addons/mail/static/src/backend_components/activity_button_view/activity_button_view.js create mode 100644 addons/mail/static/src/backend_components/activity_button_view/activity_button_view.xml create mode 100644 addons/mail/static/src/backend_components/activity_list_view/activity_list_view.js create mode 100644 addons/mail/static/src/backend_components/activity_list_view/activity_list_view.scss create mode 100644 addons/mail/static/src/backend_components/activity_list_view/activity_list_view.xml create mode 100644 addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.js create mode 100644 addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.xml create mode 100644 addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.js create mode 100644 addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.xml create mode 100644 addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js create mode 100644 addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.xml create mode 100644 addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.js create mode 100644 addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.xml create mode 100644 addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js create mode 100644 addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.xml delete mode 100644 addons/mail/static/src/js/views/activity/activity_cell.js create mode 100644 addons/mail/static/src/models/activity_button_view.js create mode 100644 addons/mail/static/src/models/activity_list_view.js create mode 100644 addons/mail/static/src/models/activity_list_view_item.js create mode 100644 addons/mail/static/src/models/kanban_field_activity_view.js create mode 100644 addons/mail/static/src/models/list_field_activity_view.js diff --git a/addons/crm/static/src/js/tours/crm.js b/addons/crm/static/src/js/tours/crm.js index 320f06e246c..b1306f8d863 100644 --- a/addons/crm/static/src/js/tours/crm.js +++ b/addons/crm/static/src/js/tours/crm.js @@ -45,12 +45,12 @@ tour.register('crm_tour', { position: "right", run: "drag_and_drop_native .o_opportunity_kanban .o_kanban_group:eq(2) ", }, { - trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default", + trigger: ".o_kanban_record:not(.o_updating) .o_ActivityButtonView", extra_trigger: ".o_opportunity_kanban", content: Markup(_t("Looks like nothing is planned. :(

Tip : Schedule activities to keep track of everything you have to do!")), position: "bottom", }, { - trigger: ".o_schedule_activity", + trigger: ".o_ActivityListView_addActivityButton", extra_trigger: ".o_opportunity_kanban", content: Markup(_t("Let's Schedule an Activity.")), position: "bottom", diff --git a/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.js b/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.js new file mode 100644 index 00000000000..3f3cff3afea --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ + +import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; +import { registerMessagingComponent } from '@mail/utils/messaging_component'; + +const { Component } = owl; + +export class ActivityButtonView extends Component { + + setup() { + super.setup(); + useRefToModel({ fieldName: 'buttonRef', refName: 'button' }); + } + + get activityButtonView() { + return this.props.record; + } + +} + +Object.assign(ActivityButtonView, { + props: { record: Object }, + template: 'mail.ActivityButtonView', +}); + +registerMessagingComponent(ActivityButtonView); diff --git a/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.xml b/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.xml new file mode 100644 index 00000000000..116a5a97ecb --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_button_view/activity_button_view.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.js b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.js new file mode 100644 index 00000000000..1ad562b534f --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.js @@ -0,0 +1,20 @@ +/** @odoo-module **/ + +import { registerMessagingComponent } from '@mail/utils/messaging_component'; + +const { Component } = owl; + +export class ActivityListView extends Component { + + get activityListView() { + return this.props.record; + } + +} + +Object.assign(ActivityListView, { + props: { record: Object }, + template: 'mail.ActivityListView', +}); + +registerMessagingComponent(ActivityListView); diff --git a/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.scss b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.scss new file mode 100644 index 00000000000..7c7462a0e3c --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.scss @@ -0,0 +1,3 @@ +.o_ActivityListView { + min-width: 290px; +} diff --git a/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.xml b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.xml new file mode 100644 index 00000000000..165bc498378 --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_list_view/activity_list_view.xml @@ -0,0 +1,43 @@ + + + +
+ + Schedule activities to help you get things done. + + +
+ Overdue + + +
+ + + +
+ +
+ Today + + +
+ + + +
+ +
+ Planned + + +
+ + + +
+ +
+
+
diff --git a/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.js b/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.js new file mode 100644 index 00000000000..a31ebfaf41a --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.js @@ -0,0 +1,25 @@ +/** @odoo-module **/ + +import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; +import { registerMessagingComponent } from '@mail/utils/messaging_component'; + +const { Component } = owl; + +export class ActivityListViewItem extends Component { + + setup() { + super.setup(); + useRefToModel({ fieldName: 'markDoneButtonRef', refName: 'markDoneButton' }); + } + get activityListViewItem() { + return this.props.record; + } + +} + +Object.assign(ActivityListViewItem, { + props: { record: Object }, + template: 'mail.ActivityListViewItem', +}); + +registerMessagingComponent(ActivityListViewItem); diff --git a/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.xml b/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.xml new file mode 100644 index 00000000000..406ddf763a0 --- /dev/null +++ b/addons/mail/static/src/backend_components/activity_list_view_item/activity_list_view_item.xml @@ -0,0 +1,32 @@ + + + +
+
+ + + + + + + + + +
+ + - + +
+ +
+ + diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.js b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.js new file mode 100644 index 00000000000..042227c5d9f --- /dev/null +++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.js @@ -0,0 +1,20 @@ +/** @odoo-module **/ + +import { registerMessagingComponent } from '@mail/utils/messaging_component'; + +const { Component } = owl; + +export class KanbanFieldActivityView extends Component { + + get kanbanFieldActivityView() { + return this.props.record; + } + +} + +Object.assign(KanbanFieldActivityView, { + props: { record: Object }, + template: 'mail.KanbanFieldActivityView', +}); + +registerMessagingComponent(KanbanFieldActivityView); diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.xml b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.xml new file mode 100644 index 00000000000..7321e47f503 --- /dev/null +++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js new file mode 100644 index 00000000000..0fc4e00b7c0 --- /dev/null +++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js @@ -0,0 +1,98 @@ +/** @odoo-module **/ + +// ensure components are registered beforehand. +import '@mail/backend_components/kanban_field_activity_view/kanban_field_activity_view'; +import { getMessagingComponent } from '@mail/utils/messaging_component'; + +import { registry } from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +const { Component, onWillDestroy, onWillUpdateProps } = owl; + +const getNextId = (function () { + let tmpId = 0; + return () => { + tmpId += 1; + return tmpId; + }; +})(); + +/** + * Container for messaging component KanbanFieldActivityView ensuring messaging + * records are ready before rendering KanbanFieldActivityView component. + */ +export class KanbanFieldActivityViewContainer extends Component { + + /** + * @override + */ + setup() { + super.setup(); + this.kanbanFieldActivityView = undefined; + this.kanbanFieldActivityViewId = getNextId(); + this._insertFromProps(this.props); + onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillDestroy(() => this._deleteRecord()); + } + + /** + * @private + */ + _deleteRecord() { + if (this.kanbanFieldActivityView) { + if (this.kanbanFieldActivityView.exists()) { + this.kanbanFieldActivityView.delete(); + } + this.kanbanFieldActivityView = undefined; + } + } + + /** + * @private + */ + async _insertFromProps(props) { + const messaging = await this.env.services.messaging.get(); + if (owl.status(this) === "destroyed") { + this._deleteRecord(); + return; + } + const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({ + id: this.kanbanFieldActivityViewId, + thread: { + activities: this.props.value.records.map(activityData => { + return { + id: activityData.resId, + }; + }), + hasActivities: true, + id: props.record.resId, + model: props.record.resModel, + }, + webRecord: props.record, + }); + if (kanbanFieldActivityView !== this.kanbanFieldActivityView) { + this._deleteRecord(); + this.kanbanFieldActivityView = kanbanFieldActivityView; + } + this.render(); + } + +} + +Object.assign(KanbanFieldActivityViewContainer, { + components: { KanbanFieldActivityView: getMessagingComponent('KanbanFieldActivityView') }, + fieldDependencies: { + activity_exception_decoration: { type: 'selection' }, + activity_exception_icon: { type: 'char' }, + activity_state: { type: 'selection' }, + activity_summary: { type: 'char' }, + activity_type_icon: { type: 'char' }, + activity_type_id: { type: 'many2one', relation: 'mail.activity.type' }, + }, + props: { + ...standardFieldProps, + }, + template: 'mail.KanbanFieldActivityViewContainer', +}); + +registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer); diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.xml b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.xml new file mode 100644 index 00000000000..f29585e6434 --- /dev/null +++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.js b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.js new file mode 100644 index 00000000000..f6fd2d771b7 --- /dev/null +++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.js @@ -0,0 +1,20 @@ +/** @odoo-module **/ + +import { registerMessagingComponent } from '@mail/utils/messaging_component'; + +const { Component } = owl; + +export class ListFieldActivityView extends Component { + + get listFieldActivityView() { + return this.props.record; + } + +} + +Object.assign(ListFieldActivityView, { + props: { record: Object }, + template: 'mail.ListFieldActivityView', +}); + +registerMessagingComponent(ListFieldActivityView); diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.xml b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.xml new file mode 100644 index 00000000000..4e6cf99f748 --- /dev/null +++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js new file mode 100644 index 00000000000..f137bb34665 --- /dev/null +++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js @@ -0,0 +1,98 @@ +/** @odoo-module **/ + +// ensure components are registered beforehand. +import '@mail/backend_components/list_field_activity_view/list_field_activity_view'; +import { getMessagingComponent } from '@mail/utils/messaging_component'; + +import { registry } from "@web/core/registry"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; + +const { Component, onWillDestroy, onWillUpdateProps } = owl; + +const getNextId = (function () { + let tmpId = 0; + return () => { + tmpId += 1; + return tmpId; + }; +})(); + +/** + * Container for messaging component ListFieldActivityView ensuring messaging + * records are ready before rendering ListFieldActivityView component. + */ +export class ListFieldActivityViewContainer extends Component { + + /** + * @override + */ + setup() { + super.setup(); + this.listFieldActivityView = undefined; + this.listFieldActivityViewId = getNextId(); + this._insertFromProps(this.props); + onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillDestroy(() => this._deleteRecord()); + } + + /** + * @private + */ + _deleteRecord() { + if (this.listFieldActivityView) { + if (this.listFieldActivityView.exists()) { + this.listFieldActivityView.delete(); + } + this.listFieldActivityView = undefined; + } + } + + /** + * @private + */ + async _insertFromProps(props) { + const messaging = await this.env.services.messaging.get(); + if (owl.status(this) === "destroyed") { + this._deleteRecord(); + return; + } + const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({ + id: this.listFieldActivityViewId, + thread: { + activities: this.props.value.records.map(activityData => { + return { + id: activityData.resId, + }; + }), + hasActivities: true, + id: props.record.resId, + model: props.record.resModel, + }, + webRecord: props.record, + }); + if (listFieldActivityView !== this.listFieldActivityView) { + this._deleteRecord(); + this.listFieldActivityView = listFieldActivityView; + } + this.render(); + } + +} + +Object.assign(ListFieldActivityViewContainer, { + components: { ListFieldActivityView: getMessagingComponent('ListFieldActivityView') }, + fieldDependencies: { + activity_exception_decoration: { type: 'selection' }, + activity_exception_icon: { type: 'char' }, + activity_state: { type: 'selection' }, + activity_summary: { type: 'char' }, + activity_type_icon: { type: 'char' }, + activity_type_id: { type: 'many2one', relation: 'mail.activity.type' }, + }, + props: { + ...standardFieldProps, + }, + template: 'mail.ListFieldActivityViewContainer', +}); + +registry.category('fields').add('list_activity', ListFieldActivityViewContainer); diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.xml b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.xml new file mode 100644 index 00000000000..8de371fb8b4 --- /dev/null +++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.js b/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.js index 3f9fd1789f6..28d861f2259 100644 --- a/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.js +++ b/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.js @@ -26,8 +26,8 @@ export class ActivityMarkDonePopoverContent extends LegacyComponent { _mounted() { this._feedbackTextareaRef.el.focus(); - if (this.activityMarkDonePopoverContentView.activityViewOwner.activity.feedbackBackup) { - this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverContentView.activityViewOwner.activity.feedbackBackup; + if (this.activityMarkDonePopoverContentView.activity.feedbackBackup) { + this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverContentView.activity.feedbackBackup; } } diff --git a/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.xml b/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.xml index e87c49c3a97..af3992f810f 100644 --- a/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.xml +++ b/addons/mail/static/src/components/activity_mark_done_popover_content/activity_mark_done_popover_content.xml @@ -11,7 +11,7 @@ - + diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 9388f124817..d5c9091628b 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -37,43 +37,26 @@ const KanbanActivity = AbstractField.extend({ /** * @override */ - init(parent, name, record) { + init(parent, name, record, options = {}) { this._super.apply(this, arguments); this._draftFeedback = {}; this.selection = {}; for (const [key, value] of record.fields.activity_state.selection) { this.selection[key] = value; } + this.defaultActivityType = options.activityType; + this.isActivityViewCell = options.isActivityViewCell; }, - - /** - * @param {integer} previousActivityTypeID - * @return {Promise} - */ - scheduleActivity() { - const callback = this._reload.bind(this, { activity: true, thread: true }); - return this._openActivityForm(false, callback); + async willStart() { + await this._super(); + await owl.Component.env.services.messaging.modelManager.messagingCreatedPromise; + this.messaging = owl.Component.env.services.messaging.modelManager.messaging; }, //------------------------------------------------------------ // Private //------------------------------------------------------------ - _getActivityFormAction(id) { - return { - type: 'ir.actions.act_window', - name: _t("Schedule Activity"), - res_model: 'mail.activity', - view_mode: 'form', - views: [[false, 'form']], - target: 'new', - context: { - default_res_id: this.res_id, - default_res_model: this.model, - }, - res_id: id || false, - }; - }, /** * Send a feedback and reload page in order to mark activity as done * @@ -99,7 +82,7 @@ const KanbanActivity = AbstractField.extend({ }, context: this.record.getContext(), }); - this._reload({ activity: true, thread: true }); + this._reload(); }, /** * Send a feedback and proposes to schedule next activity @@ -136,10 +119,17 @@ const KanbanActivity = AbstractField.extend({ * @param {MouseEvent} ev * @returns {Promise} */ - _onEditActivity(ev) { + async _onEditActivity(ev) { ev.preventDefault(); - const activityID = $(ev.currentTarget).data('activity-id'); - return this._openActivityForm(activityID, this._reload.bind(this, { activity: true, thread: true })); + const activityId = $(ev.currentTarget).data('activity-id'); + const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model }); + const activity = this.messaging.models['Activity'].insert({ id: activityId, thread }); + await this.messaging.openActivityForm({ + activity, + defaultActivityTypeId: this.defaultActivityType, + thread, + }); + this._reload(); }, /** * @private @@ -353,9 +343,14 @@ const KanbanActivity = AbstractField.extend({ * @param {MouseEvent} ev * @returns {Promise} */ - _onScheduleActivity(ev) { + async _onScheduleActivity(ev) { ev.preventDefault(); - return this._openActivityForm(false, this._reload.bind(this)); + const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model }); + await this.messaging.openActivityForm({ + defaultActivityTypeId: this.defaultActivityType, + thread, + }); + this._reload(); }, /** * @private @@ -371,17 +366,7 @@ const KanbanActivity = AbstractField.extend({ method: 'activity_send_mail', args: [[this.res_id], templateID], }); - this._reload({ activity: true, thread: true, followers: true }); - }, - /** - * @private - * @param {integer} id - * @param {function} callback - * @return {Promise} - */ - _openActivityForm(id, callback) { - const action = this._getActivityFormAction(id); - return this.do_action(action, { on_close: callback }); + this._reload(); }, /** * @private @@ -394,18 +379,38 @@ const KanbanActivity = AbstractField.extend({ * @private */ _render() { - // span classes need to be updated manually because the template cannot - // be re-rendered eaasily (because of the dropdown state) - const spanClasses = ['fa', 'fa-lg', 'fa-fw']; - spanClasses.push('o_activity_color_' + (this.record.data.activity_state || 'default')); - if (this.recordData.activity_exception_decoration) { - spanClasses.push('text-' + this.recordData.activity_exception_decoration); - spanClasses.push(this.recordData.activity_exception_icon); + if (this.isActivityViewCell) { + // replace clock by closest deadline + const $date = $('
'); + 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, + })); + } } else { - spanClasses.push('fa-clock-o'); + // span classes need to be updated manually because the template cannot + // be re-rendered eaasily (because of the dropdown state) + const spanClasses = ['fa', 'fa-lg', 'fa-fw']; + spanClasses.push('o_activity_color_' + (this.record.data.activity_state || 'default')); + if (this.recordData.activity_exception_decoration) { + spanClasses.push('text-' + this.recordData.activity_exception_decoration); + spanClasses.push(this.recordData.activity_exception_icon); + } else { + spanClasses.push('fa-clock-o'); + } + this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' ')); } - this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' ')); - if (this.$el.hasClass('show')) { // note: this part of the rendering might be asynchronous this._renderDropdown(); diff --git a/addons/mail/static/src/js/views/activity/activity_cell.js b/addons/mail/static/src/js/views/activity/activity_cell.js deleted file mode 100644 index c790d924aa6..00000000000 --- a/addons/mail/static/src/js/views/activity/activity_cell.js +++ /dev/null @@ -1,53 +0,0 @@ -/** @odoo-module **/ - -import '@mail/js/activity'; - -import field_registry from 'web.field_registry'; - -const KanbanActivity = field_registry.get('kanban_activity'); - -const ActivityCell = KanbanActivity.extend({ - init(parent, name, record, options) { - this._super.apply(this, arguments); - this.activityType = options && options.activityType; - }, - /** - * @private - * @override - */ - _getActivityFormAction(id) { - const action = this._super.apply(this, arguments); - action.context['default_activity_type_id'] = this.activityType; - return action; - }, - /** - * @override - * @private - */ - _render() { - // replace clock by closest deadline - const $date = $('
'); - 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']); });