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: 3,
13 init: function (parent, dataset, view_id, options) {
14 this._super(parent, dataset, view_id, options);
15 _.defaults(this.options, {"quick_creatable": true, "creatable": true});
16 this.fields_view = {};
17 this.fields_keys = [];
24 this.form_dialog = new instance.web.form.FormDialog(this, {}, this.options.action_views_ids.form, dataset).start();
25 this.form_dialog.on_form_dialog_saved.add_last(this.do_reload);
27 this.group_operators = ['avg', 'max', 'min', 'sum', 'count'];
28 this.qweb = new QWeb2.Engine();
29 this.qweb.debug = instance.connection.debug;
30 this.qweb.default_dict = _.clone(QWeb.default_dict);
31 this.has_been_loaded = $.Deferred();
32 this.search_domain = this.search_context = this.search_group_by = null;
33 this.currently_dragging = {};
34 this.limit = options.limit || 80;
35 this.add_group_mutex = new $.Mutex();
37 on_loaded: function(data) {
38 this.fields_view = data;
39 this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this}));
40 if (this.options.$buttons) {
41 this.$buttons.appendTo(this.options.$buttons);
43 this.$element.find('.oe_kanban_buttons').replaceWith(this.$buttons);
46 .on('click','button.oe_kanban_button_new', this.do_add_record);
47 this.$groups = this.$element.find('.oe_kanban_groups tr');
48 this.fields_keys = _.keys(this.fields_view.fields);
49 this.add_qweb_template();
50 this.has_been_loaded.resolve();
53 _is_quick_create_enabled: function() {
54 if (! this.options.quick_creatable)
56 if (this.fields_view.arch.attrs.quick_create !== undefined)
57 return JSON.parse(this.fields_view.arch.attrs.quick_create);
58 return !! this.group_by;
60 _is_create_enabled: function() {
61 if (! this.options.creatable)
63 if (this.fields_view.arch.attrs.create !== undefined)
64 return JSON.parse(this.fields_view.arch.attrs.create);
67 add_qweb_template: function() {
68 for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) {
69 var child = this.fields_view.arch.children[i];
70 if (child.tag === "templates") {
71 this.transform_qweb_template(child);
72 this.qweb.add_template(instance.web.json_node_to_xml(child));
74 } else if (child.tag === 'field') {
75 this.extract_aggregates(child);
79 extract_aggregates: function(node) {
80 for (var j = 0, jj = this.group_operators.length; j < jj; j++) {
81 if (node.attrs[this.group_operators[j]]) {
82 this.aggregates[node.attrs.name] = node.attrs[this.group_operators[j]];
87 transform_qweb_template: function(node) {
88 var qweb_prefix = QWeb.prefix;
91 node.tag = qweb_prefix;
92 node.attrs[qweb_prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
93 this.extract_aggregates(node);
97 var type = node.attrs.type || '';
98 if (_.indexOf('action,object,edit,delete,color'.split(','), type) !== -1) {
99 _.each(node.attrs, function(v, k) {
100 if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) {
101 node.attrs['data-' + k] = v;
102 delete(node.attrs[k]);
105 if (node.attrs['data-states']) {
106 var states = _.map(node.attrs['data-states'].split(','), function(state) {
107 return "record.state.raw_value == '" + _.str.trim(state) + "'";
109 node.attrs[qweb_prefix + '-if'] = states.join(' or ');
111 if (node.attrs['data-kanban_states']) {
112 var states = _.map(node.attrs['data-kanban_states'].split(','), function(state) {
113 return "record.kanban_state.raw_value == '" + _.str.trim(state) + "'";
115 node.attrs[qweb_prefix + '-if'] = states.join(' or ');
117 if (node.attrs['data-string']) {
118 node.attrs.title = node.attrs['data-string'];
120 if (node.attrs['data-icon']) {
124 src: instance.connection.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png',
130 if (node.tag == 'a') {
131 node.attrs.href = '#';
133 node.attrs.type = 'button';
135 node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag;
140 for (var i = 0, ii = node.children.length; i < ii; i++) {
141 this.transform_qweb_template(node.children[i]);
145 do_add_record: function() {
146 this.dataset.index = null;
147 this.do_switch_view('form');
149 do_search: function(domain, context, group_by) {
151 this.$element.find('.oe_view_nocontent').remove();
152 this.search_domain = domain;
153 this.search_context = context;
154 this.search_group_by = group_by;
155 $.when(this.has_been_loaded).then(function() {
156 self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
157 self.datagroup = new instance.web.DataGroup(self, self.dataset.model, domain, context, self.group_by ? [self.group_by] : []);
158 self.datagroup.list(self.fields_keys, self.do_process_groups, self.do_process_dataset);
161 do_process_groups: function(groups) {
163 this.add_group_mutex.exec(function() {
164 self.do_clear_groups();
165 self.dataset.ids = [];
166 var remaining = groups.length - 1,
168 return $.when.apply(null, _.map(groups, function (group, index) {
169 var dataset = new instance.web.DataSetSearch(self, self.dataset.model, group.context, group.domain);
170 return dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit })
171 .pipe(function(records) {
172 self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
173 groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);
175 self.dataset.index = self.dataset.size() ? 0 : null;
176 return self.do_add_groups(groups_array);
182 do_process_dataset: function(dataset) {
184 this.add_group_mutex.exec(function() {
185 var def = $.Deferred();
186 self.do_clear_groups();
187 self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).then(function(records) {
188 if (_.isEmpty(records)) {
192 var kgroup = new instance.web_kanban.KanbanGroup(self, records, null, self.dataset);
193 self.do_add_groups([kgroup]).then(function() {
197 }).then(null, function() {
203 do_reload: function() {
204 this.do_search(this.search_domain, this.search_context, this.search_group_by);
206 do_clear_groups: function() {
207 _.each(this.groups, function(group) {
211 this.$element.find('.oe_kanban_groups_headers, .oe_kanban_groups_records').empty();
213 do_add_groups: function(groups) {
215 _.each(groups, function(group) {
216 self.groups[group.undefined_title ? 'unshift' : 'push'](group);
218 var groups_started = _.map(this.groups, function(group) {
219 return group.appendTo(self.$element.find('.oe_kanban_groups_headers'));
221 return $.when.apply(null, groups_started).then(function () {
222 self.on_groups_started();
225 on_groups_started: function() {
227 this.compute_groups_width();
229 this.$element.find('.oe_kanban_column').sortable({
230 connectWith: '.oe_kanban_column',
231 handle : '.oe_kanban_draghandle',
232 start: function(event, ui) {
233 self.currently_dragging.index = ui.item.index();
234 self.currently_dragging.group = ui.item.parents('.oe_kanban_column:first').data('widget');
236 stop: function(event, ui) {
237 var record = ui.item.data('widget'),
238 old_index = self.currently_dragging.index,
239 new_index = ui.item.index(),
240 old_group = self.currently_dragging.group,
241 new_group = ui.item.parents('.oe_kanban_column:first').data('widget');
242 if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) {
243 self.on_record_moved(record, old_group, old_index, new_group, new_index);
249 this.$element.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
252 on_record_moved : function(record, old_group, old_index, new_group, new_index) {
255 $(old_group.$element).add(new_group.$element).find('.oe_kanban_aggregates, .oe_kanban_group_length').hide();
256 if (old_group === new_group) {
257 new_group.records.splice(old_index, 1);
258 new_group.records.splice(new_index, 0, record);
259 new_group.do_save_sequences();
261 old_group.records.splice(old_index, 1);
262 new_group.records.splice(new_index, 0, record);
263 record.group = new_group;
265 data[this.group_by] = new_group.value;
266 this.dataset.write(record.id, data, {}, function() {
268 new_group.do_save_sequences();
269 }).fail(function(error, evt) {
270 evt.preventDefault();
271 alert("An error has occured while moving the record to this group.");
272 self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails
276 compute_groups_width: function() {
278 _.each(this.groups, function(group) {
279 unfolded += group.state.folded ? 0 : 1;
280 group.$element.css('width', '');
282 _.each(this.groups, function(group) {
283 if (!group.state.folded) {
284 group.$element.css('width', Math.round(100/unfolded) + '%');
289 do_show: function() {
291 this.$buttons.show();
293 this.do_push_state({});
294 return this._super();
296 do_hide: function () {
298 this.$buttons.hide();
300 return this._super();
302 open_record: function(id) {
303 if (this.dataset.select_id(id)) {
304 this.do_switch_view('form');
306 this.do_warn("Kanban: could not find id#" + id);
309 no_result: function() {
310 if (this.groups.group_by
311 || !this.options.action
312 || !this.options.action.help) {
315 this.$element.find('.oe_view_nocontent').remove();
316 this.$element.prepend(
317 $('<div class="oe_view_nocontent">')
318 .append($('<img>', { src: '/web/static/src/img/view_empty_arrow.png' }))
319 .append($('<div>').html(this.options.action.help))
324 instance.web_kanban.KanbanGroup = instance.web.OldWidget.extend({
325 template: 'KanbanView.group_header',
326 init: function (parent, records, group, dataset) {
329 this.$has_been_started = $.Deferred();
332 this.dataset = dataset;
333 this.dataset_offset = 0;
334 this.aggregates = {};
335 this.value = this.title = null;
337 this.value = group.value;
338 this.title = group.value;
339 if (this.value instanceof Array) {
340 this.title = this.value[1];
341 this.value = this.value[0];
343 var field = this.view.fields_view.fields[this.view.group_by];
346 this.title = instance.web.format_value(group.value, field, false);
349 _.each(this.view.aggregates, function(value, key) {
350 self.aggregates[value] = group.aggregates[key];
354 if (this.title === false) {
355 this.title = _t('Undefined');
356 this.undefined_title = true;
358 var key = this.view.group_by + '-' + this.value;
359 if (!this.view.state.groups[key]) {
360 this.view.state.groups[key] = {
364 this.state = this.view.state.groups[key];
365 this.$records = null;
368 this.$has_been_started.then(function() {
369 self.do_add_records(records);
375 if (! self.view.group_by) {
376 self.$element.addClass("oe_kanban_no_group");
377 self.quick = new instance.web_kanban.QuickCreate(this, self.dataset, {}, false)
378 .on('added', self, self.proxy('quick_created'));
379 self.quick.replace($(".oe_kanban_no_group_qc_placeholder"));
381 this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this}));
382 this.$records.appendTo(this.view.$element.find('.oe_kanban_groups_records'));
383 this.$element.find(".oe_kanban_fold_icon").click(function() {
384 self.do_toggle_fold();
385 self.view.compute_groups_width();
388 this.$element.find('.oe_kanban_add').click(function () {
389 if (self.quick) { return; }
391 ctx['default_' + self.view.group_by] = self.value;
392 self.quick = new instance.web_kanban.QuickCreate(this, self.dataset, ctx, true)
393 .on('added', self, self.proxy('quick_created'))
394 .on('close', self, function() {
395 this.quick.destroy();
398 self.quick.appendTo(self.$element.find('.oe_kanban_group_header'));
401 this.$records.find('.oe_kanban_show_more').click(this.do_show_more);
402 if (this.state.folded) {
403 this.do_toggle_fold();
405 this.$element.data('widget', this);
406 this.$records.data('widget', this);
407 this.$has_been_started.resolve();
408 this.compute_cards_height();
411 compute_cards_height: function() {
414 _.each(this.records, function(r) {
415 min_height = Math.max(min_height, r.$element.outerHeight());
417 _.each(this.records, function(r) {
418 r.$element.css('min-height', min_height);
421 destroy: function() {
424 this.$records.remove();
427 do_show_more: function(evt) {
429 this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), {
430 'limit': self.view.limit,
431 'offset': self.dataset_offset += self.view.limit
432 }).then(this.do_add_records);
434 do_add_records: function(records, prepend) {
436 _.each(records, function(record) {
437 var rec = new instance.web_kanban.KanbanRecord(self, record);
439 rec.insertBefore(self.$records.find('.oe_kanban_show_more'));
440 self.records.push(rec);
442 rec.prependTo(self.$records);
443 self.records.unshift(rec);
446 this.$records.find('.oe_kanban_show_more').toggle(this.records.length < this.dataset.size())
447 .find('.oe_kanban_remaining').text(this.dataset.size() - this.records.length);
449 remove_record: function(id, remove_from_dataset) {
450 for (var i = 0, ii = this.records.length; i < ii; i++) {
451 if (this.records[i]['id'] === id) {
452 this.records.splice(i, 1);
456 do_toggle_fold: function(compute_width) {
457 this.$element.add(this.$records).toggleClass('oe_kanban_group_folded');
458 this.state.folded = this.$element.is('.oe_kanban_group_folded');
460 do_save_sequences: function() {
462 if (_.indexOf(this.view.fields_keys, 'sequence') > -1) {
463 _.each(this.records, function(record, index) {
464 self.view.dataset.write(record.id, { sequence : index });
469 * Handles a non-erroneous response from name_create
471 * @param {(Id, String)} record name_get format for the newly created record
473 quick_created: function (record) {
474 var id = record[0], self = this;
475 this.dataset.read_ids([id], this.view.fields_keys)
476 .then(function (records) {
477 self.view.dataset.ids.push(id);
478 self.do_add_records(records, 'prepend');
483 instance.web_kanban.KanbanRecord = instance.web.OldWidget.extend({
484 template: 'KanbanView.record',
485 init: function (parent, record) {
488 this.view = parent.view;
490 this.set_record(record);
491 if (!this.view.state.records[this.id]) {
492 this.view.state.records[this.id] = {
496 this.state = this.view.state.records[this.id];
498 set_record: function(record) {
500 this.record = this.transform_record(record);
504 this.$element.data('widget', this);
507 transform_record: function(record) {
510 _.each(record, function(value, name) {
511 var r = _.clone(self.view.fields_view.fields[name] || {});
512 if ((r.type === 'date' || r.type === 'datetime') && value) {
513 r.raw_value = instance.web.auto_str_to_date(value);
517 r.value = instance.web.format_value(value, r);
518 new_record[name] = r;
523 this.qweb_context = {
527 for (var p in this) {
528 if (_.str.startsWith(p, 'kanban_')) {
529 this.qweb_context[p] = _.bind(this[p], this);
533 'content': this.view.qweb.render('kanban-box', this.qweb_context)
536 bind_events: function() {
538 $show_on_click = self.$element.find('.oe_kanban_box_show_onclick');
539 $show_on_click.toggle(this.state.folded);
540 this.$element.find('.oe_kanban_box_show_onclick_trigger').click(function() {
541 $show_on_click.toggle();
542 self.state.folded = !self.state.folded;
545 this.$element.find('[tooltip]').tipsy({
550 var template = $(this).attr('tooltip');
551 if (!self.view.qweb.has_template(template)) {
554 return self.view.qweb.render(template, self.qweb_context);
562 this.$element.find('.oe_kanban_action').click(function() {
563 var $action = $(this),
564 type = $action.data('type') || 'button',
565 method = 'do_action_' + (type === 'action' ? 'object' : type);
566 if (_.str.startsWith(type, 'switch_')) {
567 self.view.do_switch_view(type.substr(7));
568 } else if (typeof self[method] === 'function') {
569 self[method]($action);
571 self.do_warn("Kanban: no action for type : " + type);
576 do_action_delete: function($action) {
578 if (confirm(_t("Are you sure you want to delete this record ?"))) {
579 return $.when(this.view.dataset.unlink([this.id])).then(function() {
580 self.group.remove_record(self.id);
585 do_action_edit: function($action) {
587 if ($action.attr('target') === 'dialog') {
588 this.view.form_dialog.select_id(this.id).then(function() {
589 self.view.form_dialog.open();
592 this.view.open_record(this.id);
595 do_action_color: function($action) {
597 colors = '#FFFFFF,#CCCCCC,#FFC7C7,#FFF1C7,#E3FFC7,#C7FFD5,#C7FFFF,#C7D5FF,#E3C7FF,#FFC7F1'.split(','),
598 $cpicker = $(QWeb.render('KanbanColorPicker', { colors : colors, columns: 2 }));
599 $action.after($cpicker);
600 $cpicker.mouseenter(function() {
601 clearTimeout($cpicker.data('timeoutId'));
602 }).mouseleave(function(evt) {
603 var timeoutId = setTimeout(function() { $cpicker.remove() }, 500);
604 $cpicker.data('timeoutId', timeoutId);
606 $cpicker.find('a').click(function() {
608 data[$action.data('name')] = $(this).data('color');
609 self.view.dataset.write(self.id, data, {}, function() {
610 self.record[$action.data('name')] = $(this).data('color');
617 do_action_object: function ($action) {
618 var button_attrs = $action.data();
619 this.view.do_execute_action(button_attrs, this.view.dataset, this.id, this.do_reload);
621 do_reload: function() {
623 this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).then(function(records) {
624 if (records.length) {
625 self.set_record(records[0]);
632 do_render: function() {
633 this.$element.html(this.render());
636 kanban_color: function(variable) {
637 var number_of_color_schemes = 10,
639 switch (typeof(variable)) {
641 for (var i=0, ii=variable.length; i<ii; i++) {
642 index += variable.charCodeAt(i);
646 index = Math.round(variable);
651 var color = (index % number_of_color_schemes);
652 return 'oe_kanban_color_' + color;
654 kanban_gravatar: function(email, size) {
656 email = _.str.trim(email || '').toLowerCase();
657 var default_ = _.str.isBlank(email) ? 'mm' : 'identicon';
658 var email_md5 = $.md5(email);
659 return 'http://www.gravatar.com/avatar/' + email_md5 + '.png?s=' + size + '&d=' + default_;
661 kanban_image: function(model, field, id) {
663 var url = instance.connection.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + id;
664 if (this.record.__last_update && this.record.__last_update.raw_value) {
665 var time = instance.web.str_to_datetime(this.record.__last_update.raw_value).getTime();
670 kanban_text_ellipsis: function(s, size) {
674 } else if (s.length <= size) {
677 return s.substr(0, size) + '...';
683 * Quick creation view.
685 * Triggers a single event "added" with a single parameter "name", which is the
686 * name entered by the user
691 instance.web_kanban.QuickCreate = instance.web.Widget.extend({
692 template: 'KanbanView.quick_create',
695 * close_btn: If true, the widget will display a "Close" button able to trigger
698 init: function(parent, dataset, context, buttons) {
700 this._dataset = dataset;
701 this._buttons = buttons || false;
702 this._context = context || {};
706 self.$input = this.$element.find('input');
707 self.$input.keyup(function(event){
708 if(event.keyCode == 13){
712 $(".oe-kanban-quick_create_add", this.$element).click(function () {
715 $(".oe-kanban-quick_create_close", this.$element).click(function () {
716 self.trigger('close');
720 this.$element.find('input').focus();
723 * Handles user event from nested quick creation view
725 quick_add: function () {
728 'name_create', [self.$input.val(), new instance.web.CompoundContext(
729 this._dataset.get_context(), this._context)])
730 .pipe(function(record) {
732 self.trigger('added', record);
733 }, function(error, event) {
734 event.preventDefault();
735 return self.slow_create();
738 slow_create: function() {
740 var pop = new instance.web.form.SelectCreatePopup(this);
744 title: _t("Create: ") + (this.string || this.name),
745 initial_view: "form",
746 disable_multiple_selection: true
749 {"default_name": self.$input.val()}
751 pop.on_select_elements.add(function(element_ids) {
753 self.trigger('added', element_ids);
759 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: