[IMP] mail: create kanban and list activity field components

closes odoo/odoo#101330

X-original-commit: 069220ac94b3e33201991aa04506f1aa4227be1d
Related: odoo/enterprise#31869
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Sébastien Theys
2022-09-27 19:14:51 +02:00
parent d7e0760ce8
commit 7621343501
39 changed files with 1070 additions and 313 deletions
+2 -2
View File
@@ -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. :(<br><br><i>Tip : Schedule activities to keep track of everything you have to do!</i>")),
position: "bottom",
}, {
trigger: ".o_schedule_activity",
trigger: ".o_ActivityListView_addActivityButton",
extra_trigger: ".o_opportunity_kanban",
content: Markup(_t("Let's <b>Schedule an Activity.</b>")),
position: "bottom",
@@ -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);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityButtonView" owl="1">
<a class="o_ActivityButtonView" role="button" t-on-click.prevent="activityButtonView.onClick" t-ref="button">
<i class="o_ActivityButtonView_icon fa fa-fw fa-lg" t-att-class="activityButtonView.buttonClass" role="img"/>
</a>
</t>
</templates>
@@ -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);
@@ -0,0 +1,3 @@
.o_ActivityListView {
min-width: 290px;
}
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityListView" owl="1">
<div class="o_ActivityListView d-flex flex-column" t-ref="root">
<t t-if="activityListView.activityListViewItems.length === 0">
<span class="p-3 text-center fst-italic text-500 border-bottom">Schedule activities to help you get things done.</span>
</t>
<t t-if="activityListView.overdueActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-danger fw-bold mx-3">Overdue</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-danger border-0 mx-3 align-self-center" t-esc="activityListView.overdueActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.overdueActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
<t t-if="activityListView.todayActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-warning fw-bold mx-3">Today</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-warning border-0 mx-3 align-self-center" t-esc="activityListView.todayActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.todayActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
<t t-if="activityListView.plannedActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-success fw-bold mx-3">Planned</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-success border-0 mx-3 align-self-center" t-esc="activityListView.plannedActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.plannedActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
<button class="o_ActivityListView_addActivityButton btn btn-secondary p-3 text-center" t-on-click="activityListView.onClickAddActivityButton">
<i class="fa fa-plus fa-fw"></i><strong>Schedule an activity</strong>
</button>
</div>
</t>
</templates>
@@ -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);
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityListViewItem" owl="1">
<div class="o_ActivityListViewItem d-flex flex-column border-bottom px-3 py-2">
<div class="d-flex align-items-baseline">
<i class="fa small me-2" t-attf-class="{{ activityListViewItem.activity.icon }}" role="img"/>
<t t-if="activityListViewItem.activity.summary">
<b class="text-900 me-2" t-esc="activityListViewItem.activity.summary"/>
</t>
<t t-if="!activityListViewItem.activity.summary and activityListViewItem.activity.type">
<b class="text-900 me-2" t-esc="activityListViewItem.activity.type.displayName"/>
</t>
<button t-if="activityListViewItem.activity.chaining_type === 'suggest' and activityListViewItem.activity.canWrite" class="btn btn-sm btn-link" t-on-click="activityListViewItem.onClickEditActivityButton">
<i class="fa fa-pencil"/>
</button>
<div class="flex-grow-1"/>
<t t-if="activityListViewItem.activity.canWrite" class="flex-grow-1 text-end">
<a t-if="activityListViewItem.fileUploader" class="fa fa-upload" title="Upload file" role="img" t-on-click="activityListViewItem.onClickUploadDocument"/>
<a t-else="" class="o_ActivityListViewItem_markAsDone fa fa-check-circle" t-on-click="activityListViewItem.onClickMarkAsDone" title="Mark as done" role="img" aria-label="Mark as done" t-ref="markDoneButton"/>
<!-- TODO sign override to change the button -->
<!-- TODO approvals override to change stuff -->
</t>
</div>
<div t-if="activityListViewItem.activity.state !== 'today'" class="d-flex align-items-baseline">
<i class="fa fa-clock-o me-2 text-muted" role="img" aria-label="Deadline" title="Deadline"/>
<small t-if="!activityListViewItem.activity.isCurrentPartnerAssignee and activityListViewItem.activity.assignee" class="me-1"><span t-esc="activityListViewItem.activity.assignee.displayName"/> -</small>
<small t-att-title="activityListViewItem.activity.dateDeadline" t-esc="activityListViewItem.delayLabel"/>
</div>
<!-- TODO handle "mail_template_ids" -->
</div>
</t>
</templates>
@@ -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);
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.KanbanFieldActivityView" owl="1">
<ActivityButtonView record="kanbanFieldActivityView.activityButtonView"/>
</t>
</templates>
@@ -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);
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.KanbanFieldActivityViewContainer" owl="1">
<KanbanFieldActivityView t-if="kanbanFieldActivityView" record="kanbanFieldActivityView"/>
</t>
</templates>
@@ -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);
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ListFieldActivityView" owl="1">
<ActivityButtonView record="listFieldActivityView.activityButtonView"/>
<span class="o_ListFieldActivityView_summary" t-out="listFieldActivityView.summaryText"/>
</t>
</templates>
@@ -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);
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ListFieldActivityViewContainer" owl="1">
<ListFieldActivityView t-if="listFieldActivityView" record="listFieldActivityView"/>
</t>
</templates>
@@ -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;
}
}
@@ -11,7 +11,7 @@
<button type="button" class="o_ActivityMarkDonePopoverContent_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="activityMarkDonePopoverContentView.onClickDoneAndScheduleNext">
Done &amp; Schedule Next
</button>
<t t-if="activityMarkDonePopoverContentView.activityViewOwner.activity.chaining_type === 'suggest'">
<t t-if="activityMarkDonePopoverContentView.activity.chaining_type === 'suggest'">
<button type="button" class="o_ActivityMarkDonePopoverContent_doneButton btn btn-sm btn-primary mx-2" t-on-click="activityMarkDonePopoverContentView.onClickDone">
Done
</button>
+56 -51
View File
@@ -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 = $('<div class="o_closest_deadline">');
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($('<span>', {
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();
@@ -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 = $('<div class="o_closest_deadline">');
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($('<span>', {
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;
@@ -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();
},
@@ -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,
};
}
+7 -24
View File
@@ -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;
@@ -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,
}),
},
});
@@ -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,
}),
},
});
@@ -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,
}),
},
});
@@ -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();
},
}),
},
});
+5 -20
View File
@@ -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.
@@ -10,6 +10,10 @@ registerModel({
name: 'ClockWatcher',
identifyingMode: 'xor',
fields: {
activityListViewItemOwner: one('ActivityListViewItem', {
identifying: true,
inverse: 'clockWatcher',
}),
activityViewOwner: one('ActivityView', {
identifying: true,
inverse: 'clockWatcher',
+15 -1
View File
@@ -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;
}
@@ -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,
}),
},
});
@@ -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,
}),
},
});
@@ -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.
*
+40 -1
View File
@@ -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';
}
+14
View File
@@ -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))]
+2 -2
View File
@@ -61,8 +61,8 @@
<t t-set="isCellHidden" t-value="activeFilter.resIds.length and !activeFilter.resIds.includes(resId) and activeFilter.activityTypeId === type[0]"/>
<td t-if="activityGroup.state and !isCellHidden" t-att-data-res-id="resId" t-att-data-activity-type-id="type[0]"
t-attf-class="o_activity_summary_cell {{activityGroup.state}} {{ activeFilter.resIds.includes(resId) ? 'o_activity_filter_' + activeFilter.state.value : '' }}">
<ActivityCellAdapter Component="widgetComponents.ActivityCell"
widgetArgs="['activity_ids', props.getKanbanActivityData(activityGroup, resId), { activityType: type[0] }]"/>
<ActivityCellAdapter Component="widgetComponents.KanbanActivity"
widgetArgs="['activity_ids', props.getKanbanActivityData(activityGroup, resId), { activityType: type[0], isActivityViewCell: true }]"/>
</td>
<td t-else="" t-att-data-res-id="resId" t-att-data-activity-type-id="type[0]"
class="o_activity_summary_cell o_activity_empty_cell"
@@ -677,7 +677,7 @@ patch(MockServer.prototype, 'mail', {
res_model: 'mail.activity',
views: [[false, 'form']],
type: 'ir.actions.act_window',
}
};
},
/**
* Simulates `get_activity_data` on `mail.activity`.
@@ -34,8 +34,8 @@ QUnit.test('list activity widget with no activity', async function (assert) {
views: [[false, 'list']],
});
assert.containsOnce(document.body, '.o_mail_activity .o_activity_color_default');
assert.strictEqual(document.querySelector('.o_activity_summary').innerText, '');
assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-muted');
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, '');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
@@ -83,12 +83,12 @@ QUnit.test('list activity widget with activities', async function (assert) {
});
const firstRow = document.querySelector('.o_data_row');
assert.containsOnce(firstRow, '.o_mail_activity .o_activity_color_today.fa-phone');
assert.strictEqual(firstRow.querySelector('.o_activity_summary').innerText, 'Call with Al');
assert.containsOnce(firstRow, '.o_ActivityButtonView_icon.text-warning.fa-phone');
assert.strictEqual(firstRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al');
const secondRow = document.querySelectorAll('.o_data_row')[1];
assert.containsOnce(secondRow, '.o_mail_activity .o_activity_color_planned.fa-clock-o');
assert.strictEqual(secondRow.querySelector('.o_activity_summary').innerText, 'Type 2');
assert.containsOnce(secondRow, '.o_ActivityButtonView_icon.text-success.fa-clock-o');
assert.strictEqual(secondRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Type 2');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
@@ -127,8 +127,8 @@ QUnit.test('list activity widget with exception', async function (assert) {
views: [[false, 'list']],
});
assert.containsOnce(document.body, '.o_activity_color_today.text-warning.fa-warning');
assert.strictEqual(document.querySelector('.o_activity_summary').innerText, 'Warning');
assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-warning.fa-warning');
assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Warning');
assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']);
});
@@ -168,7 +168,7 @@ QUnit.test('list activity widget: open dropdown', async function (assert) {
const views = {
'res.users,false,list': '<list><field name="activity_ids" widget="list_activity"/></list>',
};
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':
`<list>
<field name="activity_ids" widget="list_activity"/>
</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':
`<list>
<field name="activity_ids" widget="list_activity"/>
</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) {
@@ -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']);
});