diff options
| author | Géry Debongnie <ged@odoo.com> | 2015-06-15 16:31:27 +0200 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2015-06-15 16:31:27 +0200 |
| commit | 2c7367a8cc8d9e200b1fa2083ce0d80f8f4e4bd3 (patch) | |
| tree | af916cff4d94554b1e9667913e3feb331ec81a13 /addons/web_kanban/static/src/js | |
| parent | 5cb10d4fc9594f0c6825757c24d66d63664ce026 (diff) | |
| parent | 82186a93bb3f93c36667835df68fa31dcacf97a2 (diff) | |
[MERGE] new kanban view
This branch introduces the new kanban view. It is not completely
compatible with the previous version. Here are a few importants
points to be aware of:
* the kanban view template is rendered directly, not put inside a div.
It means that if a template has more than one root, chaos will ensue
* kanban view has been split into several files, and various js modules
have changed. For example, the KanbanRecord class is now exported by
the 'web_kanban.Record' module. So, require statements need to be
updated
* records do not have access to the view (i.e., record.view.is_action_enabled('edit'))
You can use the 'editable' and 'deletable' key instead
Diffstat (limited to 'addons/web_kanban/static/src/js')
| -rw-r--r-- | addons/web_kanban/static/src/js/compatibility.js | 10 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_column.js | 245 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_common.js | 928 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_quick_create.js | 127 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_record.js | 275 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_view.js | 1153 | ||||
| -rw-r--r-- | addons/web_kanban/static/src/js/kanban_widgets.js | 247 |
7 files changed, 1529 insertions, 1456 deletions
diff --git a/addons/web_kanban/static/src/js/compatibility.js b/addons/web_kanban/static/src/js/compatibility.js index 256086b9ef1..a8ecd06f8d6 100644 --- a/addons/web_kanban/static/src/js/compatibility.js +++ b/addons/web_kanban/static/src/js/compatibility.js @@ -1,15 +1,17 @@ odoo.define('web_kanban.compatibility', function (require) { "use strict"; -var common = require('web_kanban.common'); +var kanban_widgets = require('web_kanban.widgets'); +var KanbanRecord = require('web_kanban.Record'); +var KanbanColumn = require('web_kanban.Column'); var KanbanView = require('web_kanban.KanbanView'); return; openerp = window.openerp || {}; openerp.web_kanban = openerp.web_kanban || {}; -openerp.web_kanban.AbstractField = common.AbstractField; -openerp.web_kanban.KanbanGroup = common.KanbanGroup; -openerp.web_kanban.KanbanRecord = common.KanbanRecord; +openerp.web_kanban.AbstractField = kanban_widgets.AbstractField; +openerp.web_kanban.KanbanGroup = KanbanColumn; +openerp.web_kanban.KanbanRecord = KanbanRecord; openerp.web_kanban.KanbanView = KanbanView; }); diff --git a/addons/web_kanban/static/src/js/kanban_column.js b/addons/web_kanban/static/src/js/kanban_column.js new file mode 100644 index 00000000000..85333ffbe3f --- /dev/null +++ b/addons/web_kanban/static/src/js/kanban_column.js @@ -0,0 +1,245 @@ +odoo.define('web_kanban.Column', function (require) { +"use strict"; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var form_common = require('web.form_common'); +var Widget = require('web.Widget'); +var quick_create = require('web_kanban.quick_create'); +var KanbanRecord = require('web_kanban.Record'); + +var _t = core._t; +var QWeb = core.qweb; +var RecordQuickCreate = quick_create.RecordQuickCreate; + +var KanbanColumn = Widget.extend({ + template: "KanbanView.Group", + + events: { + 'click .o_kanban_toggle_fold': function (e) { + e.preventDefault(); + e.stopPropagation(); + this.folded = true; + this.update_column(); + }, + 'click .o_column_edit': 'edit_column', + 'click .o_column_delete': 'delete_column', + 'click .o_kanban_quick_add': 'add_quick_create', + 'click .o_kanban_load_more': 'load_more', + }, + + custom_events: { + 'kanban_record_delete': 'delete_record', + 'cancel_quick_create': 'cancel_quick_create', + 'quick_create_add_record': 'quick_create_add_record', + }, + + init: function(parent, group_data, options, record_options) { + this._super(parent); + this.data_records = group_data.records; + this.dataset = group_data.dataset; + this.records = []; + this.title = group_data.title; + this.id = group_data.id; + this.folded = group_data.attributes.folded; + this.fields = options.fields; + this.values = group_data.values; + + this.quick_create = options.quick_create; + this.grouped_by_m2o = options.grouped_by_m2o; + this.editable = options.editable; + this.deletable = options.deletable; + this.records_editable = options.records_editable; + this.records_deletable = options.records_deletable; + this.relation = options.relation; + this.offset = 0; + this.remaining = this.dataset.size() - this.data_records.length; + + this.record_options = _.extend(_.clone(record_options), { + group_info: this.values, + }); + }, + + start: function() { + var self = this; + this.$header = this.$('.o_kanban_header'); + + for (var i = 0; i < this.data_records.length; i++) { + this.add_record(this.data_records[i], {no_update: true}); + } + this.$header.tooltip(); + + this.update_column(); + this.$el.sortable({ + connectWith: '.o_kanban_group', + revert: 150, + delay: 100, + items: '> .o_kanban_record', + helper: 'clone', + cursor: 'move', + over: function () { + self.folded = false; + self.update_column(); + }, + update: function (event, ui) { + var record = ui.item.data('record'); + var index = self.records.indexOf(record); + var test2 = $.contains(self.$el[0], record.$el[0]); + if (index >= 0 && test2) { + // resequencing records + record.$el.removeAttr('style'); // jqueryui sortable add display:block inline + self.trigger_up('kanban_column_resequence'); + } else if (index >= 0 && !test2) { + // removing record from this column + self.records.splice(self.records.indexOf(record), 1); + self.dataset.remove_ids([record.id]); + } else { + // adding record to this column + self.records.push(record); + record.setParent(self); + self.trigger_up('kanban_column_add_record', {record: record}); + } + self.update_column(); + } + }); + this.$el.click(function (event) { + if (self.$el.hasClass('o_column_folded')) { + event.preventDefault(); + self.folded = false; + self.update_column(); + } + }); + }, + + is_empty: function () { + return !this.records.length; + }, + + add_record: function (data, options) { + var record = new KanbanRecord(this, data, this.record_options); + this.records.push(record); + this.dataset.ids = _.uniq(this.dataset.ids.concat(record.id)); + if (options.position === 'before') { + record.insertAfter(this.quick_create_widget ? this.quick_create_widget.$el : this.$header); + } else { + var $load_more = this.$('.o_kanban_load_more'); + if ($load_more.length) { + record.insertBefore($load_more); + } else { + record.appendTo(this.$el); + } + } + if (!options.no_update) { + this.update_column(); + } + }, + + get_ids: function () { + var ids = []; + this.$('.o_kanban_record').each(function (index, r) { + ids.push($(r).data('record').id); + }); + return ids; + }, + + update_column: function () { + var title = this.folded ? this.title + ' (' + this.records.length + ')' : this.title; + this.$header.find('.o_column_title').text(title); + this.$header.find('.o-kanban-count').text(this.records.length); + + this.$el.toggleClass('o_column_folded', this.folded); + var tooltip; + if (this.remaining) { + tooltip = this.records.length + '/' + this.dataset.size() + _t(' records'); + } else { + tooltip = this.records.length + _t(' records'); + } + this.$header.tooltip().attr('data-original-title', tooltip); + if (!this.remaining) { + this.$('.o_kanban_load_more').remove(); + } else { + this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget:this})) + } + }, + + delete_column: function (event) { + event.preventDefault(); + var buttons = [ + { + text: _t("Ok"), + classes: 'btn-primary', + close: true, + click: this.trigger_up.bind(this, 'kanban_column_delete') + }, + {text: _t("Cancel"), close: true} + ]; + new Dialog(this, { + size: 'medium', + buttons: buttons, + $content: $('<div>', { + text: _t("Are you sure that you want to remove this column ?") + }), + }).open(); + }, + + edit_column: function (event) { + event.preventDefault(); + var dialog = new form_common.FormViewDialog(this, { + res_model: this.relation, + res_id: this.id, + title: _t("Edit Column"), + }).open(); + + dialog.on('record_saved', this, function () { + this.trigger_up('kanban_reload'); + }); + }, + + delete_record: function (event) { + event.stopped = false; + var self = this; + event.data.after = function cleanup () { + var index = self.records.indexOf(event.data.record); + self.records.splice(index, 1); + self.update_column(); + }; + }, + + add_quick_create: function () { + if (this.quick_create_widget) { + return; + } + var self = this; + var width = this.records.length ? this.records[0].$el.innerWidth() : this.$el.width() - 8; + this.quick_create_widget = new RecordQuickCreate(this, width); + this.quick_create_widget.insertAfter(this.$header); + this.quick_create_widget.$el.focusout(function () { + setTimeout(function() { + var hasFocus = !! (self.quick_create_widget.$(':focus').length > 0); + if (! hasFocus && self.quick_create_widget) { + self.cancel_quick_create(); + } + }, 10); + }); + + }, + + cancel_quick_create: function () { + this.quick_create_widget.destroy(); + this.quick_create_widget = undefined; + }, + + quick_create_add_record: function (event) { + this.trigger_up('column_add_record', event.data); + }, + + load_more: function (event) { + event.preventDefault(); + this.trigger_up('kanban_load_more'); + }, + +}); + +return KanbanColumn; + +}); diff --git a/addons/web_kanban/static/src/js/kanban_common.js b/addons/web_kanban/static/src/js/kanban_common.js deleted file mode 100644 index b63758aaa28..00000000000 --- a/addons/web_kanban/static/src/js/kanban_common.js +++ /dev/null @@ -1,928 +0,0 @@ -odoo.define('web_kanban.common', function (require) { -"use strict"; - -var core = require('web.core'); -var data = require('web.data'); -var form_common = require('web.form_common'); -var formats = require('web.formats'); -var framework = require('web.framework'); -var ProgressBar = require('web.ProgressBar'); -var pyeval = require('web.pyeval'); -var Registry = require('web.Registry'); -var session = require('web.session'); -var time = require('web.time'); -var utils = require('web.utils'); -var web_client = require('web.web_client'); -var Widget = require('web.Widget'); - -var QWeb = core.qweb; -var _t = core._t; -/** - * Interface to be implemented by kanban fields. - * - */ -var FieldInterface = { - /** - Constructor. - - parent: The widget's parent. - - field: A dictionary giving details about the field, including the current field's value in the - raw_value field. - - $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object. - */ - init: function(parent, field, $node) {}, -}; - -/** - * Abstract class for classes implementing FieldInterface. - * - * Properties: - * - value: useful property to hold the value of the field. By default, the constructor - * sets value property. - * - */ -var AbstractField = Widget.extend(FieldInterface, { - /** - Constructor that saves the field and $node parameters and sets the "value" property. - */ - init: function(parent, field, $node) { - this._super(parent); - this.field = field; - this.$node = $node; - this.options = pyeval.py_eval(this.$node.attr("options") || '{}'); - this.set("value", field.raw_value); - }, -}); - -var Priority = AbstractField.extend({ - init: function(parent, field, $node) { - this._super.apply(this, arguments); - this.name = $node.attr('name'); - this.parent = parent; - }, - prepare_priority: function() { - var self = this; - var selection = this.field.selection || []; - var init_value = selection && selection[0][0] || 0; - var data = _.map(selection.slice(1), function(element, index) { - var value = { - 'value': element[0], - 'name': element[1], - 'click_value': element[0], - }; - if (index === 0 && self.get('value') == element[0]) { - value.click_value = init_value; - } - return value; - }); - return data; - }, - renderElement: function() { - this.record_id = this.parent.id; - this.priorities = this.prepare_priority(); - var readonly = this.field && this.field.readonly; - if (readonly){ - this.set('readonly', true); - } - this.$el = $(QWeb.render("Priority", {'widget': this})); - if (!readonly){ - this.$el.find('li').click(this.do_action.bind(this)); - } - }, - do_action: function(e) { - var self = this; - var li = $(e.target).closest( "li" ); - if (li.length) { - var value = {}; - value[self.name] = String(li.data('value')); - 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)); - } - }, - reload_record: function() { - this.do_reload(); - }, -}); - -var KanbanSelection = AbstractField.extend({ - init: function(parent, field, $node) { - this._super.apply(this, arguments); - this.name = $node.attr('name'); - this.parent = parent; - }, - prepare_dropdown_selection: function() { - var self = this; - var data = []; - _.map(this.field.selection || [], function(res) { - var value = { - 'name': res[0], - 'tooltip': res[1], - 'state_name': res[1], - }; - var leg_opt = self.options && self.options.states_legend || null; - if (leg_opt && leg_opt[res[0]] && self.parent.group.values && self.parent.group.values[leg_opt[res[0]]]) { - value['state_name'] = self.parent.group.values[leg_opt[res[0]]]; - value['tooltip'] = self.parent.group.values[leg_opt[res[0]]]; - } - if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; } - else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; } - else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; } - data.push(value); - }); - return data; - }, - renderElement: function() { - var self = this; - this.record_id = self.parent.id; - this.states = self.prepare_dropdown_selection(); - this.$el = $(QWeb.render("KanbanSelection", {'widget': self})); - this.$el.find('li').click(self.do_action.bind(self)); - }, - do_action: function(e) { - var self = this; - var li = $(e.target).closest( "li" ); - if (li.length) { - var value = {}; - value[self.name] = String(li.data('value')); - 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)); - } - }, - reload_record: function() { - this.do_reload(); - }, -}); - - -/** - * Quick creation view. - * - * Triggers a single event "added" with a single parameter "name", which is the - * name entered by the user - * - * @class - * @type {*} - */ -var QuickCreate = Widget.extend({ - template: 'KanbanView.quick_create', - - /** - * close_btn: If true, the widget will display a "Close" button able to trigger - * a "close" event. - */ - init: function(parent, dataset, context, buttons) { - this._super(parent); - this._dataset = dataset; - this._buttons = buttons || false; - this._context = context || {}; - }, - start: function () { - var self = this; - self.$input = this.$el.find('input'); - self.$input.keyup(function(event){ - if(event.keyCode == 13){ - self.quick_add(); - } - }); - $(".oe_kanban_quick_create").focusout(function (e) { - var val = self.$el.find('input').val(); - if (/^\s*$/.test(val)) { self.trigger('close'); } - e.stopImmediatePropagation(); - }); - $(".oe_kanban_quick_create_add", this.$el).click(function () { - self.quick_add(); - self.focus(); - }); - $(".oe_kanban_quick_create_close", this.$el).click(function (ev) { - ev.preventDefault(); - self.trigger('close'); - }); - self.$input.keyup(function(e) { - if (e.keyCode == 27 && self._buttons) { - self.trigger('close'); - } - }); - }, - focus: function() { - this.$el.find('input').focus(); - }, - /** - * Handles user event from nested quick creation view - */ - quick_add: function () { - var self = this; - var val = this.$input.val(); - if (/^\s*$/.test(val)) { this.$el.remove(); return; } - this._dataset.call( - 'name_create', [val, new data.CompoundContext( - this._dataset.get_context(), this._context)]) - .then(function(record) { - self.$input.val(""); - self.trigger('added', record[0]); - }, function(error, event) { - event.preventDefault(); - return self.slow_create(); - }); - }, - slow_create: function() { - var self = this; - var popup = new form_common.SelectCreatePopup(this); - popup.select_element( - self._dataset.model, - { - title: _t("Create: ") + (this.string || this.name), - initial_view: "form", - disable_multiple_selection: true - }, - [], - {"default_name": self.$input.val()} - ); - popup.on("elements_selected", self, function(element_ids) { - self.$input.val(""); - self.trigger('added', element_ids[0]); - }); - } -}); - -var KanbanRecord = Widget.extend({ - template: 'KanbanView.record', - - events: { - 'click .o_kanban_manage_toggle_button': 'toggle_manage_pane', - }, - - init: function (parent, record) { - this._super(parent); - this.group = parent; - this.view = parent.view; - this.id = null; - this.set_record(record); - if (!this.view.state.records[this.id]) { - this.view.state.records[this.id] = { - folded: false - }; - } - this.state = this.view.state.records[this.id]; - this.fields = {}; - }, - set_record: function(record) { - var self = this; - this.id = record.id; - this.values = {}; - _.each(record, function(v, k) { - self.values[k] = { - value: v - }; - }); - this.record = this.transform_record(record); - }, - start: function() { - var self = this; - this._super(); - this.init_content(); - }, - init_content: function() { - var self = this; - self.sub_widgets = []; - this.$("[data-field_id]").each(function() { - self.add_widget($(this)); - }); - this.$el.data('widget', this); - this.bind_events(); - }, - transform_record: function(record) { - var self = this, - new_record = {}; - _.each(record, function(value, name) { - var r = _.clone(self.view.fields_view.fields[name] || {}); - if ((r.type === 'date' || r.type === 'datetime') && value) { - r.raw_value = time.auto_str_to_date(value); - } else { - r.raw_value = value; - } - r.value = formats.format_value(value, r); - new_record[name] = r; - }); - return new_record; - }, - renderElement: function() { - this.qweb_context = { - record: this.record, - widget: this, - read_only_mode: this.view.options.read_only_mode, - }; - for (var p in this) { - if (_.str.startsWith(p, 'kanban_')) { - this.qweb_context[p] = _.bind(this[p], this); - } - } - var $el = QWeb.render(this.template, { - 'widget': this, - 'content': this.view.qweb.render('kanban-box', this.qweb_context) - }); - this.replaceElement($el); - this.replace_fields(); - }, - replace_fields: function() { - var self = this; - this.$("field").each(function() { - var $field = $(this); - var $nfield = $("<span></span"); - var id = _.uniqueId("kanbanfield"); - self.fields[id] = $field; - $nfield.attr("data-field_id", id); - $field.replaceWith($nfield); - }); - }, - add_widget: function($node) { - var $orig = this.fields[$node.data("field_id")]; - var field = this.record[$orig.attr("name")]; - var type = field.type; - type = $orig.attr("widget") ? $orig.attr("widget") : type; - var obj = fields_registry.get(type); - var widget = new obj(this, field, $orig); - this.sub_widgets.push(widget); - widget.replace($node); - }, - bind_events: function() { - var self = this; - this.setup_color_picker(); - this.$el.find('[title]').each(function(){ - $(this).tooltip({ - delay: { show: 500, hide: 0}, - title: function() { - var template = $(this).attr('tooltip'); - if (!self.view.qweb.has_template(template)) { - return false; - } - return self.view.qweb.render(template, self.qweb_context); - }, - }); - }); - - // If no draghandle is found, make the whole card as draghandle (provided one can edit) - if (!this.$el.find('.oe_kanban_draghandle').length) { - this.$el.children(':first') - .toggleClass('oe_kanban_draghandle', this.view.is_action_enabled('edit')); - } - - this.$el.find('.oe_kanban_action').click(function(ev) { - ev.preventDefault(); - var $action = $(this), - type = $action.data('type') || 'button', - method = 'do_action_' + (type === 'action' ? 'object' : type); - if ((type === 'edit' || type === 'delete') && ! self.view.is_action_enabled(type)) { - self.view.open_record(self.id, true); - } else if (_.str.startsWith(type, 'switch_')) { - self.view.do_switch_view(type.substr(7)); - } else if (typeof self[method] === 'function') { - self[method]($action); - } else { - self.do_warn("Kanban: no action for type : " + type); - } - }); - - if (this.$el.find('.oe_kanban_global_click,.oe_kanban_global_click_edit').length) { - this.$el.on('click', function(ev) { - if (!ev.isTrigger && !$._data(ev.target, 'events')) { - var trigger = true; - var elem = ev.target; - var ischild = true; - var children = []; - while (elem) { - var events = $._data(elem, 'events'); - if (elem == ev.currentTarget) { - ischild = false; - } - if (ischild) { - children.push(elem); - if (events && events.click) { - // do not trigger global click if one child has a click event registered - trigger = false; - } - } - if (trigger && events && events.click) { - _.each(events.click, function(click_event) { - if (click_event.selector) { - // For each parent of original target, check if a - // delegated click is bound to any previously found children - _.each(children, function(child) { - if ($(child).is(click_event.selector)) { - trigger = false; - } - }); - } - }); - } - elem = elem.parentElement; - } - if (trigger) { - self.on_card_clicked(ev); - } - } - }); - } - }, - /* actions when user click on the block with a specific class - * open on normal view : oe_kanban_global_click - * open on form/edit view : oe_kanban_global_click_edit - */ - on_card_clicked: function(ev) { - if (this.$el.find('.oe_kanban_global_click').size() > 0 && this.$el.find('.oe_kanban_global_click').data('routing')) { - framework.redirect(this.$el.find('.oe_kanban_global_click').data('routing') + "/" + this.id); - } - else if (this.$el.find('.oe_kanban_global_click_edit').size()>0) - this.do_action_edit(); - else - this.do_action_open(); - }, - setup_color_picker: function() { - var self = this; - var $el = this.$el.find('ul.oe_kanban_colorpicker'); - if ($el.length) { - $el.html(QWeb.render('KanbanColorPicker', { - widget: this - })); - $el.on('click', 'a', function(ev) { - ev.preventDefault(); - var color_field = $(this).parents('.oe_kanban_colorpicker').first().data('field') || 'color'; - var data = {}; - data[color_field] = $(this).data('color'); - self.view.dataset.write(self.id, data, {}).done(function() { - self.record[color_field] = $(this).data('color'); - self.do_reload(); - }); - }); - } - }, - toggle_manage_pane: function(){ - this.$('.o_kanban_card_content').toggleClass('o_visible o_invisible'); - this.$('.o_kanban_card_manage_pane').toggleClass('o_visible o_invisible'); - this.$('.o_kanban_manage_button_section').toggleClass(this.kanban_color((this.values['color'] && this.values['color'].value) || 0)); - }, - - do_action_delete: function($action) { - var self = this; - function do_it() { - return $.when(self.view.dataset.unlink([self.id])).done(function() { - self.group.remove_record(self.id); - self.destroy(); - }); - } - if (this.view.options.confirm_on_delete) { - if (confirm(_t("Are you sure you want to delete this record ?"))) { - return do_it(); - } - } else - return do_it(); - }, - do_action_edit: function($action) { - this.view.open_record(this.id, true); - }, - do_action_open: function($action) { - this.view.open_record(this.id); - }, - do_action_object: function ($action) { - var button_attrs = $action.data(); - var node_context = button_attrs.context || {}; - var context = new data.CompoundContext(node_context); - context.set_eval_context({ - active_id: this.id, - active_ids: [this.id], - active_model: this.view.dataset.model - }); - var action_data = _.extend({}, button_attrs, {context: context}); - this.view.do_execute_action(action_data, this.view.dataset, this.id, this.do_reload); - }, - do_action_url: function($action) { - return framework.redirect($action.attr("href")); - }, - do_reload: function() { - var self = this; - this.view.dataset.read_ids([this.id], this.view.fields_keys.concat(['__last_update'])).done(function(records) { - _.each(self.sub_widgets, function(el) { - el.destroy(); - }); - self.sub_widgets = []; - if (records.length) { - self.set_record(records[0]); - self.renderElement(); - self.init_content(); - self.group.compute_cards_auto_height(); - self.view.postprocess_m2m_tags(); - } else { - self.destroy(); - } - }); - }, - kanban_getcolor: function(variable) { - var index = 0; - switch (typeof(variable)) { - case 'string': - for (var i=0, ii=variable.length; i<ii; i++) { - index += variable.charCodeAt(i); - } - break; - case 'number': - index = Math.round(variable); - break; - default: - return ''; - } - var color = (index % this.view.number_of_color_schemes); - return color; - }, - kanban_color: function(variable) { - var color = this.kanban_getcolor(variable); - return color === '' ? '' : 'oe_kanban_color_' + color; - }, - kanban_image: function(model, field, id, cache, options) { - options = options || {}; - var url; - if (this.record[field] && this.record[field].value && !utils.is_bin_size(this.record[field].value)) { - url = 'data:image/png;base64,' + this.record[field].value; - } else if (this.record[field] && ! this.record[field].value) { - url = "/web/static/src/img/placeholder.png"; - } else { - id = JSON.stringify(id); - if (options.preview_image) - field = options.preview_image; - url = session.url('/web/binary/image', {model: model, field: field, id: id}); - if (cache !== undefined) { - // Set the cache duration in seconds. - url += '&cache=' + parseInt(cache, 10); - } - } - return url; - }, - kanban_text_ellipsis: function(s, size) { - size = size || 160; - if (!s) { - return ''; - } else if (s.length <= size) { - return s; - } else { - return s.substr(0, size) + '...'; - } - }, - kanban_compute_domain: function(domain) { - return data.compute_domain(domain, this.values); - } -}); - - -var KanbanGroup = Widget.extend({ - template: 'KanbanView.group_header', - init: function (parent, records, group, dataset) { - var self = this; - this._super(parent); - this.$has_been_started = $.Deferred(); - this.view = parent; - this.group = group; - this.dataset = dataset; - this.dataset_offset = 0; - this.value = this.title = this.values = null; - if (this.group) { - this.values = group.values; - this.value = group.get('value'); - this.title = group.get('value'); - if (this.value instanceof Array) { - this.title = this.value[1]; - this.value = this.value[0]; - } - var field = this.view.group_by_field; - if (!_.isEmpty(field)) { - try { - this.title = formats.format_value(group.get('value'), field, false); - } catch(e) {} - } - } - - if (this.title === false) { - this.title = _t('Undefined'); - this.undefined_title = true; - } - var key = this.view.group_by + '-' + this.value; - if (!this.view.state.groups[key]) { - this.view.state.groups[key] = { - folded: group ? group.get('folded') : false - }; - } - this.state = this.view.state.groups[key]; - this.$records = null; - - this.records = []; - this.$has_been_started.done(function() { - self.do_add_records(records); - }); - }, - start: function() { - var self = this, - def = this._super(); - if (! self.view.group_by) { - self.$el.addClass("oe_kanban_no_group"); - var QuickCreateClass = self.view.get_quick_create_class(); - self.quick = (new QuickCreateClass(this, self.dataset, {}, false)) - .on('added', self, self.proxy('quick_created')); - self.quick.replace($(".oe_kanban_no_group_qc_placeholder")); - } - this.$records = $(QWeb.render('KanbanView.group_records_container', { widget : this})); - this.$records.insertBefore(this.view.$el.find('.oe_kanban_groups_records td:last')); - - this.$el.on('click', '.oe_kanban_group_dropdown li a', function(ev) { - var fn = 'do_action_' + $(ev.target).data().action; - if (typeof(self[fn]) === 'function') { - self[fn]($(ev.target)); - } - }); - - this.$el.find('.oe_kanban_add').click(function () { - if (self.view.quick) { - self.view.quick.trigger('close'); - } - if (self.quick) { - return false; - } - self.view.$el.find('.oe_view_nocontent').hide(); - var ctx = {}; - ctx['default_' + self.view.group_by] = self.value; - self.quick = new (self.view.get_quick_create_class())(this, self.dataset, ctx, true) - .on('added', self, self.proxy('quick_created')) - .on('close', self, function() { - self.view.$el.find('.oe_view_nocontent').show(); - this.quick.destroy(); - delete self.view.quick; - delete this.quick; - }); - self.quick.appendTo($(".oe_kanban_group_list_header", self.$records)); - self.quick.focus(); - self.view.quick = self.quick; - }); - // Add bounce effect on image '+' of kanban header when click on empty space of kanban grouped column. - this.$records.on('click', '.oe_kanban_show_more', this.do_show_more); - if (this.state.folded) { - this.do_toggle_fold(); - } - this.$el.data('widget', this); - this.$records.data('widget', this); - this.$has_been_started.resolve(); - var add_btn = this.$el.find('.oe_kanban_add'); - add_btn.tooltip({delay: { show: 500, hide:1000 }}); - this.$records.find(".oe_kanban_column_cards").click(function (ev) { - if (ev.target == ev.currentTarget) { - if (!self.state.folded) { - add_btn.openerpBounce(); - } - } - }); - this.is_started = true; - this.fetch_tooltip(); - return def; - }, - /* - * Form the tooltip, based on optional group_by_tooltip on the grouping field. - * This function goes through the arch of the view, finding the declaration - * of the field used to group. If group_by_tooltip is defined, use the previously - * computed values of the group to form the tooltip. */ - fetch_tooltip: function() { - var self = this; - if (! this.group) - return; - var options = null; - var recurse = function(node) { - if (node.tag === "field" && node.attrs.name == self.view.group_by) { - options = pyeval.py_eval(node.attrs.options || '{}'); - return; - } - _.each(node.children, function(child) { - recurse(child); - }); - }; - recurse(this.view.fields_view.arch); - if (options && options.group_by_tooltip) { - this.tooltip = _.compact( - _.union( - [this.title], - _.map( - options.group_by_tooltip, - function (key, value, list) { return self.values && self.values[value] && (key + "\n" + self.values[value]) || ''; })) - ).join('\n\n'); - this.$(".oe_kanban_group_title_text").attr("title", this.tooltip || this.title || ""); - } - }, - compute_cards_auto_height: function() { - // oe_kanban_no_auto_height is an empty class used to disable this feature - if (!this.view.group_by) { - var min_height = 0; - var els = []; - _.each(this.records, function(r) { - var $e = r.$el.children(':first:not(.oe_kanban_no_auto_height)').css('min-height', 0); - if ($e.length) { - els.push($e[0]); - min_height = Math.max(min_height, $e.outerHeight()); - } - }); - $(els).css('min-height', min_height); - } - }, - destroy: function() { - this._super(); - if (this.$records) { - this.$records.remove(); - } - }, - do_show_more: function(evt) { - var self = this; - var ids = self.view.dataset.ids.slice(0); - return this.dataset.read_slice(this.view.fields_keys.concat(['__last_update']), { - 'limit': self.view.limit, - 'offset': self.dataset_offset += self.view.limit - }).then(function(records) { - self.view.dataset.ids = ids.concat(_.difference(self.dataset.ids, ids)); - self.do_add_records(records); - self.compute_cards_auto_height(); - self.view.postprocess_m2m_tags(); - return records; - }); - }, - do_add_records: function(records, prepend) { - var self = this; - var $list_header = this.$records.find('.oe_kanban_group_list_header'); - var $show_more = this.$records.find('.oe_kanban_show_more'); - var $cards = this.$records.find('.oe_kanban_column_cards'); - - _.each(records, function(record) { - var rec = new KanbanRecord(self, record); - if (!prepend) { - rec.appendTo($cards); - self.records.push(rec); - } else { - rec.prependTo($cards); - self.records.unshift(rec); - } - }); - if ($show_more.length) { - var size = this.dataset.size(); - $show_more.toggle(this.records.length < size).find('.oe_kanban_remaining').text(size - this.records.length); - } - }, - remove_record: function(id, remove_from_dataset) { - for (var i = 0; i < this.records.length; i++) { - if (this.records[i]['id'] === id) { - this.records.splice(i, 1); - i--; - } - } - }, - do_toggle_fold: function(compute_width) { - this.$el.add(this.$records).toggleClass('oe_kanban_group_folded'); - this.state.folded = this.$el.is('.oe_kanban_group_folded'); - this.$("ul.oe_kanban_group_dropdown li a[data-action=toggle_fold]").text((this.state.folded) ? _t("Unfold") : _t("Fold")); - }, - do_action_toggle_fold: function() { - this.do_toggle_fold(); - }, - do_action_edit: function() { - var self = this; - self.do_action({ - res_id: this.value, - name: _t("Edit column"), - res_model: self.view.group_by_field.relation, - views: [[false, 'form']], - type: 'ir.actions.act_window', - target: "new", - flags: { - action_buttons: true, - headless: true, - } - }); - // to do : remove this horrible hack - var am = web_client.action_manager; - var form = am.dialog_widget.views.form.controller; - form.on("on_button_cancel", am.dialog, function() { return am.dialog.$dialog_box.modal('hide'); }); - form.on('record_saved', self, function() { - am.dialog.$dialog_box.modal('hide'); - self.view.do_reload(); - }); - }, - do_action_delete: function() { - var self = this; - if (confirm(_t("Are you sure to remove this column ?"))) { - (new data.DataSet(self, self.view.group_by_field.relation)).unlink([self.value]).done(function(r) { - self.view.do_reload(); - }); - } - }, - do_save_sequences: function() { - var self = this; - if (_.indexOf(this.view.fields_keys, 'sequence') > -1) { - var new_sequence = _.pluck(this.records, 'id'); - self.view.dataset.resequence(new_sequence); - } - }, - /** - * Handles a newly created record - * - * @param {id} id of the newly created record - */ - quick_created: function (record) { - var id = record, self = this; - self.view.remove_no_result(); - self.trigger("add_record"); - this.dataset.read_ids([id], this.view.fields_keys) - .done(function (records) { - self.view.dataset.ids.push(id); - self.do_add_records(records, true); - }); - }, - highlight: function(show){ - if(show){ - this.$el.addClass('oe_kanban_column_higlight'); - this.$records.addClass('oe_kanban_column_higlight'); - }else{ - this.$el.removeClass('oe_kanban_column_higlight'); - this.$records.removeClass('oe_kanban_column_higlight'); - } - } -}); - - -/** - * Kanban widgets: ProgressBar - * options - * - editable: boolean if current_value is editable - * - current_value: get the current_value from the field that must be present in the view - * - max_value: get the max_value from the field that must be present in the view - * - title: title of the gauge, displayed on top of the gauge - * - on_change: action to call when cliking and setting a value - */ -var KanbanProgressBar = AbstractField.extend({ - events: { - 'click': function() { - if(!this.readonly && this.progressbar.readonly) { - this.toggle_progressbar(); - } - } - }, - - init: function (parent, field, node) { - this._super(parent, field, node); - - var record = this.getParent().record; - this.progressbar = new ProgressBar(this, { - readonly: true, - value: record[this.options.current_value].raw_value, - max_value: record[this.options.max_value].raw_value, - title: this.options.title, - edit_max_value: this.options.edit_max_value, - }); - - this.readonly = !this.options.editable; - this.on_change = this.options.on_change; - }, - - start: function () { - var self = this; - - var def = this.progressbar.appendTo('<div>').done(function() { - self.progressbar.$el.addClass(self.$el.attr('class')); - self.replaceElement(self.progressbar.$el); - }); - - return $.when(this._super(), def).then(function() { - if(!self.readonly) { - var parent = self.getParent(); - self.progressbar.on('change:value change:max_value', self, function(e) { - var value = this.progressbar.get(this.progressbar.edit_max_value ? 'max_value' : 'value') || 0; - if(!isNaN(value)) { - parent.view.dataset.call(this.on_change, [parent.id, value]).then(function() { - self.toggle_progressbar(); - }); - } - }); - } - }); - }, - - toggle_progressbar: function() { - this.progressbar.readonly = !this.progressbar.readonly; - var $div = $('<div/>').insertAfter(this.$el); - this.progressbar.replace($div); - this.setElement(this.progressbar.$el); - }, -}); - -var fields_registry = new Registry(); - -fields_registry - .add('priority', Priority) - .add('kanban_state_selection', KanbanSelection) - .add("progress", KanbanProgressBar); - - -return { - AbstractField: AbstractField, - registry: fields_registry, - QuickCreate: QuickCreate, - KanbanRecord: KanbanRecord, - KanbanGroup: KanbanGroup, -}; - -}); diff --git a/addons/web_kanban/static/src/js/kanban_quick_create.js b/addons/web_kanban/static/src/js/kanban_quick_create.js new file mode 100644 index 00000000000..3268a2b1acd --- /dev/null +++ b/addons/web_kanban/static/src/js/kanban_quick_create.js @@ -0,0 +1,127 @@ +odoo.define('web_kanban.quick_create', function (require) { +"use strict"; + +var Widget = require('web.Widget'); + +var RecordQuickCreate = Widget.extend({ + template: "KanbanView.QuickCreate", + + events: { + 'click .o_kanban_cancel': 'cancel', + 'click .o_kanban_add': 'add_record', + 'keypress input': function (event) { + if (event.keyCode === 13) { + this.add_record(); + } + }, + 'keydown': function (event) { + if (event.keyCode === 27) { + this.trigger_up('cancel_quick_create'); + } + }, + }, + + init: function (parent, width) { + this._super(parent); + this.width = width; + }, + + start: function () { + this.$el.css({width: this.width}); + this.$input = this.$('input'); + this.$input.focus(); + }, + + cancel: function () { + this.trigger_up('cancel_quick_create'); + }, + + add_record: function () { + var value = this.$input.val(); + this.$input.val(''); + if (/^\s*$/.test(value)) { return; } + this.trigger_up('quick_create_add_record', {value: value}); + this.$input.focus(); + }, +}); + +var ColumnQuickCreate = Widget.extend({ + template: 'KanbanView.ColumnQuickCreate', + + events: { + 'click .o_kanban_add': function (event) { + console.log('click okanbanadd'); + event.stopPropagation(); + this.add_column(); + }, + 'click .o_kanban_quick_create': function (event) { + console.log('click quickcreate'); + event.stopPropagation(); + }, + 'keypress input': function (event) { + if (event.keyCode === 13) { + this.add_column(); + } + }, + 'click .o_kanban_cancel': function (event) { + this.folded = true; + this.update(); + }, + 'keydown': function (event) { + if (event.keyCode === 27) { + this.folded = true; + this.update(); + } + }, + }, + + init: function (parent) { + this._super(parent); + this.folded = true; + }, + + start: function () { + this.$header = this.$('.o_column_header'); + this.$quick_create = this.$('.o_kanban_quick_create'); + this.$input = this.$('input'); + this.$el.click(this.proxy('toggle')); + var self = this; + this.$el.focusout(function () { + setTimeout(function() { + var hasFocus = !! (self.$(':focus').length > 0); + if (! hasFocus) { + self.folded = true; + self.update(); + } + }, 10); + }); + }, + + toggle: function () { + this.folded = !this.folded; + this.update(); + if (!this.folded) { + this.$input.focus(); + } + }, + + update: function () { + this.$header.toggle(this.folded); + this.$quick_create.toggle(!this.folded); + }, + + add_column: function () { + var name = this.$input.val(); + this.$input.val(''); + if (/^\s*$/.test(name)) { return; } + this.trigger_up('quick_create_add_column', {value: name}); + this.$input.focus(); + } +}); + +return { + RecordQuickCreate: RecordQuickCreate, + ColumnQuickCreate: ColumnQuickCreate, +}; + +}); diff --git a/addons/web_kanban/static/src/js/kanban_record.js b/addons/web_kanban/static/src/js/kanban_record.js new file mode 100644 index 00000000000..9357978d20c --- /dev/null +++ b/addons/web_kanban/static/src/js/kanban_record.js @@ -0,0 +1,275 @@ +odoo.define('web_kanban.Record', function (require) { +"use strict"; + +var core = require('web.core'); +var data = require('web.data'); +var formats = require('web.formats'); +var framework = require('web.framework'); +var session = require('web.session'); +var time = require('web.time'); +var utils = require('web.utils'); +var Widget = require('web.Widget'); +var kanban_widgets = require('web_kanban.widgets'); + +var QWeb = core.qweb; +var fields_registry = kanban_widgets.registry; + +var KanbanRecord = Widget.extend({ + template: 'KanbanView.record', + + events: { + 'click .oe_kanban_action': 'on_kanban_action_clicked', + 'click .o_kanban_manage_toggle_button': 'toggle_manage_pane', + }, + + custom_events: { + 'kanban_update_record': 'update_record', + }, + + init: function (parent, record, options) { + this._super(parent); + + this.fields = options.fields; + this.editable = options.editable; + this.deletable = options.deletable; + this.draggable = options.draggable; + this.read_only_mode = options.read_only_mode; + this.model = options.model; + this.group_info = options.group_info; + this.qweb = options.qweb; + this.sub_widgets = []; + + this.init_content(record); + }, + + init_content: function (record) { + var self = this; + this.id = record.id; + this.values = {}; + _.each(record, function(v, k) { + self.values[k] = { + value: v + }; + }); + this.record = this.transform_record(record); + var qweb_context = { + record: this.record, + widget: this, + read_only_mode: this.read_only_mode, + }; + for (var p in this) { + if (_.str.startsWith(p, 'kanban_')) { + qweb_context[p] = _.bind(this[p], this); + } + } + this.content = this.qweb.render('kanban-box', qweb_context); + }, + + add_widgets: function () { + var self = this; + this.$("field").each(function() { + var $field = $(this); + var field = self.record[$field.attr("name")]; + var type = $field.attr("widget") || field.type; + var Widget = fields_registry.get(type); + var widget = new Widget(self, field, $field); + widget.replace($field); + self.sub_widgets.push(widget); + }); + }, + + start: function() { + this.add_widgets(); + }, + + renderElement: function () { + this._super(); + this.setup_color_picker(); + this.$el.addClass('o_kanban_record'); + this.$el.data('record', this); + if (this.$el.hasClass('oe_kanban_global_click') || this.$el.hasClass('oe_kanban_global_click_edit')) { + this.$el.on('click', this.proxy('on_global_click')); + } + }, + transform_record: function(record) { + var self = this; + var new_record = {}; + _.each(record, function(value, name) { + var r = _.clone(self.fields[name] || {}); + if ((r.type === 'date' || r.type === 'datetime') && value) { + r.raw_value = time.auto_str_to_date(value); + } else { + r.raw_value = value; + } + r.value = formats.format_value(value, r); + new_record[name] = r; + }); + return new_record; + }, + + update: function (record) { + _.invoke(this.sub_widgets, 'destroy'); + this.sub_widgets = []; + this.init_content(record); + this.renderElement(); + this.add_widgets(); + }, + + kanban_image: function(model, field, id, cache, options) { + options = options || {}; + var url; + if (this.record[field] && this.record[field].value && !utils.is_bin_size(this.record[field].value)) { + url = 'data:image/png;base64,' + this.record[field].value; + } else if (this.record[field] && ! this.record[field].value) { + url = "/web/static/src/img/placeholder.png"; + } else { + id = JSON.stringify(id); + if (options.preview_image) + field = options.preview_image; + url = session.url('/web/binary/image', {model: model, field: field, id: id}); + if (cache !== undefined) { + // Set the cache duration in seconds. + url += '&cache=' + parseInt(cache, 10); + } + } + return url; + }, + + kanban_getcolor: function(variable) { + if (typeof(variable) === 'number') { + return Math.round(variable) % 10; + } + if (typeof(variable) === 'string') { + var index = 0; + for (var i=0; i<variable.length; i++) { + index += variable.charCodeAt(i); + } + return index % 10; + } + return 0; + }, + + kanban_color: function(variable) { + var color = this.kanban_getcolor(variable); + return 'oe_kanban_color_' + color; + }, + + on_global_click: function (ev) { + if ($(ev.target).parents('.o_dropdown_kanban').length) { + return; + } + if (!ev.isTrigger) { + var trigger = true; + var elem = ev.target; + var ischild = true; + var children = []; + while (elem) { + var events = $._data(elem, 'events'); + if (elem == ev.currentTarget) { + ischild = false; + } + if (ischild) { + children.push(elem); + if (events && events.click) { + // do not trigger global click if one child has a click event registered + trigger = false; + } + } + if (trigger && events && events.click) { + _.each(events.click, function(click_event) { + if (click_event.selector) { + // For each parent of original target, check if a + // delegated click is bound to any previously found children + _.each(children, function(child) { + if ($(child).is(click_event.selector)) { + trigger = false; + } + }); + } + }); + } + elem = elem.parentElement; + } + if (trigger) { + this.on_card_clicked(ev); + } + } + }, + + /* actions when user click on the block with a specific class + * open on normal view : oe_kanban_global_click + * open on form/edit view : oe_kanban_global_click_edit + */ + on_card_clicked: function() { + if (this.$el.hasClass('oe_kanban_global_click_edit') && this.$el.data('routing')) { + framework.redirect(this.$el.data('routing') + "/" + this.id); + } else if (this.$el.hasClass('oe_kanban_global_click_edit')) { + this.trigger_up('kanban_record_edit', {id: this.id}); + } else { + this.trigger_up('kanban_record_open', {id: this.id}); + } + }, + + toggle_manage_pane: function(event){ + event.preventDefault(); + this.$('.o_kanban_card_content').toggleClass('o_visible o_invisible'); + this.$('.o_kanban_card_manage_pane').toggleClass('o_visible o_invisible'); + this.$('.o_kanban_manage_button_section').toggleClass(this.kanban_color((this.values['color'] && this.values['color'].value) || 0)); + }, + + on_kanban_action_clicked: function (ev) { + ev.preventDefault(); + + var $action = $(ev.currentTarget); + var type = $action.data('type') || 'button'; + + switch (type) { + case 'edit': + this.trigger_up('kanban_record_edit', {id: this.id}); + break; + case 'open': + this.trigger_up('kanban_record_open', {id: this.id}); + break; + case 'delete': + this.trigger_up('kanban_record_delete', {record: this}); + break; + case 'action': + case 'object': + this.trigger_up('kanban_do_action', $action.data()); + break; + default: + this.do_warn("Kanban: no action for type : " + type); + } + }, + + kanban_compute_domain: function(domain) { + return data.compute_domain(domain, this.values); + }, + + update_record: function (event) { + this.trigger_up('kanban_record_update', event.data); + }, + + setup_color_picker: function() { + var self = this; + var $colorpicker = this.$('ul.oe_kanban_colorpicker'); + if (!$colorpicker.length) { + return; + } + $colorpicker.html(QWeb.render('KanbanColorPicker', { + widget: this + })); + $colorpicker.on('click', 'a', function(ev) { + ev.preventDefault(); + var color_field = $colorpicker.data('field') || 'color'; + var data = {}; + data[color_field] = $(this).data('color'); + self.trigger_up('kanban_record_update', data); + }); + }, + +}); + +return KanbanRecord; + +}); diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index a488267752a..5d60fc1aeb8 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -4,24 +4,49 @@ odoo.define('web_kanban.KanbanView', function (require) { var core = require('web.core'); var data = require('web.data'); var Model = require('web.Model'); +var Dialog = require('web.Dialog'); +var form_common = require('web.form_common'); +var Pager = require('web.Pager'); var pyeval = require('web.pyeval'); var session = require('web.session'); var utils = require('web.utils'); var View = require('web.View'); -var kanban_common = require('web_kanban.common'); + +var KanbanColumn = require('web_kanban.Column'); +var quick_create = require('web_kanban.quick_create'); +var KanbanRecord = require('web_kanban.Record'); +var kanban_widgets = require('web_kanban.widgets'); var QWeb = core.qweb; -var _t = core._t; var _lt = core._lt; -var fields_registry = kanban_common.registry; -var KanbanGroup = kanban_common.KanbanGroup; +var _t = core._t; +var ColumnQuickCreate = quick_create.ColumnQuickCreate; +var fields_registry = kanban_widgets.registry; var KanbanView = View.extend({ - template: "KanbanView", - display_name: _lt('Kanban'), - default_nr_columns: 1, + display_name: _lt("Kanban"), view_type: "kanban", - number_of_color_schemes: 10, + className: "o_kanban_view", + mobile_friendly: true, + + custom_events: { + 'kanban_record_open': 'open_record', + 'kanban_record_edit': 'edit_record', + 'kanban_record_delete': 'delete_record', + 'kanban_do_action': 'open_action', + 'kanban_reload': 'do_reload', + 'kanban_column_resequence': function (event) { + this.resequence_column(event.target); + }, + 'kanban_record_update': 'update_record', + 'kanban_column_add_record': 'add_record_to_column', + 'kanban_column_delete': 'delete_column', + 'column_add_record': 'column_add_record', + 'quick_create_add_column': 'add_new_column', + 'kanban_load_more': 'load_more', + 'kanban_call_method': 'call_method', + }, + init: function (parent, dataset, view_id, options) { this._super(parent, dataset, view_id, options); _.defaults(this.options, { @@ -31,111 +56,42 @@ var KanbanView = View.extend({ "read_only_mode": false, "confirm_on_delete": true, }); - this.fields_view = {}; - this.fields_keys = []; - this.group_by = null; - this.group_by_field = {}; - this.grouped_by_m2o = false; - this.many2manys = []; - this.m2m_context = {}; - this.state = { - groups : {}, - records : {} - }; - this.groups = []; + + // qweb setup this.qweb = new QWeb2.Engine(); this.qweb.debug = session.debug; this.qweb.default_dict = _.clone(QWeb.default_dict); - this.has_been_loaded = $.Deferred(); - this.search_domain = this.search_context = this.search_group_by = null; - this.currently_dragging = {}; + + this.model = this.dataset.model; this.limit = options.limit || 40; - this.add_group_mutex = new utils.Mutex(); - }, - view_loading: function(r) { - return this.load_kanban(r); - }, - start: function() { - var self = this; - this._super.apply(this, arguments); - this.$el.on('click', '.oe_kanban_dummy_cell', function() { - if (self.$buttons) { - self.$buttons.find('.oe_kanban_add_column').openerpBounce(); - } - }); - }, - destroy: function() { - this._super.apply(this, arguments); - $('html').off('click.kanban'); - }, - get_quick_create_class: function () { - return kanban_common.QuickCreate; + this.grouped = undefined; + this.group_by_field = undefined; + this.default_group_by = undefined; + this.grouped_by_m2o = undefined; + this.relation = undefined; + this.is_empty = undefined; + this.many2manys = []; + this.m2m_context = {}; + this.widgets = []; + this.data = undefined; + this.model = dataset.model; + this.quick_creatable = options.quick_creatable; + this.no_content_msg = options.action && (options.action.get_empty_list_help || options.action.help); + }, - load_kanban: function(data) { - this.fields_view = data; - // use default order if defined in xml description - var default_order = this.fields_view.arch.attrs.default_order, - unsorted = !this.dataset._sort.length; - if (unsorted && default_order) { - this.dataset.set_sort(default_order.split(',')); - } - this.$el.addClass(this.fields_view.arch.attrs['class']); - this.$groups = this.$el.find('.oe_kanban_groups tr'); + view_loading: function(fvg) { + this.$el.addClass(fvg.arch.attrs.class); + this.fields_view = fvg; + this.default_group_by = fvg.arch.attrs.default_group_by; + this.fields_keys = _.keys(this.fields_view.fields); - this.add_qweb_template(); - this.has_been_loaded.resolve(); - this.trigger('kanban_view_loaded', data); - return $.when(); - }, - /** - * Render the buttons according to the KanbanView.buttons template and - * add listeners on it. - * Set this.$buttons with the produced jQuery element - * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted - * $node may be undefined, in which case the ListView inserts them into this.options.$buttons - * or into a div of its template - */ - render_buttons: function($node) { - var display = false; - if (this.options.action_buttons !== false) { - display = this.is_action_enabled('create'); - } else if (!this.view_id && !this.options.read_only_mode) { - display = this.is_action_enabled('write') || this.is_action_enabled('create'); - } - this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this, display: display})); - this.$buttons - .on('click', 'button.oe_kanban_button_new', this.do_add_record) - .on('click', '.oe_kanban_add_column', this.do_add_group); - // Important: This should be done after do_search is finished so that - // this.grouped_by_m2o is set - this.$buttons.find('.oe_alternative').toggle(this.grouped_by_m2o); - $node = $node || this.options.$buttons; - if ($node) { - this.$buttons.appendTo($node); - } else { - this.$('.oe_kanban_buttons').replaceWith(this.$buttons); - } - }, - _is_quick_create_enabled: function() { - if (!this.options.quick_creatable || !this.is_action_enabled('create')) - return false; - if (this.fields_view.arch.attrs.quick_create !== undefined) - return JSON.parse(this.fields_view.arch.attrs.quick_create); - return !! this.group_by; - }, - is_action_enabled: function(action) { - if (action === 'create' && !this.options.creatable) - return false; - return this._super(action); - }, - /* add_qweb_template */ - add_qweb_template: function() { + // add qweb templates for (var i=0, ii=this.fields_view.arch.children.length; i < ii; i++) { var child = this.fields_view.arch.children[i]; if (child.tag === "templates") { - this.transform_qweb_template(child); + transform_qweb_template(child, fvg, this.many2manys); this.qweb.add_template(utils.json_node_to_xml(child)); break; } else if (child.tag === 'field') { @@ -146,164 +102,130 @@ var KanbanView = View.extend({ } } }, - transform_qweb_template: function(node) { - var qweb_add_if = function(node, condition) { - if (node.attrs[QWeb.prefix + '-if']) { - condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition); - } - node.attrs[QWeb.prefix + '-if'] = condition; - }; - // Process modifiers - if (node.tag && node.attrs.modifiers) { - var modifiers = JSON.parse(node.attrs.modifiers || '{}'); - if (modifiers.invisible) { - qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible))); - } - } - switch (node.tag) { - case 'field': - var ftype = this.fields_view.fields[node.attrs.name].type; - ftype = node.attrs.widget ? node.attrs.widget : ftype; - if (ftype === 'many2many') { - if (_.indexOf(this.many2manys, node.attrs.name) < 0) { - this.many2manys.push(node.attrs.name); - } - node.tag = 'div'; - node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags'; - } else if (fields_registry.contains(ftype)) { - // do nothing, the kanban record will handle it - } else { - node.tag = QWeb.prefix; - node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value'; - } - break; - case 'button': - case 'a': - var type = node.attrs.type || ''; - if (_.indexOf('action,object,edit,open,delete,url'.split(','), type) !== -1) { - _.each(node.attrs, function(v, k) { - if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) { - node.attrs['data-' + k] = v; - delete(node.attrs[k]); - } - }); - if (node.attrs['data-string']) { - node.attrs.title = node.attrs['data-string']; - } - if (node.attrs['data-icon']) { - node.children = [{ - tag: 'img', - attrs: { - src: session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png', - width: '16', - height: '16' - } - }]; - } - if (node.tag == 'a' && node.attrs['data-type'] != "url") { - node.attrs.href = '#'; - } else { - node.attrs.type = 'button'; - } - node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag; - } - break; - } - if (node.children) { - for (var i = 0, ii = node.children.length; i < ii; i++) { - this.transform_qweb_template(node.children[i]); - } - } + + do_search: function(domain, context, group_by) { + this.search_domain = domain; + this.search_context = context; + this.group_by_field = group_by[0] || this.default_group_by; + this.grouped = group_by.length || this.default_group_by; + + var field = this.fields_view.fields[this.group_by_field]; + this.grouped_by_m2o = field && (field.type === 'many2one'); + this.relation = this.grouped_by_m2o ? field.relation : undefined; + + return this.load_data() + .then(this.proxy('render')) + .then(this.proxy('update_pager')); }, - do_add_record: function() { - this.dataset.index = null; - this.do_switch_view('form'); + + do_show: function() { + this.do_push_state({}); + return this._super(); }, - do_add_group: function() { + + do_reload: function() { + this.do_search(this.search_domain, this.search_context, [this.group_by_field]); }, - do_search: function(domain, context, group_by) { + + load_data: function () { var self = this; - this.search_domain = domain; - this.search_context = context; - this.search_group_by = group_by; - return $.when(this.has_been_loaded).then(function() { - self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by; - self.group_by_field = self.fields_view.fields[self.group_by] || {}; - self.grouped_by_m2o = (self.group_by_field.type === 'many2one'); - self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o); - var grouping_fields = self.group_by ? [self.group_by] : undefined; - if (!_.isEmpty(grouping_fields)) { - // ensure group_by fields are read. - self.fields_keys = _.unique(self.fields_keys.concat(grouping_fields)); - } - var grouping = new Model(self.dataset.model, context, domain).query(self.fields_keys).group_by(grouping_fields); - return self.alive($.when(grouping)).then(function(groups) { - self.remove_no_result(); - if (groups) { - return self.do_process_groups(groups); - } else { - return self.do_process_dataset(); - } - }); + var deferred = this.grouped ? this.load_groups() : this.load_records(); + return deferred.then(function (data) { + self.data = data; }); }, - do_process_groups: function(groups) { - var self = this; - // Check in the arch the fields to fetch on the stage to get tooltips data. - // Fetching data is done in batch for all stages, to avoid doing multiple - // calls. The first naive implementation of group_by_tooltip made a call - // for each displayed stage and was quite limited. - // Data for the group tooltip (group_by_tooltip) and to display stage-related - // legends for kanban state management (states_legend) are fetched in - // one call. - var group_by_fields_to_read = []; - var recurse = function(node) { - if (node.tag === "field" && node.attrs && node.attrs.options) { - var options = pyeval.py_eval(node.attrs.options); - var states_fields_to_read = _.map( - options && options.states_legend || {}, - function (value, key, list) { return value; }); - var tooltip_fields_to_read = _.map( - options && options.group_by_tooltip || {}, - function (value, key, list) { return key; }); - group_by_fields_to_read = _.union( - group_by_fields_to_read, - states_fields_to_read, - tooltip_fields_to_read); - } - _.each(node.children, function(child) { - recurse(child); - }); + load_records: function (offset, dataset) { + var options = { + 'limit': this.limit, + 'offset': offset, }; - recurse(this.fields_view.arch); - var group_ids = _.without(_.map(groups, function (elem) { return elem.attributes.value[0];}), undefined); - if (this.grouped_by_m2o && group_ids.length && group_by_fields_to_read.length) { - var group_data = new data.DataSet( - this, - this.group_by_field.relation).read_ids(group_ids, _.union(['display_name'], group_by_fields_to_read)); - } - else { var group_data = $.Deferred().resolve({}); } - - this.$el.find('table:first').show(); - this.$el.removeClass('oe_kanban_ungrouped').addClass('oe_kanban_grouped'); - return $.when(group_data).then(function (results) { - _.each(results, function (group_by_data) { - var group = _.find(groups, function (elem) {return elem.attributes.value[0] == group_by_data.id;}); - if (group) { - group.values = group_by_data; - } + dataset = dataset || this.dataset; + return dataset + .read_slice(this.fields_keys.concat(['__last_update']), options) + .then(function(records) { + return { + records: records, + is_empty: !records.length, + grouped: false, + }; }); - }).done( function () {return self.add_group_mutex.exec(function() { - self.do_clear_groups(); - self.dataset.ids = []; - if (!groups.length) { - self.no_result(); - return $.when(); + }, + + load_groups: function () { + var self = this; + var group_by_field = this.group_by_field || this.default_group_by; + this.fields_keys = _.uniq(this.fields_keys.concat(group_by_field)); + + return new Model(this.model, this.search_context, this.search_domain) + .query(this.fields_keys) + .group_by([group_by_field]) + .then(function (groups) { + + // Check in the arch the fields to fetch on the stage to get tooltips data. + // Fetching data is done in batch for all stages, to avoid doing multiple + // calls. The first naive implementation of group_by_tooltip made a call + // for each displayed stage and was quite limited. + // Data for the group tooltip (group_by_tooltip) and to display stage-related + // legends for kanban state management (states_legend) are fetched in + // one call. + var group_by_fields_to_read = []; + var recurse = function(node) { + if (node.tag === "field" && node.attrs && node.attrs.options) { + var options = pyeval.py_eval(node.attrs.options); + var states_fields_to_read = _.map( + options && options.states_legend || {}, + function (value, key, list) { return value; }); + var tooltip_fields_to_read = _.map( + options && options.group_by_tooltip || {}, + function (value, key, list) { return key; }); + group_by_fields_to_read = _.union( + group_by_fields_to_read, + states_fields_to_read, + tooltip_fields_to_read); + } + _.each(node.children, function(child) { + recurse(child); + }); + }; + recurse(self.fields_view.arch); + + // fetch group data (display information) + var group_ids = _.without(_.map(groups, function (elem) { return elem.attributes.value[0];}), undefined); + if (self.grouped_by_m2o && group_ids.length) { + return new data.DataSet(self, self.relation) + .read_ids(group_ids, _.union(['display_name'], group_by_fields_to_read)) + .then(function(results) { + _.each(groups, function (group) { + var group_id = group.attributes.value[0]; + var result = _.find(results, function (data) {return group_id === data.id;}); + group.title = result ? result.display_name : _t("Undefined"); + group.values = result; + group.id = group_id; + }); + return groups; + }); + } else { + _.each(groups, function (group) { + var value = group.attributes.value; + group.title = (value instanceof Array ? value[1] : value) || _t("Undefined"); + }); + return $.when(groups); } - self.nb_records = 0; - var groups_array = []; - return $.when.apply(null, _.map(groups, function (group, index) { + }) + .then(function (groups) { + var undef_index = _.findIndex(groups, function (g) { return g.title === _t("Undefined");}); + if (undef_index >= 1) { + var undef_group = groups[undef_index]; + groups.splice(undef_index, 1); + groups.unshift(undef_group); + } + return groups; + }) + .then(function (groups) { + // load records for each group + var is_empty = true; + return $.when.apply(null, _.map(groups, function (group) { var def = $.when([]); var dataset = new data.DataSetSearch(self, self.dataset.model, new data.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain()); @@ -313,275 +235,273 @@ var KanbanView = View.extend({ if (group.attributes.length >= 1) { def = dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }); } - return def.then(function(records) { - self.nb_records += records.length; - self.dataset.ids.push.apply(self.dataset.ids, dataset.ids); - groups_array[index] = new KanbanGroup(self, records, group, dataset); + return def.then(function (records) { + self.dataset.ids.push.apply(self.dataset.ids, dataset.ids); + group.records = records; + group.dataset = dataset; + is_empty = is_empty && !records.length; + return group; }); })).then(function () { - if(!self.nb_records) { - self.no_result(); - } - if (self.dataset.index >= self.nb_records){ - self.dataset.index = self.dataset.size() ? 0 : null; - } - return self.do_add_groups(groups_array).done(function() { - self.trigger('kanban_groups_processed'); - }); - }); - });}); - }, - do_process_dataset: function() { - var self = this; - this.$el.find('table:first').show(); - this.$el.removeClass('oe_kanban_grouped').addClass('oe_kanban_ungrouped'); - var def = $.Deferred(); - this.add_group_mutex.exec(function() { - self.do_clear_groups(); - self.dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit }).done(function(records) { - var kgroup = new KanbanGroup(self, records, null, self.dataset); - if (!_.isEmpty(self.dataset.ids) && (self.dataset.index === null || self.dataset.index >= self.dataset.ids.length)) { - self.dataset.index = 0; - } else if (_.isEmpty(self.dataset.ids)){ - self.dataset.index = null; - } - self.do_add_groups([kgroup]).done(function() { - if (_.isEmpty(records)) { - self.no_result(); - } - self.trigger('kanban_dataset_processed'); - def.resolve(); - }); - }).done(null, function() { - def.reject(); + return { + groups: Array.prototype.slice.call(arguments, 0), + is_empty: is_empty, + grouped: true, + }; }); return def; }); - return def; }, - do_reload: function() { - this.do_search(this.search_domain, this.search_context, this.search_group_by); + + is_action_enabled: function(action) { + if (action === 'create' && !this.options.creatable) { + return false; + } + return this._super(action); }, - do_clear_groups: function() { - var groups = this.groups.slice(0); - this.groups = []; - _.each(groups, function(group) { - group.destroy(); - }); + _is_quick_create_enabled: function() { + if (!this.quick_creatable || !this.is_action_enabled('create')) + return false; + if (this.fields_view.arch.attrs.quick_create !== undefined) + return JSON.parse(this.fields_view.arch.attrs.quick_create); + return !!this.grouped; }, - do_add_groups: function(groups) { + /** + * Render the buttons according to the KanbanView.buttons template and + * add listeners on it. + * Set this.$buttons with the produced jQuery element + * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted + * $node may be undefined, in which case the ListView inserts them into this.options.$buttons + */ + render_buttons: function($node) { + var display = false; + if (this.options.action_buttons !== false) { + display = this.is_action_enabled('create'); + } else if (!this.view_id && !this.options.read_only_mode) { + display = this.is_action_enabled('write') || this.is_action_enabled('create'); + } + this.$buttons = $(QWeb.render("KanbanView.buttons", {'widget': this, display: display})); + this.$buttons.on('click', 'button.o-kanban-button-new', this.add_record.bind(this)); + + $node = $node || this.options.$buttons; + this.$buttons.appendTo($node); + }, + + render_pager: function($node, options) { var self = this; - var $parent = this.$el.parent(); - this.$el.detach(); - _.each(groups, function(group) { - self.groups[group.undefined_title ? 'unshift' : 'push'](group); - }); - var $last_td = self.$el.find('.oe_kanban_groups_headers td:last'); - var groups_started = _.map(this.groups, function(group) { - if (!group.is_started) { - group.on("add_record", self, function () { - self.remove_no_result(); - }); - return group.insertBefore($last_td); - } - }); - return $.when.apply(null, groups_started).done(function () { - self.on_groups_started(); - self.$el.appendTo($parent); - _.each(self.groups, function(group) { - group.compute_cards_auto_height(); - }); + this.pager = new Pager(this, this.dataset.size(), 1, this.limit, options); + this.pager.appendTo($node); + this.pager.on('pager_changed', this, function (state) { + self.load_records(state.current_min - 1) + .then(function (data) { + self.data = data; + }) + .done(this.proxy('render')); }); + this.update_pager(); }, - on_groups_started: function() { - var self = this; - if (this.group_by || this.fields_keys.indexOf("sequence") !== -1) { - // Kanban cards drag'n'drop - var prev_widget, is_folded, record, $columns; - if (this.group_by) { - $columns = this.$el.find('.oe_kanban_column .oe_kanban_column_cards, .oe_kanban_column .oe_kanban_folded_column_cards'); + + update_pager: function() { + if (this.pager) { + if (this.grouped) { + this.pager.do_hide(); } else { - $columns = this.$el.find('.oe_kanban_column_cards'); - } - $columns.sortable({ - handle : '.oe_kanban_draghandle', - start: function(event, ui) { - self.currently_dragging.index = ui.item.parent().children('.oe_kanban_record').index(ui.item); - self.currently_dragging.group = prev_widget = ui.item.parents('.oe_kanban_column:first').data('widget'); - ui.item.find('*').on('click.prevent', function(ev) { - return false; - }); - record = ui.item.data('widget'); - record.$el.bind('mouseup',function(ev,ui){ - if (is_folded) { - record.$el.hide(); - } - record.$el.unbind('mouseup'); - }); - ui.placeholder.height(ui.item.height()); - }, - over: function(event, ui) { - var parent = $(event.target).parent(); - prev_widget.highlight(false); - is_folded = parent.hasClass('oe_kanban_group_folded'); - if (is_folded) { - var widget = parent.data('widget'); - widget.highlight(true); - prev_widget = widget; - } - }, - revert: 150, - stop: function(event, ui) { - prev_widget.highlight(false); - var old_index = self.currently_dragging.index; - var new_index = ui.item.parent().children('.oe_kanban_record').index(ui.item); - var old_group = self.currently_dragging.group; - var new_group = ui.item.parents('.oe_kanban_column:first').data('widget'); - if (!(old_group.title === new_group.title && old_group.value === new_group.value && old_index == new_index)) { - self.on_record_moved(record, old_group, old_index, new_group, new_index); - } - setTimeout(function() { - // A bit hacky but could not find a better solution for Firefox (problem not present in chrome) - // http://stackoverflow.com/questions/274843/preventing-javascript-click-event-with-scriptaculous-drag-and-drop - ui.item.find('*').off('click.prevent'); - }, 0); - }, - scroll: false - }); - // Keep connectWith out of the sortable initialization for performance sake: - // http://www.planbox.com/blog/development/coding/jquery-ui-sortable-slow-to-bind.html - $columns.sortable({ connectWith: $columns }); - - // Kanban groups drag'n'drop - var start_index; - if (this.grouped_by_m2o) { - this.$('.oe_kanban_groups_headers').sortable({ - items: '.oe_kanban_group_header', - helper: 'clone', - axis: 'x', - opacity: 0.5, - scroll: false, - start: function(event, ui) { - start_index = ui.item.index(); - self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'hidden' }); - }, - stop: function(event, ui) { - var stop_index = ui.item.index(); - if (start_index !== stop_index) { - var $start_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(start_index); - var $stop_column = self.$('.oe_kanban_groups_records .oe_kanban_column').eq(stop_index); - var method = (start_index > stop_index) ? 'insertBefore' : 'insertAfter'; - $start_column[method]($stop_column); - var tmp_group = self.groups.splice(start_index, 1)[0]; - self.groups.splice(stop_index, 0, tmp_group); - var new_sequence = _.pluck(self.groups, 'value'); - (new data.DataSet(self, self.group_by_field.relation)).resequence(new_sequence).done(function(r) { - if (r === false) { - console.error("Kanban: could not resequence model '%s'. Probably no 'sequence' field.", self.group_by_field.relation); - } - }); - } - self.$('.oe_kanban_record, .oe_kanban_quick_create').css({ visibility: 'visible' }); - } - }); + this.pager.set_state({size: this.dataset.size(), current_min: 1}); } + } + }, + + render: function () { + // cleanup + this.$el.css({display:'-webkit-flex'}); + this.$el.css({display:'flex'}); + this.$el.removeClass('o_kanban_ungrouped o_kanban_grouped'); + _.invoke(this.widgets, 'destroy'); + this.$el.empty(); + this.widgets = []; + if (this.column_quick_create) { + this.column_quick_create.destroy(); + this.column_quick_create = undefined; + } + + this.record_options = { + editable: this.is_action_enabled('edit'), + deletable: this.is_action_enabled('delete'), + fields: this.fields_view.fields, + qweb: this.qweb, + model: this.model, + read_only_mode: this.options.read_only_mode, + }; + + // actual rendering + var fragment = document.createDocumentFragment(); + if (this.data.grouped) { + this.$el.addClass('o_kanban_grouped'); + this.render_grouped(fragment); + } else if (this.data.is_empty) { + this.render_no_content(fragment); } else { - this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle'); + this.$el.addClass('o_kanban_ungrouped'); + this.render_ungrouped(fragment); + } + this.$el.append(fragment); + }, + + render_no_content: function (fragment) { + var content = QWeb.render('KanbanView.nocontent', {content: this.no_content_msg}); + $(content).appendTo(fragment); + }, + + render_ungrouped: function (fragment) { + var self = this; + var options = _.clone(this.record_options); + _.each(this.data.records, function (record) { + var kanban_record = new KanbanRecord(self, record, options); + self.widgets.push(kanban_record); + kanban_record.appendTo(fragment); + }); + + // add empty invisible divs to make sure that all kanban records are left aligned + for (var i = 0, ghost_div; i < 6; i++) { + ghost_div = $("<div>").addClass("o_kanban_record o_kanban_ghost"); + ghost_div.appendTo(fragment); } this.postprocess_m2m_tags(); }, - on_record_moved : function(record, old_group, old_index, new_group, new_index) { + + get_column_options: function () { + return { + editable: this.is_action_enabled('group_edit'), + deletable: this.is_action_enabled('group_delete'), + grouped_by_m2o: this.grouped_by_m2o, + relation: this.relation, + qweb: this.qweb, + fields: this.fields_view.fields, + quick_create: this._is_quick_create_enabled(), + }; + }, + + render_grouped: function (fragment) { var self = this; - record.$el.find('[title]').tooltip('destroy'); - $(old_group.$el).add(new_group.$el).find('.oe_kanban_group_length').hide(); - if (old_group === new_group) { - new_group.records.splice(old_index, 1); - new_group.records.splice(new_index, 0, record); - new_group.do_save_sequences(); - } else { - old_group.records.splice(old_index, 1); - new_group.records.splice(new_index, 0, record); - record.group = new_group; - var data = {}; - data[this.group_by] = new_group.value; - this.dataset.write(record.id, data, {}).done(function() { - record.do_reload(); - new_group.do_save_sequences(); - if (new_group.state.folded) { - new_group.do_action_toggle_fold(); - record.prependTo(new_group.$records.find('.oe_kanban_column_cards')); - } - }).fail(function(error, evt) { - evt.preventDefault(); - alert(_t("An error has occured while moving the record to this group: ") + error.data.message); - self.do_reload(); // TODO: use draggable + sortable in order to cancel the dragging when the rcp fails - }); + var record_options = _.extend(this.record_options, { + draggable: true, + }); + + var column_options = this.get_column_options(); + + _.each(this.data.groups, function (group) { + var column = new KanbanColumn(self, group, column_options, record_options); + column.appendTo(fragment); + self.widgets.push(column); + }); + this.$el.sortable({ + axis: 'x', + items: '> .o_kanban_group', + cursor: 'move', + revert: 150, + delay: 100, + tolerance: 'pointer', + forcePlaceholderSize: true, + stop: function () { + var ids = []; + self.$('.o_kanban_group').each(function (index, u) { + ids.push($(u).data('id')); + }); + self.resequence(ids); + }, + }); + if (this.is_action_enabled('group_create')) { + this.column_quick_create = new ColumnQuickCreate(this); + this.column_quick_create.appendTo(fragment); } + this.postprocess_m2m_tags(); }, - do_show: function() { - this.do_push_state({}); - return this._super(); + add_record: function() { + this.dataset.index = null; + this.do_switch_view('form'); }, - open_record: function(id, editable) { - if (this.dataset.select_id(id)) { - this.do_switch_view('form', null, { mode: editable ? "edit" : undefined }); + + open_record: function (event, options) { + if (this.dataset.select_id(event.data.id)) { + this.do_switch_view('form', null, options); //, null, { mode: "edit" }); } else { - this.do_warn("Kanban: could not find id#" + id); + this.do_warn("Kanban: could not find id#" + event.data.id); } }, - no_result: function() { + + edit_record: function (event) { + this.open_record(event, {mode: 'edit'}); + }, + + delete_record: function (event) { var self = this; - if (this.groups.group_by - || !this.options.action - || (!this.options.action.help && !this.options.action.get_empty_list_help)) { - return; + var record = event.data.record; + function do_it() { + return $.when(self.dataset.unlink([record.id])).done(function() { + record.destroy(); + if (event.data.after) { + event.data.after(); + } + }); + } + if (this.options.confirm_on_delete) { + Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), do_it); + } else { + do_it(); } - this.$el.css("position", "relative"); - $(QWeb.render('KanbanView.nocontent', { content : this.options.action.get_empty_list_help || this.options.action.help})).insertBefore(this.$('table:first')); - this.$el.find('.oe_view_nocontent').click(function() { - self.$buttons.openerpBounce(); - }); }, - remove_no_result: function() { - this.$el.css("position", ""); - this.$el.find('.oe_view_nocontent').remove(); + + open_action: function (event) { + var self = this; + var node_context = event.data.context || {}; + var context = new data.CompoundContext(node_context); + context.set_eval_context({ + active_id: event.target.id, + active_ids: [event.target.id], + active_model: this.dataset.model, + }); + this.do_execute_action(event.data, this.dataset, event.target.id).then(function () { + self.reload_record(event.target); + }); }, /* * postprocessing of fields type many2many * make the rpc request for all ids/model and insert value inside .oe_tags fields */ - postprocess_m2m_tags: function() { + postprocess_m2m_tags: function(records) { var self = this; if (!this.many2manys.length) { return; } var relations = {}; - this.groups.forEach(function(group) { - group.records.forEach(function(record) { - self.many2manys.forEach(function(name) { - var field = record.record[name]; - var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']').empty(); - // fields declared in the kanban view may not be used directly - // in the template declaration, for example fields for which the - // raw value is used -> $el[0] is undefined, leading to errors - // in the following process. Preventing to add push the id here - // prevents to make unnecessary calls to name_get - if (! $el[0]) { - return; - } - if (!relations[field.relation]) { - relations[field.relation] = { ids: [], elements: {}, context: self.m2m_context[name]}; + records = records ? (records instanceof Array ? records : [records]) : + this.grouped ? Array.prototype.concat.apply([], _.pluck(this.widgets, 'records')) : + this.widgets; + + records.forEach(function(record) { + self.many2manys.forEach(function(name) { + var field = record.record[name]; + var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']'); + // fields declared in the kanban view may not be used directly + // in the template declaration, for example fields for which the + // raw value is used -> $el[0] is undefined, leading to errors + // in the following process. Preventing to add push the id here + // prevents to make unnecessary calls to name_get + if (! $el[0]) { + return; + } + if (!relations[field.relation]) { + relations[field.relation] = { ids: [], elements: {}, context: self.m2m_context[name]}; + } + var rel = relations[field.relation]; + field.raw_value.forEach(function(id) { + rel.ids.push(id); + if (!rel.elements[id]) { + rel.elements[id] = []; } - var rel = relations[field.relation]; - field.raw_value.forEach(function(id) { - rel.ids.push(id); - if (!rel.elements[id]) { - rel.elements[id] = []; - } - rel.elements[id].push($el[0]); - }); + rel.elements[id].push($el[0]); }); }); }); @@ -589,68 +509,253 @@ var KanbanView = View.extend({ var dataset = new data.DataSetSearch(self, rel_name, self.dataset.get_context(rel.context)); dataset.name_get(_.uniq(rel.ids)).done(function(result) { result.forEach(function(nameget) { - $(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>'); + // white color (0) should not be selected, 1 instead + var color = KanbanRecord.prototype.kanban_getcolor(nameget[1]) + 1; + var $tag = $('<span>') + .addClass('o_tag oe_kanban_color_' + color) + .attr('title', _.str.escapeHTML(nameget[1])); + $(rel.elements[nameget[0]]).append($tag); }); + // we use boostrap tooltips for better and faster display + self.$('span.o_tag').tooltip(); }); }); - } -}); + }, + resequence: function (ids) { + if ((ids.length <= 1) || !this.relation) { + return; + } + new data.DataSet(this, this.relation).resequence(ids).done(function (r) { + if (!r) { + console.warn('Resequence could not be complete. ' + + 'Maybe the model does not have a "sequence" field?'); + } + }); + }, -core.view_registry.add('kanban', KanbanView); + resequence_column: function (col) { + if (_.indexOf(this.fields_keys, 'sequence') > -1) { + this.dataset.resequence(col.get_ids()); + } + }, -return KanbanView; + delete_column: function (event) { + var self = this; + var column = event.target; + (new data.DataSet(this, this.relation)).unlink([column.id]).done(function() { + if (column.is_empty()) { + var index = self.widgets.indexOf(column); + self.widgets.splice(index,1); + column.destroy(); + } else { + self.do_reload(); + } + }); + }, -}); + reload_record: function (record) { + var self = this; + this.dataset.read_ids([record.id], this.fields_keys.concat(['__last_update'])).done(function(records) { + if (records.length) { + record.update(records[0]); + self.postprocess_m2m_tags(record); + } else { + record.destroy(); + } + }); + }, + add_record_to_column: function (event) { + var self = this; + var column = event.target; + var record = event.data.record; + var data = {}; + data[this.group_by_field] = event.target.id; + this.dataset.write(record.id, data, {}).done(function () { + if (!self.isDestroyed()) { + self.reload_record(record); + self.resequence_column(column); + } + }); + }, -odoo.define(function (require) { + update_record: function(event) { + var self = this; + var record = event.target; + return this.dataset.write(record.id, event.data) + .done(function () { + if (!self.isDestroyed()) { + self.reload_record(record); + } + }); + }, -var core = require('web.core'); -var data = require('web.data'); -var web_client = require('web.web_client'); -var kanban_common = require('web_kanban.common'); -var KanbanView = require('web_kanban.KanbanView'); + column_add_record: function (event) { + var self = this; + var column = event.target; + var context = {}; + context['default_' + this.group_by_field] = column.value; + var name = event.data.value; + this.dataset.name_create(name, context).then(function on_success (data) { + add_record(data[0]); + }, function on_fail (event) { + event.preventDefault(); + var popup = new form_common.SelectCreatePopup(this); + popup.select_element( + self.dataset.model, + { + title: _t("Create: "), + initial_view: "form", + disable_multiple_selection: true + }, + [], + {"default_name": name} + ); + popup.on("elements_selected", self, function(element_ids) { + add_record(element_ids[0]); + }); + }); -var KanbanGroup = kanban_common.KanbanGroup; -var _t = core._t; + function add_record(id) { + self.dataset.add_ids([id], -1); + self.dataset.read_ids([id], self.fields_keys.concat(['__last_update'])).done(function(record) { + column.add_record(record[0], {position: 'before'}); + }); + } + }, + + add_new_column: function (event) { + var self = this; + var model = new Model(this.relation, this.search_context); + model.call('create', [{name: event.data.value}], { + context: this.search_context, + }).then(function (id) { + var dataset = new data.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), []); + var group_data = { + records: [], + title: event.data.value, + id: id, + attributes: {folded: false}, + dataset: dataset, + values: {}, + }; + var options = self.get_column_options(); + var record_options = _.clone(self.record_options); + var column = new KanbanColumn(self, group_data, options, record_options); + column.insertBefore(self.$('.o_column_quick_create')); + self.widgets.push(column); + }); + }, -KanbanView.include({ - do_add_group: function() { + load_more: function (event) { var self = this; - self.do_action({ - name: _t("Add column"), - res_model: self.group_by_field.relation, - views: [[false, 'form']], - type: 'ir.actions.act_window', - target: "new", - context: self.dataset.get_context(), - flags: { - action_buttons: true, + var column = event.target; + var offset = column.offset + this.limit; + return this.load_records(offset, column.dataset).then(function (result) { + _.each(result.records, function (r) { + column.add_record(r, {no_update: true}); + }); + column.offset += self.limit; + column.remaining = Math.max(column.remaining - self.limit, 0); + column.update_column(); + self.postprocess_m2m_tags(column.records); + }); + }, + + call_method: function (event) { + var data = event.data; + this.dataset.call(data.method, data.params).then(function() { + if (data.callback) { + data.callback(); } }); - var am = web_client.action_manager; - var form = am.dialog_widget.views.form.controller; - form.on("on_button_cancel", am.dialog, am.dialog.close); - form.on('record_created', self, function(r) { - (new data.DataSet(self, self.group_by_field.relation)).name_get([r]).done(function(new_record) { - am.dialog.close(); - var domain = self.dataset.domain.slice(0); - domain.push([self.group_by, '=', new_record[0][0]]); - var dataset = new data.DataSetSearch(self, self.dataset.model, self.dataset.get_context(), domain); - var datagroup = { - get: function(key) { - return this[key]; - }, - value: new_record[0], - length: 0, - }; - var new_group = new KanbanGroup(self, [], datagroup, dataset); - self.do_add_groups([new_group]).done(function() { - $(window).scrollTo(self.groups.slice(-1)[0].$el, { axis: 'x' }); + } +}); + +function qweb_add_if(node, condition) { + if (node.attrs[QWeb.prefix + '-if']) { + condition = _.str.sprintf("(%s) and (%s)", node.attrs[QWeb.prefix + '-if'], condition); + } + node.attrs[QWeb.prefix + '-if'] = condition; +} + +function transform_qweb_template (node, fvg, many2manys) { + // Process modifiers + if (node.tag && node.attrs.modifiers) { + var modifiers = JSON.parse(node.attrs.modifiers || '{}'); + if (modifiers.invisible) { + qweb_add_if(node, _.str.sprintf("!kanban_compute_domain(%s)", JSON.stringify(modifiers.invisible))); + } + } + switch (node.tag) { + case 'field': + var ftype = fvg.fields[node.attrs.name].type; + ftype = node.attrs.widget ? node.attrs.widget : ftype; + if (ftype === 'many2many') { + if (_.indexOf(many2manys, node.attrs.name) < 0) { + many2manys.push(node.attrs.name); + } + node.tag = 'div'; + node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags o_kanban_tags'; + } else if (fields_registry.contains(ftype)) { + // do nothing, the kanban record will handle it + } else { + node.tag = QWeb.prefix; + node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value'; + } + break; + case 'button': + case 'a': + var type = node.attrs.type || ''; + if (_.indexOf('action,object,edit,open,delete,url'.split(','), type) !== -1) { + _.each(node.attrs, function(v, k) { + if (_.indexOf('icon,type,name,args,string,context,states,kanban_states'.split(','), k) != -1) { + node.attrs['data-' + k] = v; + delete(node.attrs[k]); + } }); - }); + if (node.attrs['data-string']) { + node.attrs.title = node.attrs['data-string']; + } + if (node.attrs['data-icon']) { + node.children = [{ + tag: 'img', + attrs: { + src: session.prefix + '/web/static/src/img/icons/' + node.attrs['data-icon'] + '.png', + width: '16', + height: '16' + } + }]; + } + if (node.tag == 'a' && node.attrs['data-type'] != "url") { + node.attrs.href = '#'; + } else { + node.attrs.type = 'button'; + } + node.attrs['class'] = (node.attrs['class'] || '') + ' oe_kanban_action oe_kanban_action_' + node.tag; + } + break; + } + if (node.children) { + for (var i = 0, ii = node.children.length; i < ii; i++) { + transform_qweb_template(node.children[i], fvg, many2manys); + } + } +} + +var One2ManyKanbanView = KanbanView.extend({ + render_pager: function($node, options) { + options = _.extend(options || {}, { + single_page_hidden: true, }); + this._super($node, options); }, }); +core.view_registry.add('kanban', KanbanView); +core.one2many_view_registry.add('kanban', One2ManyKanbanView); + +return KanbanView; + }); + diff --git a/addons/web_kanban/static/src/js/kanban_widgets.js b/addons/web_kanban/static/src/js/kanban_widgets.js new file mode 100644 index 00000000000..29942075e79 --- /dev/null +++ b/addons/web_kanban/static/src/js/kanban_widgets.js @@ -0,0 +1,247 @@ +odoo.define('web_kanban.widgets', function (require) { +"use strict"; + +var core = require('web.core'); +var ProgressBar = require('web.ProgressBar'); +var pyeval = require('web.pyeval'); +var Registry = require('web.Registry'); +var session = require('web.session'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +/** + * Interface to be implemented by kanban fields. + * + */ +var FieldInterface = { + /** + Constructor. + - parent: The widget's parent. + - field: A dictionary giving details about the field, including the current field's value in the + raw_value field. + - $node: The field <field> tag as it appears in the view, encapsulated in a jQuery object. + */ + init: function(parent, field, $node) {}, +}; + +/** + * Abstract class for classes implementing FieldInterface. + * + * Properties: + * - value: useful property to hold the value of the field. By default, the constructor + * sets value property. + * + */ +var AbstractField = Widget.extend(FieldInterface, { + /** + Constructor that saves the field and $node parameters and sets the "value" property. + */ + init: function(parent, field, $node) { + this._super(parent); + this.field = field; + this.$node = $node; + this.options = pyeval.py_eval(this.$node.attr("options") || '{}'); + this.set("value", field.raw_value); + }, +}); + +var Priority = AbstractField.extend({ + init: function(parent, field, $node) { + this._super.apply(this, arguments); + this.name = $node.attr('name'); + }, + prepare_priority: function() { + var self = this; + var selection = this.field.selection || []; + var init_value = selection && selection[0][0] || 0; + var data = _.map(selection.slice(1), function(element, index) { + var value = { + 'value': element[0], + 'name': element[1], + 'click_value': element[0], + }; + if (index === 0 && self.get('value') == element[0]) { + value.click_value = init_value; + } + return value; + }); + return data; + }, + renderElement: function() { + this.priorities = this.prepare_priority(); + var readonly = this.field && this.field.readonly; + if (readonly){ + this.set('readonly', true); + } + this.$el = $(QWeb.render("Priority", {'widget': this})); + if (!readonly){ + this.$el.on('click', 'a', this.do_action.bind(this)); + } + }, + do_action: function(event) { + event.preventDefault(); + var $a = $(event.currentTarget); + var data = {}; + data[this.name] = String($a.data('value')); + this.trigger_up('kanban_update_record', data); + }, +}); + +var KanbanSelection = AbstractField.extend({ + + init: function(parent, field, $node) { + this._super.apply(this, arguments); + this.name = $node.attr('name'); + this.parent = parent; + }, + prepare_dropdown_selection: function() { + var self = this; + var data = []; + _.map(this.field.selection || [], function(res) { + var value = { + 'name': res[0], + 'tooltip': res[1], + 'state_name': res[1], + }; + var leg_opt = self.options && self.options.states_legend || null; + if (leg_opt) { + var key = leg_opt[value.name]; + var legend = self.parent.group_info[key]; + if (legend) { + value.state_name = legend; + value.tooltip = legend; + } + } + if (res[0] == 'normal') { value.state_class = 'oe_kanban_status'; } + else if (res[0] == 'done') { value.state_class = 'oe_kanban_status oe_kanban_status_green'; } + else { value.state_class = 'oe_kanban_status oe_kanban_status_red'; } + data.push(value); + }); + return data; + }, + renderElement: function() { + var self = this; + this.states = this.prepare_dropdown_selection(); + + var state; + for(var i = 0 ; i < this.states.length ; i++) { + if(this.states[i].name === this.get('value')) { + state = this.states[i]; + } + } + + this.$el = $(QWeb.render("KanbanSelection", {'widget': this})); + this.$('a').first().find('span').removeClass().addClass(state.state_class); + this.$('ul li').show().filter(function() { + return ($(this).data('value') === state.name); + }).hide(); + + this.$('a').click(function (ev) { + ev.preventDefault(); + }); + + this.$('li').click(this.set_kanban_selection.bind(this)); + }, + set_kanban_selection: function(e) { + e.preventDefault(); + var self = this; + var $li = $(e.target).closest( "li" ); + if ($li.length) { + var value = {}; + value[self.name] = String($li.data('value')); + this.trigger_up('kanban_update_record', value, this); + } + }, +}); + +var KanbanAttachmentImage = AbstractField.extend({ + template: 'KanbanAttachmentImage', +}); + + +/** + * Kanban widgets: ProgressBar + * options + * - editable: boolean if current_value is editable + * - current_value: get the current_value from the field that must be present in the view + * - max_value: get the max_value from the field that must be present in the view + * - title: title of the gauge, displayed on top of the gauge + * - on_change: action to call when cliking and setting a value + */ +var KanbanProgressBar = AbstractField.extend({ + events: { + 'click': function() { + if(!this.readonly && this.progressbar.readonly) { + this.toggle_progressbar(); + } + } + }, + + init: function (parent, field, node) { + this._super(parent, field, node); + + var record = this.getParent().record; + this.progressbar = new ProgressBar(this, { + readonly: true, + value: record[this.options.current_value].raw_value, + max_value: record[this.options.max_value].raw_value, + title: this.options.title, + edit_max_value: this.options.edit_max_value, + }); + + this.readonly = !this.options.editable; + this.on_change = this.options.on_change; + }, + + start: function () { + var self = this; + + var def = this.progressbar.appendTo('<div>').done(function() { + self.progressbar.$el.addClass(self.$el.attr('class')); + self.replaceElement(self.progressbar.$el); + }); + + return $.when(this._super(), def).then(function() { + if(!self.readonly) { + var parent = self.getParent(); + self.progressbar.on('change:value change:max_value', self, function(e) { + var value = this.progressbar.get(this.progressbar.edit_max_value ? 'max_value' : 'value') || 0; + if(!isNaN(value)) { + var data = { + method: this.on_change, + params: [parent.id, value], + callback: self.proxy('toggle_progressbar'), + }; + self.trigger_up('kanban_call_method', data); + + // parent.view.dataset.call(this.on_change, [parent.id, value]).then(function() { + // self.toggle_progressbar(); + // }); + } + }); + } + }); + }, + + toggle_progressbar: function() { + this.progressbar.readonly = !this.progressbar.readonly; + var $div = $('<div/>').insertAfter(this.$el); + this.progressbar.replace($div); + this.setElement(this.progressbar.$el); + }, +}); + +var fields_registry = new Registry(); + +fields_registry + .add('priority', Priority) + .add('kanban_state_selection', KanbanSelection) + .add("attachment_image", KanbanAttachmentImage) + .add('progress', KanbanProgressBar); + +return { + AbstractField: AbstractField, + registry: fields_registry, +}; + +}); |
