1 openerp.web_kanban = function (openerp) {
3 var _t = openerp.web._t,
5 var QWeb = openerp.web.qweb;
6 openerp.web.views.add('kanban', 'openerp.web_kanban.KanbanView');
8 openerp.web_kanban.KanbanView = openerp.web.View.extend({
9 template: "KanbanView",
10 display_name: _lt('Kanban'),
11 default_nr_columns: 3,
12 init: function (parent, dataset, view_id, options) {
14 this.set_default_options(options);
15 this.dataset = dataset;
16 this.view_id = view_id;
17 this.fields_view = {};
18 this.fields_keys = [];
25 this.form_dialog = new openerp.web.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
26 this.form_dialog.on_form_dialog_saved.add_last(this.do_reload);
28 this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
29 this.qweb = new QWeb2.Engine();
30 this.qweb.debug = openerp.connection.debug;
31 this.qweb.default_dict = _.clone(QWeb.default_dict);
32 this.has_been_loaded = $.Deferred();
33 this.search_domain = this.search_context = this.search_group_by = null;
34 this.currently_dragging = {};
38 this.$element.find('button.oe_kanban_button_new').click(this.do_add_record);
39 this.$groups = this.$element.find('.oe_kanban_groups tr');
40 var context = new openerp.web.CompoundContext(this.dataset.get_context());
41 return this.rpc('/web/view/load', {
42 'model': this.dataset.model,
43 'view_id': this.view_id,
44 'view_type': 'kanban',
48 on_loaded: function(data) {
49 this.fields_view = data;
50 this.fields_keys = _.keys(this.fields_view.fields);
51 this.add_qweb_template();
52 this.has_been_loaded.resolve();
54 add_qweb_template: function() {
55 for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
56 var child = this.fields_view.arch.children[i];
57 if (child.tag === "templates") {
58 this.transform_qweb_template(child);
59 this.qweb.add_template(openerp.web.json_node_to_xml(child));
61 } else if (child.tag === 'field') {
62 this.extract_aggregates(child);
66 extract_aggregates: function(node) {
67 for (var j = 0, jj = this.group_operators.length; j < jj; j++) {
68 if (node.attrs[this.group_operators[j]]) {
69 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
74 transform_qweb_template: function(node) {
75 var qweb_prefix = QWeb.prefix;
78 node.tag = qweb_prefix;
79 node.attrs[qweb_prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
80 this.extract_aggregates(node);
84 var type = node.attrs.type || '';
85 if (_.indexOf('action,object,edit,delete,color'.split(','), type) !== -1) {
86 _.each(node.attrs, function(v, k) {
87 if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
88 node.attrs['data-' + k] = v;
89 delete(node.attrs[k]);
92 if (node.attrs['data-states']) {
93 var states = _.map(node.attrs['data-states'].split(','), function(state) {
94 return "record.state.raw_value == '" + _.str.trim(state) + "'";
96 node.attrs[qweb_prefix + '-if'] = states.join(' or ');
98 if (node.attrs['data-kanban_states']) {
99 var states = _.map(node.attrs['data-kanban_states'].split(','), function(state) {
100 return "record.kanban_state.raw_value == '" + _.str.trim(state) + "'";
102 node.attrs[qweb_prefix + '-if'] = states.join(' or ');
104 if (node.attrs['data-string']) {
105 node.attrs.title = node.attrs['data-string'];
107 if (node.attrs['data-icon']) {
111 src: openerp.connection.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
117 if (node.tag == 'a') {
118 node.attrs.href = '#';
120 node.attrs.type = 'button';
122 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
127 for (var i = 0, ii = node.children.length; i < ii; i++) {
128 this.transform_qweb_template(node.children[i]);
132 do_add_record: function() {
133 this.dataset.index = null;
134 this.do_switch_view('form');
136 do_search: function(domain, context, group_by) {
138 this.search_domain = domain;
139 this.search_context = context;
140 this.search_group_by = group_by;
141 $.when(this.has_been_loaded).then(function() {
142 self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
143 self.datagroup = new openerp.web.DataGroup(self, self.dataset.model, domain, context, self.group_by ? [self.group_by] : []);
144 self.datagroup.list(self.fields_keys, self.do_process_groups, self.do_process_dataset);
147 do_process_groups: function(groups) {
148 this.do_clear_groups();
149 this.dataset.ids = [];
151 remaining = groups.length - 1,
153 _.each(groups, function (group, index) {
154 var group_name = group.value,
155 group_value = group.value,
156 group_aggregates = {};
157 if (group.value instanceof Array) {
158 group_name = group.value[1];
159 group_value = group.value[0];
161 _.each(self.aggregates, function(value, key) {
162 group_aggregates[value] = group.aggregates[key];
164 var dataset = new openerp.web.DataSetSearch(self, self.dataset.model, group.context, group.domain);
165 dataset.read_slice(self.fields_keys, {'domain': group.domain, 'context': group.context}).then(function(records) {
166 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
167 groups_array[index] = new openerp.web_kanban.KanbanGroup(self, records, group_value, group_name, group_aggregates);
169 self.dataset.index = self.dataset.ids.length ? 0 : null;
170 self.do_add_groups(groups_array);
175 do_process_dataset: function(dataset) {
177 this.do_clear_groups();
178 this.dataset.read_slice(this.fields_keys).then(function(records) {
180 while (records.length) {
181 for (var i = 0; i < self.default_nr_columns; i++) {
185 groups[i].push(records.shift());
188 for (var i = 0; i < groups.length; i++) {
189 groups[i] = new openerp.web_kanban.KanbanGroup(self, _.compact(groups[i]));
191 self.do_add_groups(groups);
194 do_reload: function() {
195 this.do_search(this.search_domain, this.search_context, this.search_group_by);
197 do_clear_groups: function() {
198 _.each(this.groups, function(group) {
202 this.$element.find('.oe_kanban_groups_headers, .oe_kanban_groups_records').empty();
204 do_add_groups: function(groups) {
206 _.each(groups, function(group) {
207 self.groups[group.undefined_title ? 'unshift' : 'push'](group);
209 _.each(this.groups, function(group) {
210 group.appendTo(self.$element.find('.oe_kanban_groups_headers'));
212 this.on_groups_started();
214 on_groups_started: function() {
216 this.compute_groups_width();
218 this.$element.find('.oe_kanban_column').sortable({
219 connectWith: '.oe_kanban_column',
220 handle : '.oe_kanban_draghandle',
221 start: function(event, ui) {
222 self.currently_dragging.index = ui.item.index();
223 self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
225 stop: function(event, ui) {
226 var record = ui.item.data('widget'),
227 old_index = self.currently_dragging.index,
228 new_index = ui.item.index(),
229 old_group = self.currently_dragging.group,
230 new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
231 if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
232 self.on_record_moved(record, old_group, old_index, new_group, new_index);
238 this.$element.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
241 on_record_moved : function(record, old_group, old_index, new_group, new_index) {
243 if (old_group === new_group) {
244 new_group.records.splice(old_index, 1);
245 new_group.records.splice(new_index, 0, record);
246 new_group.do_save_sequences();
248 old_group.records.splice(old_index, 1);
249 new_group.records.splice(new_index, 0, record);
250 record.group = new_group;
252 data[this.group_by] = new_group.value;
253 this.dataset.write(record.id, data, {}, function() {
255 new_group.do_save_sequences();
256 }).fail(function(error, evt) {
257 evt.preventDefault();
258 alert("An error has occured while moving the record to this group.");
259 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
263 compute_groups_width: function() {
265 _.each(this.groups, function(group) {
266 unfolded += group.state.folded ? 0 : 1;
267 group.$element.css('width', '');
269 _.each(this.groups, function(group) {
270 if (!group.state.folded) {
271 group.$element.css('width', Math.round(100/unfolded) + '%');
276 do_show: function() {
277 this.do_push_state({});
278 return this._super();
282 openerp.web_kanban.KanbanGroup = openerp.web.Widget.extend({
283 template: 'KanbanView.group_header',
284 init: function (parent, records, value, title, aggregates) {
290 if (title === false) {
291 this.title = _t('Undefined');
292 this.undefined_title = true;
294 this.aggregates = aggregates || {};
295 var key = this.view.group_by + '-' + value;
296 if (!this.view.state.groups[key]) {
297 this.view.state.groups[key] = {
301 this.state = this.view.state.groups[key];
302 this.$records = null;
303 this.records = _.map(records, function(record) {
304 return new openerp.web_kanban.KanbanRecord(self, record);
310 this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
311 this.$records.appendTo(this.view.$element.find('.oe_kanban_groups_records'));
312 _.each(this.records, function(record) {
313 record.appendTo(self.$records);
315 this.$element.find(".oe_kanban_fold_icon").click(function() {
316 self.do_toggle_fold();
317 self.view.compute_groups_width();
320 if (this.state.folded) {
321 this.do_toggle_fold();
323 this.$element.data('widget', this);
324 this.$records.data('widget', this);
330 this.$records.remove();
333 remove_record: function(id, remove_from_dataset) {
334 for (var i = 0, ii = this.records.length; i < ii; i++) {
335 if (this.records[i]['id'] === id) {
336 this.records.splice(i, 1);
340 do_toggle_fold: function(compute_width) {
341 this.$element.toggleClass('oe_kanban_group_folded');
342 this.$records.find('.oe_kanban_record').toggle();
343 this.state.folded = this.$element.is('.oe_kanban_group_folded');
345 do_save_sequences: function() {
347 if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
348 _.each(this.records, function(record, index) {
349 self.view.dataset.write(record.id, { sequence : index });
355 openerp.web_kanban.KanbanRecord = openerp.web.Widget.extend({
356 template: 'KanbanView.record',
357 init: function (parent, record) {
360 this.view = parent.view;
362 this.set_record(record);
363 if (!this.view.state.records[this.id]) {
364 this.view.state.records[this.id] = {
368 this.state = this.view.state.records[this.id];
370 set_record: function(record) {
372 this.record = this.transform_record(record);
376 this.$element.data('widget', this);
379 transform_record: function(record) {
382 _.each(record, function(value, name) {
383 var r = _.clone(self.view.fields_view.fields[name] || {});
384 if ((r.type === 'date' || r.type === 'datetime') && value) {
385 r.raw_value = openerp.web.auto_str_to_date(value);
389 r.value = openerp.web.format_value(value, r);
390 new_record[name] = r;
395 this.qweb_context = {
399 for (var p in this) {
400 if (_.str.startsWith(p, 'kanban_')) {
401 this.qweb_context[p] = _.bind(this[p], this);
405 'content': this.view.qweb.render('kanban-box', this.qweb_context)
408 bind_events: function() {
410 $show_on_click = self.$element.find('.oe_kanban_box_show_onclick');
411 $show_on_click.toggle(this.state.folded);
412 this.$element.find('.oe_kanban_box_show_onclick_trigger').click(function() {
413 $show_on_click.toggle();
414 self.state.folded = !self.state.folded;
417 this.$element.find('[tooltip]').tipTip({
419 defaultPosition: 'top',
420 content: function() {
421 var template = $(this).attr('tooltip');
422 if (!self.view.qweb.has_template(template)) {
425 return self.view.qweb.render(template, self.qweb_context);
429 this.$element.find('.oe_kanban_action').click(function() {
430 var $action = $(this),
431 type = $action.data('type') || 'button',
432 method = 'do_action_' + (type === 'action' ? 'object' : type);
433 if (_.str.startsWith(type, 'switch_')) {
434 self.view.do_switch_view(type.substr(7));
435 } else if (typeof self[method] === 'function') {
436 self[method]($action);
438 self.do_warn("Kanban: no action for type : " + type);
443 do_action_delete: function($action) {
445 if (confirm(_t("Are you sure you want to delete this record ?"))) {
446 return $.when(this.view.dataset.unlink([this.id])).then(function() {
447 self.group.remove_record(self.id)
452 do_action_edit: function($action) {
454 if ($action.attr('target') === 'dialog') {
455 this.view.form_dialog.select_id(this.id).then(function() {
456 self.view.form_dialog.open();
459 if (self.view.dataset.select_id(this.id)) {
460 this.view.do_switch_view('form');
462 this.do_warn("Kanban: could not find id#" + id);
466 do_action_color: function($action) {
468 colors = '#FFFFFF,#CCCCCC,#FFC7C7,#FFF1C7,#E3FFC7,#C7FFD5,#C7FFFF,#C7D5FF,#E3C7FF,#FFC7F1'.split(','),
469 $cpicker = $(QWeb.render('KanbanColorPicker', { colors : colors, columns: 2 }));
470 $action.after($cpicker);
471 $cpicker.mouseenter(function() {
472 clearTimeout($cpicker.data('timeoutId'));
473 }).mouseleave(function(evt) {
474 var timeoutId = setTimeout(function() { $cpicker.remove() }, 500);
475 $cpicker.data('timeoutId', timeoutId);
477 $cpicker.find('a').click(function() {
479 data[$action.data('name')] = $(this).data('color');
480 self.view.dataset.write(self.id, data, {}, function() {
481 self.record[$action.data('name')] = $(this).data('color');
488 do_action_object: function ($action) {
489 var button_attrs = $action.data();
490 this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
492 do_reload: function() {
494 this.view.dataset.read_ids([this.id], this.view.fields_keys).then(function(records) {
495 if (records.length) {
496 self.set_record(records[0]);
503 do_render: function() {
504 this.$element.html(this.render());
507 kanban_color: function(variable) {
508 var number_of_color_schemes = 10,
510 switch (typeof(variable)) {
512 for (var i=0, ii=variable.length; i<ii; i++) {
513 index += variable.charCodeAt(i);
517 index = Math.round(variable);
522 var color = (index % number_of_color_schemes);
523 return 'oe_kanban_color_' + color;
525 kanban_gravatar: function(email, size) {
527 email = _.str.trim(email || '').toLowerCase();
528 var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
529 var email_md5 = $.md5(email);
530 return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
532 kanban_image: function(model, field, id) {
534 return openerp.connection.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + id;
536 kanban_text_ellipsis: function(s, size) {
540 } else if (s.length <= size) {
543 return s.substr(0, size) + '...';
549 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: