[FIX] Propagate context to kanban's add_group() action
[odoo/odoo.git] / addons / web_kanban / static / src / js / kanban.js
1 openerp.web_kanban = function (instance) {
2
3 var _t = instance.web._t,
4    _lt = instance.web._lt;
5 var QWeb = instance.web.qweb;
6 instance.web.views.add('kanban', 'instance.web_kanban.KanbanView');
7
8 instance.web_kanban.KanbanView = instance.web.View.extend({
9     template: "KanbanView",
10     display_name: _lt('Kanban'),
11     default_nr_columns: 1,
12     view_type: "kanban",
13     quick_create_class: "instance.web_kanban.QuickCreate",
14     number_of_color_schemes: 10,
15     init: function (parent, dataset, view_id, options) {
16         this._super(parent, dataset, view_id, options);
17         var self = this;
18         _.defaults(this.options, {
19             "quick_creatable": true,
20             "creatable": true,
21             "create_text": undefined,
22             "read_only_mode": false,
23             "confirm_on_delete": true,
24         });
25         this.fields_view = {};
26         this.fields_keys = [];
27         this.group_by = null;
28         this.group_by_field = {};
29         this.grouped_by_m2o = false;
30         this.many2manys = [];
31         this.state = {
32             groups : {},
33             records : {}
34         };
35         this.groups = [];
36         this.aggregates = {};
37         this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
38         this.qweb = new QWeb2.Engine();
39         this.qweb.debug = instance.session.debug;
40         this.qweb.default_dict = _.clone(QWeb.default_dict);
41         this.has_been_loaded = $.Deferred();
42         this.search_domain = this.search_context = this.search_group_by = null;
43         this.currently_dragging = {};
44         this.limit = options.limit || 40;
45         this.add_group_mutex = new $.Mutex();
46         this.on('view_loaded', self, self.load_kanban);
47     },
48     start: function() {
49         var self = this;
50         this._super.apply(this, arguments);
51         this.$el.on('click', '.oe_kanban_dummy_cell', function() {
52             if (self.$buttons) {
53                 self.$buttons.find('.oe_kanban_add_column').effect('bounce', {distance: 18, times: 5}, 150);
54             }
55         });
56     },
57     destroy: function() {
58         this._super.apply(this, arguments);
59         $('html').off('click.kanban');
60     },
61     load_kanban: function(data) {
62         this.fields_view = data;
63         this.$el.addClass(this.fields_view.arch.attrs['class']);
64         this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
65         if (this.options.$buttons) {
66             this.$buttons.appendTo(this.options.$buttons);
67         } else {
68             this.$el.find('.oe_kanban_buttons').replaceWith(this.$buttons);
69         }
70         this.$buttons
71             .on('click', 'button.oe_kanban_button_new', this.do_add_record)
72             .on('click', '.oe_kanban_add_column', this.do_add_group);
73         this.$groups = this.$el.find('.oe_kanban_groups tr');
74         this.fields_keys = _.keys(this.fields_view.fields);
75         this.add_qweb_template();
76         this.has_been_loaded.resolve();
77         this.trigger('kanban_view_loaded', data);
78         return $.when();
79     },
80     _is_quick_create_enabled: function() {
81         if (!this.options.quick_creatable || !this.is_action_enabled('create'))
82             return false;
83         if (this.fields_view.arch.attrs.quick_create !== undefined)
84             return JSON.parse(this.fields_view.arch.attrs.quick_create);
85         return !! this.group_by;
86     },
87     is_action_enabled: function(action) {
88         if (action === 'create' && !this.options.creatable)
89             return false;
90         return this._super(action);
91     },
92     /*  add_qweb_template
93     *   select the nodes into the xml and send to extract_aggregates the nodes with TagName="field"
94     */
95     add_qweb_template: function() {
96         for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
97             var child = this.fields_view.arch.children[i];
98             if (child.tag === "templates") {
99                 this.transform_qweb_template(child);
100                 this.qweb.add_template(instance.web.json_node_to_xml(child));
101                 break;
102             } else if (child.tag === 'field') {
103                 this.extract_aggregates(child);
104             }
105         }
106     },
107     /*  extract_aggregates
108     *   extract the agggregates from the nodes (TagName="field")
109     */
110     extract_aggregates: function(node) {
111         for (var j = 0, jj = this.group_operators.length; j < jj;  j++) {
112             if (node.attrs[this.group_operators[j]]) {
113                 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
114                 break;
115             }
116         }
117     },
118     transform_qweb_template: function(node) {
119         var qweb_add_if = function(node, condition) {
120             if (node.attrs[QWeb.prefix + '-if']) {
121                 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
122             }
123             node.attrs[QWeb.prefix + '-if'] = condition;
124         };
125         // Process modifiers
126         if (node.tag && node.attrs.modifiers) {
127             var modifiers = JSON.parse(node.attrs.modifiers || '{}');
128             if (modifiers.invisible) {
129                 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
130             }
131         }
132         switch (node.tag) {
133             case 'field':
134                 if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
135                     this.many2manys.push(node.attrs.name);
136                     node.tag = 'div';
137                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
138                 } else {
139                     node.tag = QWeb.prefix;
140                     node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
141                 }
142                 break;
143             case 'button':
144             case 'a':
145                 var type = node.attrs.type || '';
146                 if (_.indexOf('action,object,edit,open,delete'.split(','), type) !== -1) {
147                     _.each(node.attrs, function(v, k) {
148                         if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
149                             node.attrs['data-' + k] = v;
150                             delete(node.attrs[k]);
151                         }
152                     });
153                     if (node.attrs['data-string']) {
154                         node.attrs.title = node.attrs['data-string'];
155                     }
156                     if (node.attrs['data-icon']) {
157                         node.children = [{
158                             tag: 'img',
159                             attrs: {
160                                 src: instance.session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
161                                 width: '16',
162                                 height: '16'
163                             }
164                         }];
165                     }
166                     if (node.tag == 'a') {
167                         node.attrs.href = '#';
168                     } else {
169                         node.attrs.type = 'button';
170                     }
171                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
172                 }
173                 break;
174         }
175         if (node.children) {
176             for (var i = 0, ii = node.children.length; i < ii; i++) {
177                 this.transform_qweb_template(node.children[i]);
178             }
179         }
180     },
181     do_add_record: function() {
182         this.dataset.index = null;
183         this.do_switch_view('form');
184     },
185     do_add_group: function() {
186         var self = this;
187         self.do_action({
188             name: _t("Add column"),
189             res_model: self.group_by_field.relation,
190             views: [[false, 'form']],
191             type: 'ir.actions.act_window',
192             target: "new",
193             context: self.dataset.get_context(),
194             flags: {
195                 action_buttons: true,
196             }
197         });
198         var am = instance.webclient.action_manager;
199         var form = am.dialog_widget.views.form.controller;
200         form.on("on_button_cancel", am.dialog, am.dialog.close);
201         form.on('record_created', self, function(r) {
202             (new instance.web.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) {
203                 am.dialog.close();
204                 var domain = self.dataset.domain.slice(0);
205                 domain.push([self.group_by, '=', new_record[0][0]]);
206                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain);
207                 var datagroup = {
208                     get: function(key) {
209                         return this[key];
210                     },
211                     value: new_record[0],
212                     length: 0,
213                     aggregates: {},
214                 };
215                 var new_group = new instance.web_kanban.KanbanGroup(self, [], datagroup, dataset);
216                 self.do_add_groups([new_group]).done(function() {
217                     $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' });
218                 });
219             });
220         });
221     },
222     do_search: function(domain, context, group_by) {
223         var self = this;
224         this.$el.find('.oe_view_nocontent').remove();
225         this.search_domain = domain;
226         this.search_context = context;
227         this.search_group_by = group_by;
228         $.when(this.has_been_loaded).done(function() {
229             self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
230             self.group_by_field = self.fields_view.fields[self.group_by] || {};
231             self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
232             self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
233             self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
234             var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(self.group_by);
235             $.when(grouping).done(function(groups) {
236                 if (groups) {
237                     self.do_process_groups(groups);
238                 } else {
239                     self.do_process_dataset();
240                 }
241             });
242         });
243     },
244     do_process_groups: function(groups) {
245         var self = this;
246         this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
247         this.add_group_mutex.exec(function() {
248             self.dataset.ids = [];
249             var remaining = groups.length - 1,
250                 groups_array = [];
251             return $.when.apply(null, _.map(groups, function (group, index) {
252                 self.do_clear_groups();
253                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
254                     new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
255                 return dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit })
256                     .then(function(records) {
257                         self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
258                         groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
259                         if (!remaining--) {
260                             self.dataset.index = self.dataset.size() ? 0 : null;
261                             return self.do_add_groups(groups_array);
262                         }
263                 });
264             }));
265         });
266     },
267     do_process_dataset: function() {
268         var self = this;
269         this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
270         this.add_group_mutex.exec(function() {
271             var def = $.Deferred();
272             self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
273                 self.do_clear_groups();
274                 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
275                 self.do_add_groups([kgroup]).done(function() {
276                     if (_.isEmpty(records)) {
277                         self.no_result();
278                     }
279                     def.resolve();
280                 });
281             }).done(null, function() {
282                 def.reject();
283             });
284             return def;
285         });
286     },
287     do_reload: function() {
288         this.do_search(this.search_domain, this.search_context, this.search_group_by);
289     },
290     do_clear_groups: function() {
291         var groups = this.groups.slice(0);
292         this.groups = [];
293         _.each(groups, function(group) {
294             group.destroy();
295         });
296     },
297     do_add_groups: function(groups) {
298         var self = this;
299         var $parent = this.$el.parent();
300         this.$el.detach();
301         _.each(groups, function(group) {
302             self.groups[group.undefined_title ? 'unshift' : 'push'](group);
303         });
304         var $last_td = self.$el.find('.oe_kanban_groups_headers td:last');
305         var groups_started = _.map(this.groups, function(group) {
306             if (!group.is_started) {
307                 return group.insertBefore($last_td);
308             }
309         });
310         return $.when.apply(null, groups_started).done(function () {
311             self.on_groups_started();
312             self.$el.appendTo($parent);
313             _.each(self.groups, function(group) {
314                 group.compute_cards_auto_height();
315             });
316         });
317     },
318     on_groups_started: function() {
319         var self = this;
320         this.compute_groups_width();
321         if (this.group_by) {
322             // Kanban cards drag'n'drop
323             var $columns = this.$el.find('.oe_kanban_column');
324             $columns.sortable({
325                 handle : '.oe_kanban_draghandle',
326                 start: function(event, ui) {
327                     self.currently_dragging.index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
328                     self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
329                     ui.item.find('*').on('click.prevent', function(ev) {
330                         return false;
331                     });
332                     ui.placeholder.height(ui.item.height());
333                 },
334                 revert: 150,
335                 stop: function(event, ui) {
336                     var record = ui.item.data('widget');
337                     var old_index = self.currently_dragging.index;
338                     var new_index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
339                     var old_group = self.currently_dragging.group;
340                     var new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
341                     if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
342                         self.on_record_moved(record, old_group, old_index, new_group, new_index);
343                     }
344                     setTimeout(function() {
345                         // A bit hacky but could not find a better solution for Firefox (problem not present in chrome)
346                         // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop
347                         ui.item.find('*').off('click.prevent');
348                     }, 0);
349                 },
350                 scroll: false
351             });
352             // Keep connectWith out of the sortable initialization for performance sake:
353             // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html
354             $columns.sortable({ connectWith: $columns });
355
356             // Kanban groups drag'n'drop
357             var start_index;
358             if (this.grouped_by_m2o) {
359                 this.$('.oe_kanban_groups_headers').sortable({
360                     items: '.oe_kanban_group_header',
361                     helper: 'clone',
362                     axis: 'x',
363                     opacity: 0.5,
364                     scroll: false,
365                     start: function(event, ui) {
366                         start_index = ui.item.index();
367                         self.$('.oe_kanban_record').css({ visibility: 'hidden' });
368                     },
369                     stop: function(event, ui) {
370                         var stop_index = ui.item.index();
371                         if (start_index !== stop_index) {
372                             var $start_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(start_index);
373                             var $stop_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index);
374                             var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter';
375                             $start_column[method]($stop_column);
376                             var tmp_group = self.groups.splice(start_index, 1)[0];
377                             self.groups.splice(stop_index, 0, tmp_group);
378                             var new_sequence = _.pluck(self.groups, 'value');
379                             (new instance.web.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) {
380                                 if (r === false) {
381                                     console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation);
382                                 }
383                             });
384                         }
385                         self.$('.oe_kanban_record').css({ visibility: 'visible' });
386                     }
387                 });
388             }
389         } else {
390             this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
391         }
392         this.postprocess_m2m_tags();
393     },
394     on_record_moved : function(record, old_group, old_index, new_group, new_index) {
395         var self = this;
396         $.fn.tipsy.clear();
397         $(old_group.$el).add(new_group.$el).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
398         if (old_group === new_group) {
399             new_group.records.splice(old_index, 1);
400             new_group.records.splice(new_index, 0, record);
401             new_group.do_save_sequences();
402         } else {
403             old_group.records.splice(old_index, 1);
404             new_group.records.splice(new_index, 0, record);
405             record.group = new_group;
406             var data = {};
407             data[this.group_by] = new_group.value;
408             this.dataset.write(record.id, data, {}).done(function() {
409                 record.do_reload();
410                 new_group.do_save_sequences();
411             }).fail(function(error, evt) {
412                 evt.preventDefault();
413                 alert("An error has occured while moving the record to this group.");
414                 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
415             });
416         }
417     },
418     compute_groups_width: function() {
419         var unfolded = 0;
420         var self = this;
421         _.each(this.groups, function(group) {
422             unfolded += group.state.folded ? 0 : 1;
423             group.$el.children(':first').css('width', '');
424         });
425         _.each(this.groups, function(group) {
426             if (!group.state.folded) {
427                 if (182*unfolded>=self.$el.width()) {
428                     group.$el.children(':first').css('width', "170px");
429                 } else if (262*unfolded<self.$el.width()) {
430                     group.$el.children(':first').css('width', "250px");
431                 } else {
432                     // -12 because of padding 6 between cards
433                     // -1 because of the border of the latest dummy column
434                     group.$el.children(':first').css('width', Math.floor((self.$el.width()-1)/unfolded)-12 + 'px');
435                 }
436             }
437         });
438     },
439
440     do_show: function() {
441         if (this.$buttons) {
442             this.$buttons.show();
443         }
444         this.do_push_state({});
445         return this._super();
446     },
447     do_hide: function () {
448         if (this.$buttons) {
449             this.$buttons.hide();
450         }
451         return this._super();
452     },
453     open_record: function(id, editable) {
454         if (this.dataset.select_id(id)) {
455             this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
456         } else {
457             this.do_warn("Kanban: could not find id#" + id);
458         }
459     },
460     no_result: function() {
461         if (this.groups.group_by
462             || !this.options.action
463             || !this.options.action.help) {
464             return;
465         }
466         this.$el.find('.oe_view_nocontent').remove();
467         this.$el.prepend(
468             $('<div class="oe_view_nocontent">').html(this.options.action.help)
469         );
470         var create_nocontent = this.$buttons;
471         this.$el.find('.oe_view_nocontent').click(function() {
472             create_nocontent.effect('bounce', {distance: 18, times: 5}, 150);
473         });
474     },
475
476     /*
477     *  postprocessing of fields type many2many
478     *  make the rpc request for all ids/model and insert value inside .oe_tags fields
479     */
480     postprocess_m2m_tags: function() {
481         var self = this;
482         if (!this.many2manys.length) {
483             return;
484         }
485         var relations = {};
486         this.groups.forEach(function(group) {
487             group.records.forEach(function(record) {
488                 self.many2manys.forEach(function(name) {
489                     var field = record.record[name];
490                     var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty();
491                     if (!relations[field.relation]) {
492                         relations[field.relation] = { ids: [], elements: {}};
493                     }
494                     var rel = relations[field.relation];
495                     field.raw_value.forEach(function(id) {
496                         rel.ids.push(id);
497                         if (!rel.elements[id]) {
498                             rel.elements[id] = [];
499                         }
500                         rel.elements[id].push($el[0]);
501                     });
502                 });
503             });
504         });
505        _.each(relations, function(rel, rel_name) {
506             var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
507             dataset.name_get(_.uniq(rel.ids)).done(function(result) {
508                 result.forEach(function(nameget) {
509                     $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
510                 });
511             });
512         });
513     }
514 });
515
516
517 function get_class(name) {
518     return new instance.web.Registry({'tmp' : name}).get_object("tmp");
519 }
520
521 instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
522     template: 'KanbanView.group_header',
523     init: function (parent, records, group, dataset) {
524         var self = this;
525         this._super(parent);
526         this.$has_been_started = $.Deferred();
527         this.view = parent;
528         this.group = group;
529         this.dataset = dataset;
530         this.dataset_offset = 0;
531         this.aggregates = {};
532         this.value = this.title = null;
533         if (this.group) {
534             this.value = group.get('value');
535             this.title = group.get('value');
536             if (this.value instanceof Array) {
537                 this.title = this.value[1];
538                 this.value = this.value[0];
539             }
540             var field = this.view.group_by_field;
541             if (!_.isEmpty(field)) {
542                 try {
543                     this.title = instance.web.format_value(group.get('value'), field, false);
544                 } catch(e) {}
545             }
546             _.each(this.view.aggregates, function(value, key) {
547                 self.aggregates[value] = group.get('aggregates')[key];
548             });
549         }
550
551         if (this.title === false) {
552             this.title = _t('Undefined');
553             this.undefined_title = true;
554         }
555         var key = this.view.group_by + '-' + this.value;
556         if (!this.view.state.groups[key]) {
557             this.view.state.groups[key] = {
558                 folded: group ? group.get('folded') : false
559             };
560         }
561         this.state = this.view.state.groups[key];
562         this.$records = null;
563
564         this.records = [];
565         this.$has_been_started.done(function() {
566             self.do_add_records(records);
567         });
568     },
569     start: function() {
570         var self = this,
571             def = this._super();
572         if (! self.view.group_by) {
573             self.$el.addClass("oe_kanban_no_group");
574             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
575                 .on('added', self, self.proxy('quick_created'));
576             self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
577         }
578         this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
579         this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
580
581         this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
582             var fn = 'do_action_' + $(ev.target).data().action;
583             if (typeof(self[fn]) === 'function') {
584                 self[fn]($(ev.target));
585             }
586         });
587
588         this.$el.find('.oe_kanban_add').click(function () {
589             if (self.quick) {
590                 return self.quick.trigger('close');
591             }
592             var ctx = {};
593             ctx['default_' + self.view.group_by] = self.value;
594             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
595                 .on('added', self, self.proxy('quick_created'))
596                 .on('close', self, function() {
597                     this.quick.destroy();
598                     delete this.quick;
599                 });
600             self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
601             self.quick.focus();
602         });
603         // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
604         this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
605         if (this.state.folded) {
606             this.do_toggle_fold();
607         }
608         this.$el.data('widget', this);
609         this.$records.data('widget', this);
610         this.$has_been_started.resolve();
611         var add_btn = this.$el.find('.oe_kanban_add');
612         add_btn.tipsy({delayIn: 500, delayOut: 1000});
613         this.$records.click(function (ev) {
614             if (ev.target == ev.currentTarget) {
615                 if (!self.state.folded) {
616                     add_btn.effect('bounce', {distance: 18, times: 5}, 150);                    
617                 }
618             }
619         });
620         this.is_started = true;
621         return def;
622     },
623     compute_cards_auto_height: function() {
624         // oe_kanban_no_auto_height is an empty class used to disable this feature
625         if (!this.view.group_by) {
626             var min_height = 0;
627             var els = [];
628             _.each(this.records, function(r) {
629                 var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
630                 if ($e.length) {
631                     els.push($e[0]);
632                     min_height = Math.max(min_height, $e.outerHeight());
633                 }
634             });
635             $(els).css('min-height', min_height);
636         }
637     },
638     destroy: function() {
639         this._super();
640         if (this.$records) {
641             this.$records.remove();
642         }
643     },
644     do_show_more: function(evt) {
645         var self = this;
646         var ids = self.view.dataset.ids.splice(0);
647         return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
648             'limit': self.view.limit,
649             'offset': self.dataset_offset += self.view.limit
650         }).then(function(records) {
651             self.view.dataset.ids = ids.concat(self.view.dataset.ids);
652             self.do_add_records(records);
653             self.compute_cards_auto_height();
654             return records;
655         });
656     },
657     do_add_records: function(records, prepend) {
658         var self = this;
659         var $list_header = this.$records.find('.oe_kanban_group_list_header');
660         var $show_more = this.$records.find('.oe_kanban_show_more');
661
662         _.each(records, function(record) {
663             var rec = new instance.web_kanban.KanbanRecord(self, record);
664             if (!prepend) {
665                 rec.insertBefore($show_more);
666                 self.records.push(rec);
667             } else {
668                 rec.insertAfter($list_header);
669                 self.records.unshift(rec);
670             }
671         });
672         if ($show_more.length) {
673             var size = this.dataset.size();
674             $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
675         }
676     },
677     remove_record: function(id, remove_from_dataset) {
678         for (var i = 0; i < this.records.length; i++) {
679             if (this.records[i]['id'] === id) {
680                 this.records.splice(i, 1);
681                 i--;
682             }
683         }
684     },
685     do_toggle_fold: function(compute_width) {
686         this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
687         this.state.folded = this.$el.is('.oe_kanban_group_folded');
688         this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
689     },
690     do_action_toggle_fold: function() {
691         this.do_toggle_fold();
692         this.view.compute_groups_width();
693     },
694     do_action_edit: function() {
695         var self = this;
696         self.do_action({
697             res_id: this.value,
698             name: _t("Edit column"),
699             res_model: self.view.group_by_field.relation,
700             views: [[false, 'form']],
701             type: 'ir.actions.act_window',
702             target: "new",
703             flags: {
704                 action_buttons: true,
705             }
706         });
707         var am = instance.webclient.action_manager;
708         var form = am.dialog_widget.views.form.controller;
709         form.on("on_button_cancel", am.dialog, am.dialog.close);
710         form.on('record_saved', self, function() {
711             am.dialog.close();
712             self.view.do_reload();
713         });
714     },
715     do_action_delete: function() {
716         var self = this;
717         if (confirm(_t("Are you sure to remove this column ?"))) {
718             (new instance.web.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
719                 self.view.do_reload();
720             });
721         }
722     },
723     do_save_sequences: function() {
724         var self = this;
725         if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
726             var new_sequence = _.pluck(this.records, 'id');
727             self.view.dataset.resequence(new_sequence);
728         }
729     },
730     /**
731      * Handles a newly created record
732      *
733      * @param {id} id of the newly created record
734      */
735     quick_created: function (record) {
736         var id = record, self = this;
737         this.dataset.read_ids([id], this.view.fields_keys)
738             .done(function (records) {
739                 self.view.dataset.ids.push(id);
740                 self.do_add_records(records, true);
741             });
742     }
743 });
744
745 instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
746     template: 'KanbanView.record',
747     init: function (parent, record) {
748         this._super(parent);
749         this.group = parent;
750         this.view = parent.view;
751         this.id = null;
752         this.set_record(record);
753         if (!this.view.state.records[this.id]) {
754             this.view.state.records[this.id] = {
755                 folded: false
756             };
757         }
758         this.state = this.view.state.records[this.id];
759     },
760     set_record: function(record) {
761         var self = this;
762         this.id = record.id;
763         this.values = {};
764         _.each(record, function(v, k) {
765             self.values[k] = {
766                 value: v
767             };
768         });
769         this.record = this.transform_record(record);
770     },
771     start: function() {
772         this._super();
773         this.$el.data('widget', this);
774         this.bind_events();
775     },
776     transform_record: function(record) {
777         var self = this,
778             new_record = {};
779         _.each(record, function(value, name) {
780             var r = _.clone(self.view.fields_view.fields[name] || {});
781             if ((r.type === 'date' || r.type === 'datetime') && value) {
782                 r.raw_value = instance.web.auto_str_to_date(value);
783             } else {
784                 r.raw_value = value;
785             }
786             r.value = instance.web.format_value(value, r);
787             new_record[name] = r;
788         });
789         return new_record;
790     },
791     renderElement: function() {
792         this.qweb_context = {
793             record: this.record,
794             widget: this,
795             read_only_mode: this.view.options.read_only_mode,
796         };
797         for (var p in this) {
798             if (_.str.startsWith(p, 'kanban_')) {
799                 this.qweb_context[p] = _.bind(this[p], this);
800             }
801         }
802         var $el = instance.web.qweb.render(this.template, {
803             'widget': this,
804             'content': this.view.qweb.render('kanban-box', this.qweb_context)
805         });
806         this.replaceElement($el);
807     },
808     bind_events: function() {
809         var self = this;
810         this.setup_color_picker();
811         this.$el.find('[tooltip]').tipsy({
812             delayIn: 500,
813             delayOut: 0,
814             fade: true,
815             title: function() {
816                 var template = $(this).attr('tooltip');
817                 if (!self.view.qweb.has_template(template)) {
818                     return false;
819                 }
820                 return self.view.qweb.render(template, self.qweb_context);
821             },
822             gravity: 's',
823             html: true,
824             opacity: 0.8,
825             trigger: 'hover'
826         });
827
828         // If no draghandle is found, make the whole card as draghandle (provided one can edit)
829         if (!this.$el.find('.oe_kanban_draghandle').length) {
830             this.$el.children(':first')
831                 .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
832         }
833
834         this.$el.find('.oe_kanban_action').click(function(ev) {
835             ev.preventDefault();
836             var $action = $(this),
837                 type = $action.data('type') || 'button',
838                 method = 'do_action_' + (type === 'action' ? 'object' : type);
839             if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
840                 self.view.open_record(self.id, true);
841             } else if (_.str.startsWith(type, 'switch_')) {
842                 self.view.do_switch_view(type.substr(7));
843             } else if (typeof self[method] === 'function') {
844                 self[method]($action);
845             } else {
846                 self.do_warn("Kanban: no action for type : " + type);
847             }
848         });
849
850         if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
851             this.$el.on('click', function(ev) {
852                 if (!ev.isTrigger && !$._data(ev.target, 'events')) {
853                     var trigger = true;
854                     var elem = ev.target;
855                     var ischild = true;
856                     var children = [];
857                     while (elem) {
858                         var events = $._data(elem, 'events');
859                         if (elem == ev.currentTarget) {
860                             ischild = false;
861                         }
862                         if (ischild) {
863                             children.push(elem);
864                             if (events && events.click) {
865                                 // do not trigger global click if one child has a click event registered
866                                 trigger = false;
867                             }
868                         }
869                         if (trigger && events && events.click) {
870                             _.each(events.click, function(click_event) {
871                                 if (click_event.selector) {
872                                     // For each parent of original target, check if a
873                                     // delegated click is bound to any previously found children
874                                     _.each(children, function(child) {
875                                         if ($(child).is(click_event.selector)) {
876                                             trigger = false;
877                                         }
878                                     });
879                                 }
880                             });
881                         }
882                         elem = elem.parentElement;
883                     }
884                     if (trigger) {
885                         self.on_card_clicked(ev);
886                     }
887                 }
888             });
889         }
890     },
891     /* actions when user click on the block with a specific class
892      *  open on normal view : oe_kanban_global_click
893      *  open on form/edit view : oe_kanban_global_click_edit
894      */
895     on_card_clicked: function(ev) {
896         if(this.$el.find('.oe_kanban_global_click_edit').size()>0)
897             this.do_action_edit();
898         else
899             this.do_action_open();
900     },
901     setup_color_picker: function() {
902         var self = this;
903         var $el = this.$el.find('ul.oe_kanban_colorpicker');
904         if ($el.length) {
905             $el.html(QWeb.render('KanbanColorPicker', {
906                 widget: this
907             }));
908             $el.on('click', 'a', function(ev) {
909                 ev.preventDefault();
910                 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
911                 var data = {};
912                 data[color_field] = $(this).data('color');
913                 self.view.dataset.write(self.id, data, {}).done(function() {
914                     self.record[color_field] = $(this).data('color');
915                     self.do_reload();
916                 });
917             });
918         }
919     },
920     do_action_delete: function($action) {
921         var self = this;
922         function do_it() {
923             return $.when(self.view.dataset.unlink([self.id])).done(function() {
924                 self.group.remove_record(self.id);
925                 self.destroy();
926             });
927         }
928         if (this.view.options.confirm_on_delete) {
929             if (confirm(_t("Are you sure you want to delete this record ?"))) {
930                 return do_it();
931             }
932         } else
933             return do_it();
934     },
935     do_action_edit: function($action) {
936         this.view.open_record(this.id, true);
937     },
938     do_action_open: function($action) {
939         this.view.open_record(this.id);
940     },
941     do_action_object: function ($action) {
942         var button_attrs = $action.data();
943         this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
944     },
945     do_reload: function() {
946         var self = this;
947         this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
948             if (records.length) {
949                 self.set_record(records[0]);
950                 self.renderElement();
951                 self.$el.data('widget', self);
952                 self.bind_events();
953                 self.group.compute_cards_auto_height();
954                 self.view.postprocess_m2m_tags();
955             } else {
956                 self.destroy();
957             }
958         });
959     },
960     kanban_getcolor: function(variable) {
961         var index = 0;
962         switch (typeof(variable)) {
963             case 'string':
964                 for (var i=0, ii=variable.length; i<ii; i++) {
965                     index += variable.charCodeAt(i);
966                 }
967                 break;
968             case 'number':
969                 index = Math.round(variable);
970                 break;
971             default:
972                 return '';
973         }
974         var color = (index % this.view.number_of_color_schemes);
975         return color;
976     },
977     kanban_color: function(variable) {
978         var color = this.kanban_getcolor(variable);
979         return color === '' ? '' : 'oe_kanban_color_' + color;
980     },
981     kanban_gravatar: function(email, size) {
982         size = size || 22;
983         email = _.str.trim(email || '').toLowerCase();
984         var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
985         var email_md5 = $.md5(email);
986         return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
987     },
988     kanban_image: function(model, field, id, cache, options) {
989         options = options || {};
990         var url;
991         if (this.record[field] && this.record[field].value && ! /^\d+(\.\d*)? \w+$/.test(this.record[field].value)) {
992             url = 'data:image/png;base64,' + this.record[field].value;
993         } else if (this.record[field] && ! this.record[field].value) {
994             url = "/web/static/src/img/placeholder.png";
995         } else {
996             id = JSON.stringify(id);
997             if (options.preview_image)
998                 field = options.preview_image;
999             url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
1000             if (cache !== undefined) {
1001                 // Set the cache duration in seconds.
1002                 url += '&cache=' + parseInt(cache, 10);
1003             }
1004         }
1005         return url;
1006     },
1007     kanban_text_ellipsis: function(s, size) {
1008         size = size || 160;
1009         if (!s) {
1010             return '';
1011         } else if (s.length <= size) {
1012             return s;
1013         } else {
1014             return s.substr(0, size) + '...';
1015         }
1016     },
1017     kanban_compute_domain: function(domain) {
1018         return instance.web.form.compute_domain(domain, this.values);
1019     }
1020 });
1021
1022 /**
1023  * Quick creation view.
1024  *
1025  * Triggers a single event "added" with a single parameter "name", which is the
1026  * name entered by the user
1027  *
1028  * @class
1029  * @type {*}
1030  */
1031 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
1032     template: 'KanbanView.quick_create',
1033     
1034     /**
1035      * close_btn: If true, the widget will display a "Close" button able to trigger
1036      * a "close" event.
1037      */
1038     init: function(parent, dataset, context, buttons) {
1039         this._super(parent);
1040         this._dataset = dataset;
1041         this._buttons = buttons || false;
1042         this._context = context || {};
1043     },
1044     start: function () {
1045         var self = this;
1046         self.$input = this.$el.find('input');
1047         self.$input.keyup(function(event){
1048             if(event.keyCode == 13){
1049                 self.quick_add();
1050             }
1051         });
1052         $(".oe_kanban_quick_create_add", this.$el).click(function () {
1053             self.quick_add();
1054             self.focus();
1055         });
1056         $(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
1057             ev.preventDefault();
1058             self.trigger('close');
1059         });
1060         self.$input.keyup(function(e) {
1061             if (e.keyCode == 27 && self._buttons) {
1062                 self.trigger('close');
1063             }
1064         });
1065     },
1066     focus: function() {
1067         this.$el.find('input').focus();
1068     },
1069     /**
1070      * Handles user event from nested quick creation view
1071      */
1072     quick_add: function () {
1073         var self = this;
1074         var val = this.$input.val();
1075         if (/^\s*$/.test(val)) { return; }
1076         this._dataset.call(
1077             'name_create', [val, new instance.web.CompoundContext(
1078                     this._dataset.get_context(), this._context)])
1079             .then(function(record) {
1080                 self.$input.val("");
1081                 self.trigger('added', record[0]);
1082             }, function(error, event) {
1083                 event.preventDefault();
1084                 return self.slow_create();
1085             });
1086     },
1087     slow_create: function() {
1088         var self = this;
1089         var pop = new instance.web.form.SelectCreatePopup(this);
1090         pop.select_element(
1091             self._dataset.model,
1092             {
1093                 title: _t("Create: ") + (this.string || this.name),
1094                 initial_view: "form",
1095                 disable_multiple_selection: true
1096             },
1097             [],
1098             {"default_name": self.$input.val()}
1099         );
1100         pop.on("elements_selected", self, function(element_ids) {
1101             self.$input.val("");
1102             self.trigger('added', element_ids[0]);
1103         });
1104     }
1105 });
1106 };
1107
1108 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: