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);
17 _.defaults(this.options, {
18 "quick_creatable": true,
20 "create_text": undefined,
21 "read_only_mode": false,
22 "confirm_on_delete": true,
24 this.fields_view = {};
25 this.fields_keys = [];
32 this.form_dialog = new instance.web.form.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
33 this.form_dialog.on_form_dialog_saved.add_last(this.do_reload);
35 this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
36 this.qweb = new QWeb2.Engine();
37 this.qweb.debug = instance.connection.debug;
38 this.qweb.default_dict = _.clone(QWeb.default_dict);
39 this.has_been_loaded = $.Deferred();
40 this.search_domain = this.search_context = this.search_group_by = null;
41 this.currently_dragging = {};
42 this.limit = options.limit || 80;
43 this.add_group_mutex = new $.Mutex();
46 this._super.apply(this, arguments);
47 $('html').off('click.kanban');
49 on_loaded: function(data) {
50 this.fields_view = data;
51 this.$element.addClass(this.fields_view.arch.attrs['class']);
52 this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
53 if (this.options.$buttons) {
54 this.$buttons.appendTo(this.options.$buttons);
56 this.$element.find('.oe_kanban_buttons').replaceWith(this.$buttons);
59 .on('click','button.oe_kanban_button_new', this.do_add_record);
60 this.$groups = this.$element.find('.oe_kanban_groups tr');
61 this.fields_keys = _.keys(this.fields_view.fields);
62 this.add_qweb_template();
63 this.has_been_loaded.resolve();
66 _is_quick_create_enabled: function() {
67 if (! this.options.quick_creatable)
69 if (this.fields_view.arch.attrs.quick_create !== undefined)
70 return JSON.parse(this.fields_view.arch.attrs.quick_create);
71 return !! this.group_by;
73 _is_create_enabled: function() {
74 if (! this.options.creatable)
76 if (this.fields_view.arch.attrs.create !== undefined)
77 return JSON.parse(this.fields_view.arch.attrs.create);
80 add_qweb_template: function() {
81 for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
82 var child = this.fields_view.arch.children[i];
83 if (child.tag === "templates") {
84 this.transform_qweb_template(child);
85 this.qweb.add_template(instance.web.json_node_to_xml(child));
87 } else if (child.tag === 'field') {
88 this.extract_aggregates(child);
92 extract_aggregates: function(node) {
93 for (var j = 0, jj = this.group_operators.length; j < jj; j++) {
94 if (node.attrs[this.group_operators[j]]) {
95 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
100 transform_qweb_template: function(node) {
101 var qweb_add_if = function(node, condition) {
102 if (node.attrs[QWeb.prefix + '-if']) {
103 condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition);
105 node.attrs[QWeb.prefix + '-if'] = condition;
108 if (node.tag && node.attrs.modifiers) {
109 var modifiers = JSON.parse(node.attrs.modifiers || '{}');
110 if (modifiers.invisible) {
111 qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible)));
116 node.tag = QWeb.prefix;
117 node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
118 this.extract_aggregates(node);
122 var type = node.attrs.type || '';
123 if (_.indexOf('action,object,edit,delete'.split(','), type) !== -1) {
124 _.each(node.attrs, function(v, k) {
125 if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
126 node.attrs['data-' + k] = v;
127 delete(node.attrs[k]);
130 if (node.attrs['data-string']) {
131 node.attrs.title = node.attrs['data-string'];
133 if (node.attrs['data-icon']) {
137 src: instance.connection.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
143 if (node.tag == 'a') {
144 node.attrs.href = '#';
146 node.attrs.type = 'button';
148 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
153 for (var i = 0, ii = node.children.length; i < ii; i++) {
154 this.transform_qweb_template(node.children[i]);
158 do_add_record: function() {
159 this.dataset.index = null;
160 this.do_switch_view('form');
162 do_search: function(domain, context, group_by) {
164 this.$element.find('.oe_view_nocontent').remove();
165 this.search_domain = domain;
166 this.search_context = context;
167 this.search_group_by = group_by;
168 $.when(this.has_been_loaded).then(function() {
169 self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
170 self.datagroup = new instance.web.DataGroup(self, self.dataset.model, domain, context, self.group_by ? [self.group_by] : []);
171 self.datagroup.list(self.fields_keys, self.do_process_groups, self.do_process_dataset);
174 do_process_groups: function(groups) {
176 this.add_group_mutex.exec(function() {
177 self.do_clear_groups();
178 self.dataset.ids = [];
179 var remaining = groups.length - 1,
181 return $.when.apply(null, _.map(groups, function (group, index) {
182 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, group.context, group.domain);
183 return dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit })
184 .pipe(function(records) {
185 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
186 groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
188 self.dataset.index = self.dataset.size() ? 0 : null;
189 return self.do_add_groups(groups_array);
195 do_process_dataset: function(dataset) {
197 this.add_group_mutex.exec(function() {
198 var def = $.Deferred();
199 self.do_clear_groups();
200 self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).then(function(records) {
201 if (_.isEmpty(records)) {
205 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
206 self.do_add_groups([kgroup]).then(function() {
210 }).then(null, function() {
216 do_reload: function() {
217 this.do_search(this.search_domain, this.search_context, this.search_group_by);
219 do_clear_groups: function() {
220 _.each(this.groups, function(group) {
224 this.$element.find('.oe_kanban_groups_headers, .oe_kanban_groups_records').empty();
226 do_add_groups: function(groups) {
228 _.each(groups, function(group) {
229 self.groups[group.undefined_title ? 'unshift' : 'push'](group);
231 var groups_started = _.map(this.groups, function(group) {
232 return group.appendTo(self.$element.find('.oe_kanban_groups_headers'));
234 return $.when.apply(null, groups_started).then(function () {
235 self.on_groups_started();
238 on_groups_started: function() {
240 this.compute_groups_width();
242 this.$element.find('.oe_kanban_column').sortable({
243 connectWith: '.oe_kanban_column',
244 handle : '.oe_kanban_draghandle',
245 start: function(event, ui) {
246 self.currently_dragging.index = ui.item.index();
247 self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
249 stop: function(event, ui) {
250 var record = ui.item.data('widget'),
251 old_index = self.currently_dragging.index,
252 new_index = ui.item.index(),
253 old_group = self.currently_dragging.group,
254 new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
255 if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
256 self.on_record_moved(record, old_group, old_index, new_group, new_index);
262 this.$element.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
265 on_record_moved : function(record, old_group, old_index, new_group, new_index) {
268 $(old_group.$element).add(new_group.$element).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
269 if (old_group === new_group) {
270 new_group.records.splice(old_index, 1);
271 new_group.records.splice(new_index, 0, record);
272 new_group.do_save_sequences();
274 old_group.records.splice(old_index, 1);
275 new_group.records.splice(new_index, 0, record);
276 record.group = new_group;
278 data[this.group_by] = new_group.value;
279 this.dataset.write(record.id, data, {}, function() {
281 new_group.do_save_sequences();
282 }).fail(function(error, evt) {
283 evt.preventDefault();
284 alert("An error has occured while moving the record to this group.");
285 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
289 compute_groups_width: function() {
291 _.each(this.groups, function(group) {
292 unfolded += group.state.folded ? 0 : 1;
293 group.$element.css('width', '');
295 _.each(this.groups, function(group) {
296 if (!group.state.folded) {
297 group.$element.css('width', Math.round(100/unfolded) + '%');
302 do_show: function() {
304 this.$buttons.show();
306 this.do_push_state({});
307 return this._super();
309 do_hide: function () {
311 this.$buttons.hide();
313 return this._super();
315 open_record: function(id, editable) {
316 if (this.dataset.select_id(id)) {
317 this.do_switch_view('form', null);
319 this.do_warn("Kanban: could not find id#" + id);
322 no_result: function() {
323 if (this.groups.group_by
324 || !this.options.action
325 || !this.options.action.help) {
328 this.$element.find('.oe_view_nocontent').remove();
329 this.$element.prepend(
330 $('<div class="oe_view_nocontent">')
331 .append($('<img>', { src: '/web/static/src/img/view_empty_arrow.png' }))
332 .append($('<div>').html(this.options.action.help))
337 function get_class(name) {
338 return new instance.web.Registry({'tmp' : name}).get_object("tmp");
341 instance.web_kanban.KanbanGroup = instance.web.OldWidget.extend({
342 template: 'KanbanView.group_header',
343 init: function (parent, records, group, dataset) {
346 this.$has_been_started = $.Deferred();
349 this.dataset = dataset;
350 this.dataset_offset = 0;
351 this.aggregates = {};
352 this.value = this.title = null;
354 this.value = group.value;
355 this.title = group.value;
356 if (this.value instanceof Array) {
357 this.title = this.value[1];
358 this.value = this.value[0];
360 var field = this.view.fields_view.fields[this.view.group_by];
363 this.title = instance.web.format_value(group.value, field, false);
366 _.each(this.view.aggregates, function(value, key) {
367 self.aggregates[value] = group.aggregates[key];
371 if (this.title === false) {
372 this.title = _t('Undefined');
373 this.undefined_title = true;
375 var key = this.view.group_by + '-' + this.value;
376 if (!this.view.state.groups[key]) {
377 this.view.state.groups[key] = {
381 this.state = this.view.state.groups[key];
382 this.$records = null;
385 this.$has_been_started.then(function() {
386 self.do_add_records(records);
392 if (! self.view.group_by) {
393 self.$element.addClass("oe_kanban_no_group");
394 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, {}, false)
395 .on('added', self, self.proxy('quick_created'));
396 self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
398 this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
399 this.$records.appendTo(this.view.$element.find('.oe_kanban_groups_records'));
400 this.$element.find(".oe_kanban_fold_icon").click(function() {
401 self.do_toggle_fold();
402 self.view.compute_groups_width();
405 this.$element.find('.oe_kanban_add').click(function () {
406 if (self.quick) { return; }
408 ctx['default_' + self.view.group_by] = self.value;
409 self.quick = new (get_class(self.view.quick_create_class))(this, self.dataset, ctx, true)
410 .on('added', self, self.proxy('quick_created'))
411 .on('close', self, function() {
412 this.quick.destroy();
415 self.quick.appendTo($(".oe_kanban_group_list_header", self.$records));
418 this.$records.find('.oe_kanban_show_more').click(this.do_show_more);
419 if (this.state.folded) {
420 this.do_toggle_fold();
422 this.$element.data('widget', this);
423 this.$records.data('widget', this);
424 this.$has_been_started.resolve();
425 this.compute_cards_auto_height();
428 compute_cards_auto_height: function() {
429 // oe_kanban_auto_height is an empty class used by the kanban view in order
430 // to normalize height amongst kanban cards. (by group)
434 _.each(this.records, function(r) {
435 var $e = r.$element.find('.oe_kanban_auto_height').first().css('min-height', 0);
438 min_height = Math.max(min_height, $e.outerHeight());
441 $(els).css('min-height', min_height);
443 destroy: function() {
446 this.$records.remove();
449 do_show_more: function(evt) {
451 this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
452 'limit': self.view.limit,
453 'offset': self.dataset_offset += self.view.limit
454 }).then(this.do_add_records);
456 do_add_records: function(records, prepend) {
458 _.each(records, function(record) {
459 var rec = new instance.web_kanban.KanbanRecord(self, record);
461 rec.insertBefore(self.$records.find('.oe_kanban_show_more'));
462 self.records.push(rec);
464 rec.insertAfter($(".oe_kanban_group_list_header", self.$records));
465 self.records.unshift(rec);
468 this.$records.find('.oe_kanban_show_more').toggle(this.records.length < this.dataset.size())
469 .find('.oe_kanban_remaining').text(this.dataset.size() - this.records.length);
471 remove_record: function(id, remove_from_dataset) {
472 for (var i = 0; i < this.records.length; i++) {
473 if (this.records[i]['id'] === id) {
474 this.records.splice(i, 1);
479 do_toggle_fold: function(compute_width) {
480 this.$element.add(this.$records).toggleClass('oe_kanban_group_folded');
481 this.state.folded = this.$element.is('.oe_kanban_group_folded');
483 do_save_sequences: function() {
485 if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
486 _.each(this.records, function(record, index) {
487 self.view.dataset.write(record.id, { sequence : index });
492 * Handles a non-erroneous response from name_create
494 * @param {(Id, String)} record name_get format for the newly created record
496 quick_created: function (record) {
497 var id = record, self = this;
498 this.dataset.read_ids([id], this.view.fields_keys)
499 .then(function (records) {
500 self.view.dataset.ids.push(id);
501 self.do_add_records(records, true);
506 instance.web_kanban.KanbanRecord = instance.web.OldWidget.extend({
507 template: 'KanbanView.record',
508 init: function (parent, record) {
511 this.view = parent.view;
513 this.set_record(record);
514 if (!this.view.state.records[this.id]) {
515 this.view.state.records[this.id] = {
519 this.state = this.view.state.records[this.id];
521 set_record: function(record) {
525 _.each(record, function(v, k) {
530 this.record = this.transform_record(record);
534 this.$element.data('widget', this);
537 transform_record: function(record) {
540 _.each(record, function(value, name) {
541 var r = _.clone(self.view.fields_view.fields[name] || {});
542 if ((r.type === 'date' || r.type === 'datetime') && value) {
543 r.raw_value = instance.web.auto_str_to_date(value);
547 r.value = instance.web.format_value(value, r);
548 new_record[name] = r;
553 this.qweb_context = {
556 read_only_mode: this.view.options.read_only_mode,
558 for (var p in this) {
559 if (_.str.startsWith(p, 'kanban_')) {
560 this.qweb_context[p] = _.bind(this[p], this);
564 'content': this.view.qweb.render('kanban-box', this.qweb_context)
567 bind_events: function() {
569 this.setup_color_picker();
570 var $show_on_click = self.$element.find('.oe_kanban_box_show_onclick');
571 $show_on_click.toggle(this.state.folded);
572 this.$element.find('.oe_kanban_box_show_onclick_trigger').click(function() {
573 $show_on_click.toggle();
574 self.state.folded = !self.state.folded;
577 this.$element.find('[tooltip]').tipsy({
582 var template = $(this).attr('tooltip');
583 if (!self.view.qweb.has_template(template)) {
586 return self.view.qweb.render(template, self.qweb_context);
594 // If no draghandle is found, make the whole card as draghandle
595 if (!this.$element.find('.oe_kanban_draghandle').length) {
596 this.$element.children(':first').addClass('oe_kanban_draghandle');
599 this.$element.find('.oe_kanban_action').click(function() {
600 var $action = $(this),
601 type = $action.data('type') || 'button',
602 method = 'do_action_' + (type === 'action' ? 'object' : type);
603 if (_.str.startsWith(type, 'switch_')) {
604 self.view.do_switch_view(type.substr(7));
605 } else if (typeof self[method] === 'function') {
606 self[method]($action);
608 self.do_warn("Kanban: no action for type : " + type);
612 if (this.$element.find('.oe_kanban_global_click').length) {
613 this.$element.on('click', function(ev) {
614 if (!ev.isTrigger && !$(ev.target).data('events')) {
616 var elem = ev.target;
620 var events = $(elem).data('events');
621 if (elem == ev.currentTarget) {
626 if (events && events.click) {
627 // do not trigger global click if one child has a click event registered
631 if (trigger && events && events.click) {
632 _.each(events.click, function(click_event) {
633 if (click_event.selector) {
634 // For each parent of original target, check if a
635 // delegated click is bound to any previously found children
636 _.each(children, function(child) {
637 if ($(child).is(click_event.selector)) {
644 elem = elem.parentElement;
647 self.on_card_clicked(ev);
653 on_card_clicked: function(ev) {
654 this.view.open_record(this.id);
656 setup_color_picker: function() {
658 var $el = this.$element.find('ul.oe_kanban_colorpicker');
660 $el.html(QWeb.render('KanbanColorPicker', {
663 $el.on('click', 'a', function(ev) {
665 var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color';
667 data[color_field] = $(this).data('color');
668 self.view.dataset.write(self.id, data, {}, function() {
669 self.record[color_field] = $(this).data('color');
675 do_action_delete: function($action) {
678 return $.when(self.view.dataset.unlink([self.id])).then(function() {
679 self.group.remove_record(self.id);
683 if (this.view.options.confirm_on_delete) {
684 if (confirm(_t("Are you sure you want to delete this record ?"))) {
690 do_action_edit: function($action) {
692 if ($action.attr('target') === 'dialog') {
693 this.view.form_dialog.select_id(this.id).then(function() {
694 self.view.form_dialog.open();
697 this.view.open_record(this.id, true);
700 do_action_object: function ($action) {
701 var button_attrs = $action.data();
702 this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
704 do_reload: function() {
706 this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).then(function(records) {
707 if (records.length) {
708 self.set_record(records[0]);
709 var $render = $(self.render());
710 self.$element.replaceWith($render);
711 self.$element = $render;
712 self.$element.data('widget', self);
714 self.group.compute_cards_auto_height();
720 kanban_getcolor: function(variable) {
722 switch (typeof(variable)) {
724 for (var i=0, ii=variable.length; i<ii; i++) {
725 index += variable.charCodeAt(i);
729 index = Math.round(variable);
734 var color = (index % this.view.number_of_color_schemes);
737 kanban_color: function(variable) {
738 var color = this.kanban_getcolor(variable);
739 return color === '' ? '' : 'oe_kanban_color_' + color;
741 kanban_gravatar: function(email, size) {
743 email = _.str.trim(email || '').toLowerCase();
744 var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
745 var email_md5 = $.md5(email);
746 return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
748 kanban_image: function(model, field, id, cache) {
750 var url = instance.connection.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + id;
751 if (cache !== undefined) {
752 // Set the cache duration in seconds.
753 url += '&cache=' + parseInt(cache, 10);
757 kanban_text_ellipsis: function(s, size) {
761 } else if (s.length <= size) {
764 return s.substr(0, size) + '...';
767 kanban_compute_domain: function(domain) {
768 return instance.web.form.compute_domain(domain, this.values);
773 * Quick creation view.
775 * Triggers a single event "added" with a single parameter "name", which is the
776 * name entered by the user
781 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
782 template: 'KanbanView.quick_create',
785 * close_btn: If true, the widget will display a "Close" button able to trigger
788 init: function(parent, dataset, context, buttons) {
790 this._dataset = dataset;
791 this._buttons = buttons || false;
792 this._context = context || {};
796 self.$input = this.$element.find('input');
797 self.$input.keyup(function(event){
798 if(event.keyCode == 13){
802 $(".oe_kanban_quick_create_add", this.$element).click(function () {
805 $(".oe_kanban_quick_create_close", this.$element).click(function () {
806 self.trigger('close');
808 self.$input.keyup(function(e) {
809 if (e.keyCode == 27 && self._buttons) {
810 self.trigger('close');
815 this.$element.find('input').focus();
818 * Handles user event from nested quick creation view
820 quick_add: function () {
823 'name_create', [self.$input.val(), new instance.web.CompoundContext(
824 this._dataset.get_context(), this._context)])
825 .pipe(function(record) {
827 self.trigger('added', record[0]);
828 }, function(error, event) {
829 event.preventDefault();
830 return self.slow_create();
833 slow_create: function() {
835 var pop = new instance.web.form.SelectCreatePopup(this);
839 title: _t("Create: ") + (this.string || this.name),
840 initial_view: "form",
841 disable_multiple_selection: true
844 {"default_name": self.$input.val()}
846 pop.on_select_elements.add(function(element_ids) {
848 self.trigger('added', element_ids[0]);
854 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: