From 8c03aec6d9d6d89d5f54e00e8afbd15b7408737a Mon Sep 17 00:00:00 2001 From: tarteo Date: Tue, 23 Oct 2018 11:27:20 +0200 Subject: [PATCH] [MIG] web_timeline: Migration to 12.0 --- web_timeline/README.rst | 90 +- web_timeline/__manifest__.py | 6 +- web_timeline/readme/CONFIGURE.rst | 71 ++ web_timeline/readme/CONTRIBUTORS.rst | 6 + web_timeline/readme/CREDITS.rst | 4 + web_timeline/readme/DESCRIPTION.rst | 4 + web_timeline/readme/ROADMAP.rst | 1 + web_timeline/readme/USAGE.rst | 29 + web_timeline/static/src/js/timeline_canvas.js | 63 +- .../static/src/js/timeline_controller.js | 550 ++++++----- web_timeline/static/src/js/timeline_model.js | 107 +- .../static/src/js/timeline_renderer.js | 918 ++++++++++-------- web_timeline/static/src/js/timeline_view.js | 14 +- 13 files changed, 1129 insertions(+), 734 deletions(-) create mode 100644 web_timeline/readme/CONFIGURE.rst create mode 100644 web_timeline/readme/CONTRIBUTORS.rst create mode 100644 web_timeline/readme/CREDITS.rst create mode 100644 web_timeline/readme/DESCRIPTION.rst create mode 100644 web_timeline/readme/ROADMAP.rst create mode 100644 web_timeline/readme/USAGE.rst diff --git a/web_timeline/README.rst b/web_timeline/README.rst index 47aa282f..cae22b74 100755 --- a/web_timeline/README.rst +++ b/web_timeline/README.rst @@ -1,16 +1,40 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg +============ +Web timeline +============ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 - -============= -Timeline view -============= +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/12.0/web_timeline + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_timeline + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/162/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| Define a new view displaying events in an interactive visualization chart. -The widget is based on the external library +The widget is based on the external library http://visjs.org/timeline_examples.html +**Table of contents** + +.. contents:: + :local: + Configuration ============= @@ -119,11 +143,6 @@ new record with the group and start date linked to the area you clicked in. By holding the Ctrl key and dragging left to right, you can create a new record with the dragged start and end date. - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/162/11.0 - Known issues / Roadmap ====================== @@ -132,21 +151,26 @@ Known issues / Roadmap Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* ACSONE SA/NV +* Tecnativa +* Monk Software +* Onestein Contributors ------------- +~~~~~~~~~~~~ * Laurent Mignon * Adrien Peiffer @@ -155,19 +179,35 @@ Contributors * Adrien Didenot * Dennis Sluijk -Do not contact contributors directly about support or help with technical issues. +Other credits +~~~~~~~~~~~~~ -Maintainer ----------- +Images +------ + +* Odoo Community Association: `Icon `_. + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. +.. |maintainer-tarteo| image:: https://github.com/tarteo.png?size=40px + :target: https://github.com/tarteo + :alt: tarteo + +Current `maintainer `_: + +|maintainer-tarteo| + +This module is part of the `OCA/web `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_timeline/__manifest__.py b/web_timeline/__manifest__.py index 1e087dee..a376e093 100644 --- a/web_timeline/__manifest__.py +++ b/web_timeline/__manifest__.py @@ -4,7 +4,8 @@ { 'name': "Web timeline", 'summary': "Interactive visualization chart to show events in time", - "version": "11.0.1.4.0", + "version": "12.0.1.0.0", + "development_status": "Production/Stable", 'author': 'ACSONE SA/NV, ' 'Tecnativa, ' 'Monk Software, ' @@ -14,7 +15,7 @@ "license": "AGPL-3", "application": False, "installable": True, - "website": "http://acsone.eu", + "website": "https://github.com/OCA/web", 'depends': [ 'web', ], @@ -24,4 +25,5 @@ 'data': [ 'views/web_timeline.xml', ], + "maintainers": ["tarteo"], } diff --git a/web_timeline/readme/CONFIGURE.rst b/web_timeline/readme/CONFIGURE.rst new file mode 100644 index 00000000..c9ffd15c --- /dev/null +++ b/web_timeline/readme/CONFIGURE.rst @@ -0,0 +1,71 @@ +You need to define a view with the tag as base element. These are +the possible attributes for the tag: + +* date_start (required): it defines the name of the field of type date that + contains the start of the event. +* date_stop (optional): it defines the name of the field of type date that + contains the end of the event. The date_stop can be equal to the attribute + date_start to display events has 'point' on the Timeline (instantaneous event) +* date_delay (optional): it defines the name of the field of type float/integer + that contain the duration in hours of the event, default = 1 +* default_group_by (required): it defines the name of the field that will be + taken as default group by when accessing the view or when no other group by + is selected. +* zoomKey (optional): Specifies whether the Timeline is only zoomed when an + additional key is down. Available values are '' (does not apply), 'altKey', + 'ctrlKey', or 'metaKey'. Set this option if you want to be able to use the + scroll to navigate vertically on views with a lot of events. +* mode (optional): Specifies the initial visible window. Available values are: + 'day' to display the current day, 'week', 'month' and 'fit'. + Default value is 'fit' to adjust the visible window such that it fits all items +* event_open_popup (optional): when set to true, it allows to edit the events + in a popup. If not (default value), the record is edited changing to form + view. +* colors (optional): it allows to set certain specific colors if the expressed + condition (JS syntax) is met. +* dependency_arrow (optional): set this attribute to a x2many field to draw + arrows between the records referenced in the x2many field. + +Optionally you can declare a custom template, which will be used to render the +timeline items. You have to name the template 'timeline-item'. +These are the variables available in template rendering: + +* ``record``: to access the fields values selected in the timeline definition. +* ``field_utils``: used to format and parse values (see available functions in ``web.field_utils``). + +You also need to declare the view in an action window of the involved model. + +Example: + +.. code-block:: xml + + + + + project.task + timeline + + + + +
+
+ Assigned to: + +
+ + + + + + + kanban,tree,form,calendar,gantt,timeline,graph + + diff --git a/web_timeline/readme/CONTRIBUTORS.rst b/web_timeline/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..4a76c233 --- /dev/null +++ b/web_timeline/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* Laurent Mignon +* Adrien Peiffer +* Pedro M. Baeza +* Leonardo Donelli +* Adrien Didenot +* Dennis Sluijk diff --git a/web_timeline/readme/CREDITS.rst b/web_timeline/readme/CREDITS.rst new file mode 100644 index 00000000..9baa7efb --- /dev/null +++ b/web_timeline/readme/CREDITS.rst @@ -0,0 +1,4 @@ +Images +------ + +* Odoo Community Association: `Icon `_. diff --git a/web_timeline/readme/DESCRIPTION.rst b/web_timeline/readme/DESCRIPTION.rst new file mode 100644 index 00000000..d01554ef --- /dev/null +++ b/web_timeline/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +Define a new view displaying events in an interactive visualization chart. + +The widget is based on the external library +http://visjs.org/timeline_examples.html diff --git a/web_timeline/readme/ROADMAP.rst b/web_timeline/readme/ROADMAP.rst new file mode 100644 index 00000000..fa65410b --- /dev/null +++ b/web_timeline/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Implement a more efficient way of refreshing timeline after a record update. diff --git a/web_timeline/readme/USAGE.rst b/web_timeline/readme/USAGE.rst new file mode 100644 index 00000000..63ede3d2 --- /dev/null +++ b/web_timeline/readme/USAGE.rst @@ -0,0 +1,29 @@ +For accessing the timeline view, you have to click on the button with the clock +icon in the view switcher. The first time you access to it, the timeline window +is zoomed to fit all the current elements, the same as when you perform a +search, filter or group by operation. + +You can use the mouse scroll to zoom in or out in the timeline, and click on +any free area and drag for panning the view in that direction. + +The records of your model will be shown as rectangles whose widths are the +duration of the event according our definition. You can select them clicking +on this rectangle. You can also use Ctrl or Shift keys for adding discrete +or range selections. Selected records are hightlighted with a different color +(but the difference will be more noticeable depending on the background color). +Once selected, you can drag and move the selected records across the timeline. + +When a record is selected, a red cross button appears on the upper left corner +that allows to remove that record. This doesn't work for multiple records +although they were selected. + +Records are grouped in different blocks depending on the group by criteria +selected (if none is specified, then the default group by is applied). +Dragging a record from one block to another change the corresponding field to +the value that represents the block. You can also click on the group name to +edit the involved record directly. + +Double-click on the record to edit it. Double-click in open area to create a +new record with the group and start date linked to the area you clicked in. +By holding the Ctrl key and dragging left to right, you can create a new record +with the dragged start and end date. diff --git a/web_timeline/static/src/js/timeline_canvas.js b/web_timeline/static/src/js/timeline_canvas.js index e589b559..6022ac36 100644 --- a/web_timeline/static/src/js/timeline_canvas.js +++ b/web_timeline/static/src/js/timeline_canvas.js @@ -5,14 +5,37 @@ odoo.define('web_timeline.TimelineCanvas', function (require) { "use strict"; var Widget = require('web.Widget'); + /** + * Used to draw stuff on upon the timeline view. + */ var TimelineCanvas = Widget.extend({ template: 'TimelineView.Canvas', - clear: function() { + /** + * Clears all drawings (svg elements) from the canvas. + */ + clear: function () { this.$el.find(' > :not(defs)').remove(); }, - get_polyline_points: function(coordx1, coordy1, coordx2, coordy2, width1, height1, width2, height2, widthMarker, breakAt) { + /** + * Gets the path from one point to another. + * + * @param {Number} coordx1 + * @param {Number} coordy1 + * @param {Number} coordx2 + * @param {Number} coordy2 + * @param {Number} width1 + * @param {Number} height1 + * @param {Number} width2 + * @param {Number} height2 + * @param {Number} widthMarker The marker's width of the polyline + * @param {Number} breakAt The space between the line turns + * @returns {Array} Each item represents a coordinate + */ + get_polyline_points: function (coordx1, coordy1, coordx2, coordy2, + width1, height1, width2, height2, + widthMarker, breakAt) { var halfHeight1 = height1 / 2; var halfHeight2 = height2 / 2; var x1 = coordx1 - widthMarker; @@ -37,21 +60,42 @@ odoo.define('web_timeline.TimelineCanvas', function (require) { points.push([x2 + breakAt, y2]); } } else if(x1 < x2) { - points.push([x1 - breakAt, y1]); - points.push([x1 - breakAt, y1 + spaceY]); - points.push([x2 + breakAt, y2 + spaceY]); - points.push([x2 + breakAt, y2]); + points.push([x1 - breakAt, y1]); + points.push([x1 - breakAt, y1 + spaceY]); + points.push([x2 + breakAt, y2 + spaceY]); + points.push([x2 + breakAt, y2]); } points.push([x2, y2]); return points; }, - draw_arrow: function(from, to, color, width) { + /** + * Draws an arrow. + * + * @param {HTMLElement} from Element to draw the arrow from + * @param {HTMLElement} to Element to draw the arrow to + * @param {String} color Color of the line + * @param {Number} width Width of the line + * @returns {HTMLElement} The created SVG polyline + */ + draw_arrow: function (from, to, color, width) { return this.draw_line(from, to, color, width, '#arrowhead', 10, 12); }, - draw_line: function(from, to, color, width, markerStart, widthMarker, breakLineAt) { + /** + * Draws a line. + * + * @param {HTMLElement} from Element to draw the line from + * @param {HTMLElement} to Element to draw the line to + * @param {String} color Color of the line + * @param {Number} width Width of the line + * @param {String} markerStart Start marker of the line + * @param {Number} widthMarker The marker's width of the polyline + * @param {Number} breakLineAt The space between the line turns + * @returns {HTMLElement} The created SVG polyline + */ + draw_line: function (from, to, color, width, markerStart, widthMarker, breakLineAt) { var x1 = from.offsetLeft, y1 = from.offsetTop + from.parentElement.offsetTop, x2 = to.offsetLeft, @@ -81,8 +125,7 @@ odoo.define('web_timeline.TimelineCanvas', function (require) { } this.$el.append(line); return line; - } - + }, }); return TimelineCanvas; diff --git a/web_timeline/static/src/js/timeline_controller.js b/web_timeline/static/src/js/timeline_controller.js index edd87a8b..2301fe54 100644 --- a/web_timeline/static/src/js/timeline_controller.js +++ b/web_timeline/static/src/js/timeline_controller.js @@ -1,256 +1,307 @@ odoo.define('web_timeline.TimelineController', function (require) { -"use strict"; + "use strict"; -var AbstractController = require('web.AbstractController'); -var dialogs = require('web.view_dialogs'); -var core = require('web.core'); -var time = require('web.time'); -var Dialog = require('web.Dialog'); + var AbstractController = require('web.AbstractController'); + var dialogs = require('web.view_dialogs'); + var core = require('web.core'); + var time = require('web.time'); + var Dialog = require('web.Dialog'); -var _t = core._t; + var _t = core._t; -var CalendarController = AbstractController.extend({ - custom_events: _.extend({}, AbstractController.prototype.custom_events, { - onGroupClick: '_onGroupClick', - onUpdate: '_onUpdate', - onRemove: '_onRemove', - onMove: '_onMove', - onAdd: '_onAdd', - }), + var TimelineController = AbstractController.extend({ + custom_events: _.extend({}, AbstractController.prototype.custom_events, { + onGroupClick: '_onGroupClick', + onUpdate: '_onUpdate', + onRemove: '_onRemove', + onMove: '_onMove', + onAdd: '_onAdd', + }), - init: function (parent, model, renderer, params) { - this._super.apply(this, arguments); - this.open_popup_action = params.open_popup_action; - this.date_start = params.date_start; - this.date_stop = params.date_stop; - this.date_delay = params.date_delay; - this.context = params.actionContext; - this.moveQueue = []; - this.debouncedInternalMove = _.debounce(this.internalMove, 0); - }, + /** + * @constructor + * @override + */ + init: function (parent, model, renderer, params) { + this._super.apply(this, arguments); + this.open_popup_action = params.open_popup_action; + this.date_start = params.date_start; + this.date_stop = params.date_stop; + this.date_delay = params.date_delay; + this.context = params.actionContext; + this.moveQueue = []; + this.debouncedInternalMove = _.debounce(this.internalMove, 0); + }, - update: function(params, options) { - this._super.apply(this, arguments); - if (_.isEmpty(params)){ - return; - } - var defaults = _.defaults({}, options, { - adjust_window: true - }); - var self = this; - var domains = params.domain; - var contexts = params.context; - var group_bys = params.groupBy; - this.last_domains = domains; - this.last_contexts = contexts; - // select the group by - var n_group_bys = []; - if (this.renderer.arch.attrs.default_group_by) { - n_group_bys = this.renderer.arch.attrs.default_group_by.split(','); - } - if (group_bys.length) { - n_group_bys = group_bys; - } - this.renderer.last_group_bys = n_group_bys; - this.renderer.last_domains = domains; - - var fields = this.renderer.fieldNames; - fields = _.uniq(fields.concat(n_group_bys)); - self._rpc({ - model: self.model.modelName, - method: 'search_read', - kwargs: { - fields: fields, - domain: domains, - }, - context: self.getSession().user_context, - }).then(function(data) { - return self.renderer.on_data_loaded(data, n_group_bys, defaults.adjust_window); - }); - }, - - _onGroupClick: function (event) { - var groupField = this.renderer.last_group_bys[0]; - return this.do_action({ - type: 'ir.actions.act_window', - res_model: this.renderer.view.fields[groupField].relation, - res_id: event.data.item.group, - target: 'new', - views: [[false, 'form']] - }); - }, + /** + * @override + */ + update: function (params, options) { + this._super.apply(this, arguments); + if (_.isEmpty(params)){ + return; + } + var defaults = _.defaults({}, options, { + adjust_window: true + }); + var self = this; + var domains = params.domain; + var contexts = params.context; + var group_bys = params.groupBy; + this.last_domains = domains; + this.last_contexts = contexts; + // select the group by + var n_group_bys = []; + if (this.renderer.arch.attrs.default_group_by) { + n_group_bys = this.renderer.arch.attrs.default_group_by.split(','); + } + if (group_bys.length) { + n_group_bys = group_bys; + } + this.renderer.last_group_bys = n_group_bys; + this.renderer.last_domains = domains; - _onUpdate: function(event) { - var self = this; - this.renderer = event.data.renderer; - var rights = event.data.rights; - var item = event.data.item; - var id = item.evt.id; - var title = item.evt.__name; - if (this.open_popup_action) { - new dialogs.FormViewDialog(this, { - res_model: this.model.modelName, - res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id, - context: this.getSession().user_context, - title: title, - view_id: Number(this.open_popup_action), - on_saved: function () { - self.write_completed(); + var fields = this.renderer.fieldNames; + fields = _.uniq(fields.concat(n_group_bys)); + self._rpc({ + model: self.model.modelName, + method: 'search_read', + kwargs: { + fields: fields, + domain: domains, }, - }).open(); - } else { - var mode = 'readonly'; - if (rights.write) { - mode = 'edit'; - } - this.trigger_up('switch_view', { - view_type: 'form', - res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id, - mode: mode, - model: this.model.modelName, + context: self.getSession().user_context, + }).then(function(data) { + return self.renderer.on_data_loaded(data, n_group_bys, defaults.adjust_window); }); - } - }, + }, - _onMove: function(event) { - var item = event.data.item; - var view = this.renderer.view; - var fields = view.fields; - var event_start = item.start; - var event_end = item.end; - var group = false; - if (item.group !== -1) { - group = item.group; - } - var data = {}; - // In case of a move event, the date_delay stay the same, only date_start and stop must be updated - data[this.date_start] = time.auto_date_to_str(event_start, fields[this.date_start].type); - if (this.date_stop) { - // In case of instantaneous event, item.end is not defined - if (event_end) { - data[this.date_stop] = time.auto_date_to_str(event_end, fields[this.date_stop].type); + /** + * Gets triggered when a group in the timeline is clicked (by the TimelineRenderer). + * + * @private + * @returns {jQuery.Deferred} + */ + _onGroupClick: function (event) { + var groupField = this.renderer.last_group_bys[0]; + return this.do_action({ + type: 'ir.actions.act_window', + res_model: this.renderer.view.fields[groupField].relation, + res_id: event.data.item.group, + target: 'new', + views: [[false, 'form']] + }); + }, + + /** + * Opens a form view of a clicked timeline item (triggered by the TimelineRenderer). + * + * @private + */ + _onUpdate: function (event) { + var self = this; + this.renderer = event.data.renderer; + var rights = event.data.rights; + var item = event.data.item; + var id = item.evt.id; + var title = item.evt.__name; + if (this.open_popup_action) { + new dialogs.FormViewDialog(this, { + res_model: this.model.modelName, + res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id, + context: this.getSession().user_context, + title: title, + view_id: Number(this.open_popup_action), + on_saved: function () { + self.write_completed(); + }, + }).open(); } else { - data[this.date_stop] = data[this.date_start]; + var mode = 'readonly'; + if (rights.write) { + mode = 'edit'; + } + this.trigger_up('switch_view', { + view_type: 'form', + res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id, + mode: mode, + model: this.model.modelName, + }); } - } - if (this.date_delay && event_end) { - var diff_seconds = Math.round((event_end.getTime() - event_start.getTime()) / 1000); - data[this.date_delay] = diff_seconds / 3600; - } - if (this.renderer.last_group_bys && this.renderer.last_group_bys instanceof Array) { - data[this.renderer.last_group_bys[0]] = group; - } + }, - this.moveQueue.push({ - id: event.data.item.id, - data: data, - event: event - }); + /** + * Gets triggered when a timeline item is moved (triggered by the TimelineRenderer). + * + * @private + */ + _onMove: function (event) { + var item = event.data.item; + var view = this.renderer.view; + var fields = view.fields; + var event_start = item.start; + var event_end = item.end; + var group = false; + if (item.group !== -1) { + group = item.group; + } + var data = {}; + // In case of a move event, the date_delay stay the same, only date_start and stop must be updated + data[this.date_start] = time.auto_date_to_str(event_start, fields[this.date_start].type); + if (this.date_stop) { + // In case of instantaneous event, item.end is not defined + if (event_end) { + data[this.date_stop] = time.auto_date_to_str(event_end, fields[this.date_stop].type); + } else { + data[this.date_stop] = data[this.date_start]; + } + } + if (this.date_delay && event_end) { + var diff_seconds = Math.round((event_end.getTime() - event_start.getTime()) / 1000); + data[this.date_delay] = diff_seconds / 3600; + } + if (this.renderer.last_group_bys && this.renderer.last_group_bys instanceof Array) { + data[this.renderer.last_group_bys[0]] = group; + } - this.debouncedInternalMove(); - }, + this.moveQueue.push({ + id: event.data.item.id, + data: data, + event: event + }); - internalMove: function() { - var self = this; - var queue = this.moveQueue.slice(); - this.moveQueue = []; - var defers = []; - _.each(queue, function(item) { - defers.push(self._rpc({ - model: self.model.modelName, - method: 'write', - args: [ - [item.event.data.item.id], - item.data, - ], - context: self.getSession().user_context, - }).then(function() { - item.event.data.callback(item.event.data.item); - })); - }); - return $.when.apply($, defers).done(function() { - self.write_completed({ - adjust_window: false + this.debouncedInternalMove(); + }, + + /** + * Write enqueued moves to Odoo. After all writes are finished it updates the view once + * (prevents flickering of the view when multiple timeline items are moved at once). + * + * @returns {jQuery.Deferred} + */ + internalMove: function () { + var self = this; + var queue = this.moveQueue.slice(); + this.moveQueue = []; + var defers = []; + _.each(queue, function(item) { + defers.push(self._rpc({ + model: self.model.modelName, + method: 'write', + args: [ + [item.event.data.item.id], + item.data, + ], + context: self.getSession().user_context, + }).then(function() { + item.event.data.callback(item.event.data.item); + })); + }); + return $.when.apply($, defers).done(function() { + self.write_completed({ + adjust_window: false + }); }); - }); - }, + }, - _onRemove: function(e) { - var self = this; + /** + * Triggered when a timeline item gets removed from the view. + * Requires user confirmation before it gets actually deleted. + * + * @private + * @returns {jQuery.Deferred} + */ + _onRemove: function (e) { + var self = this; - function do_it(event) { - return self._rpc({ - model: self.model.modelName, - method: 'unlink', - args: [ - [event.data.item.id], - ], - context: self.getSession().user_context, - }).then(function() { - var unlink_index = false; - for (var i=0; i 0) { - default_context['default_'.concat(this.renderer.last_group_bys[0])] = item.group; - } - // Show popup - new dialogs.FormViewDialog(this, { - res_model: this.model.modelName, - res_id: null, - context: _.extend(default_context, this.context), - view_id: Number(this.open_popup_action), - on_saved: function (record) { - self.create_completed([record.res_id]); - }, - }).open().on('closed', this, function() { - event.data.callback(); - }); + /** + * Triggered when a timeline item gets added and opens a form view. + * + * @private + */ + _onAdd: function (event) { + var self = this; + var item = event.data.item; + // Initialize default values for creation + var default_context = {}; + default_context['default_'.concat(this.date_start)] = item.start; + if (this.date_delay) { + default_context['default_'.concat(this.date_delay)] = 1; + } + if (this.date_start) { + default_context['default_'.concat(this.date_start)] = moment(item.start).add(1, 'hours').format( + 'YYYY-MM-DD HH:mm:ss' + ); + } + if (this.date_stop && item.end) { + default_context['default_'.concat(this.date_stop)] = moment(item.end).add(1, 'hours').format( + 'YYYY-MM-DD HH:mm:ss' + ); + } + if (item.group > 0) { + default_context['default_'.concat(this.renderer.last_group_bys[0])] = item.group; + } + // Show popup + new dialogs.FormViewDialog(this, { + res_model: this.model.modelName, + res_id: null, + context: _.extend(default_context, this.context), + view_id: Number(this.open_popup_action), + on_saved: function (record) { + self.create_completed([record.res_id]); + }, + }).open().on('closed', this, function () { + event.data.callback(); + }); - return false; - }, + return false; + }, - create_completed: function (id) { - var self = this; - return this._rpc({ + /** + * Triggered upon completion of a new record. + * Updates the timeline view with the new record. + * + * @returns {jQuery.Deferred} + */ + create_completed: function (id) { + var self = this; + return this._rpc({ model: this.model.modelName, method: 'read', args: [ @@ -258,26 +309,29 @@ var CalendarController = AbstractController.extend({ this.model.fieldNames, ], context: this.context, - }) - .then(function (records) { - var new_event = self.renderer.event_data_transform(records[0]); - var items = self.renderer.timeline.itemsData; - items.add(new_event); - self.renderer.timeline.setItems(items); - self.reload(); - }); - }, + }) + .then(function (records) { + var new_event = self.renderer.event_data_transform(records[0]); + var items = self.renderer.timeline.itemsData; + items.add(new_event); + self.renderer.timeline.setItems(items); + self.reload(); + }); + }, - write_completed: function (options) { - var params = { - domain: this.renderer.last_domains, - context: this.context, - groupBy: this.renderer.last_group_bys, - }; + /** + * Triggered upon completion of writing a record. + */ + write_completed: function (options) { + var params = { + domain: this.renderer.last_domains, + context: this.context, + groupBy: this.renderer.last_group_bys, + }; - this.update(params, options); - }, -}); + this.update(params, options); + }, + }); -return CalendarController; + return TimelineController; }); diff --git a/web_timeline/static/src/js/timeline_model.js b/web_timeline/static/src/js/timeline_model.js index 7734dbd3..b17aab73 100644 --- a/web_timeline/static/src/js/timeline_model.js +++ b/web_timeline/static/src/js/timeline_model.js @@ -1,58 +1,71 @@ odoo.define('web_timeline.TimelineModel', function (require) { -"use strict"; - -var AbstractModel = require('web.AbstractModel'); - -var TimelineModel = AbstractModel.extend({ - init: function () { - this._super.apply(this, arguments); - }, - - load: function (params) { - var self = this; - this.modelName = params.modelName; - this.fieldNames = params.fieldNames; - if (!this.preload_def) { - this.preload_def = $.Deferred(); - $.when( - this._rpc({model: this.modelName, method: 'check_access_rights', args: ["write", false]}), - this._rpc({model: this.modelName, method: 'check_access_rights', args: ["unlink", false]}), - this._rpc({model: this.modelName, method: 'check_access_rights', args: ["create", false]})) - .then(function (write, unlink, create) { - self.write_right = write; - self.unlink_right = unlink; - self.create_right = create; - self.preload_def.resolve(); - }); - } + "use strict"; + + var AbstractModel = require('web.AbstractModel'); + + var TimelineModel = AbstractModel.extend({ - this.data = { - domain: params.domain, - context: params.context, - }; + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + }, - return this.preload_def.then(this._loadTimeline.bind(this)); - }, + /** + * @override + */ + load: function (params) { + var self = this; + this.modelName = params.modelName; + this.fieldNames = params.fieldNames; + if (!this.preload_def) { + this.preload_def = $.Deferred(); + $.when( + this._rpc({model: this.modelName, method: 'check_access_rights', args: ["write", false]}), + this._rpc({model: this.modelName, method: 'check_access_rights', args: ["unlink", false]}), + this._rpc({model: this.modelName, method: 'check_access_rights', args: ["create", false]})) + .then(function (write, unlink, create) { + self.write_right = write; + self.unlink_right = unlink; + self.create_right = create; + self.preload_def.resolve(); + }); + } + + this.data = { + domain: params.domain, + context: params.context, + }; - _loadTimeline: function () { - var self = this; - return self._rpc({ + return this.preload_def.then(this._loadTimeline.bind(this)); + }, + + /** + * Read the records for the timeline. + * + * @private + * @returns {jQuery.Deferred} + */ + _loadTimeline: function () { + var self = this; + return self._rpc({ model: self.modelName, method: 'search_read', context: self.data.context, fields: self.fieldNames, domain: self.data.domain, - }) - .then(function (events) { - self.data.data = events; - self.data.rights = { - 'unlink': self.unlink_right, - 'create': self.create_right, - 'write': self.write_right, - }; - }); - }, -}); + }) + .then(function (events) { + self.data.data = events; + self.data.rights = { + 'unlink': self.unlink_right, + 'create': self.create_right, + 'write': self.write_right, + }; + }); + }, + }); -return TimelineModel; + return TimelineModel; }); diff --git a/web_timeline/static/src/js/timeline_renderer.js b/web_timeline/static/src/js/timeline_renderer.js index df70a751..4a0038ce 100644 --- a/web_timeline/static/src/js/timeline_renderer.js +++ b/web_timeline/static/src/js/timeline_renderer.js @@ -1,421 +1,537 @@ odoo.define('web_timeline.TimelineRenderer', function (require) { -"use strict"; - -var AbstractRenderer = require('web.AbstractRenderer'); -var core = require('web.core'); -var time = require('web.time'); -var utils = require('web.utils'); -var session = require('web.session'); -var QWeb = require('web.QWeb'); -var field_utils = require('web.field_utils'); -var TimelineCanvas = require('web_timeline.TimelineCanvas'); - - -var _t = core._t; - -var CalendarRenderer = AbstractRenderer.extend({ - template: "TimelineView", - events: _.extend({}, AbstractRenderer.prototype.events, { - }), - - init: function (parent, state, params) { - this._super.apply(this, arguments); - this.modelName = params.model; - this.mode = params.mode; - this.options = params.options; - this.permissions = params.permissions; - this.timeline = params.timeline; - this.min_height = params.min_height; - this.date_start = params.date_start; - this.date_stop = params.date_stop; - this.date_delay = params.date_delay; - this.colors = params.colors; - this.fieldNames = params.fieldNames; - this.dependency_arrow = params.dependency_arrow; - this.view = params.view; - this.modelClass = this.view.model; - }, - - start: function () { - var self = this; - var attrs = this.arch.attrs; - this.current_window = { - start: new moment(), - end: new moment().add(24, 'hours') - }; - - this.$el.addClass(attrs.class); - this.$timeline = this.$el.find(".oe_timeline_widget"); - - if (!this.date_start) { - throw new Error(_t("Timeline view has not defined 'date_start' attribute.")); - } - this._super.apply(this, self); - }, - - on_attach_callback: function() { - var height = this.$el.parent().height() - this.$el.find('.oe_timeline_buttons').height(); - if (height > this.min_height) { - this.timeline.setOptions({ - height: height + "use strict"; + + var AbstractRenderer = require('web.AbstractRenderer'); + var core = require('web.core'); + var time = require('web.time'); + var utils = require('web.utils'); + var session = require('web.session'); + var QWeb = require('web.QWeb'); + var field_utils = require('web.field_utils'); + var TimelineCanvas = require('web_timeline.TimelineCanvas'); + + + var _t = core._t; + + var TimelineRenderer = AbstractRenderer.extend({ + template: "TimelineView", + + events: _.extend({}, AbstractRenderer.prototype.events, { + 'click .oe_timeline_button_today': '_onTodayClicked', + 'click .oe_timeline_button_scale_day': '_onScaleDayClicked', + 'click .oe_timeline_button_scale_week': '_onScaleWeekClicked', + 'click .oe_timeline_button_scale_month': '_onScaleMonthClicked', + 'click .oe_timeline_button_scale_year': '_onScaleYearClicked', + }), + + /** + * @constructor + */ + init: function (parent, state, params) { + this._super.apply(this, arguments); + this.modelName = params.model; + this.mode = params.mode; + this.options = params.options; + this.permissions = params.permissions; + this.timeline = params.timeline; + this.min_height = params.min_height; + this.date_start = params.date_start; + this.date_stop = params.date_stop; + this.date_delay = params.date_delay; + this.colors = params.colors; + this.fieldNames = params.fieldNames; + this.dependency_arrow = params.dependency_arrow; + this.view = params.view; + this.modelClass = this.view.model; + }, + + /** + * @override + */ + start: function () { + var self = this; + var attrs = this.arch.attrs; + this.current_window = { + start: new moment(), + end: new moment().add(24, 'hours') + }; + + this.$el.addClass(attrs.class); + this.$timeline = this.$el.find(".oe_timeline_widget"); + + if (!this.date_start) { + throw new Error(_t("Timeline view has not defined 'date_start' attribute.")); + } + this._super.apply(this, self); + }, + + /** + * Triggered when the timeline is attached to the DOM. + */ + on_attach_callback: function() { + var height = this.$el.parent().height() - this.$el.find('.oe_timeline_buttons').height(); + if (height > this.min_height) { + this.timeline.setOptions({ + height: height + }); + } + }, + + /** + * @override + */ + _render: function () { + var self = this; + return $.when().then(function () { + // Prevent Double Rendering on Updates + if (!self.timeline) { + self.init_timeline(); + $(window).trigger('resize'); + } }); - } - }, - - _render: function () { - this.add_events(); - var self = this; - return $.when().then(function () { - // Prevent Double Rendering on Updates - if (!self.timeline) { - self.init_timeline(); - $(window).trigger('resize'); + }, + + /** + * Set the timeline window to today (day). + * + * @private + */ + _onTodayClicked: function () { + this.current_window = { + start: new moment(), + end: new moment().add(24, 'hours') + }; + + if (this.timeline) { + this.timeline.setWindow(this.current_window); } - }); - }, - - add_events: function() { - var self = this; - this.$(".oe_timeline_button_today").click(function() { - self._onTodayClicked(); - }); - this.$(".oe_timeline_button_scale_day").click(function() { - self._onScaleDayClicked(); - }); - this.$(".oe_timeline_button_scale_week").click(function() { - self._onScaleWeekClicked(); - }); - this.$(".oe_timeline_button_scale_month").click(function() { - self._onScaleMonthClicked(); - }); - this.$(".oe_timeline_button_scale_year").click(function() { - self._onScaleYearClicked(); - }); - }, - - _onTodayClicked: function () { - this.current_window = { - start: new moment(), - end: new moment().add(24, 'hours') - }; - - if (this.timeline) { - this.timeline.setWindow(this.current_window); - } - }, - - _onScaleDayClicked: function () { - this._scaleCurrentWindow(24); - }, - - _onScaleWeekClicked: function () { - this._scaleCurrentWindow(24 * 7); - }, - - _onScaleMonthClicked: function () { - this._scaleCurrentWindow(24 * 30); - }, - - _onScaleYearClicked: function () { - this._scaleCurrentWindow(24 * 365); - }, - - _scaleCurrentWindow: function (factor) { - if (this.timeline) { - this.current_window = this.timeline.getWindow(); - this.current_window.end = moment(this.current_window.start).add(factor, 'hours'); - this.timeline.setWindow(this.current_window); - } - }, - - _computeMode: function() { - if (this.mode) { - var start = false, end = false; - switch (this.mode) { - case 'day': - start = new moment().startOf('day'); - end = new moment().endOf('day'); - break; - case 'week': - start = new moment().startOf('week'); - end = new moment().endOf('week'); - break; - case 'month': - start = new moment().startOf('month'); - end = new moment().endOf('month'); - break; + }, + + /** + * Scale the timeline window to a day. + * + * @private + */ + _onScaleDayClicked: function () { + this._scaleCurrentWindow(24); + }, + + /** + * Scale the timeline window to a week. + * + * @private + */ + _onScaleWeekClicked: function () { + this._scaleCurrentWindow(24 * 7); + }, + + /** + * Scale the timeline window to a month. + * + * @private + */ + _onScaleMonthClicked: function () { + this._scaleCurrentWindow(24 * 30); + }, + + /** + * Scale the timeline window to a year. + * + * @private + */ + _onScaleYearClicked: function () { + this._scaleCurrentWindow(24 * 365); + }, + + /** + * Scales the timeline window based on the current window. + * + * @param {Integer} factor The timespan (in hours) the window must be scaled to. + * @private + */ + _scaleCurrentWindow: function (factor) { + if (this.timeline) { + this.current_window = this.timeline.getWindow(); + this.current_window.end = moment(this.current_window.start).add(factor, 'hours'); + this.timeline.setWindow(this.current_window); } - if (end && start) { - this.options.start = start; - this.options.end = end; - } else { - this.mode = 'fit'; + }, + + /** + * Computes the initial visible window. + * + * @private + */ + _computeMode: function () { + if (this.mode) { + var start = false, end = false; + switch (this.mode) { + case 'day': + start = new moment().startOf('day'); + end = new moment().endOf('day'); + break; + case 'week': + start = new moment().startOf('week'); + end = new moment().endOf('week'); + break; + case 'month': + start = new moment().startOf('month'); + end = new moment().endOf('month'); + break; + } + if (end && start) { + this.options.start = start; + this.options.end = end; + } else { + this.mode = 'fit'; + } } - } - }, - - init_timeline: function () { - var self = this; - this._computeMode(); - this.options.editable = { - // add new items by double tapping - add: this.modelClass.data.rights.create, - // drag items horizontally - updateTime: this.modelClass.data.rights.write, - // drag items from one group to another - updateGroup: this.modelClass.data.rights.write, - // delete an item by tapping the delete button top right - remove: this.modelClass.data.rights.unlink, - }; - $.extend(this.options, { - onAdd: self.on_add, - onMove: self.on_move, - onUpdate: self.on_update, - onRemove: self.on_remove - }); - this.qweb = new QWeb(session.debug, {_s: session.origin}, false); - if (this.arch.children.length) { - var tmpl = utils.json_node_to_xml( - _.filter(this.arch.children, function(item) { - return item.tag === 'templates'; - })[0] - ); - this.qweb.add_template(tmpl); - } - - this.timeline = new vis.Timeline(self.$timeline.empty().get(0)); - this.timeline.setOptions(this.options); - if (self.mode && self['on_scale_' + self.mode + '_clicked']) { - self['on_scale_' + self.mode + '_clicked'](); - } - this.timeline.on('click', self.on_group_click); - var group_bys = this.arch.attrs.default_group_by.split(','); - this.last_group_bys = group_bys; - this.last_domains = this.modelClass.data.domain; - this.on_data_loaded(this.modelClass.data.data, group_bys); - this.$centerContainer = $(this.timeline.dom.centerContainer); - this.canvas = new TimelineCanvas(this); - this.canvas.appendTo(this.$centerContainer); - this.timeline.on('changed', function() { - self.draw_canvas(); - self.canvas.$el.attr( - 'style', - self.$el.find('.vis-content').attr('style') + self.$el.find('.vis-itemset').attr('style') - ); - }); - }, - - draw_canvas: function() { - this.canvas.clear(); - if (this.dependency_arrow) { - this.draw_dependencies(); - } - }, - - draw_dependencies: function() { - var self = this; - var items = this.timeline.itemSet.items; - _.each(items, function(item) { - if (!item.data.evt) { - return; + }, + + /** + * Initializes the timeline (http://visjs.org/docs/timeline/). + * + * @private + */ + init_timeline: function () { + var self = this; + this._computeMode(); + this.options.editable = { + // add new items by double tapping + add: this.modelClass.data.rights.create, + // drag items horizontally + updateTime: this.modelClass.data.rights.write, + // drag items from one group to another + updateGroup: this.modelClass.data.rights.write, + // delete an item by tapping the delete button top right + remove: this.modelClass.data.rights.unlink, + }; + $.extend(this.options, { + onAdd: self.on_add, + onMove: self.on_move, + onUpdate: self.on_update, + onRemove: self.on_remove + }); + this.qweb = new QWeb(session.debug, {_s: session.origin}, false); + if (this.arch.children.length) { + var tmpl = utils.json_node_to_xml( + _.filter(this.arch.children, function(item) { + return item.tag === 'templates'; + })[0] + ); + this.qweb.add_template(tmpl); } - _.each(item.data.evt[self.dependency_arrow], function(id) { - if (id in items) { - self.draw_dependency(item, items[id]); + + this.timeline = new vis.Timeline(self.$timeline.empty().get(0)); + this.timeline.setOptions(this.options); + if (self.mode && self['on_scale_' + self.mode + '_clicked']) { + self['on_scale_' + self.mode + '_clicked'](); + } + this.timeline.on('click', self.on_group_click); + var group_bys = this.arch.attrs.default_group_by.split(','); + this.last_group_bys = group_bys; + this.last_domains = this.modelClass.data.domain; + this.on_data_loaded(this.modelClass.data.data, group_bys); + this.$centerContainer = $(this.timeline.dom.centerContainer); + this.canvas = new TimelineCanvas(this); + this.canvas.appendTo(this.$centerContainer); + this.timeline.on('changed', function() { + self.draw_canvas(); + self.canvas.$el.attr( + 'style', + self.$el.find('.vis-content').attr('style') + self.$el.find('.vis-itemset').attr('style') + ); + }); + }, + + /** + * Clears and draws the canvas items. + * + * @private + */ + draw_canvas: function () { + this.canvas.clear(); + if (this.dependency_arrow) { + this.draw_dependencies(); + } + }, + + /** + * Draw item dependencies on canvas. + * + * @private + */ + draw_dependencies: function () { + var self = this; + var items = this.timeline.itemSet.items; + _.each(items, function(item) { + if (!item.data.evt) { + return; } + _.each(item.data.evt[self.dependency_arrow], function(id) { + if (id in items) { + self.draw_dependency(item, items[id]); + } + }); }); - }); - }, - - draw_dependency: function(from, to, options) { - if (!from.displayed || !to.displayed) { - return; - } - - var defaults = _.defaults({}, options, { - line_color: 'black', - line_width: 1 - }); - - this.canvas.draw_arrow(from.dom.box, to.dom.box, defaults.line_color, defaults.line_width); - }, - - on_data_loaded: function (events, group_bys, adjust_window) { - var self = this; - var ids = _.pluck(events, "id"); - return this._rpc({ - model: this.modelName, - method: 'name_get', - args: [ - ids, - ], - context: this.getSession().user_context, - }).then(function(names) { - var nevents = _.map(events, function (event) { - return _.extend({ - __name: _.detect(names, function (name) { - return name[0] === event.id; - })[1] - }, event); + }, + + /** + * Draws a dependency arrow between 2 timeline items. + * + * @param {Object} from Start timeline item + * @param {Object} to Destination timeline item + * @param {Object} options + * @param {Object} options.line_color Color of the line + * @param {Object} options.line_width The width of the line + * @private + */ + draw_dependency: function (from, to, options) { + if (!from.displayed || !to.displayed) { + return; + } + + var defaults = _.defaults({}, options, { + line_color: 'black', + line_width: 1 + }); + + this.canvas.draw_arrow(from.dom.box, to.dom.box, defaults.line_color, defaults.line_width); + }, + + /** + * Load display_name of records. + * + * @private + * @returns {jQuery.Deferred} + */ + on_data_loaded: function (events, group_bys, adjust_window) { + var self = this; + var ids = _.pluck(events, "id"); + return this._rpc({ + model: this.modelName, + method: 'name_get', + args: [ + ids, + ], + context: this.getSession().user_context, + }).then(function(names) { + var nevents = _.map(events, function (event) { + return _.extend({ + __name: _.detect(names, function (name) { + return name[0] === event.id; + })[1] + }, event); + }); + return self.on_data_loaded_2(nevents, group_bys, adjust_window); }); - return self.on_data_loaded_2(nevents, group_bys, adjust_window); - }); - }, - - on_data_loaded_2: function (events, group_bys, adjust_window) { - var self = this; - var data = []; - var groups = []; - this.grouped_by = group_bys; - _.each(events, function (event) { - if (event[self.date_start]) { - data.push(self.event_data_transform(event)); + }, + + /** + * Set groups and events. + * + * @private + */ + on_data_loaded_2: function (events, group_bys, adjust_window) { + var self = this; + var data = []; + var groups = []; + this.grouped_by = group_bys; + _.each(events, function (event) { + if (event[self.date_start]) { + data.push(self.event_data_transform(event)); + } + }); + groups = this.split_groups(events, group_bys); + this.timeline.setGroups(groups); + this.timeline.setItems(data); + var mode = !this.mode || this.mode === 'fit'; + var adjust = _.isUndefined(adjust_window) || adjust_window; + if (mode && adjust) { + this.timeline.fit(); + } + }, + + /** + * Get the groups. + * + * @private + * @returns {Array} + */ + split_groups: function (events, group_bys) { + if (group_bys.length === 0) { + return events; } - }); - groups = this.split_groups(events, group_bys); - this.timeline.setGroups(groups); - this.timeline.setItems(data); - var mode = !this.mode || this.mode === 'fit'; - var adjust = _.isUndefined(adjust_window) || adjust_window; - if (mode && adjust) { - this.timeline.fit(); - } - }, - - // get the groups - split_groups: function (events, group_bys) { - if (group_bys.length === 0) { - return events; - } - var groups = []; - groups.push({id: -1, content: _t('-')}); - _.each(events, function (event) { - var group_name = event[_.first(group_bys)]; - if (group_name) { - if (group_name instanceof Array) { - var group = _.find(groups, function (existing_group) { - return _.isEqual(existing_group.id, group_name[0]); - }); - - if (_.isUndefined(group)) { - group = { - id: group_name[0], - content: group_name[1] - }; - groups.push(group); + var groups = []; + groups.push({id: -1, content: _t('-')}); + _.each(events, function (event) { + var group_name = event[_.first(group_bys)]; + if (group_name) { + if (group_name instanceof Array) { + var group = _.find(groups, function (existing_group) { + return _.isEqual(existing_group.id, group_name[0]); + }); + + if (_.isUndefined(group)) { + group = { + id: group_name[0], + content: group_name[1] + }; + groups.push(group); + } } } + }); + return groups; + }, + + /** + * Transform Odoo event object to timeline event object. + * + * @private + * @returns {Object} + */ + event_data_transform: function (evt) { + var self = this; + var date_start = new moment(); + var date_stop = null; + + var date_delay = evt[this.date_delay] || false, + all_day = this.all_day ? evt[this.all_day] : false; + + if (all_day) { + date_start = time.auto_str_to_date(evt[this.date_start].split(' ')[0], 'start'); + if (this.no_period) { + date_stop = date_start; + } else { + date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop].split(' ')[0], 'stop') : null; + } + } else { + date_start = time.auto_str_to_date(evt[this.date_start]); + date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop]) : null; + } + + if (!date_stop && date_delay) { + date_stop = moment(date_start).add(date_delay, 'hours').toDate(); } - }); - return groups; - }, - - /* Transform Odoo event object to timeline event object */ - event_data_transform: function (evt) { - var self = this; - var date_start = new moment(); - var date_stop = null; - - var date_delay = evt[this.date_delay] || false, - all_day = this.all_day ? evt[this.all_day] : false; - - if (all_day) { - date_start = time.auto_str_to_date(evt[this.date_start].split(' ')[0], 'start'); - if (this.no_period) { - date_stop = date_start; + + var group = evt[self.last_group_bys[0]]; + if (group && group instanceof Array) { + group = _.first(group); } else { - date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop].split(' ')[0], 'stop') : null; + group = -1; } - } else { - date_start = time.auto_str_to_date(evt[this.date_start]); - date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop]) : null; - } - - if (!date_stop && date_delay) { - date_stop = moment(date_start).add(date_delay, 'hours').toDate(); - } - - var group = evt[self.last_group_bys[0]]; - if (group && group instanceof Array) { - group = _.first(group); - } else { - group = -1; - } - _.each(self.colors, function (color) { - if (eval("'" + evt[color.field] + "' " + color.opt + " '" + color.value + "'")) { - self.color = color.color; + _.each(self.colors, function (color) { + if (eval("'" + evt[color.field] + "' " + color.opt + " '" + color.value + "'")) { + self.color = color.color; + } + }); + + var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name; + if (this.arch.children.length) { + content = this.render_timeline_item(evt); + } + + var r = { + 'start': date_start, + 'content': content, + 'id': evt.id, + 'group': group, + 'evt': evt, + 'style': 'background-color: ' + self.color + ';' + }; + // Check if the event is instantaneous, if so, display it with a point on the timeline (no 'end') + if (date_stop && !moment(date_start).isSame(date_stop)) { + r.end = date_stop; + } + self.color = null; + return r; + }, + + /** + * Render timeline item template. + * + * @param {Object} evt Record + * @private + * @returns {String} Rendered template + */ + render_timeline_item: function (evt) { + if(this.qweb.has_template('timeline-item')) { + return this.qweb.render('timeline-item', { + 'record': evt, + 'field_utils': field_utils + }); } - }); - - var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name; - if (this.arch.children.length) { - content = this.render_timeline_item(evt); - } - - var r = { - 'start': date_start, - 'content': content, - 'id': evt.id, - 'group': group, - 'evt': evt, - 'style': 'background-color: ' + self.color + ';' - }; - // Check if the event is instantaneous, if so, display it with a point on the timeline (no 'end') - if (date_stop && !moment(date_start).isSame(date_stop)) { - r.end = date_stop; - } - self.color = null; - return r; - }, - - render_timeline_item: function(evt) { - if(this.qweb.has_template('timeline-item')) { - return this.qweb.render('timeline-item', { - 'record': evt, - 'field_utils': field_utils + + console.error( + _t('Template "timeline-item" not present in timeline view definition.') + ); + }, + + /** + * Handle a click on a group header. + * + * @private + */ + on_group_click: function (e) { + if (e.what === 'group-label' && e.group !== -1) { + this._trigger(e, function() { + // Do nothing + }, 'onGroupClick'); + } + }, + + /** + * Trigger onUpdate. + * + * @private + */ + on_update: function (item, callback) { + this._trigger(item, callback, 'onUpdate'); + }, + + /** + * Trigger onMove. + * + * @private + */ + on_move: function (item, callback) { + this._trigger(item, callback, 'onMove'); + }, + + /** + * Trigger onRemove. + * + * @private + */ + on_remove: function (item, callback) { + this._trigger(item, callback, 'onRemove'); + }, + + /** + * Trigger onAdd. + * + * @private + */ + on_add: function (item, callback) { + this._trigger(item, callback, 'onAdd'); + }, + + /** + * trigger_up encapsulation adds by default the rights, and the renderer. + * + * @private + */ + _trigger: function (item, callback, trigger) { + this.trigger_up(trigger, { + 'item': item, + 'callback': callback, + 'rights': this.modelClass.data.rights, + 'renderer': this, }); - } - - console.error( - _t('Template "timeline-item" not present in timeline view definition.') - ); - }, - - on_group_click: function (e) { - // handle a click on a group header - if (e.what === 'group-label' && e.group !== -1) { - this._trigger(e, function() { - // Do nothing - }, 'onGroupClick'); - } - }, - - on_update: function (item, callback) { - this._trigger(item, callback, 'onUpdate'); - }, - - on_move: function (item, callback) { - this._trigger(item, callback, 'onMove'); - }, - - on_remove: function (item, callback) { - this._trigger(item, callback, 'onRemove'); - }, - - on_add: function (item, callback) { - this._trigger(item, callback, 'onAdd'); - }, - - _trigger: function (item, callback, trigger) { - this.trigger_up(trigger, { - 'item': item, - 'callback': callback, - 'rights': this.modelClass.data.rights, - 'renderer': this, - }); - }, + }, -}); + }); -return CalendarRenderer; + return TimelineRenderer; }); diff --git a/web_timeline/static/src/js/timeline_view.js b/web_timeline/static/src/js/timeline_view.js index 78460da6..99564324 100644 --- a/web_timeline/static/src/js/timeline_view.js +++ b/web_timeline/static/src/js/timeline_view.js @@ -39,11 +39,15 @@ odoo.define('web_timeline.TimelineView', function (require) { Renderer: TimelineRenderer, }, + /** + * @constructor + * @override + */ init: function (viewInfo, params) { this._super.apply(this, arguments); var self = this; this.timeline = false; - this.arch = viewInfo.arch; + this.arch = this.rendererParams.arch; var attrs = this.arch.attrs; this.fields = viewInfo.fields; this.modelName = this.controllerParams.modelName; @@ -144,6 +148,9 @@ odoo.define('web_timeline.TimelineView', function (require) { return this; }, + /** + * Order function for groups. + */ group_order: function (grp1, grp2) { // display non grouped elements first if (grp1.id === -1) { @@ -156,6 +163,11 @@ odoo.define('web_timeline.TimelineView', function (require) { }, + /** + * Parse the colors attribute. + * + * @private + */ parse_colors: function () { if (this.arch.attrs.colors) { this.colors = _(this.arch.attrs.colors.split(';')).chain().compact().map(function (color_pair) {