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