[MERGE] forward port of branch 8.0 up to e883193
[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     },
47     view_loading: function(r) {
48         return this.load_kanban(r);
49     },
50     start: function() {
51         var self = this;
52         this._super.apply(this, arguments);
53         this.$el.on('click', '.oe_kanban_dummy_cell', function() {
54             if (self.$buttons) {
55                 self.$buttons.find('.oe_kanban_add_column').openerpBounce();
56             }
57         });
58     },
59     destroy: function() {
60         this._super.apply(this, arguments);
61         $('html').off('click.kanban');
62     },
63     load_kanban: function(data) {
64         this.fields_view = data;
65
66         // use default order if defined in xml description
67         var default_order = this.fields_view.arch.attrs.default_order,
68             unsorted = !this.dataset._sort.length;
69         if (unsorted && default_order) {
70             this.dataset.set_sort(default_order.split(','));
71         }
72
73         this.$el.addClass(this.fields_view.arch.attrs['class']);
74         this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
75         if (this.options.$buttons) {
76             this.$buttons.appendTo(this.options.$buttons);
77         } else {
78             this.$el.find('.oe_kanban_buttons').replaceWith(this.$buttons);
79         }
80         this.$buttons
81             .on('click', 'button.oe_kanban_button_new', this.do_add_record)
82             .on('click', '.oe_kanban_add_column', this.do_add_group);
83         this.$groups = this.$el.find('.oe_kanban_groups tr');
84         this.fields_keys = _.keys(this.fields_view.fields);
85         this.add_qweb_template();
86         this.has_been_loaded.resolve();
87         this.trigger('kanban_view_loaded', data);
88         return $.when();
89     },
90     _is_quick_create_enabled: function() {
91         if (!this.options.quick_creatable || !this.is_action_enabled('create'))
92             return false;
93         if (this.fields_view.arch.attrs.quick_create !== undefined)
94             return JSON.parse(this.fields_view.arch.attrs.quick_create);
95         return !! this.group_by;
96     },
97     is_action_enabled: function(action) {
98         if (action === 'create' && !this.options.creatable)
99             return false;
100         return this._super(action);
101     },
102     /*  add_qweb_template
103     *   select the nodes into the xml and send to extract_aggregates the nodes with TagName="field"
104     */
105     add_qweb_template: function() {
106         for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
107             var child = this.fields_view.arch.children[i];
108             if (child.tag === "templates") {
109                 this.transform_qweb_template(child);
110                 this.qweb.add_template(instance.web.json_node_to_xml(child));
111                 break;
112             } else if (child.tag === 'field') {
113                 this.extract_aggregates(child);
114             }
115         }
116     },
117     /*  extract_aggregates
118     *   extract the agggregates from the nodes (TagName="field")
119     */
120     extract_aggregates: function(node) {
121         for (var j = 0, jj = this.group_operators.length; j < jj;  j++) {
122             if (node.attrs[this.group_operators[j]]) {
123                 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
124                 break;
125             }
126         }
127     },
128     transform_qweb_template: function(node) {
129         var qweb_add_if = function(node, condition) {
130             if (node.attrs[QWeb.prefix + '-if']) {
131                 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
132             }
133             node.attrs[QWeb.prefix + '-if'] = condition;
134         };
135         // Process modifiers
136         if (node.tag && node.attrs.modifiers) {
137             var modifiers = JSON.parse(node.attrs.modifiers || '{}');
138             if (modifiers.invisible) {
139                 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
140             }
141         }
142         switch (node.tag) {
143             case 'field':
144                 var ftype = this.fields_view.fields[node.attrs.name].type;
145                 ftype = node.attrs.widget ? node.attrs.widget : ftype;
146                 if (ftype === 'many2many') {
147                     if (_.indexOf(this.many2manys, node.attrs.name) < 0) {
148                         this.many2manys.push(node.attrs.name);
149                     }
150                     node.tag = 'div';
151                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
152                 } else if (instance.web_kanban.fields_registry.contains(ftype)) {
153                     // do nothing, the kanban record will handle it
154                 } else {
155                     node.tag = QWeb.prefix;
156                     node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
157                 }
158                 break;
159             case 'button':
160             case 'a':
161                 var type = node.attrs.type || '';
162                 if (_.indexOf('action,object,edit,open,delete,url'.split(','), type) !== -1) {
163                     _.each(node.attrs, function(v, k) {
164                         if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
165                             node.attrs['data-' + k] = v;
166                             delete(node.attrs[k]);
167                         }
168                     });
169                     if (node.attrs['data-string']) {
170                         node.attrs.title = node.attrs['data-string'];
171                     }
172                     if (node.attrs['data-icon']) {
173                         node.children = [{
174                             tag: 'img',
175                             attrs: {
176                                 src: instance.session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
177                                 width: '16',
178                                 height: '16'
179                             }
180                         }];
181                     }
182                     if (node.tag == 'a' && node.attrs['data-type'] != "url") {
183                         node.attrs.href = '#';
184                     } else {
185                         node.attrs.type = 'button';
186                     }
187                     node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
188                 }
189                 break;
190         }
191         if (node.children) {
192             for (var i = 0, ii = node.children.length; i < ii; i++) {
193                 this.transform_qweb_template(node.children[i]);
194             }
195         }
196     },
197     do_add_record: function() {
198         this.dataset.index = null;
199         this.do_switch_view('form');
200     },
201     do_add_group: function() {
202         var self = this;
203         self.do_action({
204             name: _t("Add column"),
205             res_model: self.group_by_field.relation,
206             views: [[false, 'form']],
207             type: 'ir.actions.act_window',
208             target: "new",
209             context: self.dataset.get_context(),
210             flags: {
211                 action_buttons: true,
212             }
213         });
214         var am = instance.webclient.action_manager;
215         var form = am.dialog_widget.views.form.controller;
216         form.on("on_button_cancel", am.dialog, am.dialog.close);
217         form.on('record_created', self, function(r) {
218             (new instance.web.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) {
219                 am.dialog.close();
220                 var domain = self.dataset.domain.slice(0);
221                 domain.push([self.group_by, '=', new_record[0][0]]);
222                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain);
223                 var datagroup = {
224                     get: function(key) {
225                         return this[key];
226                     },
227                     value: new_record[0],
228                     length: 0,
229                     aggregates: {},
230                 };
231                 var new_group = new instance.web_kanban.KanbanGroup(self, [], datagroup, dataset);
232                 self.do_add_groups([new_group]).done(function() {
233                     $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' });
234                 });
235             });
236         });
237     },
238     do_search: function(domain, context, group_by) {
239         var self = this;
240         this.search_domain = domain;
241         this.search_context = context;
242         this.search_group_by = group_by;
243         return $.when(this.has_been_loaded).then(function() {
244             self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
245             self.group_by_field = self.fields_view.fields[self.group_by] || {};
246             self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
247             self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
248             self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
249             var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
250             if (!_.isEmpty(grouping_fields)) {
251                 // ensure group_by fields are read.
252                 self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields));
253             }
254             var grouping = new instance.web.Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields);
255             return self.alive($.when(grouping)).done(function(groups) {
256                 self.remove_no_result();
257                 if (groups) {
258                     self.do_process_groups(groups);
259                 } else {
260                     self.do_process_dataset();
261                 }
262             });
263         });
264     },
265     do_process_groups: function(groups) {
266         var self = this;
267         this.$el.find('table:first').show();
268         this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
269         this.add_group_mutex.exec(function() {
270             self.do_clear_groups();
271             self.dataset.ids = [];
272             if (!groups.length) {
273                 self.no_result();
274                 return false;
275             }
276             self.nb_records = 0;
277             var remaining = groups.length - 1,
278                 groups_array = [];
279             return $.when.apply(null, _.map(groups, function (group, index) {
280                 var def = $.when([]);
281                 var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
282                     new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
283                 if (group.attributes.length >= 1) {
284                     def = dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit });
285                 }
286                 return def.then(function(records) {
287                         self.nb_records += records.length;
288                         self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
289                         groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
290                         if (self.dataset.index >= records.length){
291                             self.dataset.index = self.dataset.size() ? 0 : null;
292                         }
293                         if (!remaining--) {
294                             return self.do_add_groups(groups_array);
295                         }
296                 });
297             })).then(function () {
298                 if(!self.nb_records) {
299                     self.no_result();
300                 }
301                 self.trigger('kanban_groups_processed');
302             });
303         });
304     },
305     do_process_dataset: function() {
306         var self = this;
307         this.$el.find('table:first').show();
308         this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
309         this.add_group_mutex.exec(function() {
310             var def = $.Deferred();
311             self.do_clear_groups();
312             self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
313                 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
314                 if (!_.isEmpty(self.dataset.ids) && (self.dataset.index === null || self.dataset.index >= self.dataset.ids.length)) {
315                     self.dataset.index = 0;
316                 } else if (_.isEmpty(self.dataset.ids)){
317                     self.dataset.index = null;
318                 }
319                 self.do_add_groups([kgroup]).done(function() {
320                     if (_.isEmpty(records)) {
321                         self.no_result();
322                     }
323                     self.trigger('kanban_dataset_processed');
324                     def.resolve();
325                 });
326             }).done(null, function() {
327                 def.reject();
328             });
329             return def;
330         });
331     },
332     do_reload: function() {
333         this.do_search(this.search_domain, this.search_context, this.search_group_by);
334     },
335     do_clear_groups: function() {
336         var groups = this.groups.slice(0);
337         this.groups = [];
338         _.each(groups, function(group) {
339             group.destroy();
340         });
341     },
342     do_add_groups: function(groups) {
343         var self = this;
344         var $parent = this.$el.parent();
345         this.$el.detach();
346         _.each(groups, function(group) {
347             self.groups[group.undefined_title ? 'unshift' : 'push'](group);
348         });
349         var $last_td = self.$el.find('.oe_kanban_groups_headers td:last');
350         var groups_started = _.map(this.groups, function(group) {
351             if (!group.is_started) {
352                 group.on("add_record", self, function () {
353                     self.remove_no_result();
354                 });
355                 return group.insertBefore($last_td);
356             }
357         });
358         return $.when.apply(null, groups_started).done(function () {
359             self.on_groups_started();
360             self.$el.appendTo($parent);
361             _.each(self.groups, function(group) {
362                 group.compute_cards_auto_height();
363             });
364         });
365     },
366     on_groups_started: function() {
367         var self = this;
368         if (this.group_by || this.fields_keys.indexOf("sequence") !== -1) {
369             // Kanban cards drag'n'drop
370             var prev_widget, is_folded, record, $columns;
371             if (this.group_by) {
372                 $columns = this.$el.find('.oe_kanban_column .oe_kanban_column_cards, .oe_kanban_column .oe_kanban_folded_column_cards');
373             } else {
374                 $columns = this.$el.find('.oe_kanban_column_cards');
375             }
376             $columns.sortable({
377                 handle : '.oe_kanban_draghandle',
378                 start: function(event, ui) {
379                     self.currently_dragging.index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
380                     self.currently_dragging.group = prev_widget = ui.item.parents('.oe_kanban_column:first').data('widget');
381                     ui.item.find('*').on('click.prevent', function(ev) {
382                         return false;
383                     });
384                     record = ui.item.data('widget');
385                     record.$el.bind('mouseup',function(ev,ui){
386                         if (is_folded) {
387                             record.$el.hide();
388                         }
389                         record.$el.unbind('mouseup');
390                     })
391                     ui.placeholder.height(ui.item.height());
392                 },
393                 over: function(event, ui) {
394                     var parent = $(event.target).parent();
395                     prev_widget.highlight(false);
396                     is_folded = parent.hasClass('oe_kanban_group_folded'); 
397                     if (is_folded) {
398                         var widget = parent.data('widget');
399                         widget.highlight(true);
400                         prev_widget = widget;
401                     }
402                  },
403                 revert: 150,
404                 stop: function(event, ui) {
405                     prev_widget.highlight(false);
406                     var old_index = self.currently_dragging.index;
407                     var new_index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
408                     var old_group = self.currently_dragging.group;
409                     var new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
410                     if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
411                         self.on_record_moved(record, old_group, old_index, new_group, new_index);
412                     }
413                     setTimeout(function() {
414                         // A bit hacky but could not find a better solution for Firefox (problem not present in chrome)
415                         // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop
416                         ui.item.find('*').off('click.prevent');
417                     }, 0);
418                 },
419                 scroll: false
420             });
421             // Keep connectWith out of the sortable initialization for performance sake:
422             // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html
423             $columns.sortable({ connectWith: $columns });
424
425             // Kanban groups drag'n'drop
426             var start_index;
427             if (this.grouped_by_m2o) {
428                 this.$('.oe_kanban_groups_headers').sortable({
429                     items: '.oe_kanban_group_header',
430                     helper: 'clone',
431                     axis: 'x',
432                     opacity: 0.5,
433                     scroll: false,
434                     start: function(event, ui) {
435                         start_index = ui.item.index();
436                         self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'hidden' });
437                     },
438                     stop: function(event, ui) {
439                         var stop_index = ui.item.index();
440                         if (start_index !== stop_index) {
441                             var $start_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(start_index);
442                             var $stop_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index);
443                             var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter';
444                             $start_column[method]($stop_column);
445                             var tmp_group = self.groups.splice(start_index, 1)[0];
446                             self.groups.splice(stop_index, 0, tmp_group);
447                             var new_sequence = _.pluck(self.groups, 'value');
448                             (new instance.web.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) {
449                                 if (r === false) {
450                                     console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation);
451                                 }
452                             });
453                         }
454                         self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'visible' });
455                     }
456                 });
457             }
458         } else {
459             this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
460         }
461         this.postprocess_m2m_tags();
462     },
463     on_record_moved : function(record, old_group, old_index, new_group, new_index) {
464         var self = this;
465         record.$el.find('[title]').tooltip('destroy');
466         $(old_group.$el).add(new_group.$el).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
467         if (old_group === new_group) {
468             new_group.records.splice(old_index, 1);
469             new_group.records.splice(new_index, 0, record);
470             new_group.do_save_sequences();
471         } else {
472             old_group.records.splice(old_index, 1);
473             new_group.records.splice(new_index, 0, record);
474             record.group = new_group;
475             var data = {};
476             data[this.group_by] = new_group.value;
477             this.dataset.write(record.id, data, {}).done(function() {
478                 record.do_reload();
479                 new_group.do_save_sequences();
480                 if (new_group.state.folded) {
481                     new_group.do_action_toggle_fold();
482                     record.prependTo(new_group.$records.find('.oe_kanban_column_cards'));
483                 }
484             }).fail(function(error, evt) {
485                 evt.preventDefault();
486                 alert(_t("An error has occured while moving the record to this group: ") + error.data.message);
487                 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
488             });
489         }
490     },
491
492     do_show: function() {
493         if (this.$buttons) {
494             this.$buttons.show();
495         }
496         this.do_push_state({});
497         return this._super();
498     },
499     do_hide: function () {
500         if (this.$buttons) {
501             this.$buttons.hide();
502         }
503         return this._super();
504     },
505     open_record: function(id, editable) {
506         if (this.dataset.select_id(id)) {
507             this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
508         } else {
509             this.do_warn("Kanban: could not find id#" + id);
510         }
511     },
512     no_result: function() {
513         var self = this;
514         if (this.groups.group_by
515             || !this.options.action
516             || (!this.options.action.help && !this.options.action.get_empty_list_help)) {
517             return;
518         }
519         this.$el.css("position", "relative");
520         $(QWeb.render('KanbanView.nocontent', { content : this.options.action.get_empty_list_help || this.options.action.help})).insertBefore(this.$('table:first'));
521         this.$el.find('.oe_view_nocontent').click(function() {
522             self.$buttons.openerpBounce();
523         });
524     },
525     remove_no_result: function() {
526         this.$el.css("position", "");
527         this.$el.find('.oe_view_nocontent').remove();
528     },
529
530     /*
531     *  postprocessing of fields type many2many
532     *  make the rpc request for all ids/model and insert value inside .oe_tags fields
533     */
534     postprocess_m2m_tags: function() {
535         var self = this;
536         if (!this.many2manys.length) {
537             return;
538         }
539         var relations = {};
540         this.groups.forEach(function(group) {
541             group.records.forEach(function(record) {
542                 self.many2manys.forEach(function(name) {
543                     var field = record.record[name];
544                     var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty();
545                     if (!relations[field.relation]) {
546                         relations[field.relation] = { ids: [], elements: {}};
547                     }
548                     var rel = relations[field.relation];
549                     field.raw_value.forEach(function(id) {
550                         rel.ids.push(id);
551                         if (!rel.elements[id]) {
552                             rel.elements[id] = [];
553                         }
554                         rel.elements[id].push($el[0]);
555                     });
556                 });
557             });
558         });
559        _.each(relations, function(rel, rel_name) {
560             var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
561             dataset.name_get(_.uniq(rel.ids)).done(function(result) {
562                 result.forEach(function(nameget) {
563                     $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
564                 });
565             });
566         });
567     }
568 });
569
570
571 function get_class(name) {
572     return new instance.web.Registry({'tmp' : name}).get_object("tmp");
573 }
574
575 instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
576     template: 'KanbanView.group_header',
577     init: function (parent, records, group, dataset) {
578         var self = this;
579         this._super(parent);
580         this.$has_been_started = $.Deferred();
581         this.view = parent;
582         this.group = group;
583         this.dataset = dataset;
584         this.dataset_offset = 0;
585         this.aggregates = {};
586         this.value = this.title = null;
587         if (this.group) {
588             this.value = group.get('value');
589             this.title = group.get('value');
590             if (this.value instanceof Array) {
591                 this.title = this.value[1];
592                 this.value = this.value[0];
593             }
594             var field = this.view.group_by_field;
595             if (!_.isEmpty(field)) {
596                 try {
597                     this.title = instance.web.format_value(group.get('value'), field, false);
598                 } catch(e) {}
599             }
600             _.each(this.view.aggregates, function(value, key) {
601                 self.aggregates[value] = instance.web.format_value(group.get('aggregates')[key], {type: 'float'});
602             });
603         }
604
605         if (this.title === false) {
606             this.title = _t('Undefined');
607             this.undefined_title = true;
608         }
609         var key = this.view.group_by + '-' + this.value;
610         if (!this.view.state.groups[key]) {
611             this.view.state.groups[key] = {
612                 folded: group ? group.get('folded') : false
613             };
614         }
615         this.state = this.view.state.groups[key];
616         this.$records = null;
617
618         this.records = [];
619         this.$has_been_started.done(function() {
620             self.do_add_records(records);
621         });
622     },
623     start: function() {
624         var self = this;
625         if (! self.view.group_by) {
626             self.$el.addClass("oe_kanban_no_group");
627             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
628                 .on('added', self, self.proxy('quick_created'));
629             self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
630         }
631         this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
632         this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
633
634         this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
635             var fn = 'do_action_' + $(ev.target).data().action;
636             if (typeof(self[fn]) === 'function') {
637                 self[fn]($(ev.target));
638             }
639         });
640
641         this.$el.find('.oe_kanban_add').click(function () {
642             if (self.view.quick) {
643                 self.view.quick.trigger('close');
644             }
645             if (self.quick) {
646                 return false;
647             }
648             self.view.$el.find('.oe_view_nocontent').hide();
649             var ctx = {};
650             ctx['default_' + self.view.group_by] = self.value;
651             self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
652                 .on('added', self, self.proxy('quick_created'))
653                 .on('close', self, function() {
654                     self.view.$el.find('.oe_view_nocontent').show();
655                     this.quick.destroy();
656                     delete self.view.quick;
657                     delete this.quick;
658                 });
659             self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
660             self.quick.focus();
661             self.view.quick = self.quick;
662         });
663         // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
664         this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
665         if (this.state.folded) {
666             this.do_toggle_fold();
667         }
668         this.$el.data('widget', this);
669         this.$records.data('widget', this);
670         this.$has_been_started.resolve();
671         var add_btn = this.$el.find('.oe_kanban_add');
672         add_btn.tooltip({delay: { show: 500, hide:1000 }});
673         this.$records.find(".oe_kanban_column_cards").click(function (ev) {
674             if (ev.target == ev.currentTarget) {
675                 if (!self.state.folded) {
676                     add_btn.openerpBounce();
677                 }
678             }
679         });
680         this.is_started = true;
681         var def_tooltip = this.fetch_tooltip();
682         return $.when(def_tooltip);
683     },
684     fetch_tooltip: function() {
685         if (! this.group)
686             return;
687         var field_name = this.view.group_by;
688         var field = this.view.group_by_field;
689         var field_desc = null;
690         var recurse = function(node) {
691             if (node.tag === "field" && node.attrs.name === field_name) {
692                 field_desc = node;
693                 return;
694             }
695             _.each(node.children, function(child) {
696                 if (field_desc === null)
697                     recurse(child);
698             });
699         };
700         recurse(this.view.fields_view.arch);
701         if (! field_desc)
702             return;
703         var options = instance.web.py_eval(field_desc.attrs.options || '{}')
704         if (! options.tooltip_on_group_by)
705             return;
706
707         var self = this;
708         if (this.value) {
709             return (new instance.web.Model(field.relation)).query([options.tooltip_on_group_by])
710                     .filter([["id", "=", this.value]]).first().then(function(res) {
711                 self.tooltip = res[options.tooltip_on_group_by];
712                 self.$(".oe_kanban_group_title_text").attr("title", self.tooltip || self.title || "").tooltip();
713             });
714         }
715     },
716     compute_cards_auto_height: function() {
717         // oe_kanban_no_auto_height is an empty class used to disable this feature
718         if (!this.view.group_by) {
719             var min_height = 0;
720             var els = [];
721             _.each(this.records, function(r) {
722                 var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
723                 if ($e.length) {
724                     els.push($e[0]);
725                     min_height = Math.max(min_height, $e.outerHeight());
726                 }
727             });
728             $(els).css('min-height', min_height);
729         }
730     },
731     destroy: function() {
732         this._super();
733         if (this.$records) {
734             this.$records.remove();
735         }
736     },
737     do_show_more: function(evt) {
738         var self = this;
739         var ids = self.view.dataset.ids.splice(0);
740         return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
741             'limit': self.view.limit,
742             'offset': self.dataset_offset += self.view.limit
743         }).then(function(records) {
744             self.view.dataset.ids = ids.concat(self.dataset.ids);
745             self.do_add_records(records);
746             self.compute_cards_auto_height();
747             self.view.postprocess_m2m_tags();
748             return records;
749         });
750     },
751     do_add_records: function(records, prepend) {
752         var self = this;
753         var $list_header = this.$records.find('.oe_kanban_group_list_header');
754         var $show_more = this.$records.find('.oe_kanban_show_more');
755         var $cards = this.$records.find('.oe_kanban_column_cards');
756
757         _.each(records, function(record) {
758             var rec = new instance.web_kanban.KanbanRecord(self, record);
759             if (!prepend) {
760                 rec.appendTo($cards);
761                 self.records.push(rec);
762             } else {
763                 rec.prependTo($cards);
764                 self.records.unshift(rec);
765             }
766         });
767         if ($show_more.length) {
768             var size = this.dataset.size();
769             $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
770         }
771     },
772     remove_record: function(id, remove_from_dataset) {
773         for (var i = 0; i < this.records.length; i++) {
774             if (this.records[i]['id'] === id) {
775                 this.records.splice(i, 1);
776                 i--;
777             }
778         }
779     },
780     do_toggle_fold: function(compute_width) {
781         this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
782         this.state.folded = this.$el.is('.oe_kanban_group_folded');
783         this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
784     },
785     do_action_toggle_fold: function() {
786         this.do_toggle_fold();
787     },
788     do_action_edit: function() {
789         var self = this;
790         self.do_action({
791             res_id: this.value,
792             name: _t("Edit column"),
793             res_model: self.view.group_by_field.relation,
794             views: [[false, 'form']],
795             type: 'ir.actions.act_window',
796             target: "new",
797             flags: {
798                 action_buttons: true,
799             }
800         });
801         var am = instance.webclient.action_manager;
802         var form = am.dialog_widget.views.form.controller;
803         form.on("on_button_cancel", am.dialog, function() { return am.dialog.$dialog_box.modal('hide'); });
804         form.on('record_saved', self, function() {
805             am.dialog.$dialog_box.modal('hide');
806             self.view.do_reload();
807         });
808     },
809     do_action_delete: function() {
810         var self = this;
811         if (confirm(_t("Are you sure to remove this column ?"))) {
812             (new instance.web.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
813                 self.view.do_reload();
814             });
815         }
816     },
817     do_save_sequences: function() {
818         var self = this;
819         if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
820             var new_sequence = _.pluck(this.records, 'id');
821             self.view.dataset.resequence(new_sequence);
822         }
823     },
824     /**
825      * Handles a newly created record
826      *
827      * @param {id} id of the newly created record
828      */
829     quick_created: function (record) {
830         var id = record, self = this;
831         self.view.remove_no_result();
832         self.trigger("add_record");
833         this.dataset.read_ids([id], this.view.fields_keys)
834             .done(function (records) {
835                 self.view.dataset.ids.push(id);
836                 self.do_add_records(records, true);
837             });
838     },
839     highlight: function(show){
840         if(show){
841             this.$el.addClass('oe_kanban_column_higlight');
842             this.$records.addClass('oe_kanban_column_higlight');
843         }else{
844             this.$el.removeClass('oe_kanban_column_higlight');
845             this.$records.removeClass('oe_kanban_column_higlight');
846         }
847     }
848 });
849
850 instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
851     template: 'KanbanView.record',
852     init: function (parent, record) {
853         this._super(parent);
854         this.group = parent;
855         this.view = parent.view;
856         this.id = null;
857         this.set_record(record);
858         if (!this.view.state.records[this.id]) {
859             this.view.state.records[this.id] = {
860                 folded: false
861             };
862         }
863         this.state = this.view.state.records[this.id];
864         this.fields = {};
865     },
866     set_record: function(record) {
867         var self = this;
868         this.id = record.id;
869         this.values = {};
870         _.each(record, function(v, k) {
871             self.values[k] = {
872                 value: v
873             };
874         });
875         this.record = this.transform_record(record);
876     },
877     start: function() {
878         var self = this;
879         this._super();
880         this.init_content();
881     },
882     init_content: function() {
883         var self = this;
884         self.sub_widgets = [];
885         this.$("[data-field_id]").each(function() {
886             self.add_widget($(this));
887         });
888         this.$el.data('widget', this);
889         this.bind_events();
890     },
891     transform_record: function(record) {
892         var self = this,
893             new_record = {};
894         _.each(record, function(value, name) {
895             var r = _.clone(self.view.fields_view.fields[name] || {});
896             if ((r.type === 'date' || r.type === 'datetime') && value) {
897                 r.raw_value = instance.web.auto_str_to_date(value);
898             } else {
899                 r.raw_value = value;
900             }
901             r.value = instance.web.format_value(value, r);
902             new_record[name] = r;
903         });
904         return new_record;
905     },
906     renderElement: function() {
907         this.qweb_context = {
908             instance: instance,
909             record: this.record,
910             widget: this,
911             read_only_mode: this.view.options.read_only_mode,
912         };
913         for (var p in this) {
914             if (_.str.startsWith(p, 'kanban_')) {
915                 this.qweb_context[p] = _.bind(this[p], this);
916             }
917         }
918         var $el = instance.web.qweb.render(this.template, {
919             'widget': this,
920             'content': this.view.qweb.render('kanban-box', this.qweb_context)
921         });
922         this.replaceElement($el);
923         this.replace_fields();
924     },
925     replace_fields: function() {
926         var self = this;
927         this.$("field").each(function() {
928             var $field = $(this);
929             var $nfield = $("<span></span");
930             var id = _.uniqueId("kanbanfield");
931             self.fields[id] = $field;
932             $nfield.attr("data-field_id", id);
933             $field.replaceWith($nfield);
934         });
935     },
936     add_widget: function($node) {
937         var $orig = this.fields[$node.data("field_id")];
938         var field = this.record[$orig.attr("name")];
939         var type = field.type;
940         type = $orig.attr("widget") ? $orig.attr("widget") : type;
941         var obj = instance.web_kanban.fields_registry.get_object(type);
942         var widget = new obj(this, field, $orig);
943         this.sub_widgets.push(widget);
944         widget.replace($node);
945     },
946     bind_events: function() {
947         var self = this;
948         this.setup_color_picker();
949         this.$el.find('[title]').each(function(){
950             $(this).tooltip({
951                 delay: { show: 500, hide: 0},
952                 container: $(this),
953                 title: function() {
954                     var template = $(this).attr('tooltip');
955                     if (!self.view.qweb.has_template(template)) {
956                         return false;
957                     }
958                     return self.view.qweb.render(template, self.qweb_context);
959                 },
960             });
961         });
962
963         // If no draghandle is found, make the whole card as draghandle (provided one can edit)
964         if (!this.$el.find('.oe_kanban_draghandle').length) {
965             this.$el.children(':first')
966                 .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
967         }
968
969         this.$el.find('.oe_kanban_action').click(function(ev) {
970             ev.preventDefault();
971             var $action = $(this),
972                 type = $action.data('type') || 'button',
973                 method = 'do_action_' + (type === 'action' ? 'object' : type);
974             if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
975                 self.view.open_record(self.id, true);
976             } else if (_.str.startsWith(type, 'switch_')) {
977                 self.view.do_switch_view(type.substr(7));
978             } else if (typeof self[method] === 'function') {
979                 self[method]($action);
980             } else {
981                 self.do_warn("Kanban: no action for type : " + type);
982             }
983         });
984
985         if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
986             this.$el.on('click', function(ev) {
987                 if (!ev.isTrigger && !$._data(ev.target, 'events')) {
988                     var trigger = true;
989                     var elem = ev.target;
990                     var ischild = true;
991                     var children = [];
992                     while (elem) {
993                         var events = $._data(elem, 'events');
994                         if (elem == ev.currentTarget) {
995                             ischild = false;
996                         }
997                         if (ischild) {
998                             children.push(elem);
999                             if (events && events.click) {
1000                                 // do not trigger global click if one child has a click event registered
1001                                 trigger = false;
1002                             }
1003                         }
1004                         if (trigger && events && events.click) {
1005                             _.each(events.click, function(click_event) {
1006                                 if (click_event.selector) {
1007                                     // For each parent of original target, check if a
1008                                     // delegated click is bound to any previously found children
1009                                     _.each(children, function(child) {
1010                                         if ($(child).is(click_event.selector)) {
1011                                             trigger = false;
1012                                         }
1013                                     });
1014                                 }
1015                             });
1016                         }
1017                         elem = elem.parentElement;
1018                     }
1019                     if (trigger) {
1020                         self.on_card_clicked(ev);
1021                     }
1022                 }
1023             });
1024         }
1025     },
1026     /* actions when user click on the block with a specific class
1027      *  open on normal view : oe_kanban_global_click
1028      *  open on form/edit view : oe_kanban_global_click_edit
1029      */
1030     on_card_clicked: function(ev) {
1031         if (this.$el.find('.oe_kanban_global_click').size() > 0 && this.$el.find('.oe_kanban_global_click').data('routing')) {
1032             instance.web.redirect(this.$el.find('.oe_kanban_global_click').data('routing') + "/" + this.id);
1033         }
1034         else if (this.$el.find('.oe_kanban_global_click_edit').size()>0)
1035             this.do_action_edit();
1036         else
1037             this.do_action_open();
1038     },
1039     setup_color_picker: function() {
1040         var self = this;
1041         var $el = this.$el.find('ul.oe_kanban_colorpicker');
1042         if ($el.length) {
1043             $el.html(QWeb.render('KanbanColorPicker', {
1044                 widget: this
1045             }));
1046             $el.on('click', 'a', function(ev) {
1047                 ev.preventDefault();
1048                 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
1049                 var data = {};
1050                 data[color_field] = $(this).data('color');
1051                 self.view.dataset.write(self.id, data, {}).done(function() {
1052                     self.record[color_field] = $(this).data('color');
1053                     self.do_reload();
1054                 });
1055             });
1056         }
1057     },
1058     do_action_delete: function($action) {
1059         var self = this;
1060         function do_it() {
1061             return $.when(self.view.dataset.unlink([self.id])).done(function() {
1062                 self.group.remove_record(self.id);
1063                 self.destroy();
1064             });
1065         }
1066         if (this.view.options.confirm_on_delete) {
1067             if (confirm(_t("Are you sure you want to delete this record ?"))) {
1068                 return do_it();
1069             }
1070         } else
1071             return do_it();
1072     },
1073     do_action_edit: function($action) {
1074         this.view.open_record(this.id, true);
1075     },
1076     do_action_open: function($action) {
1077         this.view.open_record(this.id);
1078     },
1079     do_action_object: function ($action) {
1080         var button_attrs = $action.data();
1081         this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
1082     },
1083     do_action_url: function($action) {
1084         return instance.web.redirect($action.attr("href"));
1085      },
1086     do_reload: function() {
1087         var self = this;
1088         this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
1089              _.each(self.sub_widgets, function(el) {
1090                  el.destroy();
1091              });
1092              self.sub_widgets = [];
1093             if (records.length) {
1094                 self.set_record(records[0]);
1095                 self.renderElement();
1096                 self.init_content();
1097                 self.group.compute_cards_auto_height();
1098                 self.view.postprocess_m2m_tags();
1099             } else {
1100                 self.destroy();
1101             }
1102         });
1103     },
1104     kanban_getcolor: function(variable) {
1105         var index = 0;
1106         switch (typeof(variable)) {
1107             case 'string':
1108                 for (var i=0, ii=variable.length; i<ii; i++) {
1109                     index += variable.charCodeAt(i);
1110                 }
1111                 break;
1112             case 'number':
1113                 index = Math.round(variable);
1114                 break;
1115             default:
1116                 return '';
1117         }
1118         var color = (index % this.view.number_of_color_schemes);
1119         return color;
1120     },
1121     kanban_color: function(variable) {
1122         var color = this.kanban_getcolor(variable);
1123         return color === '' ? '' : 'oe_kanban_color_' + color;
1124     },
1125     kanban_image: function(model, field, id, cache, options) {
1126         options = options || {};
1127         var url;
1128         if (this.record[field] && this.record[field].value && !instance.web.form.is_bin_size(this.record[field].value)) {
1129             url = 'data:image/png;base64,' + this.record[field].value;
1130         } else if (this.record[field] && ! this.record[field].value) {
1131             url = "/web/static/src/img/placeholder.png";
1132         } else {
1133             id = JSON.stringify(id);
1134             if (options.preview_image)
1135                 field = options.preview_image;
1136             url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
1137             if (cache !== undefined) {
1138                 // Set the cache duration in seconds.
1139                 url += '&cache=' + parseInt(cache, 10);
1140             }
1141         }
1142         return url;
1143     },
1144     kanban_text_ellipsis: function(s, size) {
1145         size = size || 160;
1146         if (!s) {
1147             return '';
1148         } else if (s.length <= size) {
1149             return s;
1150         } else {
1151             return s.substr(0, size) + '...';
1152         }
1153     },
1154     kanban_compute_domain: function(domain) {
1155         return instance.web.form.compute_domain(domain, this.values);
1156     }
1157 });
1158
1159 /**
1160  * Quick creation view.
1161  *
1162  * Triggers a single event "added" with a single parameter "name", which is the
1163  * name entered by the user
1164  *
1165  * @class
1166  * @type {*}
1167  */
1168 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
1169     template: 'KanbanView.quick_create',
1170
1171     /**
1172      * close_btn: If true, the widget will display a "Close" button able to trigger
1173      * a "close" event.
1174      */
1175     init: function(parent, dataset, context, buttons) {
1176         this._super(parent);
1177         this._dataset = dataset;
1178         this._buttons = buttons || false;
1179         this._context = context || {};
1180     },
1181     start: function () {
1182         var self = this;
1183         self.$input = this.$el.find('input');
1184         self.$input.keyup(function(event){
1185             if(event.keyCode == 13){
1186                 self.quick_add();
1187             }
1188         });
1189         $(".oe_kanban_quick_create").focusout(function (e) {
1190             var val = self.$el.find('input').val();
1191             if (/^\s*$/.test(val)) { self.trigger('close'); }
1192             e.stopImmediatePropagation();
1193         });
1194         $(".oe_kanban_quick_create_add", this.$el).click(function () {
1195             self.quick_add();
1196             self.focus();
1197         });
1198         $(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
1199             ev.preventDefault();
1200             self.trigger('close');
1201         });
1202         self.$input.keyup(function(e) {
1203             if (e.keyCode == 27 && self._buttons) {
1204                 self.trigger('close');
1205             }
1206         });
1207     },
1208     focus: function() {
1209         this.$el.find('input').focus();
1210     },
1211     /**
1212      * Handles user event from nested quick creation view
1213      */
1214     quick_add: function () {
1215         var self = this;
1216         var val = this.$input.val();
1217         if (/^\s*$/.test(val)) { this.$el.remove(); return; }
1218         this._dataset.call(
1219             'name_create', [val, new instance.web.CompoundContext(
1220                     this._dataset.get_context(), this._context)])
1221             .then(function(record) {
1222                 self.$input.val("");
1223                 self.trigger('added', record[0]);
1224             }, function(error, event) {
1225                 event.preventDefault();
1226                 return self.slow_create();
1227             });
1228     },
1229     slow_create: function() {
1230         var self = this;
1231         var pop = new instance.web.form.SelectCreatePopup(this);
1232         pop.select_element(
1233             self._dataset.model,
1234             {
1235                 title: _t("Create: ") + (this.string || this.name),
1236                 initial_view: "form",
1237                 disable_multiple_selection: true
1238             },
1239             [],
1240             {"default_name": self.$input.val()}
1241         );
1242         pop.on("elements_selected", self, function(element_ids) {
1243             self.$input.val("");
1244             self.trigger('added', element_ids[0]);
1245         });
1246     }
1247 });
1248
1249 /**
1250  * Interface to be implemented by kanban fields.
1251  *
1252  */
1253 instance.web_kanban.FieldInterface = {
1254     /**
1255         Constructor.
1256         - parent: The widget's parent.
1257         - field: A dictionary giving details about the field, including the current field's value in the
1258             raw_value field.
1259         - $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object.
1260     */
1261     init: function(parent, field, $node) {},
1262 };
1263
1264 /**
1265  * Abstract class for classes implementing FieldInterface.
1266  *
1267  * Properties:
1268  *     - value: useful property to hold the value of the field. By default, the constructor
1269  *     sets value property.
1270  *
1271  */
1272 instance.web_kanban.AbstractField = instance.web.Widget.extend(instance.web_kanban.FieldInterface, {
1273     /**
1274         Constructor that saves the field and $node parameters and sets the "value" property.
1275     */
1276     init: function(parent, field, $node) {
1277         this._super(parent);
1278         this.field = field;
1279         this.$node = $node;
1280         this.options = instance.web.py_eval(this.$node.attr("options") || '{}');
1281         this.set("value", field.raw_value);
1282     },
1283 });
1284
1285 instance.web_kanban.Priority = instance.web_kanban.AbstractField.extend({
1286     init: function(parent, field, $node) {
1287         this._super.apply(this, arguments);
1288         this.name = $node.attr('name')
1289         this.parent = parent;
1290     },
1291     prepare_priority: function() {
1292         var self = this;
1293         var selection = this.field.selection || [];
1294         var init_value = selection && selection[0][0] || 0;
1295         var data = _.map(selection.slice(1), function(element, index) {
1296             var value = {
1297                 'value': element[0],
1298                 'name': element[1],
1299                 'click_value': element[0],
1300             }
1301             if (index == 0 && self.get('value') == element[0]) {
1302                 value['click_value'] = init_value;
1303             }
1304             return value;
1305         });
1306         return data;
1307     },
1308     renderElement: function() {
1309         var self = this;
1310         this.record_id = self.parent.id;
1311         this.priorities = self.prepare_priority();
1312         this.$el = $(QWeb.render("Priority", {'widget': this}));
1313         this.$el.find('li').click(self.do_action.bind(self));
1314     },
1315     do_action: function(e) {
1316         var self = this;
1317         var li = $(e.target).closest( "li" );
1318         if (li.length) {
1319             var value = {};
1320             value[self.name] = String(li.data('value'));
1321             return self.parent.view.dataset._model.call('write', [[self.record_id], value, self.parent.view.dataset.get_context()]).done(self.reload_record.bind(self.parent));
1322         }
1323     },
1324     reload_record: function() {
1325         this.do_reload();
1326     },
1327 });
1328
1329 instance.web_kanban.KanbanSelection = instance.web_kanban.AbstractField.extend({
1330     init: function(parent, field, $node) {
1331         this._super.apply(this, arguments);
1332         this.name = $node.attr('name')
1333         this.parent = parent;
1334     },
1335     prepare_dropdown_selection: function() {
1336         var data = [];
1337         _.map(this.field.selection || [], function(res) {
1338             var value = {
1339                 'name': res[0],
1340                 'tooltip': res[1],
1341                 'state_name': res[1],
1342             }
1343             if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
1344             else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
1345             else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
1346             data.push(value);
1347         });
1348         return data;
1349     },
1350     renderElement: function() {
1351         var self = this;
1352         this.record_id = self.parent.id;
1353         this.states = self.prepare_dropdown_selection();;
1354         this.$el = $(QWeb.render("KanbanSelection", {'widget': self}));
1355         this.$el.find('li').click(self.do_action.bind(self));
1356     },
1357     do_action: function(e) {
1358         var self = this;
1359         var li = $(e.target).closest( "li" );
1360         if (li.length) {
1361             var value = {};
1362             value[self.name] = String(li.data('value'));
1363             return self.parent.view.dataset._model.call('write', [[self.record_id], value, self.parent.view.dataset.get_context()]).done(self.reload_record.bind(self.parent));
1364         }
1365     },
1366     reload_record: function() {
1367         this.do_reload();
1368     },
1369 });
1370
1371 instance.web_kanban.fields_registry = new instance.web.Registry({});
1372 instance.web_kanban.fields_registry.add('priority','instance.web_kanban.Priority');
1373 instance.web_kanban.fields_registry.add('kanban_state_selection','instance.web_kanban.KanbanSelection');
1374 };
1375
1376 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: