diff options
Diffstat (limited to 'addons/mail/static/src/js/views/activity')
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; + +}); |
