mirror of https://github.com/OCA/web.git
[MIG] web_timeline: Migration to 12.0
parent
82af9cc159
commit
8d6b546bab
|
@ -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
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
:alt: License: AGPL-3
|
:alt: License: AGPL-3
|
||||||
|
.. |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|
|
||||||
Timeline view
|
|
||||||
=============
|
|
||||||
|
|
||||||
Define a new view displaying events in an interactive visualization chart.
|
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
|
http://visjs.org/timeline_examples.html
|
||||||
|
|
||||||
|
**Table of contents**
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
|
||||||
Configuration
|
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
|
By holding the Ctrl key and dragging left to right, you can create a new record
|
||||||
with the dragged start and end date.
|
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
|
Known issues / Roadmap
|
||||||
======================
|
======================
|
||||||
|
|
||||||
|
@ -132,21 +151,26 @@ Known issues / Roadmap
|
||||||
Bug Tracker
|
Bug Tracker
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Bugs are tracked on `GitHub Issues
|
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
|
||||||
<https://github.com/OCA/web/issues>`_. In case of trouble, please
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
check there if your issue has already been reported. If you spotted it first,
|
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||||
help us smashing it by providing a detailed and welcomed feedback.
|
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_timeline%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||||
|
|
||||||
|
Do not contact contributors directly about support or help with technical issues.
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Images
|
Authors
|
||||||
------
|
~~~~~~~
|
||||||
|
|
||||||
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
* ACSONE SA/NV
|
||||||
|
* Tecnativa
|
||||||
|
* Monk Software
|
||||||
|
* Onestein
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
------------
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||||
|
@ -155,19 +179,35 @@ Contributors
|
||||||
* Adrien Didenot <adrien.didenot@horanet.com>
|
* Adrien Didenot <adrien.didenot@horanet.com>
|
||||||
* Dennis Sluijk <d.sluijk@onestein.nl>
|
* Dennis Sluijk <d.sluijk@onestein.nl>
|
||||||
|
|
||||||
Do not contact contributors directly about support or help with technical issues.
|
Other credits
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
Maintainer
|
Images
|
||||||
----------
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||||
|
|
||||||
|
Maintainers
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/logo.png
|
.. image:: https://odoo-community.org/logo.png
|
||||||
:alt: Odoo Community Association
|
:alt: Odoo Community Association
|
||||||
:target: https://odoo-community.org
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
This module is maintained by the OCA.
|
|
||||||
|
|
||||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
mission is to support the collaborative development of Odoo features and
|
mission is to support the collaborative development of Odoo features and
|
||||||
promote its widespread use.
|
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 <https://odoo-community.org/page/maintainer-role>`_:
|
||||||
|
|
||||||
|
|maintainer-tarteo|
|
||||||
|
|
||||||
|
This module is part of the `OCA/web <https://github.com/OCA/web/tree/12.0/web_timeline>`_ project on GitHub.
|
||||||
|
|
||||||
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
{
|
{
|
||||||
'name': "Web timeline",
|
'name': "Web timeline",
|
||||||
'summary': "Interactive visualization chart to show events in time",
|
'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, '
|
'author': 'ACSONE SA/NV, '
|
||||||
'Tecnativa, '
|
'Tecnativa, '
|
||||||
'Monk Software, '
|
'Monk Software, '
|
||||||
|
@ -14,7 +15,7 @@
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"application": False,
|
"application": False,
|
||||||
"installable": True,
|
"installable": True,
|
||||||
"website": "http://acsone.eu",
|
"website": "https://github.com/OCA/web",
|
||||||
'depends': [
|
'depends': [
|
||||||
'web',
|
'web',
|
||||||
],
|
],
|
||||||
|
@ -24,4 +25,5 @@
|
||||||
'data': [
|
'data': [
|
||||||
'views/web_timeline.xml',
|
'views/web_timeline.xml',
|
||||||
],
|
],
|
||||||
|
"maintainers": ["tarteo"],
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
You need to define a view with the tag <timeline> 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
|
||||||
|
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_task_timeline" model="ir.ui.view">
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="type">timeline</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<timeline date_start="date_start"
|
||||||
|
date_stop="date_end"
|
||||||
|
string="Tasks"
|
||||||
|
default_group_by="user_id"
|
||||||
|
event_open_popup="true"
|
||||||
|
zoomKey="ctrlKey"
|
||||||
|
colors="#ec7063:user_id == false;#2ecb71:kanban_state=='done';"
|
||||||
|
dependency_arrow="task_dependency_ids">
|
||||||
|
<field name="user_id"/>
|
||||||
|
<templates>
|
||||||
|
<div t-name="timeline-item">
|
||||||
|
<div t-esc="record.display_name"/>
|
||||||
|
Assigned to:
|
||||||
|
<span t-esc="record.user_id[1]"/>
|
||||||
|
</div>
|
||||||
|
</templates>
|
||||||
|
</timeline>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.action_view_task" model="ir.actions.act_window">
|
||||||
|
<field name="view_mode">kanban,tree,form,calendar,gantt,timeline,graph</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
|
@ -0,0 +1,6 @@
|
||||||
|
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||||
|
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||||
|
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||||
|
* Leonardo Donelli <donelli@webmonks.it>
|
||||||
|
* Adrien Didenot <adrien.didenot@horanet.com>
|
||||||
|
* Dennis Sluijk <d.sluijk@onestein.nl>
|
|
@ -0,0 +1,4 @@
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
* Implement a more efficient way of refreshing timeline after a record update.
|
|
@ -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.
|
|
@ -5,14 +5,37 @@ odoo.define('web_timeline.TimelineCanvas', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to draw stuff on upon the timeline view.
|
||||||
|
*/
|
||||||
var TimelineCanvas = Widget.extend({
|
var TimelineCanvas = Widget.extend({
|
||||||
template: 'TimelineView.Canvas',
|
template: 'TimelineView.Canvas',
|
||||||
|
|
||||||
clear: function() {
|
/**
|
||||||
|
* Clears all drawings (svg elements) from the canvas.
|
||||||
|
*/
|
||||||
|
clear: function () {
|
||||||
this.$el.find(' > :not(defs)').remove();
|
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 halfHeight1 = height1 / 2;
|
||||||
var halfHeight2 = height2 / 2;
|
var halfHeight2 = height2 / 2;
|
||||||
var x1 = coordx1 - widthMarker;
|
var x1 = coordx1 - widthMarker;
|
||||||
|
@ -37,21 +60,42 @@ odoo.define('web_timeline.TimelineCanvas', function (require) {
|
||||||
points.push([x2 + breakAt, y2]);
|
points.push([x2 + breakAt, y2]);
|
||||||
}
|
}
|
||||||
} else if(x1 < x2) {
|
} else if(x1 < x2) {
|
||||||
points.push([x1 - breakAt, y1]);
|
points.push([x1 - breakAt, y1]);
|
||||||
points.push([x1 - breakAt, y1 + spaceY]);
|
points.push([x1 - breakAt, y1 + spaceY]);
|
||||||
points.push([x2 + breakAt, y2 + spaceY]);
|
points.push([x2 + breakAt, y2 + spaceY]);
|
||||||
points.push([x2 + breakAt, y2]);
|
points.push([x2 + breakAt, y2]);
|
||||||
}
|
}
|
||||||
points.push([x2, y2]);
|
points.push([x2, y2]);
|
||||||
|
|
||||||
return points;
|
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);
|
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,
|
var x1 = from.offsetLeft,
|
||||||
y1 = from.offsetTop + from.parentElement.offsetTop,
|
y1 = from.offsetTop + from.parentElement.offsetTop,
|
||||||
x2 = to.offsetLeft,
|
x2 = to.offsetLeft,
|
||||||
|
@ -81,8 +125,7 @@ odoo.define('web_timeline.TimelineCanvas', function (require) {
|
||||||
}
|
}
|
||||||
this.$el.append(line);
|
this.$el.append(line);
|
||||||
return line;
|
return line;
|
||||||
}
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return TimelineCanvas;
|
return TimelineCanvas;
|
||||||
|
|
|
@ -1,256 +1,307 @@
|
||||||
odoo.define('web_timeline.TimelineController', function (require) {
|
odoo.define('web_timeline.TimelineController', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var AbstractController = require('web.AbstractController');
|
var AbstractController = require('web.AbstractController');
|
||||||
var dialogs = require('web.view_dialogs');
|
var dialogs = require('web.view_dialogs');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var time = require('web.time');
|
var time = require('web.time');
|
||||||
var Dialog = require('web.Dialog');
|
var Dialog = require('web.Dialog');
|
||||||
|
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
|
|
||||||
var CalendarController = AbstractController.extend({
|
var TimelineController = AbstractController.extend({
|
||||||
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
|
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
|
||||||
onGroupClick: '_onGroupClick',
|
onGroupClick: '_onGroupClick',
|
||||||
onUpdate: '_onUpdate',
|
onUpdate: '_onUpdate',
|
||||||
onRemove: '_onRemove',
|
onRemove: '_onRemove',
|
||||||
onMove: '_onMove',
|
onMove: '_onMove',
|
||||||
onAdd: '_onAdd',
|
onAdd: '_onAdd',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
init: function (parent, model, renderer, params) {
|
/**
|
||||||
this._super.apply(this, arguments);
|
* @constructor
|
||||||
this.open_popup_action = params.open_popup_action;
|
* @override
|
||||||
this.date_start = params.date_start;
|
*/
|
||||||
this.date_stop = params.date_stop;
|
init: function (parent, model, renderer, params) {
|
||||||
this.date_delay = params.date_delay;
|
this._super.apply(this, arguments);
|
||||||
this.context = params.actionContext;
|
this.open_popup_action = params.open_popup_action;
|
||||||
this.moveQueue = [];
|
this.date_start = params.date_start;
|
||||||
this.debouncedInternalMove = _.debounce(this.internalMove, 0);
|
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);
|
* @override
|
||||||
if (_.isEmpty(params)){
|
*/
|
||||||
return;
|
update: function (params, options) {
|
||||||
}
|
this._super.apply(this, arguments);
|
||||||
var defaults = _.defaults({}, options, {
|
if (_.isEmpty(params)){
|
||||||
adjust_window: true
|
return;
|
||||||
});
|
}
|
||||||
var self = this;
|
var defaults = _.defaults({}, options, {
|
||||||
var domains = params.domain;
|
adjust_window: true
|
||||||
var contexts = params.context;
|
});
|
||||||
var group_bys = params.groupBy;
|
var self = this;
|
||||||
this.last_domains = domains;
|
var domains = params.domain;
|
||||||
this.last_contexts = contexts;
|
var contexts = params.context;
|
||||||
// select the group by
|
var group_bys = params.groupBy;
|
||||||
var n_group_bys = [];
|
this.last_domains = domains;
|
||||||
if (this.renderer.arch.attrs.default_group_by) {
|
this.last_contexts = contexts;
|
||||||
n_group_bys = this.renderer.arch.attrs.default_group_by.split(',');
|
// select the group by
|
||||||
}
|
var n_group_bys = [];
|
||||||
if (group_bys.length) {
|
if (this.renderer.arch.attrs.default_group_by) {
|
||||||
n_group_bys = group_bys;
|
n_group_bys = this.renderer.arch.attrs.default_group_by.split(',');
|
||||||
}
|
}
|
||||||
this.renderer.last_group_bys = n_group_bys;
|
if (group_bys.length) {
|
||||||
this.renderer.last_domains = domains;
|
n_group_bys = group_bys;
|
||||||
|
}
|
||||||
|
this.renderer.last_group_bys = n_group_bys;
|
||||||
|
this.renderer.last_domains = domains;
|
||||||
|
|
||||||
var fields = this.renderer.fieldNames;
|
var fields = this.renderer.fieldNames;
|
||||||
fields = _.uniq(fields.concat(n_group_bys));
|
fields = _.uniq(fields.concat(n_group_bys));
|
||||||
self._rpc({
|
self._rpc({
|
||||||
model: self.model.modelName,
|
model: self.model.modelName,
|
||||||
method: 'search_read',
|
method: 'search_read',
|
||||||
kwargs: {
|
kwargs: {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
domain: domains,
|
domain: domains,
|
||||||
},
|
},
|
||||||
context: self.getSession().user_context,
|
context: self.getSession().user_context,
|
||||||
}).then(function(data) {
|
}).then(function(data) {
|
||||||
return self.renderer.on_data_loaded(data, n_group_bys, defaults.adjust_window);
|
return self.renderer.on_data_loaded(data, n_group_bys, defaults.adjust_window);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onGroupClick: function (event) {
|
/**
|
||||||
var groupField = this.renderer.last_group_bys[0];
|
* Gets triggered when a group in the timeline is clicked (by the TimelineRenderer).
|
||||||
return this.do_action({
|
*
|
||||||
type: 'ir.actions.act_window',
|
* @private
|
||||||
res_model: this.renderer.view.fields[groupField].relation,
|
* @returns {jQuery.Deferred}
|
||||||
res_id: event.data.item.group,
|
*/
|
||||||
target: 'new',
|
_onGroupClick: function (event) {
|
||||||
views: [[false, 'form']]
|
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']]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_onUpdate: function(event) {
|
/**
|
||||||
var self = this;
|
* Opens a form view of a clicked timeline item (triggered by the TimelineRenderer).
|
||||||
this.renderer = event.data.renderer;
|
*
|
||||||
var rights = event.data.rights;
|
* @private
|
||||||
var item = event.data.item;
|
*/
|
||||||
var id = item.evt.id;
|
_onUpdate: function (event) {
|
||||||
var title = item.evt.__name;
|
var self = this;
|
||||||
if (this.open_popup_action) {
|
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 {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.moveQueue.push({
|
||||||
|
id: event.data.item.id,
|
||||||
|
data: data,
|
||||||
|
event: event
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 < self.model.data.data.length; i++) {
|
||||||
|
if (self.model.data.data[i].id === event.data.item.id) {
|
||||||
|
unlink_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isNaN(unlink_index)) {
|
||||||
|
self.model.data.data.splice(unlink_index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.data.callback(event.data.item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = _t("Are you sure you want to delete this record?");
|
||||||
|
var def = $.Deferred();
|
||||||
|
Dialog.confirm(this, message, {
|
||||||
|
title: _t("Warning"),
|
||||||
|
confirm_callback: function() {
|
||||||
|
do_it(e)
|
||||||
|
.done(def.resolve.bind(def, true))
|
||||||
|
.fail(def.reject.bind(def));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return def.promise();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, {
|
new dialogs.FormViewDialog(this, {
|
||||||
res_model: this.model.modelName,
|
res_model: this.model.modelName,
|
||||||
res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id,
|
res_id: null,
|
||||||
context: this.getSession().user_context,
|
context: _.extend(default_context, this.context),
|
||||||
title: title,
|
|
||||||
view_id: Number(this.open_popup_action),
|
view_id: Number(this.open_popup_action),
|
||||||
on_saved: function () {
|
on_saved: function (record) {
|
||||||
self.write_completed();
|
self.create_completed([record.res_id]);
|
||||||
},
|
},
|
||||||
}).open();
|
}).open().on('closed', this, function () {
|
||||||
} else {
|
event.data.callback();
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onMove: function(event) {
|
return false;
|
||||||
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.moveQueue.push({
|
/**
|
||||||
id: event.data.item.id,
|
* Triggered upon completion of a new record.
|
||||||
data: data,
|
* Updates the timeline view with the new record.
|
||||||
event: event
|
*
|
||||||
});
|
* @returns {jQuery.Deferred}
|
||||||
|
*/
|
||||||
this.debouncedInternalMove();
|
create_completed: function (id) {
|
||||||
},
|
var self = this;
|
||||||
|
return this._rpc({
|
||||||
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;
|
|
||||||
|
|
||||||
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<self.model.data.data.length; i++) {
|
|
||||||
if (self.model.data.data[i].id === event.data.item.id) {
|
|
||||||
unlink_index = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isNaN(unlink_index)) {
|
|
||||||
self.model.data.data.splice(unlink_index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.data.callback(event.data.item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = _t("Are you sure you want to delete this record?");
|
|
||||||
var def = $.Deferred();
|
|
||||||
Dialog.confirm(this, message, {
|
|
||||||
title: _t("Warning"),
|
|
||||||
confirm_callback: function() {
|
|
||||||
do_it(e)
|
|
||||||
.done(def.resolve.bind(def, true))
|
|
||||||
.fail(def.reject.bind(def));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return def.promise();
|
|
||||||
},
|
|
||||||
|
|
||||||
_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').toDate();
|
|
||||||
}
|
|
||||||
if (this.date_stop && item.end) {
|
|
||||||
default_context['default_'.concat(this.date_stop)] = moment(item.end).add(1, 'hours').toDate();
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
|
|
||||||
create_completed: function (id) {
|
|
||||||
var self = this;
|
|
||||||
return this._rpc({
|
|
||||||
model: this.model.modelName,
|
model: this.model.modelName,
|
||||||
method: 'read',
|
method: 'read',
|
||||||
args: [
|
args: [
|
||||||
|
@ -258,26 +309,29 @@ var CalendarController = AbstractController.extend({
|
||||||
this.model.fieldNames,
|
this.model.fieldNames,
|
||||||
],
|
],
|
||||||
context: this.context,
|
context: this.context,
|
||||||
})
|
})
|
||||||
.then(function (records) {
|
.then(function (records) {
|
||||||
var new_event = self.renderer.event_data_transform(records[0]);
|
var new_event = self.renderer.event_data_transform(records[0]);
|
||||||
var items = self.renderer.timeline.itemsData;
|
var items = self.renderer.timeline.itemsData;
|
||||||
items.add(new_event);
|
items.add(new_event);
|
||||||
self.renderer.timeline.setItems(items);
|
self.renderer.timeline.setItems(items);
|
||||||
self.reload();
|
self.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
write_completed: function (options) {
|
/**
|
||||||
var params = {
|
* Triggered upon completion of writing a record.
|
||||||
domain: this.renderer.last_domains,
|
*/
|
||||||
context: this.context,
|
write_completed: function (options) {
|
||||||
groupBy: this.renderer.last_group_bys,
|
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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,58 +1,71 @@
|
||||||
odoo.define('web_timeline.TimelineModel', function (require) {
|
odoo.define('web_timeline.TimelineModel', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var AbstractModel = require('web.AbstractModel');
|
var AbstractModel = require('web.AbstractModel');
|
||||||
|
|
||||||
var TimelineModel = AbstractModel.extend({
|
var TimelineModel = AbstractModel.extend({
|
||||||
init: function () {
|
|
||||||
this._super.apply(this, arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
load: function (params) {
|
/**
|
||||||
var self = this;
|
* @constructor
|
||||||
this.modelName = params.modelName;
|
*/
|
||||||
this.fieldNames = params.fieldNames;
|
init: function () {
|
||||||
if (!this.preload_def) {
|
this._super.apply(this, arguments);
|
||||||
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,
|
* @override
|
||||||
context: params.context,
|
*/
|
||||||
};
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return this.preload_def.then(this._loadTimeline.bind(this));
|
this.data = {
|
||||||
},
|
domain: params.domain,
|
||||||
|
context: params.context,
|
||||||
|
};
|
||||||
|
|
||||||
_loadTimeline: function () {
|
return this.preload_def.then(this._loadTimeline.bind(this));
|
||||||
var self = this;
|
},
|
||||||
return self._rpc({
|
|
||||||
|
/**
|
||||||
|
* Read the records for the timeline.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {jQuery.Deferred}
|
||||||
|
*/
|
||||||
|
_loadTimeline: function () {
|
||||||
|
var self = this;
|
||||||
|
return self._rpc({
|
||||||
model: self.modelName,
|
model: self.modelName,
|
||||||
method: 'search_read',
|
method: 'search_read',
|
||||||
context: self.data.context,
|
context: self.data.context,
|
||||||
fields: self.fieldNames,
|
fields: self.fieldNames,
|
||||||
domain: self.data.domain,
|
domain: self.data.domain,
|
||||||
})
|
})
|
||||||
.then(function (events) {
|
.then(function (events) {
|
||||||
self.data.data = events;
|
self.data.data = events;
|
||||||
self.data.rights = {
|
self.data.rights = {
|
||||||
'unlink': self.unlink_right,
|
'unlink': self.unlink_right,
|
||||||
'create': self.create_right,
|
'create': self.create_right,
|
||||||
'write': self.write_right,
|
'write': self.write_right,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return TimelineModel;
|
return TimelineModel;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,421 +1,537 @@
|
||||||
odoo.define('web_timeline.TimelineRenderer', function (require) {
|
odoo.define('web_timeline.TimelineRenderer', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var AbstractRenderer = require('web.AbstractRenderer');
|
var AbstractRenderer = require('web.AbstractRenderer');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var time = require('web.time');
|
var time = require('web.time');
|
||||||
var utils = require('web.utils');
|
var utils = require('web.utils');
|
||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
var QWeb = require('web.QWeb');
|
var QWeb = require('web.QWeb');
|
||||||
var field_utils = require('web.field_utils');
|
var field_utils = require('web.field_utils');
|
||||||
var TimelineCanvas = require('web_timeline.TimelineCanvas');
|
var TimelineCanvas = require('web_timeline.TimelineCanvas');
|
||||||
|
|
||||||
|
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
|
|
||||||
var CalendarRenderer = AbstractRenderer.extend({
|
var TimelineRenderer = AbstractRenderer.extend({
|
||||||
template: "TimelineView",
|
template: "TimelineView",
|
||||||
events: _.extend({}, AbstractRenderer.prototype.events, {
|
|
||||||
}),
|
|
||||||
|
|
||||||
init: function (parent, state, params) {
|
events: _.extend({}, AbstractRenderer.prototype.events, {
|
||||||
this._super.apply(this, arguments);
|
'click .oe_timeline_button_today': '_onTodayClicked',
|
||||||
this.modelName = params.model;
|
'click .oe_timeline_button_scale_day': '_onScaleDayClicked',
|
||||||
this.mode = params.mode;
|
'click .oe_timeline_button_scale_week': '_onScaleWeekClicked',
|
||||||
this.options = params.options;
|
'click .oe_timeline_button_scale_month': '_onScaleMonthClicked',
|
||||||
this.permissions = params.permissions;
|
'click .oe_timeline_button_scale_year': '_onScaleYearClicked',
|
||||||
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;
|
* @constructor
|
||||||
var attrs = this.arch.attrs;
|
*/
|
||||||
this.current_window = {
|
init: function (parent, state, params) {
|
||||||
start: new moment(),
|
this._super.apply(this, arguments);
|
||||||
end: new moment().add(24, 'hours')
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
this.$el.addClass(attrs.class);
|
/**
|
||||||
this.$timeline = this.$el.find(".oe_timeline_widget");
|
* @override
|
||||||
|
*/
|
||||||
|
start: function () {
|
||||||
|
var self = this;
|
||||||
|
var attrs = this.arch.attrs;
|
||||||
|
this.current_window = {
|
||||||
|
start: new moment(),
|
||||||
|
end: new moment().add(24, 'hours')
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.date_start) {
|
this.$el.addClass(attrs.class);
|
||||||
throw new Error(_t("Timeline view has not defined 'date_start' attribute."));
|
this.$timeline = this.$el.find(".oe_timeline_widget");
|
||||||
}
|
|
||||||
this._super.apply(this, self);
|
|
||||||
},
|
|
||||||
|
|
||||||
on_attach_callback: function() {
|
if (!this.date_start) {
|
||||||
var height = this.$el.parent().height() - this.$el.find('.oe_timeline_buttons').height();
|
throw new Error(_t("Timeline view has not defined 'date_start' attribute."));
|
||||||
if (height > this.min_height) {
|
}
|
||||||
this.timeline.setOptions({
|
this._super.apply(this, self);
|
||||||
height: height
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
* Set the timeline window to today (day).
|
||||||
var self = this;
|
*
|
||||||
return $.when().then(function () {
|
* @private
|
||||||
// Prevent Double Rendering on Updates
|
*/
|
||||||
if (!self.timeline) {
|
_onTodayClicked: function () {
|
||||||
self.init_timeline();
|
this.current_window = {
|
||||||
$(window).trigger('resize');
|
start: new moment(),
|
||||||
|
end: new moment().add(24, 'hours')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.timeline) {
|
||||||
|
this.timeline.setWindow(this.current_window);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
},
|
|
||||||
|
|
||||||
add_events: function() {
|
/**
|
||||||
var self = this;
|
* Scale the timeline window to a day.
|
||||||
this.$(".oe_timeline_button_today").click(function() {
|
*
|
||||||
self._onTodayClicked();
|
* @private
|
||||||
});
|
*/
|
||||||
this.$(".oe_timeline_button_scale_day").click(function() {
|
_onScaleDayClicked: function () {
|
||||||
self._onScaleDayClicked();
|
this._scaleCurrentWindow(24);
|
||||||
});
|
},
|
||||||
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 = {
|
* Scale the timeline window to a week.
|
||||||
start: new moment(),
|
*
|
||||||
end: new moment().add(24, 'hours')
|
* @private
|
||||||
};
|
*/
|
||||||
|
_onScaleWeekClicked: function () {
|
||||||
|
this._scaleCurrentWindow(24 * 7);
|
||||||
|
},
|
||||||
|
|
||||||
if (this.timeline) {
|
/**
|
||||||
this.timeline.setWindow(this.current_window);
|
* Scale the timeline window to a month.
|
||||||
}
|
*
|
||||||
},
|
* @private
|
||||||
|
*/
|
||||||
|
_onScaleMonthClicked: function () {
|
||||||
|
this._scaleCurrentWindow(24 * 30);
|
||||||
|
},
|
||||||
|
|
||||||
_onScaleDayClicked: function () {
|
/**
|
||||||
this._scaleCurrentWindow(24);
|
* Scale the timeline window to a year.
|
||||||
},
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onScaleYearClicked: function () {
|
||||||
|
this._scaleCurrentWindow(24 * 365);
|
||||||
|
},
|
||||||
|
|
||||||
_onScaleWeekClicked: function () {
|
/**
|
||||||
this._scaleCurrentWindow(24 * 7);
|
* Scales the timeline window based on the current window.
|
||||||
},
|
*
|
||||||
|
* @param {Integer} factor The timespan (in hours) the window must be scaled to.
|
||||||
_onScaleMonthClicked: function () {
|
* @private
|
||||||
this._scaleCurrentWindow(24 * 30);
|
*/
|
||||||
},
|
_scaleCurrentWindow: function (factor) {
|
||||||
|
if (this.timeline) {
|
||||||
_onScaleYearClicked: function () {
|
this.current_window = this.timeline.getWindow();
|
||||||
this._scaleCurrentWindow(24 * 365);
|
this.current_window.end = moment(this.current_window.start).add(factor, 'hours');
|
||||||
},
|
this.timeline.setWindow(this.current_window);
|
||||||
|
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
if (end && start) {
|
},
|
||||||
this.options.start = start;
|
|
||||||
this.options.end = end;
|
/**
|
||||||
} else {
|
* Computes the initial visible window.
|
||||||
this.mode = 'fit';
|
*
|
||||||
|
* @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;
|
* Initializes the timeline (http://visjs.org/docs/timeline/).
|
||||||
this._computeMode();
|
*
|
||||||
this.options.editable = {
|
* @private
|
||||||
// add new items by double tapping
|
*/
|
||||||
add: this.modelClass.data.rights.create,
|
init_timeline: function () {
|
||||||
// drag items horizontally
|
var self = this;
|
||||||
updateTime: this.modelClass.data.rights.write,
|
this._computeMode();
|
||||||
// drag items from one group to another
|
this.options.editable = {
|
||||||
updateGroup: this.modelClass.data.rights.write,
|
// add new items by double tapping
|
||||||
// delete an item by tapping the delete button top right
|
add: this.modelClass.data.rights.create,
|
||||||
remove: this.modelClass.data.rights.unlink,
|
// drag items horizontally
|
||||||
};
|
updateTime: this.modelClass.data.rights.write,
|
||||||
$.extend(this.options, {
|
// drag items from one group to another
|
||||||
onAdd: self.on_add,
|
updateGroup: this.modelClass.data.rights.write,
|
||||||
onMove: self.on_move,
|
// delete an item by tapping the delete button top right
|
||||||
onUpdate: self.on_update,
|
remove: this.modelClass.data.rights.unlink,
|
||||||
onRemove: self.on_remove
|
};
|
||||||
});
|
$.extend(this.options, {
|
||||||
this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
|
onAdd: self.on_add,
|
||||||
if (this.arch.children.length) {
|
onMove: self.on_move,
|
||||||
var tmpl = utils.json_node_to_xml(
|
onUpdate: self.on_update,
|
||||||
_.filter(this.arch.children, function(item) {
|
onRemove: self.on_remove
|
||||||
return item.tag === 'templates';
|
});
|
||||||
})[0]
|
this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
|
||||||
);
|
if (this.arch.children.length) {
|
||||||
this.qweb.add_template(tmpl);
|
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 = new vis.Timeline(self.$timeline.empty().get(0));
|
||||||
this.timeline.setOptions(this.options);
|
this.timeline.setOptions(this.options);
|
||||||
if (self.mode && self['on_scale_' + self.mode + '_clicked']) {
|
if (self.mode && self['on_scale_' + self.mode + '_clicked']) {
|
||||||
self['on_scale_' + self.mode + '_clicked']();
|
self['on_scale_' + self.mode + '_clicked']();
|
||||||
}
|
}
|
||||||
this.timeline.on('click', self.on_group_click);
|
this.timeline.on('click', self.on_group_click);
|
||||||
var group_bys = this.arch.attrs.default_group_by.split(',');
|
var group_bys = this.arch.attrs.default_group_by.split(',');
|
||||||
this.last_group_bys = group_bys;
|
this.last_group_bys = group_bys;
|
||||||
this.last_domains = this.modelClass.data.domain;
|
this.last_domains = this.modelClass.data.domain;
|
||||||
this.on_data_loaded(this.modelClass.data.data, group_bys);
|
this.on_data_loaded(this.modelClass.data.data, group_bys);
|
||||||
this.$centerContainer = $(this.timeline.dom.centerContainer);
|
this.$centerContainer = $(this.timeline.dom.centerContainer);
|
||||||
this.canvas = new TimelineCanvas(this);
|
this.canvas = new TimelineCanvas(this);
|
||||||
this.canvas.appendTo(this.$centerContainer);
|
this.canvas.appendTo(this.$centerContainer);
|
||||||
this.timeline.on('changed', function() {
|
this.timeline.on('changed', function() {
|
||||||
self.draw_canvas();
|
self.draw_canvas();
|
||||||
self.canvas.$el.attr(
|
self.canvas.$el.attr(
|
||||||
'style',
|
'style',
|
||||||
self.$el.find('.vis-content').attr('style') + self.$el.find('.vis-itemset').attr('style')
|
self.$el.find('.vis-content').attr('style') + self.$el.find('.vis-itemset').attr('style')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
draw_canvas: function() {
|
/**
|
||||||
this.canvas.clear();
|
* Clears and draws the canvas items.
|
||||||
if (this.dependency_arrow) {
|
*
|
||||||
this.draw_dependencies();
|
* @private
|
||||||
}
|
*/
|
||||||
},
|
draw_canvas: function () {
|
||||||
|
this.canvas.clear();
|
||||||
|
if (this.dependency_arrow) {
|
||||||
|
this.draw_dependencies();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
draw_dependencies: function() {
|
/**
|
||||||
var self = this;
|
* Draw item dependencies on canvas.
|
||||||
var items = this.timeline.itemSet.items;
|
*
|
||||||
_.each(items, function(item) {
|
* @private
|
||||||
if (!item.data.evt) {
|
*/
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
return;
|
||||||
}
|
}
|
||||||
_.each(item.data.evt[self.dependency_arrow], function(id) {
|
|
||||||
if (id in items) {
|
var defaults = _.defaults({}, options, {
|
||||||
self.draw_dependency(item, items[id]);
|
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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
draw_dependency: function(from, to, options) {
|
var mode = !this.mode || this.mode === 'fit';
|
||||||
if (!from.displayed || !to.displayed) {
|
var adjust = _.isUndefined(adjust_window) || adjust_window;
|
||||||
return;
|
if (mode && adjust) {
|
||||||
}
|
this.timeline.fit();
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
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) {
|
* Get the groups.
|
||||||
if (group_bys.length === 0) {
|
*
|
||||||
return events;
|
* @private
|
||||||
}
|
* @returns {Array}
|
||||||
var groups = [];
|
*/
|
||||||
groups.push({id: -1, content: _t('-')});
|
split_groups: function (events, group_bys) {
|
||||||
_.each(events, function (event) {
|
if (group_bys.length === 0) {
|
||||||
var group_name = event[_.first(group_bys)];
|
return events;
|
||||||
if (group_name) {
|
}
|
||||||
if (group_name instanceof Array) {
|
var groups = [];
|
||||||
var group = _.find(groups, function (existing_group) {
|
groups.push({id: -1, content: _t('-')});
|
||||||
return _.isEqual(existing_group.id, group_name[0]);
|
_.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)) {
|
if (_.isUndefined(group)) {
|
||||||
group = {
|
group = {
|
||||||
id: group_name[0],
|
id: group_name[0],
|
||||||
content: group_name[1]
|
content: group_name[1]
|
||||||
};
|
};
|
||||||
groups.push(group);
|
groups.push(group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
return groups;
|
||||||
|
},
|
||||||
|
|
||||||
console.error(
|
/**
|
||||||
_t('Template "timeline-item" not present in timeline view definition.')
|
* 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;
|
||||||
|
|
||||||
on_group_click: function (e) {
|
var date_delay = evt[this.date_delay] || false,
|
||||||
// handle a click on a group header
|
all_day = this.all_day ? evt[this.all_day] : false;
|
||||||
if (e.what === 'group-label' && e.group !== -1) {
|
|
||||||
this._trigger(e, function() {
|
|
||||||
// Do nothing
|
|
||||||
}, 'onGroupClick');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
on_update: function (item, callback) {
|
if (all_day) {
|
||||||
this._trigger(item, callback, 'onUpdate');
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
on_move: function (item, callback) {
|
if (!date_stop && date_delay) {
|
||||||
this._trigger(item, callback, 'onMove');
|
date_stop = moment(date_start).add(date_delay, 'hours').toDate();
|
||||||
},
|
}
|
||||||
|
|
||||||
on_remove: function (item, callback) {
|
var group = evt[self.last_group_bys[0]];
|
||||||
this._trigger(item, callback, 'onRemove');
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
on_add: function (item, callback) {
|
var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name;
|
||||||
this._trigger(item, callback, 'onAdd');
|
if (this.arch.children.length) {
|
||||||
},
|
content = this.render_timeline_item(evt);
|
||||||
|
}
|
||||||
|
|
||||||
_trigger: function (item, callback, trigger) {
|
var r = {
|
||||||
this.trigger_up(trigger, {
|
'start': date_start,
|
||||||
'item': item,
|
'content': content,
|
||||||
'callback': callback,
|
'id': evt.id,
|
||||||
'rights': this.modelClass.data.rights,
|
'group': group,
|
||||||
'renderer': this,
|
'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.
|
||||||
return CalendarRenderer;
|
*
|
||||||
|
* @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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return TimelineRenderer;
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,11 +39,15 @@ odoo.define('web_timeline.TimelineView', function (require) {
|
||||||
Renderer: TimelineRenderer,
|
Renderer: TimelineRenderer,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
init: function (viewInfo, params) {
|
init: function (viewInfo, params) {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
var self = this;
|
var self = this;
|
||||||
this.timeline = false;
|
this.timeline = false;
|
||||||
this.arch = viewInfo.arch;
|
this.arch = this.rendererParams.arch;
|
||||||
var attrs = this.arch.attrs;
|
var attrs = this.arch.attrs;
|
||||||
this.fields = viewInfo.fields;
|
this.fields = viewInfo.fields;
|
||||||
this.modelName = this.controllerParams.modelName;
|
this.modelName = this.controllerParams.modelName;
|
||||||
|
@ -144,6 +148,9 @@ odoo.define('web_timeline.TimelineView', function (require) {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order function for groups.
|
||||||
|
*/
|
||||||
group_order: function (grp1, grp2) {
|
group_order: function (grp1, grp2) {
|
||||||
// display non grouped elements first
|
// display non grouped elements first
|
||||||
if (grp1.id === -1) {
|
if (grp1.id === -1) {
|
||||||
|
@ -156,6 +163,11 @@ odoo.define('web_timeline.TimelineView', function (require) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the colors attribute.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
parse_colors: function () {
|
parse_colors: function () {
|
||||||
if (this.arch.attrs.colors) {
|
if (this.arch.attrs.colors) {
|
||||||
this.colors = _(this.arch.attrs.colors.split(';')).chain().compact().map(function (color_pair) {
|
this.colors = _(this.arch.attrs.colors.split(';')).chain().compact().map(function (color_pair) {
|
||||||
|
|
Loading…
Reference in New Issue