1 openerp.web_kanban = function (instance) {
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');
8 instance.web_kanban.KanbanView = instance.web.View.extend({
9 template: "KanbanView",
10 display_name: _lt('Kanban'),
11 default_nr_columns: 1,
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);
18 _.defaults(this.options, {
19 "quick_creatable": true,
21 "create_text": undefined,
22 "read_only_mode": false,
23 "confirm_on_delete": true,
25 this.fields_view = {};
26 this.fields_keys = [];
28 this.group_by_field = {};
29 this.grouped_by_m2o = false;
37 this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
38 this.qweb = new QWeb2.Engine();
39 this.qweb.debug = instance.session.debug;
40 this.qweb.default_dict = _.clone(QWeb.default_dict);
41 this.has_been_loaded = $.Deferred();
42 this.search_domain = this.search_context = this.search_group_by = null;
43 this.currently_dragging = {};
44 this.limit = options.limit || 40;
45 this.add_group_mutex = new $.Mutex();
46 this.on('view_loaded', self, self.load_kanban);
50 this._super.apply(this, arguments);
51 this.$el.on('click', '.oe_kanban_dummy_cell', function() {
53 self.$buttons.find('.oe_kanban_add_column').effect('bounce', {distance: 18, times: 5}, 150);
58 this._super.apply(this, arguments);
59 $('html').off('click.kanban');
61 load_kanban: function(data) {
62 this.fields_view = data;
63 this.$el.addClass(this.fields_view.arch.attrs['class']);
64 this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
65 if (this.options.$buttons) {
66 this.$buttons.appendTo(this.options.$buttons);
68 this.$el.find('.oe_kanban_buttons').replaceWith(this.$buttons);
71 .on('click', 'button.oe_kanban_button_new', this.do_add_record)
72 .on('click', '.oe_kanban_add_column', this.do_add_group);
73 this.$groups = this.$el.find('.oe_kanban_groups tr');
74 this.fields_keys = _.keys(this.fields_view.fields);
75 this.add_qweb_template();
76 this.has_been_loaded.resolve();
77 this.trigger('kanban_view_loaded', data);
80 _is_quick_create_enabled: function() {
81 if (!this.options.quick_creatable || !this.is_action_enabled('create'))
83 if (this.fields_view.arch.attrs.quick_create !== undefined)
84 return JSON.parse(this.fields_view.arch.attrs.quick_create);
85 return !! this.group_by;
87 is_action_enabled: function(action) {
88 if (action === 'create' && !this.options.creatable)
90 return this._super(action);
93 * select the nodes into the xml and send to extract_aggregates the nodes with TagName="field"
95 add_qweb_template: function() {
96 for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
97 var child = this.fields_view.arch.children[i];
98 if (child.tag === "templates") {
99 this.transform_qweb_template(child);
100 this.qweb.add_template(instance.web.json_node_to_xml(child));
102 } else if (child.tag === 'field') {
103 this.extract_aggregates(child);
107 /* extract_aggregates
108 * extract the agggregates from the nodes (TagName="field")
110 extract_aggregates: function(node) {
111 for (var j = 0, jj = this.group_operators.length; j < jj; j++) {
112 if (node.attrs[this.group_operators[j]]) {
113 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
118 transform_qweb_template: function(node) {
119 var qweb_add_if = function(node, condition) {
120 if (node.attrs[QWeb.prefix + '-if']) {
121 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
123 node.attrs[QWeb.prefix + '-if'] = condition;
126 if (node.tag && node.attrs.modifiers) {
127 var modifiers = JSON.parse(node.attrs.modifiers || '{}');
128 if (modifiers.invisible) {
129 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
134 if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
135 this.many2manys.push(node.attrs.name);
137 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
139 node.tag = QWeb.prefix;
140 node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
145 var type = node.attrs.type || '';
146 if (_.indexOf('action,object,edit,open,delete'.split(','), type) !== -1) {
147 _.each(node.attrs, function(v, k) {
148 if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
149 node.attrs['data-' + k] = v;
150 delete(node.attrs[k]);
153 if (node.attrs['data-string']) {
154 node.attrs.title = node.attrs['data-string'];
156 if (node.attrs['data-icon']) {
160 src: instance.session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
166 if (node.tag == 'a') {
167 node.attrs.href = '#';
169 node.attrs.type = 'button';
171 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
176 for (var i = 0, ii = node.children.length; i < ii; i++) {
177 this.transform_qweb_template(node.children[i]);
181 do_add_record: function() {
182 this.dataset.index = null;
183 this.do_switch_view('form');
185 do_add_group: function() {
188 name: _t("Add column"),
189 res_model: self.group_by_field.relation,
190 views: [[false, 'form']],
191 type: 'ir.actions.act_window',
194 action_buttons: true,
197 var am = instance.webclient.action_manager;
198 var form = am.dialog_widget.views.form.controller;
199 form.on("on_button_cancel", am.dialog, am.dialog.close);
200 form.on('record_created', self, function(r) {
201 (new instance.web.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) {
203 var domain = self.dataset.domain.slice(0);
204 domain.push([self.group_by, '=', new_record[0][0]]);
205 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain);
210 value: new_record[0],
214 var new_group = new instance.web_kanban.KanbanGroup(self, [], datagroup, dataset);
215 self.do_add_groups([new_group]).done(function() {
216 $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' });
221 do_search: function(domain, context, group_by) {
223 this.$el.find('.oe_view_nocontent').remove();
224 this.search_domain = domain;
225 this.search_context = context;
226 this.search_group_by = group_by;
227 $.when(this.has_been_loaded).done(function() {
228 self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
229 self.group_by_field = self.fields_view.fields[self.group_by] || {};
230 self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
231 self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
232 self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
233 var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(self.group_by);
234 $.when(grouping).done(function(groups) {
236 self.do_process_groups(groups);
238 self.do_process_dataset();
243 do_process_groups: function(groups) {
245 this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
246 this.add_group_mutex.exec(function() {
247 self.dataset.ids = [];
248 var remaining = groups.length - 1,
250 return $.when.apply(null, _.map(groups, function (group, index) {
251 self.do_clear_groups();
252 var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
253 new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
254 return dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit })
255 .then(function(records) {
256 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
257 groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
259 self.dataset.index = self.dataset.size() ? 0 : null;
260 return self.do_add_groups(groups_array);
266 do_process_dataset: function() {
268 this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
269 this.add_group_mutex.exec(function() {
270 var def = $.Deferred();
271 self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
272 self.do_clear_groups();
273 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
274 self.do_add_groups([kgroup]).done(function() {
275 if (_.isEmpty(records)) {
280 }).done(null, function() {
286 do_reload: function() {
287 this.do_search(this.search_domain, this.search_context, this.search_group_by);
289 do_clear_groups: function() {
290 var groups = this.groups.slice(0);
292 _.each(groups, function(group) {
296 do_add_groups: function(groups) {
298 var $parent = this.$el.parent();
300 _.each(groups, function(group) {
301 self.groups[group.undefined_title ? 'unshift' : 'push'](group);
303 var $last_td = self.$el.find('.oe_kanban_groups_headers td:last');
304 var groups_started = _.map(this.groups, function(group) {
305 if (!group.is_started) {
306 return group.insertBefore($last_td);
309 return $.when.apply(null, groups_started).done(function () {
310 self.on_groups_started();
311 self.$el.appendTo($parent);
312 _.each(self.groups, function(group) {
313 group.compute_cards_auto_height();
317 on_groups_started: function() {
319 this.compute_groups_width();
321 // Kanban cards drag'n'drop
322 var $columns = this.$el.find('.oe_kanban_column');
324 handle : '.oe_kanban_draghandle',
325 start: function(event, ui) {
326 self.currently_dragging.index = ui.item.index();
327 self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
328 ui.item.find('*').on('click.prevent', function(ev) {
331 ui.placeholder.height(ui.item.height());
334 stop: function(event, ui) {
335 var record = ui.item.data('widget');
336 var old_index = self.currently_dragging.index;
337 var new_index = ui.item.index();
338 var old_group = self.currently_dragging.group;
339 var new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
340 if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
341 self.on_record_moved(record, old_group, old_index, new_group, new_index);
343 setTimeout(function() {
344 // A bit hacky but could not find a better solution for Firefox (problem not present in chrome)
345 // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop
346 ui.item.find('*').off('click.prevent');
351 // Keep connectWith out of the sortable initialization for performance sake:
352 // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html
353 $columns.sortable({ connectWith: $columns });
355 // Kanban groups drag'n'drop
357 if (this.grouped_by_m2o) {
358 this.$('.oe_kanban_groups_headers').sortable({
359 items: '.oe_kanban_group_header',
364 start: function(event, ui) {
365 start_index = ui.item.index();
366 self.$('.oe_kanban_record').css({ visibility: 'hidden' });
368 stop: function(event, ui) {
369 var stop_index = ui.item.index();
370 if (start_index !== stop_index) {
371 var $start_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(start_index);
372 var $stop_column = $('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index);
373 var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter';
374 $start_column[method]($stop_column);
375 var tmp_group = self.groups.splice(start_index, 1)[0];
376 self.groups.splice(stop_index, 0, tmp_group);
377 var new_sequence = _.pluck(self.groups, 'value');
378 (new instance.web.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) {
380 console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation);
384 self.$('.oe_kanban_record').css({ visibility: 'visible' });
389 this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
391 this.postprocess_m2m_tags();
393 on_record_moved : function(record, old_group, old_index, new_group, new_index) {
396 $(old_group.$el).add(new_group.$el).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
397 if (old_group === new_group) {
398 new_group.records.splice(old_index, 1);
399 new_group.records.splice(new_index, 0, record);
400 new_group.do_save_sequences();
402 old_group.records.splice(old_index, 1);
403 new_group.records.splice(new_index, 0, record);
404 record.group = new_group;
406 data[this.group_by] = new_group.value;
407 this.dataset.write(record.id, data, {}).done(function() {
409 new_group.do_save_sequences();
410 }).fail(function(error, evt) {
411 evt.preventDefault();
412 alert("An error has occured while moving the record to this group.");
413 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
417 compute_groups_width: function() {
420 _.each(this.groups, function(group) {
421 unfolded += group.state.folded ? 0 : 1;
422 group.$el.children(':first').css('width', '');
424 _.each(this.groups, function(group) {
425 if (!group.state.folded) {
426 if (182*unfolded>=self.$el.width()) {
427 group.$el.children(':first').css('width', "170px");
428 } else if (262*unfolded<self.$el.width()) {
429 group.$el.children(':first').css('width', "250px");
431 // -12 because of padding 6 between cards
432 // -1 because of the border of the latest dummy column
433 group.$el.children(':first').css('width', Math.floor((self.$el.width()-1)/unfolded)-12 + 'px');
439 do_show: function() {
441 this.$buttons.show();
443 this.do_push_state({});
444 return this._super();
446 do_hide: function () {
448 this.$buttons.hide();
450 return this._super();
452 open_record: function(id, editable) {
453 if (this.dataset.select_id(id)) {
454 this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
456 this.do_warn("Kanban: could not find id#" + id);
459 no_result: function() {
460 if (this.groups.group_by
461 || !this.options.action
462 || !this.options.action.help) {
465 this.$el.find('.oe_view_nocontent').remove();
467 $('<div class="oe_view_nocontent">').html(this.options.action.help)
469 var create_nocontent = this.$buttons;
470 this.$el.find('.oe_view_nocontent').click(function() {
471 create_nocontent.effect('bounce', {distance: 18, times: 5}, 150);
476 * postprocessing of fields type many2many
477 * make the rpc request for all ids/model and insert value inside .oe_tags fields
479 postprocess_m2m_tags: function() {
481 if (!this.many2manys.length) {
485 this.groups.forEach(function(group) {
486 group.records.forEach(function(record) {
487 self.many2manys.forEach(function(name) {
488 var field = record.record[name];
489 var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty();
490 if (!relations[field.relation]) {
491 relations[field.relation] = { ids: [], elements: {}};
493 var rel = relations[field.relation];
494 field.raw_value.forEach(function(id) {
496 if (!rel.elements[id]) {
497 rel.elements[id] = [];
499 rel.elements[id].push($el[0]);
504 _.each(relations, function(rel, rel_name) {
505 var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
506 dataset.name_get(_.uniq(rel.ids)).done(function(result) {
507 result.forEach(function(nameget) {
508 $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
516 function get_class(name) {
517 return new instance.web.Registry({'tmp' : name}).get_object("tmp");
520 instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
521 template: 'KanbanView.group_header',
522 init: function (parent, records, group, dataset) {
525 this.$has_been_started = $.Deferred();
528 this.dataset = dataset;
529 this.dataset_offset = 0;
530 this.aggregates = {};
531 this.value = this.title = null;
533 this.value = group.get('value');
534 this.title = group.get('value');
535 if (this.value instanceof Array) {
536 this.title = this.value[1];
537 this.value = this.value[0];
539 var field = this.view.group_by_field;
540 if (!_.isEmpty(field)) {
542 this.title = instance.web.format_value(group.get('value'), field, false);
545 _.each(this.view.aggregates, function(value, key) {
546 self.aggregates[value] = group.get('aggregates')[key];
550 if (this.title === false) {
551 this.title = _t('Undefined');
552 this.undefined_title = true;
554 var key = this.view.group_by + '-' + this.value;
555 if (!this.view.state.groups[key]) {
556 this.view.state.groups[key] = {
557 folded: group ? group.get('folded') : false
560 this.state = this.view.state.groups[key];
561 this.$records = null;
564 this.$has_been_started.done(function() {
565 self.do_add_records(records);
571 if (! self.view.group_by) {
572 self.$el.addClass("oe_kanban_no_group");
573 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
574 .on('added', self, self.proxy('quick_created'));
575 self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
577 this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
578 this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
580 this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
581 var fn = 'do_action_' + $(ev.target).data().action;
582 if (typeof(self[fn]) === 'function') {
583 self[fn]($(ev.target));
587 this.$el.find('.oe_kanban_add').click(function () {
589 return self.quick.trigger('close');
592 ctx['default_' + self.view.group_by] = self.value;
593 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
594 .on('added', self, self.proxy('quick_created'))
595 .on('close', self, function() {
596 this.quick.destroy();
599 self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
602 // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
603 this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
604 if (this.state.folded) {
605 this.do_toggle_fold();
607 this.$el.data('widget', this);
608 this.$records.data('widget', this);
609 this.$has_been_started.resolve();
610 var add_btn = this.$el.find('.oe_kanban_add');
611 add_btn.tipsy({delayIn: 500, delayOut: 1000});
612 this.$records.click(function (ev) {
613 if (ev.target == ev.currentTarget) {
614 if (!self.state.folded) {
615 add_btn.effect('bounce', {distance: 18, times: 5}, 150);
619 this.is_started = true;
622 compute_cards_auto_height: function() {
623 // oe_kanban_no_auto_height is an empty class used to disable this feature
624 if (!this.view.group_by) {
627 _.each(this.records, function(r) {
628 var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
631 min_height = Math.max(min_height, $e.outerHeight());
634 $(els).css('min-height', min_height);
637 destroy: function() {
640 this.$records.remove();
643 do_show_more: function(evt) {
645 var ids = self.view.dataset.ids.splice(0);
646 return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
647 'limit': self.view.limit,
648 'offset': self.dataset_offset += self.view.limit
649 }).then(function(records) {
650 self.view.dataset.ids = self.view.dataset.ids.concat(ids);
651 self.do_add_records(records);
655 do_add_records: function(records, prepend) {
657 var $list_header = this.$records.find('.oe_kanban_group_list_header');
658 var $show_more = this.$records.find('.oe_kanban_show_more');
660 _.each(records, function(record) {
661 var rec = new instance.web_kanban.KanbanRecord(self, record);
663 rec.insertBefore($show_more);
664 self.records.push(rec);
666 rec.insertAfter($list_header);
667 self.records.unshift(rec);
670 if ($show_more.length) {
671 var size = this.dataset.size();
672 $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
675 remove_record: function(id, remove_from_dataset) {
676 for (var i = 0; i < this.records.length; i++) {
677 if (this.records[i]['id'] === id) {
678 this.records.splice(i, 1);
683 do_toggle_fold: function(compute_width) {
684 this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
685 this.state.folded = this.$el.is('.oe_kanban_group_folded');
686 this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
688 do_action_toggle_fold: function() {
689 this.do_toggle_fold();
690 this.view.compute_groups_width();
692 do_action_edit: function() {
696 name: _t("Edit column"),
697 res_model: self.view.group_by_field.relation,
698 views: [[false, 'form']],
699 type: 'ir.actions.act_window',
702 action_buttons: true,
705 var am = instance.webclient.action_manager;
706 var form = am.dialog_widget.views.form.controller;
707 form.on("on_button_cancel", am.dialog, am.dialog.close);
708 form.on('record_saved', self, function() {
710 self.view.do_reload();
713 do_action_delete: function() {
715 if (confirm(_t("Are you sure to remove this column ?"))) {
716 (new instance.web.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
717 self.view.do_reload();
721 do_save_sequences: function() {
723 if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
724 var new_sequence = _.pluck(this.records, 'id');
725 self.view.dataset.resequence(new_sequence);
729 * Handles a newly created record
731 * @param {id} id of the newly created record
733 quick_created: function (record) {
734 var id = record, self = this;
735 this.dataset.read_ids([id], this.view.fields_keys)
736 .done(function (records) {
737 self.view.dataset.ids.push(id);
738 self.do_add_records(records, true);
743 instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
744 template: 'KanbanView.record',
745 init: function (parent, record) {
748 this.view = parent.view;
750 this.set_record(record);
751 if (!this.view.state.records[this.id]) {
752 this.view.state.records[this.id] = {
756 this.state = this.view.state.records[this.id];
758 set_record: function(record) {
762 _.each(record, function(v, k) {
767 this.record = this.transform_record(record);
771 this.$el.data('widget', this);
774 transform_record: function(record) {
777 _.each(record, function(value, name) {
778 var r = _.clone(self.view.fields_view.fields[name] || {});
779 if ((r.type === 'date' || r.type === 'datetime') && value) {
780 r.raw_value = instance.web.auto_str_to_date(value);
784 r.value = instance.web.format_value(value, r);
785 new_record[name] = r;
789 renderElement: function() {
790 this.qweb_context = {
793 read_only_mode: this.view.options.read_only_mode,
795 for (var p in this) {
796 if (_.str.startsWith(p, 'kanban_')) {
797 this.qweb_context[p] = _.bind(this[p], this);
800 var $el = instance.web.qweb.render(this.template, {
802 'content': this.view.qweb.render('kanban-box', this.qweb_context)
804 this.replaceElement($el);
806 bind_events: function() {
808 this.setup_color_picker();
809 this.$el.find('[tooltip]').tipsy({
814 var template = $(this).attr('tooltip');
815 if (!self.view.qweb.has_template(template)) {
818 return self.view.qweb.render(template, self.qweb_context);
826 // If no draghandle is found, make the whole card as draghandle (provided one can edit)
827 if (!this.$el.find('.oe_kanban_draghandle').length) {
828 this.$el.children(':first')
829 .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
832 this.$el.find('.oe_kanban_action').click(function(ev) {
834 var $action = $(this),
835 type = $action.data('type') || 'button',
836 method = 'do_action_' + (type === 'action' ? 'object' : type);
837 if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
838 self.view.open_record(self.id, true);
839 } else if (_.str.startsWith(type, 'switch_')) {
840 self.view.do_switch_view(type.substr(7));
841 } else if (typeof self[method] === 'function') {
842 self[method]($action);
844 self.do_warn("Kanban: no action for type : " + type);
848 if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
849 this.$el.on('click', function(ev) {
850 if (!ev.isTrigger && !$._data(ev.target, 'events')) {
852 var elem = ev.target;
856 var events = $._data(elem, 'events');
857 if (elem == ev.currentTarget) {
862 if (events && events.click) {
863 // do not trigger global click if one child has a click event registered
867 if (trigger && events && events.click) {
868 _.each(events.click, function(click_event) {
869 if (click_event.selector) {
870 // For each parent of original target, check if a
871 // delegated click is bound to any previously found children
872 _.each(children, function(child) {
873 if ($(child).is(click_event.selector)) {
880 elem = elem.parentElement;
883 self.on_card_clicked(ev);
889 /* actions when user click on the block with a specific class
890 * open on normal view : oe_kanban_global_click
891 * open on form/edit view : oe_kanban_global_click_edit
893 on_card_clicked: function(ev) {
894 if(this.$el.find('.oe_kanban_global_click_edit').size()>0)
895 this.do_action_edit();
897 this.do_action_open();
899 setup_color_picker: function() {
901 var $el = this.$el.find('ul.oe_kanban_colorpicker');
903 $el.html(QWeb.render('KanbanColorPicker', {
906 $el.on('click', 'a', function(ev) {
908 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
910 data[color_field] = $(this).data('color');
911 self.view.dataset.write(self.id, data, {}).done(function() {
912 self.record[color_field] = $(this).data('color');
918 do_action_delete: function($action) {
921 return $.when(self.view.dataset.unlink([self.id])).done(function() {
922 self.group.remove_record(self.id);
926 if (this.view.options.confirm_on_delete) {
927 if (confirm(_t("Are you sure you want to delete this record ?"))) {
933 do_action_edit: function($action) {
934 this.view.open_record(this.id, true);
936 do_action_open: function($action) {
937 this.view.open_record(this.id);
939 do_action_object: function ($action) {
940 var button_attrs = $action.data();
941 this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
943 do_reload: function() {
945 this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
946 if (records.length) {
947 self.set_record(records[0]);
948 self.renderElement();
949 self.$el.data('widget', self);
951 self.group.compute_cards_auto_height();
952 self.view.postprocess_m2m_tags();
958 kanban_getcolor: function(variable) {
960 switch (typeof(variable)) {
962 for (var i=0, ii=variable.length; i<ii; i++) {
963 index += variable.charCodeAt(i);
967 index = Math.round(variable);
972 var color = (index % this.view.number_of_color_schemes);
975 kanban_color: function(variable) {
976 var color = this.kanban_getcolor(variable);
977 return color === '' ? '' : 'oe_kanban_color_' + color;
979 kanban_gravatar: function(email, size) {
981 email = _.str.trim(email || '').toLowerCase();
982 var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
983 var email_md5 = $.md5(email);
984 return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
986 kanban_image: function(model, field, id, cache, options) {
987 options = options || {};
989 if (this.record[field] && this.record[field].value && ! /^\d+(\.\d*)? \w+$/.test(this.record[field].value)) {
990 url = 'data:image/png;base64,' + this.record[field].value;
991 } else if (this.record[field] && ! this.record[field].value) {
992 url = "/web/static/src/img/placeholder.png";
994 id = JSON.stringify(id);
995 if (options.preview_image)
996 field = options.preview_image;
997 url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
998 if (cache !== undefined) {
999 // Set the cache duration in seconds.
1000 url += '&cache=' + parseInt(cache, 10);
1005 kanban_text_ellipsis: function(s, size) {
1009 } else if (s.length <= size) {
1012 return s.substr(0, size) + '...';
1015 kanban_compute_domain: function(domain) {
1016 return instance.web.form.compute_domain(domain, this.values);
1021 * Quick creation view.
1023 * Triggers a single event "added" with a single parameter "name", which is the
1024 * name entered by the user
1029 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
1030 template: 'KanbanView.quick_create',
1033 * close_btn: If true, the widget will display a "Close" button able to trigger
1036 init: function(parent, dataset, context, buttons) {
1037 this._super(parent);
1038 this._dataset = dataset;
1039 this._buttons = buttons || false;
1040 this._context = context || {};
1042 start: function () {
1044 self.$input = this.$el.find('input');
1045 self.$input.keyup(function(event){
1046 if(event.keyCode == 13){
1050 $(".oe_kanban_quick_create_add", this.$el).click(function () {
1054 $(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
1055 ev.preventDefault();
1056 self.trigger('close');
1058 self.$input.keyup(function(e) {
1059 if (e.keyCode == 27 && self._buttons) {
1060 self.trigger('close');
1065 this.$el.find('input').focus();
1068 * Handles user event from nested quick creation view
1070 quick_add: function () {
1073 'name_create', [self.$input.val() || false, new instance.web.CompoundContext(
1074 this._dataset.get_context(), this._context)])
1075 .then(function(record) {
1076 self.$input.val("");
1077 self.trigger('added', record[0]);
1078 }, function(error, event) {
1079 event.preventDefault();
1080 return self.slow_create();
1083 slow_create: function() {
1085 var pop = new instance.web.form.SelectCreatePopup(this);
1087 self._dataset.model,
1089 title: _t("Create: ") + (this.string || this.name),
1090 initial_view: "form",
1091 disable_multiple_selection: true
1094 {"default_name": self.$input.val()}
1096 pop.on("elements_selected", self, function(element_ids) {
1097 self.$input.val("");
1098 self.trigger('added', element_ids[0]);
1104 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: