summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/views/activity
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/js/views/activity')
-rw-r--r--addons/mail/static/src/js/views/activity/activity_cell.js42
-rw-r--r--addons/mail/static/src/js/views/activity/activity_controller.js124
-rw-r--r--addons/mail/static/src/js/views/activity/activity_model.js124
-rw-r--r--addons/mail/static/src/js/views/activity/activity_record.js62
-rw-r--r--addons/mail/static/src/js/views/activity/activity_renderer.js210
-rw-r--r--addons/mail/static/src/js/views/activity/activity_view.js53
6 files changed, 615 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/views/activity/activity_cell.js b/addons/mail/static/src/js/views/activity/activity_cell.js
new file mode 100644
index 00000000..d1cdca94
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_cell.js
@@ -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;
+
+});
diff --git a/addons/mail/static/src/js/views/activity/activity_controller.js b/addons/mail/static/src/js/views/activity/activity_controller.js
new file mode 100644
index 00000000..106c5ee9
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_controller.js
@@ -0,0 +1,124 @@
+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');
+var ViewDialogs = require('web.view_dialogs');
+
+var KanbanActivity = field_registry.get('kanban_activity');
+var _t = core._t;
+
+var ActivityController = BasicController.extend({
+ custom_events: _.extend({}, BasicController.prototype.custom_events, {
+ empty_cell_clicked: '_onEmptyCell',
+ send_mail_template: '_onSendMailTemplate',
+ schedule_activity: '_onScheduleActivity',
+ }),
+
+ //--------------------------------------------------------------------------
+ // 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;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Overridden to remove the pager as it makes no sense in this view.
+ *
+ * @override
+ */
+ _getPagingInfo: function () {
+ return null;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _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.title),
+ no_create: !this.activeActions.create,
+ disable_multiple_selection: true,
+ context: state.context,
+ on_selected: function (record) {
+ var fakeRecord = state.getKanbanActivityData({}, record[0]);
+ var widget = new KanbanActivity(self, 'activity_ids', fakeRecord, {});
+ widget.scheduleActivity();
+ },
+ }).open();
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onEmptyCell: function (ev) {
+ var state = this.model.get(this.handle);
+ this.do_action({
+ type: 'ir.actions.act_window',
+ res_model: 'mail.activity',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_res_id: ev.data.resId,
+ default_res_model: state.model,
+ default_activity_type_id: ev.data.activityTypeId,
+ },
+ res_id: false,
+ }, {
+ on_close: this.reload.bind(this),
+ });
+ },
+ /**
+ * @private
+ * @param {CustomEvent} ev
+ */
+ _onSendMailTemplate: function (ev) {
+ var templateID = ev.data.templateID;
+ var activityTypeID = ev.data.activityTypeID;
+ var state = this.model.get(this.handle);
+ var groupedActivities = state.grouped_activities;
+ var resIDS = [];
+ Object.keys(groupedActivities).forEach(function (resID) {
+ var activityByType = groupedActivities[resID];
+ var activity = activityByType[activityTypeID];
+ if (activity) {
+ resIDS.push(parseInt(resID));
+ }
+ });
+ this._rpc({
+ model: this.model.modelName,
+ method: 'activity_send_mail',
+ args: [resIDS, templateID],
+ });
+ },
+});
+
+return ActivityController;
+
+});
diff --git a/addons/mail/static/src/js/views/activity/activity_model.js b/addons/mail/static/src/js/views/activity/activity_model.js
new file mode 100644
index 00000000..cfb9e36a
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_model.js
@@ -0,0 +1,124 @@
+odoo.define('mail.ActivityModel', function (require) {
+'use strict';
+
+const BasicModel = require('web.BasicModel');
+const session = require('web.session');
+
+const ActivityModel = BasicModel.extend({
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+ /**
+ * Add the following (activity specific) keys when performing a `get` on the
+ * main list datapoint:
+ * - activity_types
+ * - activity_res_ids
+ * - grouped_activities
+ *
+ * @override
+ */
+ __get: function () {
+ var result = this._super.apply(this, arguments);
+ if (result && result.model === this.modelName && result.type === 'list') {
+ _.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
+ */
+ __load: function (params) {
+ this.originalDomain = _.extend([], params.domain);
+ params.domain.push(['activity_ids', '!=', false]);
+ this.domain = params.domain;
+ this.modelName = params.modelName;
+ params.groupedBy = [];
+ var def = this._super.apply(this, arguments);
+ return Promise.all([def, this._fetchData()]).then(function (result) {
+ return result[0];
+ });
+ },
+ /**
+ * @override
+ * @param {Array[]} [params.domain]
+ */
+ __reload: function (handle, params) {
+ if (params && 'domain' in params) {
+ this.originalDomain = _.extend([], params.domain);
+ params.domain.push(['activity_ids', '!=', false]);
+ this.domain = params.domain;
+ }
+ if (params && 'groupBy' in params) {
+ params.groupBy = [];
+ }
+ var def = this._super.apply(this, arguments);
+ return Promise.all([def, this._fetchData()]).then(function (result) {
+ return result[0];
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Fetch activity data.
+ *
+ * @private
+ * @returns {Promise}
+ */
+ _fetchData: function () {
+ var self = this;
+ return this._rpc({
+ model: "mail.activity",
+ method: 'get_activity_data',
+ kwargs: {
+ res_model: this.modelName,
+ domain: this.domain,
+ context: session.user_context,
+ }
+ }).then(function (result) {
+ self.additionalData = result;
+ });
+ },
+});
+
+return ActivityModel;
+
+});
diff --git a/addons/mail/static/src/js/views/activity/activity_record.js b/addons/mail/static/src/js/views/activity/activity_record.js
new file mode 100644
index 00000000..98da9dca
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_record.js
@@ -0,0 +1,62 @@
+odoo.define('mail.ActivityRecord', function (require) {
+"use strict";
+
+var KanbanRecord = require('web.KanbanRecord');
+
+var ActivityRecord = KanbanRecord.extend({
+ /**
+ * @override
+ */
+ init: function (parent, state) {
+ this._super.apply(this,arguments);
+
+ this.fieldsInfo = state.fieldsInfo.activity;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _render: function () {
+ this.defs = [];
+ this._replaceElement(this.qweb.render('activity-box', this.qweb_context));
+ this.$el.on('click', this._onGlobalClick.bind(this));
+ this.$el.addClass('o_activity_record');
+ this._processFields();
+ this._setupColor();
+ return Promise.all(this.defs);
+ },
+ /**
+ * @override
+ * @private
+ */
+ _setFieldDisplay: function ($el, fieldName) {
+ this._super.apply(this, arguments);
+
+ // attribute muted
+ if (this.fieldsInfo[fieldName].muted) {
+ $el.addClass('text-muted');
+ }
+ },
+ /**
+ * @override
+ * @private
+ */
+ _setState: function () {
+ this._super.apply(this, arguments);
+
+ // activity has a different qweb context
+ this.qweb_context = {
+ activity_image: this._getImageURL.bind(this),
+ record: this.record,
+ user_context: this.getSession().user_context,
+ widget: this,
+ };
+ },
+});
+return ActivityRecord;
+});
diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js
new file mode 100644
index 00000000..e62d416e
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_renderer.js
@@ -0,0 +1,210 @@
+odoo.define('mail.ActivityRenderer', function (require) {
+"use strict";
+
+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 patchMixin = require('web.patchMixin');
+const QWeb = require('web.QWeb');
+const session = require('web.session');
+const utils = require('web.utils');
+
+const _t = core._t;
+
+const { useState } = owl.hooks;
+
+/**
+ * Owl Component Adapter for ActivityRecord which is KanbanRecord (Odoo Widget)
+ * TODO: Remove this adapter when ActivityRecord is a Component
+ */
+class ActivityRecordAdapter extends ComponentAdapter {
+ renderWidget() {
+ _.invoke(_.pluck(this.widget.subWidgets, '$el'), 'detach');
+ this.widget._render();
+ }
+
+ updateWidget(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 {
+ renderWidget() {
+ this.widget._render();
+ }
+
+ updateWidget(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 {
+ renderWidget() {
+ this.widget._render();
+ }
+
+ updateWidget(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
+ //--------------------------------------------------------------------------
+
+ /**
+ * Gets all activity resIds in the view.
+ *
+ * @returns filtered resIds first then the rest.
+ */
+ 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 {
+ columnID: typeId,
+ 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',
+ selection: [
+ ['planned', _t('Planned')],
+ ['today', _t('Today')],
+ ['overdue', _t('Overdue')],
+ ],
+ },
+ },
+ progressBarValues: {
+ field: 'activity_state',
+ colors: { planned: 'success', today: 'warning', overdue: 'danger' },
+ counts: counts,
+ },
+ };
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onEmptyCellClicked(ev) {
+ this.trigger('empty_cell_clicked', {
+ resId: parseInt(ev.currentTarget.dataset.resId, 10),
+ activityTypeId: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
+ });
+ }
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onSendMailTemplateClicked(ev) {
+ this.trigger('send_mail_template', {
+ activityTypeID: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
+ templateID: parseInt(ev.currentTarget.dataset.templateId, 10),
+ });
+ }
+ /**
+ * @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 patchMixin(ActivityRenderer);
+
+});
diff --git a/addons/mail/static/src/js/views/activity/activity_view.js b/addons/mail/static/src/js/views/activity/activity_view.js
new file mode 100644
index 00000000..e2e3eded
--- /dev/null
+++ b/addons/mail/static/src/js/views/activity/activity_view.js
@@ -0,0 +1,53 @@
+odoo.define('mail.ActivityView', function (require) {
+"use strict";
+
+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');
+
+const _lt = core._lt;
+
+const ActivityView = BasicView.extend({
+ accesskey: "a",
+ display_name: _lt('Activity'),
+ icon: 'fa-clock-o',
+ config: _.extend({}, BasicView.prototype.config, {
+ Controller: ActivityController,
+ Model: ActivityModel,
+ Renderer: ActivityRenderer,
+ }),
+ viewType: 'activity',
+ searchMenuTypes: ['filter', 'favorite'],
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ this.loadParams.type = 'list';
+ // limit makes no sense in this view as we display all records having activities
+ 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);
+ },
+});
+
+view_registry.add('activity', ActivityView);
+
+return ActivityView;
+
+});