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