mirror of https://github.com/OCA/web.git
[ADD] web_timeline: New dependency_arrow attribute
Update README.rst [FIX] Remove console.log [ADD] Make timeline.fit optional [FIX] Use stringified points [IMP] Reversed the arrow head and fixed lint issues [IMP] Use options parameter for line color and width [FIX] Version number [IMP] Minor improvementspull/1090/head
parent
822e5430d6
commit
8dc622978e
|
@ -39,9 +39,11 @@ the possible attributes for the tag:
|
|||
view.
|
||||
* colors (optional): it allows to set certain specific colors if the expressed
|
||||
condition (JS syntax) is met.
|
||||
|
||||
Optionally you can declare a custom template, which will be used to render the
|
||||
timeline items. You have to name the template 'timeline-item'.
|
||||
* 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.
|
||||
|
@ -65,7 +67,8 @@ Example:
|
|||
default_group_by="user_id"
|
||||
event_open_popup="true"
|
||||
zoomKey="ctrlKey"
|
||||
colors="#ec7063:user_id == false;#2ecb71:kanban_state=='done';">
|
||||
colors="#ec7063:user_id == false;#2ecb71:kanban_state=='done';"
|
||||
dependency_arrow="task_dependency_ids">
|
||||
<field name="user_id"/>
|
||||
<templates>
|
||||
<div t-name="timeline-item">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
'name': "Web timeline",
|
||||
'summary': "Interactive visualization chart to show events in time",
|
||||
"version": "11.0.1.2.1",
|
||||
"version": "11.0.1.3.0",
|
||||
'author': 'ACSONE SA/NV, '
|
||||
'Tecnativa, '
|
||||
'Monk Software, '
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import ir_view
|
||||
|
|
|
@ -20,3 +20,12 @@
|
|||
.oe_timeline_view .vlabel .inner:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.oe_timeline_view svg.oe_timeline_view_canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/* Copyright 2018 Onestein
|
||||
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||
|
||||
odoo.define('web_timeline.TimelineCanvas', function (require) {
|
||||
"use strict";
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var TimelineCanvas = Widget.extend({
|
||||
template: 'TimelineView.Canvas',
|
||||
|
||||
clear: function() {
|
||||
this.$el.find(' > :not(defs)').remove();
|
||||
},
|
||||
|
||||
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;
|
||||
var y1 = coordy1 + halfHeight1;
|
||||
var x2 = coordx2 + width2;
|
||||
var y2 = coordy2 + halfHeight2;
|
||||
var xDiff = x1 - x2;
|
||||
var yDiff = y1 - y2;
|
||||
var threshold = breakAt + widthMarker;
|
||||
var spaceY = halfHeight2 + 6;
|
||||
|
||||
var points = [[x1, y1]];
|
||||
if (y1 !== y2) {
|
||||
if (xDiff > threshold) {
|
||||
points.push([x1 - breakAt, y1]);
|
||||
points.push([x1 - breakAt, y1 - yDiff]);
|
||||
} else if (xDiff <= threshold) {
|
||||
var yDiffSpace = yDiff > 0 ? spaceY : -spaceY;
|
||||
points.push([x1 - breakAt, y1]);
|
||||
points.push([x1 - breakAt, y2 + yDiffSpace]);
|
||||
points.push([x2 + breakAt, y2 + yDiffSpace]);
|
||||
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([x2, y2]);
|
||||
|
||||
return points;
|
||||
},
|
||||
|
||||
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) {
|
||||
var x1 = from.offsetLeft,
|
||||
y1 = from.offsetTop + from.parentElement.offsetTop,
|
||||
x2 = to.offsetLeft,
|
||||
y2 = to.offsetTop + to.parentElement.offsetTop,
|
||||
width1 = from.clientWidth,
|
||||
height1 = from.clientHeight,
|
||||
width2 = to.clientWidth,
|
||||
height2 = to.clientHeight;
|
||||
|
||||
var points = this.get_polyline_points(
|
||||
x1, y1, x2, y2, width1, height1, width2, height2, widthMarker, breakLineAt
|
||||
);
|
||||
|
||||
var polyline_points = _.map(points, function(point) {
|
||||
return point.join(',');
|
||||
}).join();
|
||||
|
||||
var line = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg', 'polyline'
|
||||
);
|
||||
line.setAttribute('points', polyline_points);
|
||||
line.setAttribute('stroke', color || '#000');
|
||||
line.setAttribute('stroke-width', width || 1);
|
||||
line.setAttribute('fill', 'none');
|
||||
if (markerStart) {
|
||||
line.setAttribute('marker-start', 'url(' + markerStart + ')');
|
||||
}
|
||||
this.$el.append(line);
|
||||
return line;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return TimelineCanvas;
|
||||
});
|
|
@ -5,6 +5,7 @@ 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;
|
||||
|
||||
|
@ -31,6 +32,9 @@ var CalendarController = AbstractController.extend({
|
|||
if (_.isEmpty(params)){
|
||||
return;
|
||||
}
|
||||
var defaults = _.defaults({}, options, {
|
||||
adjust_window: true
|
||||
});
|
||||
var self = this;
|
||||
var domains = params.domain;
|
||||
var contexts = params.context;
|
||||
|
@ -59,7 +63,7 @@ var CalendarController = AbstractController.extend({
|
|||
},
|
||||
context: self.getSession().user_context,
|
||||
}).then(function(data) {
|
||||
return self.renderer.on_data_loaded(data, n_group_bys);
|
||||
return self.renderer.on_data_loaded(data, n_group_bys, defaults.adjust_window);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -82,13 +86,13 @@ var CalendarController = AbstractController.extend({
|
|||
var id = item.evt.id;
|
||||
var title = item.evt.__name;
|
||||
if (this.open_popup_action) {
|
||||
var dialog = new dialogs.FormViewDialog(this, {
|
||||
new dialogs.FormViewDialog(this, {
|
||||
res_model: this.model.modelName,
|
||||
res_id: parseInt(id).toString() == id ? parseInt(id) : id,
|
||||
res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id,
|
||||
context: this.getSession().user_context,
|
||||
title: title,
|
||||
view_id: +this.open_popup_action,
|
||||
on_saved: function (record) {
|
||||
view_id: Number(this.open_popup_action),
|
||||
on_saved: function () {
|
||||
self.write_completed();
|
||||
},
|
||||
}).open();
|
||||
|
@ -99,7 +103,7 @@ var CalendarController = AbstractController.extend({
|
|||
}
|
||||
this.trigger_up('switch_view', {
|
||||
view_type: 'form',
|
||||
res_id: parseInt(id).toString() == id ? parseInt(id) : id,
|
||||
res_id: parseInt(id, 10).toString() === id ? parseInt(id, 10) : id,
|
||||
mode: mode,
|
||||
model: this.model.modelName,
|
||||
});
|
||||
|
@ -114,7 +118,7 @@ var CalendarController = AbstractController.extend({
|
|||
var event_start = item.start;
|
||||
var event_end = item.end;
|
||||
var group = false;
|
||||
if (item.group != -1) {
|
||||
if (item.group !== -1) {
|
||||
group = item.group;
|
||||
}
|
||||
var data = {};
|
||||
|
@ -145,10 +149,13 @@ var CalendarController = AbstractController.extend({
|
|||
context: self.getSession().user_context,
|
||||
}).then(function() {
|
||||
event.data.callback(event.data.item);
|
||||
self.write_completed({
|
||||
adjust_window: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onRemove: function(event) {
|
||||
_onRemove: function(e) {
|
||||
var self = this;
|
||||
|
||||
function do_it(event) {
|
||||
|
@ -162,8 +169,9 @@ var CalendarController = AbstractController.extend({
|
|||
}).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)
|
||||
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);
|
||||
|
@ -173,9 +181,17 @@ var CalendarController = AbstractController.extend({
|
|||
});
|
||||
}
|
||||
|
||||
if (confirm(_t("Are you sure you want to delete this record ?"))) {
|
||||
return do_it(event);
|
||||
}
|
||||
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) {
|
||||
|
@ -194,11 +210,11 @@ var CalendarController = AbstractController.extend({
|
|||
default_context['default_'.concat(this.renderer.last_group_bys[0])] = item.group;
|
||||
}
|
||||
// Show popup
|
||||
var dialog = new dialogs.FormViewDialog(this, {
|
||||
new dialogs.FormViewDialog(this, {
|
||||
res_model: this.model.modelName,
|
||||
res_id: null,
|
||||
context: _.extend(default_context, this.context),
|
||||
view_id: +this.open_popup_action,
|
||||
view_id: Number(this.open_popup_action),
|
||||
on_saved: function (record) {
|
||||
self.create_completed([record.res_id]);
|
||||
},
|
||||
|
@ -226,13 +242,14 @@ var CalendarController = AbstractController.extend({
|
|||
});
|
||||
},
|
||||
|
||||
write_completed: function () {
|
||||
write_completed: function (options) {
|
||||
var params = {
|
||||
domain: this.renderer.last_domains,
|
||||
context: this.context,
|
||||
groupBy: this.renderer.last_group_bys,
|
||||
};
|
||||
this.update(params, null);
|
||||
|
||||
this.update(params, options);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ 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;
|
||||
|
@ -29,6 +30,7 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
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;
|
||||
},
|
||||
|
@ -64,16 +66,21 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
|
||||
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();});
|
||||
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 () {
|
||||
|
@ -156,8 +163,8 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
onUpdate: self.on_update,
|
||||
onRemove: self.on_remove,
|
||||
});
|
||||
this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
|
||||
if (this.arch.children.length) {
|
||||
this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
|
||||
var tmpl = utils.json_node_to_xml(
|
||||
_.filter(this.arch.children, function(item) {
|
||||
return item.tag === 'templates';
|
||||
|
@ -176,9 +183,47 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
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();
|
||||
});
|
||||
},
|
||||
|
||||
on_data_loaded: function (events, group_bys) {
|
||||
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) {
|
||||
_.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({
|
||||
|
@ -196,11 +241,11 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
})[1]
|
||||
}, event);
|
||||
});
|
||||
return self.on_data_loaded_2(nevents, group_bys);
|
||||
return self.on_data_loaded_2(nevents, group_bys, adjust_window);
|
||||
});
|
||||
},
|
||||
|
||||
on_data_loaded_2: function (events, group_bys) {
|
||||
on_data_loaded_2: function (events, group_bys, adjust_window) {
|
||||
var self = this;
|
||||
var data = [];
|
||||
var groups = [];
|
||||
|
@ -213,7 +258,9 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
groups = this.split_groups(events, group_bys);
|
||||
this.timeline.setGroups(groups);
|
||||
this.timeline.setItems(data);
|
||||
if (!this.mode || this.mode == 'fit'){
|
||||
var mode = !this.mode || this.mode === 'fit';
|
||||
var adjust = _.isUndefined(adjust_window) || adjust_window;
|
||||
if (mode && adjust) {
|
||||
this.timeline.fit();
|
||||
}
|
||||
},
|
||||
|
@ -229,11 +276,15 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
var group_name = event[_.first(group_bys)];
|
||||
if (group_name) {
|
||||
if (group_name instanceof Array) {
|
||||
var group = _.find(groups, function (group) {
|
||||
return _.isEqual(group.id, group_name[0]);
|
||||
var group = _.find(groups, function (existing_group) {
|
||||
return _.isEqual(existing_group.id, group_name[0]);
|
||||
});
|
||||
if (group == null) {
|
||||
group = {id: group_name[0], content: group_name[1]};
|
||||
|
||||
if (_.isUndefined(group)) {
|
||||
group = {
|
||||
id: group_name[0],
|
||||
content: group_name[1]
|
||||
};
|
||||
groups.push(group);
|
||||
}
|
||||
}
|
||||
|
@ -262,8 +313,7 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
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_start) {
|
||||
}
|
||||
|
||||
if (!date_stop && date_delay) {
|
||||
date_stop = moment(date_start).add(date_delay, 'hours').toDate();
|
||||
}
|
||||
|
@ -280,16 +330,9 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
}
|
||||
});
|
||||
|
||||
var content = evt.__name != undefined ? evt.__name : evt.display_name;
|
||||
var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name;
|
||||
if (this.arch.children.length) {
|
||||
if(this.qweb.has_template('timeline-item')) {
|
||||
content = this.qweb.render('timeline-item', {
|
||||
'record': evt,
|
||||
'field_utils': field_utils
|
||||
});
|
||||
} else {
|
||||
console.error(_t('Template "timeline-item" not present in timeline view definition.'));
|
||||
}
|
||||
content = this.render_timeline_item(evt);
|
||||
}
|
||||
|
||||
var r = {
|
||||
|
@ -308,10 +351,25 @@ var CalendarRenderer = AbstractRenderer.extend({
|
|||
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.')
|
||||
);
|
||||
},
|
||||
|
||||
on_group_click: function (e) {
|
||||
// handle a click on a group header
|
||||
if (e.what === 'group-label' && e.group != -1) {
|
||||
this._trigger(e, function(){}, 'onGroupClick');
|
||||
if (e.what === 'group-label' && e.group !== -1) {
|
||||
this._trigger(e, function() {
|
||||
// Do nothing
|
||||
}, 'onGroupClick');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -84,11 +84,16 @@ odoo.define('web_timeline.TimelineView', function (require) {
|
|||
fieldNames.push(this.colors[i].field);
|
||||
}
|
||||
|
||||
if (attrs.dependency_arrow) {
|
||||
fieldNames.push(attrs.dependency_arrow);
|
||||
}
|
||||
|
||||
this.permissions = {};
|
||||
this.grouped_by = false;
|
||||
this.date_start = attrs.date_start;
|
||||
this.date_stop = attrs.date_stop;
|
||||
this.date_delay = attrs.date_delay;
|
||||
this.dependency_arrow = attrs.dependency_arrow;
|
||||
|
||||
this.no_period = this.date_start === this.date_stop;
|
||||
this.zoomKey = attrs.zoomKey || '';
|
||||
|
@ -125,6 +130,7 @@ odoo.define('web_timeline.TimelineView', function (require) {
|
|||
this.rendererParams.colors = this.colors;
|
||||
this.rendererParams.fieldNames = fieldNames;
|
||||
this.rendererParams.view = this;
|
||||
this.rendererParams.dependency_arrow = this.dependency_arrow;
|
||||
this.loadParams.modelName = this.modelName;
|
||||
this.loadParams.fieldNames = fieldNames;
|
||||
this.controllerParams.open_popup_action = this.open_popup_action;
|
||||
|
|
|
@ -14,4 +14,13 @@
|
|||
<div class="oe_timeline_widget" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<svg t-name="TimelineView.Canvas"
|
||||
class="oe_timeline_view_canvas">
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
||||
<polygon points="10 0, 10 7, 0 3.5" />
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<script type="text/javascript" src="/web_timeline/static/src/js/timeline_renderer.js"/>
|
||||
<script type="text/javascript" src="/web_timeline/static/src/js/timeline_controller.js"/>
|
||||
<script type="text/javascript" src="/web_timeline/static/src/js/timeline_model.js"/>
|
||||
<script type="text/javascript" src="/web_timeline/static/src/js/timeline_canvas.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in New Issue