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 if (!this.options.$buttons || !this.options.$buttons.length) {
47 this.options.$buttons = false;
50 view_loading: function(r) {
51 return this.load_kanban(r);
55 this._super.apply(this, arguments);
56 this.$el.on('click', '.oe_kanban_dummy_cell', function() {
58 self.$buttons.find('.oe_kanban_add_column').openerpBounce();
63 this._super.apply(this, arguments);
64 $('html').off('click.kanban');
66 load_kanban: function(data) {
67 this.fields_view = data;
69 // use default order if defined in xml description
70 var default_order = this.fields_view.arch.attrs.default_order,
71 unsorted = !this.dataset._sort.length;
72 if (unsorted && default_order) {
73 this.dataset.set_sort(default_order.split(','));
75 this.$el.addClass(this.fields_view.arch.attrs['class']);
76 this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
77 if (this.options.$buttons) {
78 this.$buttons.appendTo(this.options.$buttons);
80 this.$('.oe_kanban_buttons').replaceWith(this.$buttons);
83 .on('click', 'button.oe_kanban_button_new', this.do_add_record)
84 .on('click', '.oe_kanban_add_column', this.do_add_group);
85 this.$groups = this.$el.find('.oe_kanban_groups tr');
86 this.fields_keys = _.keys(this.fields_view.fields);
87 this.add_qweb_template();
88 this.has_been_loaded.resolve();
89 this.trigger('kanban_view_loaded', data);
92 _is_quick_create_enabled: function() {
93 if (!this.options.quick_creatable || !this.is_action_enabled('create'))
95 if (this.fields_view.arch.attrs.quick_create !== undefined)
96 return JSON.parse(this.fields_view.arch.attrs.quick_create);
97 return !! this.group_by;
99 is_action_enabled: function(action) {
100 if (action === 'create' && !this.options.creatable)
102 return this._super(action);
105 * select the nodes into the xml and send to extract_aggregates the nodes with TagName="field"
107 add_qweb_template: function() {
108 for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
109 var child = this.fields_view.arch.children[i];
110 if (child.tag === "templates") {
111 this.transform_qweb_template(child);
112 this.qweb.add_template(instance.web.json_node_to_xml(child));
114 } else if (child.tag === 'field') {
115 this.extract_aggregates(child);
119 /* extract_aggregates
120 * extract the agggregates from the nodes (TagName="field")
122 extract_aggregates: function(node) {
123 for (var j = 0, jj = this.group_operators.length; j < jj; j++) {
124 if (node.attrs[this.group_operators[j]]) {
125 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
130 transform_qweb_template: function(node) {
131 var qweb_add_if = function(node, condition) {
132 if (node.attrs[QWeb.prefix + '-if']) {
133 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
135 node.attrs[QWeb.prefix + '-if'] = condition;
138 if (node.tag && node.attrs.modifiers) {
139 var modifiers = JSON.parse(node.attrs.modifiers || '{}');
140 if (modifiers.invisible) {
141 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
146 var ftype = this.fields_view.fields[node.attrs.name].type;
147 ftype = node.attrs.widget ? node.attrs.widget : ftype;
148 if (ftype === 'many2many') {
149 if (_.indexOf(this.many2manys, node.attrs.name) < 0) {
150 this.many2manys.push(node.attrs.name);
153 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
154 } else if (instance.web_kanban.fields_registry.contains(ftype)) {
155 // do nothing, the kanban record will handle it
157 node.tag = QWeb.prefix;
158 node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
163 var type = node.attrs.type || '';
164 if (_.indexOf('action,object,edit,open,delete,url'.split(','), type) !== -1) {
165 _.each(node.attrs, function(v, k) {
166 if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
167 node.attrs['data-' + k] = v;
168 delete(node.attrs[k]);
171 if (node.attrs['data-string']) {
172 node.attrs.title = node.attrs['data-string'];
174 if (node.attrs['data-icon']) {
178 src: instance.session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
184 if (node.tag == 'a' && node.attrs['data-type'] != "url") {
185 node.attrs.href = '#';
187 node.attrs.type = 'button';
189 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
194 for (var i = 0, ii = node.children.length; i < ii; i++) {
195 this.transform_qweb_template(node.children[i]);
199 do_add_record: function() {
200 this.dataset.index = null;
201 this.do_switch_view('form');
203 do_add_group: function() {
206 name: _t("Add column"),
207 res_model: self.group_by_field.relation,
208 views: [[false, 'form']],
209 type: 'ir.actions.act_window',
211 context: self.dataset.get_context(),
213 action_buttons: true,
216 var am = instance.webclient.action_manager;
217 var form = am.dialog_widget.views.form.controller;
218 form.on("on_button_cancel", am.dialog, am.dialog.close);
219 form.on('record_created', self, function(r) {
220 (new instance.web.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) {
222 var domain = self.dataset.domain.slice(0);
223 domain.push([self.group_by, '=', new_record[0][0]]);
224 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain);
229 value: new_record[0],
233 var new_group = new instance.web_kanban.KanbanGroup(self, [], datagroup, dataset);
234 self.do_add_groups([new_group]).done(function() {
235 $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' });
240 do_search: function(domain, context, group_by) {
242 this.search_domain = domain;
243 this.search_context = context;
244 this.search_group_by = group_by;
245 return $.when(this.has_been_loaded).then(function() {
246 self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
247 self.group_by_field = self.fields_view.fields[self.group_by] || {};
248 self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
249 self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
250 self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
251 var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
252 if (!_.isEmpty(grouping_fields)) {
253 // ensure group_by fields are read.
254 self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields));
256 var grouping = new instance.web.Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields);
257 return self.alive($.when(grouping)).then(function(groups) {
258 self.remove_no_result();
260 return self.do_process_groups(groups);
262 return self.do_process_dataset();
267 do_process_groups: function(groups) {
270 // Check in the arch the fields to fetch on the stage to get tooltips data.
271 // Fetching data is done in batch for all stages, to avoid doing multiple
272 // calls. The first naive implementation of group_by_tooltip made a call
273 // for each displayed stage and was quite limited.
274 // Data for the group tooltip (group_by_tooltip) and to display stage-related
275 // legends for kanban state management (states_legend) are fetched in
277 var group_by_fields_to_read = [];
278 var recurse = function(node) {
279 if (node.tag === "field" && node.attrs && node.attrs.options) {
280 var options = instance.web.py_eval(node.attrs.options);
281 var states_fields_to_read = _.map(
282 options && options.states_legend || {},
283 function (value, key, list) { return value; });
284 var tooltip_fields_to_read = _.map(
285 options && options.group_by_tooltip || {},
286 function (value, key, list) { return key; });
287 group_by_fields_to_read = _.union(
288 group_by_fields_to_read,
289 states_fields_to_read,
290 tooltip_fields_to_read);
292 _.each(node.children, function(child) {
296 recurse(this.fields_view.arch);
297 var group_ids = _.map(groups, function (elem) { return elem.attributes.value[0]});
298 if (this.grouped_by_m2o && group_ids.length && group_by_fields_to_read.length) {
299 var group_data = new instance.web.DataSet(
301 this.group_by_field.relation).read_ids(group_ids, _.union(['display_name'], group_by_fields_to_read));
303 else { var group_data = $.Deferred().resolve({}); }
305 this.$el.find('table:first').show();
306 this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped');
307 return $.when(group_data).then(function (results) {
308 _.each(results, function (group_by_data) {
309 var group = _.find(groups, function (elem) {return elem.attributes.value[0] == group_by_data.id});
311 group.values = group_by_data;
314 }).done( function () {return self.add_group_mutex.exec(function() {
315 self.do_clear_groups();
316 self.dataset.ids = [];
317 if (!groups.length) {
322 var groups_array = [];
323 return $.when.apply(null, _.map(groups, function (group, index) {
324 var def = $.when([]);
325 var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
326 new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
327 if (group.attributes.length >= 1) {
328 def = dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit });
330 return def.then(function(records) {
331 self.nb_records += records.length;
332 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
333 groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
335 })).then(function () {
336 if(!self.nb_records) {
339 if (self.dataset.index >= self.nb_records){
340 self.dataset.index = self.dataset.size() ? 0 : null;
342 return self.do_add_groups(groups_array).done(function() {
343 self.trigger('kanban_groups_processed');
348 do_process_dataset: function() {
350 this.$el.find('table:first').show();
351 this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped');
352 var def = $.Deferred();
353 this.add_group_mutex.exec(function() {
354 self.do_clear_groups();
355 self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) {
356 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
357 if (!_.isEmpty(self.dataset.ids) && (self.dataset.index === null || self.dataset.index >= self.dataset.ids.length)) {
358 self.dataset.index = 0;
359 } else if (_.isEmpty(self.dataset.ids)){
360 self.dataset.index = null;
362 self.do_add_groups([kgroup]).done(function() {
363 if (_.isEmpty(records)) {
366 self.trigger('kanban_dataset_processed');
369 }).done(null, function() {
375 do_reload: function() {
376 this.do_search(this.search_domain, this.search_context, this.search_group_by);
378 do_clear_groups: function() {
379 var groups = this.groups.slice(0);
381 _.each(groups, function(group) {
385 do_add_groups: function(groups) {
387 var $parent = this.$el.parent();
389 _.each(groups, function(group) {
390 self.groups[group.undefined_title ? 'unshift' : 'push'](group);
392 var $last_td = self.$el.find('.oe_kanban_groups_headers td:last');
393 var groups_started = _.map(this.groups, function(group) {
394 if (!group.is_started) {
395 group.on("add_record", self, function () {
396 self.remove_no_result();
398 return group.insertBefore($last_td);
401 return $.when.apply(null, groups_started).done(function () {
402 self.on_groups_started();
403 self.$el.appendTo($parent);
404 _.each(self.groups, function(group) {
405 group.compute_cards_auto_height();
409 on_groups_started: function() {
411 if (this.group_by || this.fields_keys.indexOf("sequence") !== -1) {
412 // Kanban cards drag'n'drop
413 var prev_widget, is_folded, record, $columns;
415 $columns = this.$el.find('.oe_kanban_column .oe_kanban_column_cards, .oe_kanban_column .oe_kanban_folded_column_cards');
417 $columns = this.$el.find('.oe_kanban_column_cards');
420 handle : '.oe_kanban_draghandle',
421 start: function(event, ui) {
422 self.currently_dragging.index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
423 self.currently_dragging.group = prev_widget = ui.item.parents('.oe_kanban_column:first').data('widget');
424 ui.item.find('*').on('click.prevent', function(ev) {
427 record = ui.item.data('widget');
428 record.$el.bind('mouseup',function(ev,ui){
432 record.$el.unbind('mouseup');
434 ui.placeholder.height(ui.item.height());
436 over: function(event, ui) {
437 var parent = $(event.target).parent();
438 prev_widget.highlight(false);
439 is_folded = parent.hasClass('oe_kanban_group_folded');
441 var widget = parent.data('widget');
442 widget.highlight(true);
443 prev_widget = widget;
447 stop: function(event, ui) {
448 prev_widget.highlight(false);
449 var old_index = self.currently_dragging.index;
450 var new_index = ui.item.parent().children('.oe_kanban_record').index(ui.item);
451 var old_group = self.currently_dragging.group;
452 var new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
453 if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
454 self.on_record_moved(record, old_group, old_index, new_group, new_index);
456 setTimeout(function() {
457 // A bit hacky but could not find a better solution for Firefox (problem not present in chrome)
458 // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop
459 ui.item.find('*').off('click.prevent');
464 // Keep connectWith out of the sortable initialization for performance sake:
465 // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html
466 $columns.sortable({ connectWith: $columns });
468 // Kanban groups drag'n'drop
470 if (this.grouped_by_m2o) {
471 this.$('.oe_kanban_groups_headers').sortable({
472 items: '.oe_kanban_group_header',
477 start: function(event, ui) {
478 start_index = ui.item.index();
479 self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'hidden' });
481 stop: function(event, ui) {
482 var stop_index = ui.item.index();
483 if (start_index !== stop_index) {
484 var $start_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(start_index);
485 var $stop_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index);
486 var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter';
487 $start_column[method]($stop_column);
488 var tmp_group = self.groups.splice(start_index, 1)[0];
489 self.groups.splice(stop_index, 0, tmp_group);
490 var new_sequence = _.pluck(self.groups, 'value');
491 (new instance.web.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) {
493 console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation);
497 self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'visible' });
502 this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
504 this.postprocess_m2m_tags();
506 on_record_moved : function(record, old_group, old_index, new_group, new_index) {
508 record.$el.find('[title]').tooltip('destroy');
509 $(old_group.$el).add(new_group.$el).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
510 if (old_group === new_group) {
511 new_group.records.splice(old_index, 1);
512 new_group.records.splice(new_index, 0, record);
513 new_group.do_save_sequences();
515 old_group.records.splice(old_index, 1);
516 new_group.records.splice(new_index, 0, record);
517 record.group = new_group;
519 data[this.group_by] = new_group.value;
520 this.dataset.write(record.id, data, {}).done(function() {
522 new_group.do_save_sequences();
523 if (new_group.state.folded) {
524 new_group.do_action_toggle_fold();
525 record.prependTo(new_group.$records.find('.oe_kanban_column_cards'));
527 }).fail(function(error, evt) {
528 evt.preventDefault();
529 alert(_t("An error has occured while moving the record to this group: ") + error.data.message);
530 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
535 do_show: function() {
536 if (this.options.$buttons) {
537 this.$buttons.show();
539 this.do_push_state({});
540 return this._super();
542 do_hide: function () {
544 this.$buttons.hide();
546 return this._super();
548 open_record: function(id, editable) {
549 if (this.dataset.select_id(id)) {
550 this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
552 this.do_warn("Kanban: could not find id#" + id);
555 no_result: function() {
557 if (this.groups.group_by
558 || !this.options.action
559 || (!this.options.action.help && !this.options.action.get_empty_list_help)) {
562 this.$el.css("position", "relative");
563 $(QWeb.render('KanbanView.nocontent', { content : this.options.action.get_empty_list_help || this.options.action.help})).insertBefore(this.$('table:first'));
564 this.$el.find('.oe_view_nocontent').click(function() {
565 self.$buttons.openerpBounce();
568 remove_no_result: function() {
569 this.$el.css("position", "");
570 this.$el.find('.oe_view_nocontent').remove();
574 * postprocessing of fields type many2many
575 * make the rpc request for all ids/model and insert value inside .oe_tags fields
577 postprocess_m2m_tags: function() {
579 if (!this.many2manys.length) {
583 this.groups.forEach(function(group) {
584 group.records.forEach(function(record) {
585 self.many2manys.forEach(function(name) {
586 var field = record.record[name];
587 var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty();
588 if (!relations[field.relation]) {
589 relations[field.relation] = { ids: [], elements: {}};
591 var rel = relations[field.relation];
592 field.raw_value.forEach(function(id) {
594 if (!rel.elements[id]) {
595 rel.elements[id] = [];
597 rel.elements[id].push($el[0]);
602 _.each(relations, function(rel, rel_name) {
603 var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
604 dataset.name_get(_.uniq(rel.ids)).done(function(result) {
605 result.forEach(function(nameget) {
606 $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
614 function get_class(name) {
615 return new instance.web.Registry({'tmp' : name}).get_object("tmp");
618 instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
619 template: 'KanbanView.group_header',
620 init: function (parent, records, group, dataset) {
623 this.$has_been_started = $.Deferred();
626 this.dataset = dataset;
627 this.dataset_offset = 0;
628 this.aggregates = {};
629 this.value = this.title = this.values = null;
631 this.values = group.values;
632 this.value = group.get('value');
633 this.title = group.get('value');
634 if (this.value instanceof Array) {
635 this.title = this.value[1];
636 this.value = this.value[0];
638 var field = this.view.group_by_field;
639 if (!_.isEmpty(field)) {
641 this.title = instance.web.format_value(group.get('value'), field, false);
644 _.each(this.view.aggregates, function(value, key) {
645 self.aggregates[value] = instance.web.format_value(group.get('aggregates')[key], {type: 'float'});
649 if (this.title === false) {
650 this.title = _t('Undefined');
651 this.undefined_title = true;
653 var key = this.view.group_by + '-' + this.value;
654 if (!this.view.state.groups[key]) {
655 this.view.state.groups[key] = {
656 folded: group ? group.get('folded') : false
659 this.state = this.view.state.groups[key];
660 this.$records = null;
663 this.$has_been_started.done(function() {
664 self.do_add_records(records);
670 if (! self.view.group_by) {
671 self.$el.addClass("oe_kanban_no_group");
672 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
673 .on('added', self, self.proxy('quick_created'));
674 self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
676 this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
677 this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last'));
679 this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) {
680 var fn = 'do_action_' + $(ev.target).data().action;
681 if (typeof(self[fn]) === 'function') {
682 self[fn]($(ev.target));
686 this.$el.find('.oe_kanban_add').click(function () {
687 if (self.view.quick) {
688 self.view.quick.trigger('close');
693 self.view.$el.find('.oe_view_nocontent').hide();
695 ctx['default_' + self.view.group_by] = self.value;
696 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
697 .on('added', self, self.proxy('quick_created'))
698 .on('close', self, function() {
699 self.view.$el.find('.oe_view_nocontent').show();
700 this.quick.destroy();
701 delete self.view.quick;
704 self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
706 self.view.quick = self.quick;
708 // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column.
709 this.$records.on('click', '.oe_kanban_show_more', this.do_show_more);
710 if (this.state.folded) {
711 this.do_toggle_fold();
713 this.$el.data('widget', this);
714 this.$records.data('widget', this);
715 this.$has_been_started.resolve();
716 var add_btn = this.$el.find('.oe_kanban_add');
717 add_btn.tooltip({delay: { show: 500, hide:1000 }});
718 this.$records.find(".oe_kanban_column_cards").click(function (ev) {
719 if (ev.target == ev.currentTarget) {
720 if (!self.state.folded) {
721 add_btn.openerpBounce();
725 this.is_started = true;
726 this.fetch_tooltip();
730 * Form the tooltip, based on optional group_by_tooltip on the grouping field.
731 * This function goes through the arch of the view, finding the declaration
732 * of the field used to group. If group_by_tooltip is defined, use the previously
733 * computed values of the group to form the tooltip. */
734 fetch_tooltip: function() {
739 var recurse = function(node) {
740 if (node.tag === "field" && node.attrs.name == self.view.group_by) {
741 options = instance.web.py_eval(node.attrs.options || '{}');
744 _.each(node.children, function(child) {
748 recurse(this.view.fields_view.arch);
749 if (options && options.group_by_tooltip) {
750 this.tooltip = _.union(
753 options.group_by_tooltip,
754 function (key, value, list) { return self.values[value] || ''; })
756 this.$(".oe_kanban_group_title_text").attr("title", this.tooltip || this.title || "");
759 compute_cards_auto_height: function() {
760 // oe_kanban_no_auto_height is an empty class used to disable this feature
761 if (!this.view.group_by) {
764 _.each(this.records, function(r) {
765 var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0);
768 min_height = Math.max(min_height, $e.outerHeight());
771 $(els).css('min-height', min_height);
774 destroy: function() {
777 this.$records.remove();
780 do_show_more: function(evt) {
782 var ids = self.view.dataset.ids.splice(0);
783 return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
784 'limit': self.view.limit,
785 'offset': self.dataset_offset += self.view.limit
786 }).then(function(records) {
787 self.view.dataset.ids = ids.concat(self.dataset.ids);
788 self.do_add_records(records);
789 self.compute_cards_auto_height();
790 self.view.postprocess_m2m_tags();
794 do_add_records: function(records, prepend) {
796 var $list_header = this.$records.find('.oe_kanban_group_list_header');
797 var $show_more = this.$records.find('.oe_kanban_show_more');
798 var $cards = this.$records.find('.oe_kanban_column_cards');
800 _.each(records, function(record) {
801 var rec = new instance.web_kanban.KanbanRecord(self, record);
803 rec.appendTo($cards);
804 self.records.push(rec);
806 rec.prependTo($cards);
807 self.records.unshift(rec);
810 if ($show_more.length) {
811 var size = this.dataset.size();
812 $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length);
815 remove_record: function(id, remove_from_dataset) {
816 for (var i = 0; i < this.records.length; i++) {
817 if (this.records[i]['id'] === id) {
818 this.records.splice(i, 1);
823 do_toggle_fold: function(compute_width) {
824 this.$el.add(this.$records).toggleClass('oe_kanban_group_folded');
825 this.state.folded = this.$el.is('.oe_kanban_group_folded');
826 this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold"));
828 do_action_toggle_fold: function() {
829 this.do_toggle_fold();
831 do_action_edit: function() {
835 name: _t("Edit column"),
836 res_model: self.view.group_by_field.relation,
837 views: [[false, 'form']],
838 type: 'ir.actions.act_window',
841 action_buttons: true,
844 var am = instance.webclient.action_manager;
845 var form = am.dialog_widget.views.form.controller;
846 form.on("on_button_cancel", am.dialog, function() { return am.dialog.$dialog_box.modal('hide'); });
847 form.on('record_saved', self, function() {
848 am.dialog.$dialog_box.modal('hide');
849 self.view.do_reload();
852 do_action_delete: function() {
854 if (confirm(_t("Are you sure to remove this column ?"))) {
855 (new instance.web.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) {
856 self.view.do_reload();
860 do_save_sequences: function() {
862 if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
863 var new_sequence = _.pluck(this.records, 'id');
864 self.view.dataset.resequence(new_sequence);
868 * Handles a newly created record
870 * @param {id} id of the newly created record
872 quick_created: function (record) {
873 var id = record, self = this;
874 self.view.remove_no_result();
875 self.trigger("add_record");
876 this.dataset.read_ids([id], this.view.fields_keys)
877 .done(function (records) {
878 self.view.dataset.ids.push(id);
879 self.do_add_records(records, true);
882 highlight: function(show){
884 this.$el.addClass('oe_kanban_column_higlight');
885 this.$records.addClass('oe_kanban_column_higlight');
887 this.$el.removeClass('oe_kanban_column_higlight');
888 this.$records.removeClass('oe_kanban_column_higlight');
893 instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
894 template: 'KanbanView.record',
895 init: function (parent, record) {
898 this.view = parent.view;
900 this.set_record(record);
901 if (!this.view.state.records[this.id]) {
902 this.view.state.records[this.id] = {
906 this.state = this.view.state.records[this.id];
909 set_record: function(record) {
913 _.each(record, function(v, k) {
918 this.record = this.transform_record(record);
925 init_content: function() {
927 self.sub_widgets = [];
928 this.$("[data-field_id]").each(function() {
929 self.add_widget($(this));
931 this.$el.data('widget', this);
934 transform_record: function(record) {
937 _.each(record, function(value, name) {
938 var r = _.clone(self.view.fields_view.fields[name] || {});
939 if ((r.type === 'date' || r.type === 'datetime') && value) {
940 r.raw_value = instance.web.auto_str_to_date(value);
944 r.value = instance.web.format_value(value, r);
945 new_record[name] = r;
949 renderElement: function() {
950 this.qweb_context = {
954 read_only_mode: this.view.options.read_only_mode,
956 for (var p in this) {
957 if (_.str.startsWith(p, 'kanban_')) {
958 this.qweb_context[p] = _.bind(this[p], this);
961 var $el = instance.web.qweb.render(this.template, {
963 'content': this.view.qweb.render('kanban-box', this.qweb_context)
965 this.replaceElement($el);
966 this.replace_fields();
968 replace_fields: function() {
970 this.$("field").each(function() {
971 var $field = $(this);
972 var $nfield = $("<span></span");
973 var id = _.uniqueId("kanbanfield");
974 self.fields[id] = $field;
975 $nfield.attr("data-field_id", id);
976 $field.replaceWith($nfield);
979 add_widget: function($node) {
980 var $orig = this.fields[$node.data("field_id")];
981 var field = this.record[$orig.attr("name")];
982 var type = field.type;
983 type = $orig.attr("widget") ? $orig.attr("widget") : type;
984 var obj = instance.web_kanban.fields_registry.get_object(type);
985 var widget = new obj(this, field, $orig);
986 this.sub_widgets.push(widget);
987 widget.replace($node);
989 bind_events: function() {
991 this.setup_color_picker();
992 this.$el.find('[title]').each(function(){
994 delay: { show: 500, hide: 0},
996 var template = $(this).attr('tooltip');
997 if (!self.view.qweb.has_template(template)) {
1000 return self.view.qweb.render(template, self.qweb_context);
1005 // If no draghandle is found, make the whole card as draghandle (provided one can edit)
1006 if (!this.$el.find('.oe_kanban_draghandle').length) {
1007 this.$el.children(':first')
1008 .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit'));
1011 this.$el.find('.oe_kanban_action').click(function(ev) {
1012 ev.preventDefault();
1013 var $action = $(this),
1014 type = $action.data('type') || 'button',
1015 method = 'do_action_' + (type === 'action' ? 'object' : type);
1016 if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) {
1017 self.view.open_record(self.id, true);
1018 } else if (_.str.startsWith(type, 'switch_')) {
1019 self.view.do_switch_view(type.substr(7));
1020 } else if (typeof self[method] === 'function') {
1021 self[method]($action);
1023 self.do_warn("Kanban: no action for type : " + type);
1027 if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) {
1028 this.$el.on('click', function(ev) {
1029 if (!ev.isTrigger && !$._data(ev.target, 'events')) {
1031 var elem = ev.target;
1035 var events = $._data(elem, 'events');
1036 if (elem == ev.currentTarget) {
1040 children.push(elem);
1041 if (events && events.click) {
1042 // do not trigger global click if one child has a click event registered
1046 if (trigger && events && events.click) {
1047 _.each(events.click, function(click_event) {
1048 if (click_event.selector) {
1049 // For each parent of original target, check if a
1050 // delegated click is bound to any previously found children
1051 _.each(children, function(child) {
1052 if ($(child).is(click_event.selector)) {
1059 elem = elem.parentElement;
1062 self.on_card_clicked(ev);
1068 /* actions when user click on the block with a specific class
1069 * open on normal view : oe_kanban_global_click
1070 * open on form/edit view : oe_kanban_global_click_edit
1072 on_card_clicked: function(ev) {
1073 if (this.$el.find('.oe_kanban_global_click').size() > 0 && this.$el.find('.oe_kanban_global_click').data('routing')) {
1074 instance.web.redirect(this.$el.find('.oe_kanban_global_click').data('routing') + "/" + this.id);
1076 else if (this.$el.find('.oe_kanban_global_click_edit').size()>0)
1077 this.do_action_edit();
1079 this.do_action_open();
1081 setup_color_picker: function() {
1083 var $el = this.$el.find('ul.oe_kanban_colorpicker');
1085 $el.html(QWeb.render('KanbanColorPicker', {
1088 $el.on('click', 'a', function(ev) {
1089 ev.preventDefault();
1090 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
1092 data[color_field] = $(this).data('color');
1093 self.view.dataset.write(self.id, data, {}).done(function() {
1094 self.record[color_field] = $(this).data('color');
1100 do_action_delete: function($action) {
1103 return $.when(self.view.dataset.unlink([self.id])).done(function() {
1104 self.group.remove_record(self.id);
1108 if (this.view.options.confirm_on_delete) {
1109 if (confirm(_t("Are you sure you want to delete this record ?"))) {
1115 do_action_edit: function($action) {
1116 this.view.open_record(this.id, true);
1118 do_action_open: function($action) {
1119 this.view.open_record(this.id);
1121 do_action_object: function ($action) {
1122 var button_attrs = $action.data();
1123 this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
1125 do_action_url: function($action) {
1126 return instance.web.redirect($action.attr("href"));
1128 do_reload: function() {
1130 this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) {
1131 _.each(self.sub_widgets, function(el) {
1134 self.sub_widgets = [];
1135 if (records.length) {
1136 self.set_record(records[0]);
1137 self.renderElement();
1138 self.init_content();
1139 self.group.compute_cards_auto_height();
1140 self.view.postprocess_m2m_tags();
1146 kanban_getcolor: function(variable) {
1148 switch (typeof(variable)) {
1150 for (var i=0, ii=variable.length; i<ii; i++) {
1151 index += variable.charCodeAt(i);
1155 index = Math.round(variable);
1160 var color = (index % this.view.number_of_color_schemes);
1163 kanban_color: function(variable) {
1164 var color = this.kanban_getcolor(variable);
1165 return color === '' ? '' : 'oe_kanban_color_' + color;
1167 kanban_image: function(model, field, id, cache, options) {
1168 options = options || {};
1170 if (this.record[field] && this.record[field].value && !instance.web.form.is_bin_size(this.record[field].value)) {
1171 url = 'data:image/png;base64,' + this.record[field].value;
1172 } else if (this.record[field] && ! this.record[field].value) {
1173 url = "/web/static/src/img/placeholder.png";
1175 id = JSON.stringify(id);
1176 if (options.preview_image)
1177 field = options.preview_image;
1178 url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
1179 if (cache !== undefined) {
1180 // Set the cache duration in seconds.
1181 url += '&cache=' + parseInt(cache, 10);
1186 kanban_text_ellipsis: function(s, size) {
1190 } else if (s.length <= size) {
1193 return s.substr(0, size) + '...';
1196 kanban_compute_domain: function(domain) {
1197 return instance.web.form.compute_domain(domain, this.values);
1202 * Quick creation view.
1204 * Triggers a single event "added" with a single parameter "name", which is the
1205 * name entered by the user
1210 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
1211 template: 'KanbanView.quick_create',
1214 * close_btn: If true, the widget will display a "Close" button able to trigger
1217 init: function(parent, dataset, context, buttons) {
1218 this._super(parent);
1219 this._dataset = dataset;
1220 this._buttons = buttons || false;
1221 this._context = context || {};
1223 start: function () {
1225 self.$input = this.$el.find('input');
1226 self.$input.keyup(function(event){
1227 if(event.keyCode == 13){
1231 $(".oe_kanban_quick_create").focusout(function (e) {
1232 var val = self.$el.find('input').val();
1233 if (/^\s*$/.test(val)) { self.trigger('close'); }
1234 e.stopImmediatePropagation();
1236 $(".oe_kanban_quick_create_add", this.$el).click(function () {
1240 $(".oe_kanban_quick_create_close", this.$el).click(function (ev) {
1241 ev.preventDefault();
1242 self.trigger('close');
1244 self.$input.keyup(function(e) {
1245 if (e.keyCode == 27 && self._buttons) {
1246 self.trigger('close');
1251 this.$el.find('input').focus();
1254 * Handles user event from nested quick creation view
1256 quick_add: function () {
1258 var val = this.$input.val();
1259 if (/^\s*$/.test(val)) { this.$el.remove(); return; }
1261 'name_create', [val, new instance.web.CompoundContext(
1262 this._dataset.get_context(), this._context)])
1263 .then(function(record) {
1264 self.$input.val("");
1265 self.trigger('added', record[0]);
1266 }, function(error, event) {
1267 event.preventDefault();
1268 return self.slow_create();
1271 slow_create: function() {
1273 var pop = new instance.web.form.SelectCreatePopup(this);
1275 self._dataset.model,
1277 title: _t("Create: ") + (this.string || this.name),
1278 initial_view: "form",
1279 disable_multiple_selection: true
1282 {"default_name": self.$input.val()}
1284 pop.on("elements_selected", self, function(element_ids) {
1285 self.$input.val("");
1286 self.trigger('added', element_ids[0]);
1292 * Interface to be implemented by kanban fields.
1295 instance.web_kanban.FieldInterface = {
1298 - parent: The widget's parent.
1299 - field: A dictionary giving details about the field, including the current field's value in the
1301 - $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object.
1303 init: function(parent, field, $node) {},
1307 * Abstract class for classes implementing FieldInterface.
1310 * - value: useful property to hold the value of the field. By default, the constructor
1311 * sets value property.
1314 instance.web_kanban.AbstractField = instance.web.Widget.extend(instance.web_kanban.FieldInterface, {
1316 Constructor that saves the field and $node parameters and sets the "value" property.
1318 init: function(parent, field, $node) {
1319 this._super(parent);
1322 this.options = instance.web.py_eval(this.$node.attr("options") || '{}');
1323 this.set("value", field.raw_value);
1327 instance.web_kanban.Priority = instance.web_kanban.AbstractField.extend({
1328 init: function(parent, field, $node) {
1329 this._super.apply(this, arguments);
1330 this.name = $node.attr('name')
1331 this.parent = parent;
1333 prepare_priority: function() {
1335 var selection = this.field.selection || [];
1336 var init_value = selection && selection[0][0] || 0;
1337 var data = _.map(selection.slice(1), function(element, index) {
1339 'value': element[0],
1341 'click_value': element[0],
1343 if (index == 0 && self.get('value') == element[0]) {
1344 value['click_value'] = init_value;
1350 renderElement: function() {
1352 this.record_id = self.parent.id;
1353 this.priorities = self.prepare_priority();
1354 this.$el = $(QWeb.render("Priority", {'widget': this}));
1355 this.$el.find('li').click(self.do_action.bind(self));
1357 do_action: function(e) {
1359 var li = $(e.target).closest( "li" );
1362 value[self.name] = String(li.data('value'));
1363 return self.parent.view.dataset._model.call('write', [[self.record_id], value, self.parent.view.dataset.get_context()]).done(self.reload_record.bind(self.parent));
1366 reload_record: function() {
1371 instance.web_kanban.KanbanSelection = instance.web_kanban.AbstractField.extend({
1372 init: function(parent, field, $node) {
1373 this._super.apply(this, arguments);
1374 this.name = $node.attr('name')
1375 this.parent = parent;
1377 prepare_dropdown_selection: function() {
1380 _.map(this.field.selection || [], function(res) {
1384 'state_name': res[1],
1386 var leg_opt = self.options && self.options.states_legend || null;
1387 if (leg_opt && leg_opt[res[0]] && self.parent.group.values && self.parent.group.values[leg_opt[res[0]]]) {
1388 value['state_name'] = self.parent.group.values[leg_opt[res[0]]];
1390 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
1391 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
1392 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
1397 renderElement: function() {
1399 this.record_id = self.parent.id;
1400 this.states = self.prepare_dropdown_selection();;
1401 this.$el = $(QWeb.render("KanbanSelection", {'widget': self}));
1402 this.$el.find('li').click(self.do_action.bind(self));
1404 do_action: function(e) {
1406 var li = $(e.target).closest( "li" );
1409 value[self.name] = String(li.data('value'));
1410 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));
1413 reload_record: function() {
1418 instance.web_kanban.fields_registry = new instance.web.Registry({});
1419 instance.web_kanban.fields_registry.add('priority','instance.web_kanban.Priority');
1420 instance.web_kanban.fields_registry.add('kanban_state_selection','instance.web_kanban.KanbanSelection');
1423 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: