[REF] mail: migrate activity renderer to owl

In order to prepare the future, we want to convert everything in Owl.
This commit converts the activity renderer.

closes odoo/odoo#42278

Task: 2149408
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
mcm-odoo
2020-01-28 07:42:56 +00:00
committed by Michael Mattiello (mcm)
parent 7e405102d2
commit 5aa0822a9d
10 changed files with 367 additions and 325 deletions
@@ -0,0 +1,42 @@
odoo.define("mail.ActivityCell", function (require) {
"use strict";
require("mail.Activity");
const field_registry = require('web.field_registry');
const KanbanActivity = field_registry.get('kanban_activity');
const ActivityCell = KanbanActivity.extend({
/**
* @override
* @private
*/
_render() {
// replace clock by closest deadline
const $date = $('<div class="o_closest_deadline">');
const date = new Date(this.record.data.closest_deadline);
// 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 badge-light badge-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();
}
}
});
return ActivityCell;
});
@@ -1,6 +1,7 @@
odoo.define('mail.ActivityController', function (require) {
"use strict";
require('mail.Activity');
var BasicController = require('web.BasicController');
var core = require('web.core');
var field_registry = require('web.field_registry');
@@ -20,6 +21,18 @@ var ActivityController = BasicController.extend({
// Public
//--------------------------------------------------------------------------
/**
* @override
* @param parent
* @param model
* @param renderer
* @param {Object} params
* @param {String} params.title The title used in schedule activity dialog
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.title = params.title;
},
/**
* Overridden to remove the pager as it makes no sense in this view.
*
@@ -38,17 +51,16 @@ var ActivityController = BasicController.extend({
*/
_onScheduleActivity: function () {
var self = this;
var state = this.model.get(this.handle);
new ViewDialogs.SelectCreateDialog(this, {
res_model: state.model,
domain: this.model.originalDomain,
title: _.str.sprintf(_t("Search: %s"), this.renderer.arch.attrs.string),
title: _.str.sprintf(_t("Search: %s"), this.title),
no_create: !this.activeActions.create,
disable_multiple_selection: true,
context: state.context,
on_selected: function (record) {
var fakeRecord = self.renderer.getKanbanActivityData({}, record[0]);
var fakeRecord = state.getKanbanActivityData({}, record[0]);
var widget = new KanbanActivity(self, 'activity_ids', fakeRecord, {});
widget.scheduleActivity();
},
@@ -79,7 +91,7 @@ var ActivityController = BasicController.extend({
},
/**
* @private
* @param {OdooEvent} ev
* @param {CustomEvent} ev
*/
_onSendMailTemplate: function (ev) {
var templateID = ev.data.templateID;
@@ -1,15 +1,14 @@
odoo.define('mail.ActivityModel', function (require) {
'use strict';
var BasicModel = require('web.BasicModel');
var session = require('web.session');
const BasicModel = require('web.BasicModel');
const session = require('web.session');
var ActivityModel = BasicModel.extend({
const ActivityModel = BasicModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Add the following (activity specific) keys when performing a `get` on the
* main list datapoint:
@@ -22,10 +21,44 @@ var ActivityModel = BasicModel.extend({
get: function () {
var result = this._super.apply(this, arguments);
if (result && result.model === this.modelName && result.type === 'list') {
_.extend(result, this.additionalData);
_.extend(result, this.additionalData, {getKanbanActivityData: this.getKanbanActivityData});
}
return result;
},
/**
* @param {Object} activityGroup
* @param {integer} resId
* @returns {Object}
*/
getKanbanActivityData(activityGroup, resId) {
return {
data: {
activity_ids: {
model: 'mail.activity',
res_ids: activityGroup.ids,
},
activity_state: activityGroup.state,
closest_deadline: activityGroup.o_closest_deadline,
},
fields: {
activity_ids: {},
activity_state: {
selection: [
['overdue', "Overdue"],
['today', "Today"],
['planned', "Planned"],
],
},
},
fieldsInfo: {},
model: this.model,
type: 'record',
res_id: resId,
getContext: function () {
return {};
},
};
},
/**
* @override
* @param {Array[]} params.domain
@@ -1,163 +1,141 @@
odoo.define('mail.ActivityRenderer', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
var ActivityRecord = require('mail.ActivityRecord');
var config = require('web.config');
var core = require('web.core');
var field_registry = require('web.field_registry');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
var qweb = require('web.QWeb');
var session = require('web.session');
var utils = require('web.utils');
const AbstractRendererOwl = require('web.AbstractRendererOwl');
const ActivityCell = require('mail.ActivityCell');
const ActivityRecord = require('mail.ActivityRecord');
const { ComponentAdapter } = require('web.OwlCompatibility');
const core = require('web.core');
const KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
const QWeb = require('web.QWeb');
const session = require('web.session');
const utils = require('web.utils');
var KanbanActivity = field_registry.get('kanban_activity');
var _t = core._t;
var QWeb = core.qweb;
const _t = core._t;
var ActivityRenderer = AbstractRenderer.extend({
className: 'o_activity_view',
custom_events: {
'set_progress_bar_state': '_onSetProgressBarState',
},
events: {
'click .o_send_mail_template': '_onSenMailTemplateClicked',
'click .o_activity_empty_cell': '_onEmptyCell',
'click .o_record_selector': '_onRecordSelector',
},
const { useState } = owl.hooks;
/**
* @override
* @param {Object} params.templates
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
/**
* Owl Component Adapter for ActivityRecord which is KanbanRecord (Odoo Widget)
* TODO: Remove this adapter when ActivityRecord is a Component
*/
class ActivityRecordAdapter extends ComponentAdapter {
render() {
_.invoke(_.pluck(this.widget.subWidgets, '$el'), 'detach');
this.widget._render();
}
this.qweb = new qweb(config.isDebug(), {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(params.templates));
},
update(nextProps) {
const state = nextProps.widgetArgs[0];
this.widget._setState(state);
}
}
/**
* Owl Component Adapter for ActivityCell which is BasicActivity (AbstractField)
* TODO: Remove this adapter when ActivityCell is a Component
*/
class ActivityCellAdapter extends ComponentAdapter {
render() {
this.widget._render();
}
update(nextProps) {
const record = nextProps.widgetArgs[1];
this.widget._reset(record);
}
}
/**
* Owl Component Adapter for KanbanColumnProgressBar (Odoo Widget)
* TODO: Remove this adapter when KanbanColumnProgressBar is a Component
*/
class KanbanColumnProgressBarAdapter extends ComponentAdapter {
render() {
this.widget._render();
}
update(nextProps) {
const options = nextProps.widgetArgs[0];
const columnState = nextProps.widgetArgs[1];
const columnId = options.columnID;
const nextActiveFilter = options.progressBarStates[columnId].activeFilter;
this.widget.activeFilter = nextActiveFilter ? this.widget.activeFilter : false;
this.widget.columnState = columnState;
this.widget.computeCounters();
}
_trigger_up(ev) {
// KanbanColumnProgressBar triggers 3 events before being mounted
// but we don't need to listen to them in our case.
if (this.el) {
super._trigger_up(ev);
}
}
}
class ActivityRenderer extends AbstractRendererOwl {
constructor(parent, props) {
super(...arguments);
this.qweb = new QWeb(this.env.isDebug(), {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(props.templates));
this.activeFilter = useState({
state: null,
activityTypeId: null,
resIds: []
});
this.widgetComponents = {
ActivityRecord,
ActivityCell,
KanbanColumnProgressBar,
};
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {Object} activityGroup
* @param {integer} resID
* @returns {Object}
* Gets all activity resIds in the view.
*
* @returns filtered resIds first then the rest.
*/
getKanbanActivityData: function (activityGroup, resID) {
get activityResIds() {
const copiedActivityResIds = Array.from(this.props.activity_res_ids)
return copiedActivityResIds.sort((a, b) => this.activeFilter.resIds.includes(a) ? -1 : 0);
}
/**
* Gets all existing activity type ids.
*/
get activityTypeIds() {
const activities = Object.values(this.props.grouped_activities);
const activityIds = activities.flatMap(Object.keys);
const uniqueIds = Array.from(new Set(activityIds));
return uniqueIds.map(Number);
}
getProgressBarOptions(typeId) {
return {
data: {
activity_ids: {
model: 'mail.activity',
res_ids: activityGroup.ids,
},
activity_state: activityGroup.state,
},
fields: {
activity_ids: {},
activity_state: {
selection: [
['overdue', "Overdue"],
['today', "Today"],
['planned', "Planned"],
],
},
},
fieldsInfo: {},
model: this.state.model,
type: 'record',
res_id: resID,
getContext: function () {
return {}; // session.user_context
},
//todo intercept event or changes on record to update view
};
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_getRecord: function (recordId) {
return _.findWhere(this.state.data, { res_id: recordId });
},
/**
* @override
* @private
*/
_render: function () {
var self = this;
this.$el.addClass('table-responsive');
this.$el.html(QWeb.render('mail.ActivityView', { isEmpty: !this.state.activity_types.length }));
return Promise.all([
this._super.apply(this, arguments),
this._renderHeader(),
this._renderBody(),
]).then(function (result) {
self.$('table')
.append(result[1])
.append(result[2])
.append(self._renderFooter());
});
},
/**
* @private
* @returns {Promise<jQueryElement>} a jquery element <tbody>
*/
_renderBody: function () {
var defs = _.map(this.state.activity_res_ids, this._renderRow.bind(this));
return Promise.all(defs).then(function ($rows) {
return $('<tbody>').append($rows);
});
},
/**
* @private
* @returns {jQueryElement} a <tfoot> element
*/
_renderFooter: function () {
return QWeb.render('mail.ActivityViewFooter');
},
/**
* @private
* @returns {Promise<jQueryElement>}
*/
_renderHeader: function () {
var self = this;
var activityTypeIds = _.unique(_.flatten(_.map(this.state.grouped_activities, function (act) { return _.keys(act); })));
var $thead = $(QWeb.render('mail.ActivityViewHeader', {
types: this.state.activity_types,
activityTypeIDs: _.map(activityTypeIds, Number),
}));
var defs = [];
activityTypeIds.forEach(function (typeId) {
defs.push(self._renderProgressBar($thead, typeId));
});
return Promise.all(defs).then(function () {
return $thead;
});
},
/**
* @private
* @returns {Promise}
*/
_renderProgressBar: function ($thead, typeId) {
var counts = { planned: 0, today: 0, overdue: 0 };
_.each(this.state.grouped_activities, function (act) {
if (_.contains(_.keys(act), typeId.toString())) {
counts[act[typeId].state] += 1;
}
});
var progressBar = new KanbanColumnProgressBar(this, {
columnID: typeId,
progressBarStates: {},
}, {
count: _.reduce(_.values(counts), (x, y) => x + y),
progressBarStates: {
[typeId]: {
activeFilter: this.activeFilter.activityTypeId === typeId,
},
},
};
}
getProgressBarColumnState(typeId) {
const counts = { planned: 0, today: 0, overdue: 0 };
for (let activities of Object.values(this.props.grouped_activities)) {
if (typeId in activities) {
counts[activities[typeId].state] += 1;
}
}
return {
count: Object.values(counts).reduce((x, y) => x + y),
fields: {
activity_state: {
type: 'selection',
@@ -173,141 +151,58 @@ var ActivityRenderer = AbstractRenderer.extend({
colors: { planned: 'success', today: 'warning', overdue: 'danger' },
counts: counts,
},
});
return progressBar.appendTo($thead.find('th[data-activity-type-id=' + typeId + ']'));
},
/**
* @private
* @param {integer} resId
* @returns {Promise<jQueryElement>}
*/
_renderRow: function (resId) {
var self = this;
var defs = [];
var record = this._getRecord(resId);
var $nameTD = $('<td>', {
class: _.contains(this.filteredResIDs, resId) ? 'o_activity_filter_' + this.activeFilter : '',
});
var activityRecord = new ActivityRecord(this, record, { qweb: this.qweb });
defs.push(activityRecord.appendTo($nameTD));
var $cells = _.map(this.state.activity_types, function (node) {
var activity_type_id = node[0];
var activity_group = self.state.grouped_activities[resId];
activity_group = activity_group && activity_group[activity_type_id] || {count: 0, ids: [], state: false};
var $td = $(QWeb.render('mail.ActivityViewRow', {
resId: resId,
activityGroup: activity_group,
activityTypeId: activity_type_id,
widget: self,
}));
if (activity_group.state) {
var record = self.getKanbanActivityData(activity_group, resId);
var widget = new KanbanActivity(self, "activity_ids", record, {});
var def = widget.appendTo($td).then(function () {
// replace clock by closest deadline
var $date = $('<div class="o_closest_deadline">');
// split the year/month/day to create a date (to have a date, no tz, instead of a datetime, tz dependant)
var splitDate = activity_group.o_closest_deadline.split('-');
// the value representing the month, ranges from 0 for January up to 11 for December.
var date = new Date(splitDate[0], splitDate[1]-1, splitDate[2]);
// 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'));
}
$td.find('a').html($date);
if (activity_group.count > 1) {
$td.find('a').append($('<span>', {
class: 'badge badge-light badge-pill border-0 ' + activity_group.state,
text: activity_group.count,
}));
}
});
defs.push(def);
}
return $td;
});
return Promise.all(defs).then(function () {
var $tr = $('<tr/>', {class: 'o_data_row'}).attr('data-res-id', resId)
.append($nameTD)
.append($cells);
return $tr;
});
},
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onEmptyCell: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var data = $(ev.currentTarget).data();
this.trigger_up('empty_cell_clicked', data);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onRecordSelector: function (ev) {
ev.stopPropagation();
this.trigger_up('schedule_activity');
},
/**
* @private
* @override
* @param {MouseEvent} ev
*/
_onSenMailTemplateClicked: function (ev) {
var $target = $(ev.currentTarget);
var templateID = $target.data('template-id');
var activityTypeID = $target.closest('th').data('activity-type-id');
this.trigger_up('send_mail_template', {
activityTypeID: activityTypeID,
templateID: templateID,
_onEmptyCellClicked(ev) {
this.trigger('empty_cell_clicked', {
resId: parseInt(ev.currentTarget.dataset.resId, 10),
activityTypeId: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
});
},
}
/**
* Rearrange body part of table based on active filter.
*
* @private
* @param {OdooEvent} ev
* @param {MouseEvent} ev
*/
_onSetProgressBarState: function (ev) {
var self = this;
this.$('th[class*="o_activity_filter_"]').attr('class', 'o_activity_type_cell');
this.$('.o_kanban_counter_progress div').removeClass('active progress-bar-striped');
var data = ev.data;
var arrangedRecords = this.state.activity_res_ids;
this.activeFilter = data.values.activeFilter;
if (this.activeFilter) {
var filteredResIds = _.map(_.keys(_.pick(this.state.grouped_activities, function (act) {
return act[data.columnID] && act[data.columnID].state === self.activeFilter;
})), Number);
arrangedRecords = _.union(_.intersection(this.state.activity_res_ids, filteredResIds), this.state.activity_res_ids);
this.filteredResIDs = filteredResIds;
}
var defs = _.map(arrangedRecords, this._renderRow.bind(this));
Promise.all(defs).then(function ($rows) {
self.$('tbody').html($rows);
_onSendMailTemplateClicked(ev) {
this.trigger('send_mail_template', {
activityTypeID: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
templateID: parseInt(ev.currentTarget.dataset.templateId, 10),
});
if (this.activeFilter) {
var $header = this.$('th.o_activity_type_cell[data-activity-type-id=' + data.columnID + ']');
$header.addClass('o_activity_filter_' + this.activeFilter);
}
/**
* @private
* @param {CustomEvent} ev
*/
_onSetProgressBarState(ev) {
if (ev.detail.values.activeFilter) {
this.activeFilter.state = ev.detail.values.activeFilter;
this.activeFilter.activityTypeId = ev.detail.columnID;
this.activeFilter.resIds = Object.entries(this.props.grouped_activities)
.filter(([, resIds]) => ev.detail.columnID in resIds &&
resIds[ev.detail.columnID].state === ev.detail.values.activeFilter)
.map(([key]) => parseInt(key));
} else {
this.activeFilter.state = null;
this.activeFilter.activityTypeId = null;
this.activeFilter.resIds = [];
}
},
});
}
}
ActivityRenderer.components = {
ActivityRecordAdapter,
ActivityCellAdapter,
KanbanColumnProgressBarAdapter,
};
ActivityRenderer.template = 'mail.ActivityRenderer';
return ActivityRenderer;
@@ -1,16 +1,17 @@
odoo.define('mail.ActivityView', function (require) {
"use strict";
var ActivityController = require('mail.ActivityController');
var ActivityModel = require('mail.ActivityModel');
var ActivityRenderer = require('mail.ActivityRenderer');
var BasicView = require('web.BasicView');
var core = require('web.core');
var view_registry = require('web.view_registry');
const ActivityController = require('mail.ActivityController');
const ActivityModel = require('mail.ActivityModel');
const ActivityRenderer = require('mail.ActivityRenderer');
const BasicView = require('web.BasicView');
const core = require('web.core');
const RendererWrapper = require('web.RendererWrapper');
const view_registry = require('web.view_registry');
var _lt = core._lt;
const _lt = core._lt;
var ActivityView = BasicView.extend({
const ActivityView = BasicView.extend({
accesskey: "a",
display_name: _lt('Activity'),
icon: 'fa-clock-o',
@@ -33,6 +34,15 @@ var ActivityView = BasicView.extend({
this.loadParams.limit = false;
this.rendererParams.templates = _.findWhere(this.arch.children, { 'tag': 'templates' });
this.controllerParams.title = this.arch.attrs.string;
},
/**
*
* @override
*/
getRenderer(parent, state) {
state = Object.assign({}, state, this.rendererParams);
return new RendererWrapper(null, this.config.Renderer, state);
},
});
+62 -23
View File
@@ -1,54 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityViewHeader">
<t t-name="mail.ActivityViewHeader" owl="1">
<thead>
<tr>
<th></th>
<th t-foreach="types" t-as="type" class="o_activity_type_cell" t-att-data-activity-type-id="type[0]" t-attf-width="#{100/types.length}%">
<th t-foreach="props.activity_types" t-as="type" t-key="type[0]"
class="o_activity_type_cell" t-attf-class="{{ activeFilter.activityTypeId === type[0] ? 'o_activity_filter_' + activeFilter.state : '' }}"
t-att-data-activity-type-id="type[0]" t-attf-width="{{100/props.activity_types.length}}%">
<div>
<span><t t-esc="type[1]"/></span>
<span t-esc="type[1]"/>
<span t-if="type[2].length > 0" class="dropdown pull-right">
<i class="fa fa-ellipsis-v fa-fw" data-toggle="dropdown"/>
<div class="dropdown-menu">
<div t-foreach="type[2]" t-as="template" title="This action will send an email." class="o_template_element o_send_mail_template" t-att-data-template-id="template.id">
<i class="fa fa-envelope fa-fw"/> <t t-esc="template.name"/>
</div>
<t t-foreach="type[2]" t-as="template" t-key="template.id">
<div title="This action will send an email."
class="o_template_element o_send_mail_template"
t-att-data-activity-type-id="type[0]"
t-att-data-template-id="template.id"
t-on-click="_onSendMailTemplateClicked">
<i class="fa fa-envelope fa-fw"/> <t t-esc="template.name"/>
</div>
</t>
</div>
</span>
</div>
<div t-if="activityTypeIDs.length and !_.contains(activityTypeIDs, type[0])" class="mt24"/>
<KanbanColumnProgressBarAdapter Component="widgetComponents.KanbanColumnProgressBar"
widgetArgs="[getProgressBarOptions(type[0]), getProgressBarColumnState(type[0])]"
t-if="activityTypeIds.includes(type[0])"
t-on-set-progress-bar-state="_onSetProgressBarState"/>
<div t-else="" class="mt24"/>
</th>
</tr>
</thead>
</t>
<t t-name="mail.ActivityViewRow">
<td t-att-data-res-id="resId" t-att-data-activity-type-id="activityTypeId"
t-attf-class="o_activity_summary_cell #{activityGroup.state ? activityGroup.state : 'o_activity_empty_cell'} #{_.contains(widget.filteredResIDs, resId) ? 'o_activity_filter_' + widget.activeFilter : ''}">
<i t-if="!activityGroup.state" title="Create" class="text-center fa fa-plus"/>
<t t-name="mail.ActivityViewBody" owl="1">
<tbody>
<t t-foreach="activityResIds" t-as="resId" t-key="resId">
<t t-call="mail.ActivityViewRow"/>
</t>
</tbody>
</t>
<t t-name="mail.ActivityViewRow" owl="1">
<tr class="o_data_row" t-att-data-res-id="resId">
<t t-set="record" t-value="props.data.find(data => data.res_id === resId)"/>
<td t-attf-class="{{ activeFilter.resIds.includes(resId) ? 'o_activity_filter_' + activeFilter.state : '' }}">
<ActivityRecordAdapter Component="widgetComponents.ActivityRecord"
widgetArgs="[record, { qweb: qweb }]"/>
</td>
<t t-foreach="props.activity_types" t-as="type" t-key="type[0]">
<t t-call="mail.ActivityViewCell"/>
</t>
</tr>
</t>
<t t-name="mail.ActivityViewCell" owl="1">
<t t-set="activityGroup" t-value="props.grouped_activities[resId] and props.grouped_activities[resId][type[0]] or {count: 0, ids: [], state: false}"/>
<td t-if="activityGroup.state" 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 : '' }}">
<ActivityCellAdapter Component="widgetComponents.ActivityCell"
widgetArgs="['activity_ids', props.getKanbanActivityData(activityGroup, resId)]"/>
</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"
t-attf-class="{{ activeFilter.resIds.includes(resId) ? 'o_activity_filter_' + activeFilter.state : '' }}"
t-on-click.prevent.stop="_onEmptyCellClicked">
<i title="Create" class="text-center fa fa-plus"/>
</td>
</t>
<t t-name="mail.ActivityViewFooter">
<t t-name="mail.ActivityViewFooter" owl="1">
<tfoot>
<tr class="o_data_row">
<td class="o_record_selector p-3">
<td class="o_record_selector p-3" t-on-click.prevent.stop="trigger('schedule_activity')">
<span class="fa fa-plus pr-2"/><span>Schedule activity</span>
</td>
</tr>
</tfoot>
</t>
<t t-name="mail.ActivityView">
<div t-if="isEmpty" class="o_view_nocontent" role="alert">
<div class="o_nocontent_help">
<p class="o_view_nocontent_empty_folder">
No data to display
</p>
</div>
</div>
<table t-else="" class="table-bordered mb-5"/>
</t>
<div t-name="mail.ActivityRenderer" class="o_activity_view" owl="1">
<t t-if="!props.activity_types.length" t-call="web.NoContentHelper"/>
<table t-else="" class="table-bordered mb-5">
<t t-call="mail.ActivityViewHeader"/>
<t t-call="mail.ActivityViewBody"/>
<t t-call="mail.ActivityViewFooter"/>
</table>
</div>
</templates>
+6 -2
View File
@@ -263,7 +263,8 @@ QUnit.test('activity view: batch send mail on activity', async function (assert)
QUnit.test('activity view: activity widget', async function (assert) {
assert.expect(16);
var activity = await createView({
const params = {
View: ActivityView,
model: 'task',
data: this.data,
@@ -313,7 +314,10 @@ QUnit.test('activity view: activity widget', async function (assert) {
}
},
},
});
};
owl.Component.env = testUtils.mock.getMockedOwlEnv(params);
var activity = await createView(params);
var today = activity.$('table tbody tr:first td:nth-child(2).today');
var dropdown = today.find('.dropdown-menu.o_activity');
+1
View File
@@ -79,6 +79,7 @@
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_controller.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_renderer.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_record.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_cell.js"></script>
<!-- Attachment box -->
<script type="text/javascript" src="/mail/static/src/js/attachment_box.js"></script>
<!-- Emojis -->
@@ -80,28 +80,32 @@ var KanbanColumnProgressBar = Widget.extend({
// This should be executed when the progressbar is fully rendered
// and is in the DOM, this happens to be always the case with
// current use of progressbars
var subgroupCounts = {};
let allSubgroupCount = 0;
_.each(self.colors, function (val, key) {
var subgroupCount = self.columnState.progressBarValues.counts[key] || 0;
if (self.activeFilter === key && subgroupCount === 0) {
self.activeFilter = false;
}
subgroupCounts[key] = subgroupCount;
allSubgroupCount += subgroupCount;
});
subgroupCounts.__false = self.columnState.count - allSubgroupCount;
self.groupCount = self.columnState.count;
self.subgroupCounts = subgroupCounts;
self.prevTotalCounterValue = self.totalCounterValue;
self.totalCounterValue = self.sumField ? (self.columnState.aggregateValues[self.sumField] || 0) : self.columnState.count;
self.computeCounters();
self._notifyState();
self._render();
});
},
/**
* Computes the count of each sub group and the total count
*/
computeCounters() {
const subgroupCounts = {};
let allSubgroupCount = 0;
for (const key of Object.keys(this.colors)) {
const subgroupCount = this.columnState.progressBarValues.counts[key] || 0;
if (this.activeFilter === key && subgroupCount === 0) {
this.activeFilter = false;
}
subgroupCounts[key] = subgroupCount;
allSubgroupCount += subgroupCount;
};
subgroupCounts.__false = this.columnState.count - allSubgroupCount;
this.groupCount = this.columnState.count;
this.subgroupCounts = subgroupCounts;
this.prevTotalCounterValue = this.totalCounterValue;
this.totalCounterValue = this.sumField ? (this.columnState.aggregateValues[this.sumField] || 0) : this.columnState.count;
},
//--------------------------------------------------------------------------
// Private
@@ -187,6 +191,7 @@ var KanbanColumnProgressBar = Widget.extend({
$bar.css('width', '');
}
});
this.$('.progress-bar').css('min-width', '');
this.$('.progress-bar.o_bar_has_records').css('min-width', barMinWidth + '%');
// Display and animate the counter number
@@ -58,6 +58,7 @@ odoo.define('web.test_env', async function (require) {
device: Object.assign({
isMobile: false,
}, env.device),
isDebug: env.isDebug || (() => false),
qweb: new owl.QWeb({ templates: session.owlTemplates }),
services: Object.assign({
ajax: { // for legacy subwidgets