Browse Source

[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 improvements
pull/1090/head
tarteo 6 years ago
parent
commit
8dc622978e
  1. 5
      web_timeline/README.rst
  2. 2
      web_timeline/__manifest__.py
  3. 2
      web_timeline/models/__init__.py
  4. 9
      web_timeline/static/src/css/web_timeline.css
  5. 89
      web_timeline/static/src/js/timeline_canvas.js
  6. 49
      web_timeline/static/src/js/timeline_controller.js
  7. 112
      web_timeline/static/src/js/timeline_renderer.js
  8. 6
      web_timeline/static/src/js/timeline_view.js
  9. 9
      web_timeline/static/src/xml/web_timeline.xml
  10. 1
      web_timeline/views/web_timeline.xml

5
web_timeline/README.rst

@ -39,6 +39,8 @@ the possible attributes for the tag:
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'.
@ -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">

2
web_timeline/__manifest__.py

@ -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, '

2
web_timeline/models/__init__.py

@ -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

9
web_timeline/static/src/css/web_timeline.css

@ -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;
}

89
web_timeline/static/src/js/timeline_canvas.js

@ -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;
});

49
web_timeline/static/src/js/timeline_controller.js

@ -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,9 +169,10 @@ 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);
},
});

112
web_timeline/static/src/js/timeline_renderer.js

@ -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;
},
@ -65,15 +67,20 @@ var CalendarRenderer = AbstractRenderer.extend({
add_events: function() {
var self = this;
this.$(".oe_timeline_button_today").click(function() {
self._onTodayClicked();});
self._onTodayClicked();
});
this.$(".oe_timeline_button_scale_day").click(function() {
self._onScaleDayClicked();});
self._onScaleDayClicked();
});
this.$(".oe_timeline_button_scale_week").click(function() {
self._onScaleWeekClicked();});
self._onScaleWeekClicked();
});
this.$(".oe_timeline_button_scale_month").click(function() {
self._onScaleMonthClicked();});
self._onScaleMonthClicked();
});
this.$(".oe_timeline_button_scale_year").click(function() {
self._onScaleYearClicked();});
self._onScaleYearClicked();
});
},
_onTodayClicked: function () {
@ -156,8 +163,8 @@ var CalendarRenderer = AbstractRenderer.extend({
onUpdate: self.on_update,
onRemove: self.on_remove,
});
if (this.arch.children.length) {
this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
if (this.arch.children.length) {
var tmpl = utils.json_node_to_xml(
_.filter(this.arch.children, function(item) {
return item.tag === 'templates';
@ -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();
});
},
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) {
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');
}
},

6
web_timeline/static/src/js/timeline_view.js

@ -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;

9
web_timeline/static/src/xml/web_timeline.xml

@ -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>

1
web_timeline/views/web_timeline.xml

@ -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…
Cancel
Save