diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css
index 840597606dd..494fe5b10c6 100644
--- a/addons/web_kanban/static/src/css/kanban.css
+++ b/addons/web_kanban/static/src/css/kanban.css
@@ -577,11 +577,11 @@
background: #ffffff;
}
-.openerp .oe_popup_form .oe_kanban_buttons .oe_highlight {
+.openerp .o_popup_form .oe_kanban_buttons .oe_highlight {
color: #404040;
background: none;
}
-.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight {
+.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight {
background-color: #efefef;
background-image: -webkit-gradient(linear, left top, left bottom, from(#efefef), to(#d8d8d8));
background-image: -webkit-linear-gradient(top, #efefef, #d8d8d8);
@@ -593,7 +593,7 @@
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(255, 255, 255, 0.8) inset;
}
-.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight:active {
+.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight:active {
background-color: #e3e3e3;
background-image: -webkit-gradient(linear, left top, left bottom, from(#e3e3e3), to(#f6f6f6));
background-image: -webkit-linear-gradient(top, #e3e3e3, #f6f6f6);
@@ -605,7 +605,7 @@
-webkit-box-shadow: none;
-box-shadow: none;
}
-.openerp .oe_popup_form .oe_kanban_buttons button.oe_highlight:hover {
+.openerp .o_popup_form .oe_kanban_buttons button.oe_highlight:hover {
background-color: #f6f6f6;
background-image: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e3e3e3));
background-image: -webkit-linear-gradient(top, #f6f6f6, #e3e3e3);
diff --git a/addons/web_kanban/static/src/css/kanban.sass b/addons/web_kanban/static/src/css/kanban.sass
index b229c8c9d41..5b7d187d9a1 100644
--- a/addons/web_kanban/static/src/css/kanban.sass
+++ b/addons/web_kanban/static/src/css/kanban.sass
@@ -514,7 +514,7 @@
padding: 0px
background: #ffffff
.openerp
- .oe_popup_form
+ .o_popup_form
.oe_kanban_buttons
.oe_highlight
color: #404040
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: $('
', {
+ 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
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 = $(" 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 -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('').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 = $('
').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
= 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);
- },
- do_clear_groups: function() {
- var groups = this.groups.slice(0);
- this.groups = [];
- _.each(groups, function(group) {
- group.destroy();
- });
- },
- do_add_groups: function(groups) {
- 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();
- });
- });
- },
- 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');
- } 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' });
- }
- });
+ is_action_enabled: function(action) {
+ if (action === 'create' && !this.options.creatable) {
+ return false;
+ }
+ return this._super(action);
+ },
+ _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;
+ },
+ /**
+ * 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;
+ 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();
+ },
+
+ update_pager: function() {
+ if (this.pager) {
+ if (this.grouped) {
+ this.pager.do_hide();
+ } else {
+ 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 = $("").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();
+ 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();
+ },
+
+ add_record: function() {
+ this.dataset.index = null;
+ this.do_switch_view('form');
+ },
+
+ open_record: function (event, options) {
+ if (this.dataset.select_id(event.data.id)) {
+ this.do_switch_view('form', null, options); //, null, { mode: "edit" });
} 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
- });
+ this.do_warn("Kanban: could not find id#" + event.data.id);
}
},
- do_show: function() {
- this.do_push_state({});
- return this._super();
+ edit_record: function (event) {
+ this.open_record(event, {mode: 'edit'});
},
- open_record: function(id, editable) {
- if (this.dataset.select_id(id)) {
- this.do_switch_view('form', null, { mode: editable ? "edit" : undefined });
- } else {
- this.do_warn("Kanban: could not find id#" + id);
- }
- },
- no_result: function() {
+
+ 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;
+ 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] = [];
}
- 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] = [];
- }
- 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('
' + _.str.escapeHTML(nameget[1]) + '');
+ // white color (0) should not be selected, 1 instead
+ var color = KanbanRecord.prototype.kanban_getcolor(nameget[1]) + 1;
+ var $tag = $('
')
+ .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?');
+ }
+ });
+ },
+
+ resequence_column: function (col) {
+ if (_.indexOf(this.fields_keys, 'sequence') > -1) {
+ this.dataset.resequence(col.get_ids());
+ }
+ },
+
+ 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);
+ }
+ });
+ },
+
+ 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);
+ }
+ });
+ },
+
+ 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]);
+ });
+ });
+
+ 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);
+ });
+ },
+
+ load_more: function (event) {
+ var self = this;
+ 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();
+ }
+ });
}
});
+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;
});
-
-odoo.define(function (require) {
-
-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');
-
-var KanbanGroup = kanban_common.KanbanGroup;
-var _t = core._t;
-
-KanbanView.include({
- do_add_group: function() {
- 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 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' });
- });
- });
- });
- },
-});
-
-});
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 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('').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 = $('
').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,
+};
+
+});
diff --git a/addons/web_kanban/static/src/less/kanban_dashboard.less b/addons/web_kanban/static/src/less/kanban_dashboard.less
index 3315beef9eb..61df77edcd0 100644
--- a/addons/web_kanban/static/src/less/kanban_dashboard.less
+++ b/addons/web_kanban/static/src/less/kanban_dashboard.less
@@ -1,7 +1,7 @@
-.oe_kanban_view {
+.o_kanban_view {
&.o_kanban_mobile {
- .oe_kanban_record {
+ .o_kanban_record {
width: 400px;
}
span {
@@ -14,8 +14,6 @@
&.o_kanban_dashboard {
- @odoo-brand-optional: #21b799;
-
.o_visible {
width: 100%;
}
@@ -43,8 +41,8 @@
margin-right: 0px !important;
}
+
.oe_kanban_colorpicker {
- min-height: 21px;
li {
height: 0px;
@@ -52,45 +50,37 @@
}
}
- .oe_kanban_record {
- padding: 0;
- position: relative;
- margin: 10px;
-
- // @media (max-width: @grid-float-breakpoint) {
- // margin: 10px !important;
- // }
- }
-
- // if invisible, it will no take more than its original height
+ // If invisible, elem will no take more than its original height
a, span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
position: relative;
- width: 100%;
display: block;
}
+ .o_kanban_record {
+ padding: 0;
+ position: relative;
+ margin: 4px 8px;
+ }
+
a {
- // color: @odoo-brand-optional;
+ color: @odoo-brand-optional;
}
.o_kanban_card_header {
- display: flex;
- display: -webkit-flex;
+ .o-flex-display();
background-color: lightgrey;
position: relative;
.o_kanban_card_header_title {
- -webkit-flex: 3 0 0;
- flex: 3 0 0;
- padding: 8px;
+ .o-flex(3, 0, 0);
+ padding: 8px 16px;
.o_primary {
- // color: @odoo-brand-optional;
- font-variant: small-caps;
- font-size: large;
+ color: @odoo-brand-optional;
+ font-weight: 700;
}
.o_secondary {
@@ -98,12 +88,11 @@
}
.o_kanban_manage_button_section {
- -webkit-flex: 1 0 0;
- flex: 1 0 0;
+ .o-flex(1, 0, 0);
padding: 16px;
.o_kanban_manage_toggle_button {
- text-align: center;
+ text-align: right;
}
}
}
@@ -129,6 +118,7 @@
.o_kanban_card_manage_pane {
display: inline-block;
vertical-align: top;
+ padding: 0 8px;
}
.o_kanban_card_manage_section {
@@ -145,6 +135,7 @@
}
.o_kanban_card_manage_settings {
+ text-align: right;
padding: 8px;
border-color: lightgrey;
diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less
new file mode 100644
index 00000000000..4d7a6221dbf
--- /dev/null
+++ b/addons/web_kanban/static/src/less/kanban_view.less
@@ -0,0 +1,500 @@
+@o-kanban-record-margin: 4px;
+
+.o_kanban_view {
+
+ .o-flex-display();
+ .o-align-content(stretch);
+ @media (max-width: @grid-float-breakpoint) {
+ padding: 0px;
+ }
+
+ .o_kanban_quick_create {
+ background-color: white;
+ margin: @o-kanban-record-margin;
+ box-shadow: 0px 0px 1px 1px lighten(@odoo-main-text-color, 10%);
+ padding: 3px;
+ input {
+ width: 100%;
+ margin: 4px 0;
+ }
+ }
+
+ .o_kanban_record {
+ min-width: 150px;
+ background-color: white;
+ margin: 4px 4px 8px 4px;
+ box-shadow: 0px 0px 1px 1px lighten(@odoo-main-text-color, 10%);
+ padding: 8px;
+
+ &.o_record_dragged {
+ background-color: @odoo-main-text-color;
+ }
+
+ &.oe_kanban_global_click, &.oe_kanban_global_click_edit {
+ cursor: pointer;
+ }
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+ }
+
+ &.o_kanban_two_column {
+ .o-flex-display();
+
+ .oe_kanban_details {
+ position: relative;
+ display: inline-block;
+ .o-flex(1, 1, auto);
+ width: 0;
+ }
+ }
+
+ &.o_kanban_two_column_tags {
+ .o-flex-display();
+ .o-flex-flow(row, wrap);
+
+ .o_kanban_tags_section {
+ .o-flex(1, 1, 100%);
+ }
+ .o_kanban_image {
+ .o-flex(1, 1, 25%);
+ margin-right: 0;
+ > img {
+ box-shadow: none;
+ }
+ }
+ .oe_kanban_details {
+ position: relative;
+ .o-flex(1, 1, 75%);
+ ul {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .o_kanban_title {
+ font-weight: bold;
+ margin-bottom: 3px;
+ }
+
+ .o_kanban_image {
+ .o-flex(0, 0, auto);
+ margin-right: 8px;
+ text-align: center;
+
+ img {
+ width: 64px;
+ height: 64px;
+ box-shadow: none;
+ }
+
+ }
+
+ .o_kanban_button {
+ margin-top: 15px;
+
+ > button, > a {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+ }
+
+ &.ui-sortable-helper {
+ transform: rotate(3deg);
+ opacity: 0.9,
+ }
+
+ .o_dropdown_kanban {
+ float: right;
+
+ // Open the dropdown a the left side of the dropdown toggle
+ > .dropdown-menu {
+ left: auto;
+ right: 0;
+ padding: 3px;
+ }
+
+ > a {
+ padding: 3px;
+ color: @odoo-main-text-color;
+ }
+ &.open {
+ visibility: visible;
+ }
+ }
+
+ .o_kanban_footer {
+ .o-flex-display();
+ .o-justify-content(space-between);
+ > * {
+ flex: none;
+ }
+ }
+ &:hover {
+ .o_dropdown_kanban {
+ visibility: visible;
+ }
+ }
+ .o_dropdown_kanban {
+ @media (max-width: @grid-float-breakpoint) {
+ visibility: visible;
+ }
+ }
+ }
+
+ &.o_kanban_ungrouped {
+ .o-flex-flow(row, wrap);
+ .o-justify-content(flex-start);
+ padding: @o-kanban-record-margin @odoo-horizontal-padding - @o-kanban-record-margin;
+
+ @media (max-width: @grid-float-breakpoint) {
+ padding: 0;
+ }
+
+ .o_kanban_record {
+ .o-flex(1, 1, auto);
+ width: 320px;
+ @media (max-width: @grid-float-breakpoint) {
+ margin: 0 0 1px 0;
+ width: 100vw;
+ box-shadow: none;
+ }
+ }
+
+ .o_kanban_ghost {
+ height: 0;
+ min-height: 0 !important; // to prevent view writers to override this height
+ visibility: hidden;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding: 0;
+ }
+
+ }
+
+ &.o_kanban_grouped {
+ min-height: 100%;
+ padding: 0 @odoo-horizontal-padding - @o-kanban-record-margin;
+ .o_kanban_record {
+ width: 300px;
+
+ &.o_kanban_small {
+ width: 240px;
+ }
+ }
+ }
+
+ .o_kanban_group {
+ .o-flex(0, 0, auto);
+ margin: 0 4px;
+
+ &.o_kanban_dragged {
+ background-color: @odoo-main-text-color;
+ .o_kanban_record {
+ visibility: hidden;
+ }
+ }
+
+ .o_kanban_header + .o_kanban_record {
+ margin-top: 0; // no margin top from the first card after kanban_header
+ }
+
+ .o_kanban_header {
+ color: white;
+ padding: 0 4px;
+ height: 50px;
+ line-height: 2.2;
+ cursor: move;
+ .o-flex-display();
+ .o_column_unfold {
+ display: none;
+ }
+ .o_column_title {
+ font-size: 18px;
+ font-weight: 300;
+ .o-flex(1, 1, auto);
+ white-space: nowrap;
+ padding-top: 5px;
+ }
+
+ .o-kanban-icon() {
+ // Share the style of `.o_kanban_quick_add` and `o_kanban_config`.
+ height: 50px;
+ width: 50px;
+ line-height: 1.8;
+ text-align: center;
+ color: white;
+ padding: 8px;
+ font-size: 22px;
+ cursor: pointer;
+ }
+
+ .o_kanban_quick_add i {
+ .o-kanban-icon;
+ &:hover {
+ background-color: @odoo-brand-optional;
+ }
+ }
+ .o_kanban_config i {
+ .o-kanban-icon;
+ &:hover {
+ background-color: darken(@gray-light, 10%);
+ }
+ }
+
+ .o_kanban_number {
+ color: @odoo-main-text-color;
+ text-align: center;
+ }
+ }
+
+ .o_kanban_record {
+ &.o_kanban_dragged {
+ > * {
+ visibility: hidden;
+ }
+ background-color: @odoo-main-text-color;
+ }
+ }
+
+ &.o_column_folded {
+ position: relative;
+ border-right: 1px solid @odoo-main-text-color;
+ margin: 0;
+
+ .o_column_title {
+ line-height: 1.3;
+ text-transform: uppercase;
+ transform-origin: left bottom;
+ transform: rotate(90deg);
+ top: 40px;
+ left: 15px;
+ position: absolute;
+ }
+ > .o_kanban_record, .o_kanban_quick_add, .o_kanban_config, .o_kanban_load_more {
+ display: none;
+ }
+ &:hover {
+ cursor: pointer;
+ background-color: darken(@gray-light, 10%);
+ }
+ .o_column_unfold {
+ display: block;
+ padding: 0 10px;
+
+ > i {
+ font-size: 22px;
+ padding-top: 15px;
+ }
+
+ }
+ background-color: @gray-light;
+ }
+
+ .o_kanban_load_more {
+ text-align: center;
+ a {
+ color: white;
+ font-weight: 300;
+ }
+ }
+ }
+
+ .oe_kanban_color_0 {
+ background-color: #FFFFFF;
+ color: #5a5a5a;
+ }
+ .oe_kanban_color_1 {
+ background-color: #d8d8d8;
+ color: #5e5e5e;
+ }
+ .oe_kanban_color_2 {
+ background-color: #e5d8e3;
+ color: #7a3737;
+ }
+ .oe_kanban_color_3 {
+ background-color: #ece9c2;
+ color: #756832;
+ }
+ .oe_kanban_color_4 {
+ background-color: #dee180;
+ color: #5d6937;
+ }
+ .oe_kanban_color_5 {
+ background-color: #abd4cc;
+ color: #1a7759;
+ }
+ .oe_kanban_color_6 {
+ background-color: #b8e3e8;
+ color: #1a5d83;
+ }
+ .oe_kanban_color_7 {
+ background-color: #f4ddab;
+ color: #5e5e5e;
+ }
+ .oe_kanban_color_8 {
+ background-color: #7f637d;
+ color: #a5a5a5;
+ }
+ .oe_kanban_color_9 {
+ background-color: #d8c5d5;
+ color: #5e5e5e;
+ }
+ .oe_kanban_color_10 {
+ background-color: #46C265;
+ color: #639B89;
+ }
+
+ .oe_tags {
+ display: block;
+ }
+ .o_kanban_tags {
+
+ margin-left: -8px;
+
+ .o_tag{
+ width: 50px;
+ height: 9px;
+ display: inline-block;
+ vertical-align: top;
+ }
+ }
+ .o_kanban_footer {
+ width: 100%;
+ }
+ .oe_kanban_avatar {
+ border-radius: 12px;
+ border: 1px solid @odoo-main-color-muted;
+ }
+
+ .o_progressbar {
+ .o_progressbar_title {
+ width: 20%;
+ }
+ .o_progress {
+ width: 60%;
+ height: 15px;
+ }
+ .o_progressbar_value {
+ width: 20%;
+ text-align: right;
+ }
+ }
+
+ .o_kanban_attachment_image {
+ // The image should take the hole card
+ margin: -8px;
+ > img {
+ width: 100%;
+ }
+ }
+}
+
+.oe_kanban_colorpicker {
+ li {
+ float: left;
+ &:first-child a {
+ border: 1px solid #ccc;
+ }
+
+ a {
+ width: 15px;
+ height: 16px;
+ display: inline-block;
+ border: 1px solid white;
+ }
+ }
+}
+
+.o_dropdown_kanban {
+ visibility: hidden;
+}
+
+.oe_kanban_avatar_smallbox {
+ height: 40px;
+ width: 40px;
+ box-shadow: 0 1px 4px @gray-light;
+ // border: 1px solid
+ // border-color: #e5e5e5 #dbdbdb #d2d2d2
+ // @include radius(3px)
+ // @include box-shadow(0 1px 2px rgba(0,0,0,0.2))
+}
+
+.o_kanban_shadow {
+ background-color: @odoo-main-text-color;
+ miw-width: 100px;
+}
+
+.o_column_quick_create {
+ .o-flex(0, 0, auto);
+ margin: 0;
+ padding: 8px;
+ position: relative;
+ background-color: @odoo-main-text-color;
+ cursor: pointer;
+
+ &:hover {
+ background-color: darken(@gray-light, 10%);
+ }
+ .o_column_header {
+ .o_kanban_add_column {
+ padding: 0 10px;
+ color: white;
+
+ > i {
+ font-size: 22px;
+ padding-top: 7px;
+ }
+ }
+ .o_kanban_title {
+ font-size: 18px;
+ font-weight: 300;
+ white-space: nowrap;
+ color: @gray-lighter;
+ text-transform: uppercase;
+ transform-origin: left bottom;
+ transform: rotate(90deg);
+ top: 40px;
+ left: 15px;
+ position: absolute;
+ }
+ }
+ .o_kanban_quick_create {
+ display: none;
+ width: 180px;
+ }
+}
+
+.oe_kanban_footer_left {
+ float: left;
+}
+
+.oe_kanban_bottom_right {
+ float: right;
+}
+
+.oe_kanban_bottom_left {
+ float: left;
+}
+
+.oe_kanban_status {
+ display: inline-block;
+ height: 12px;
+ width: 12px;
+ border-radius: 6px;
+
+ background-color: @odoo-brand-secondary;
+
+ &.oe_kanban_status_green {
+ background-color: @brand-success;
+ }
+ &.oe_kanban_status_red {
+ background-color: @brand-danger;
+ }
+}
+
+.o_form_field_one2many .oe_view_nocontent {
+ display: none;
+}
\ No newline at end of file
diff --git a/addons/web_kanban/static/src/xml/web_kanban.xml b/addons/web_kanban/static/src/xml/web_kanban.xml
index 0a7bbcdcffb..497b0696025 100644
--- a/addons/web_kanban/static/src/xml/web_kanban.xml
+++ b/addons/web_kanban/static/src/xml/web_kanban.xml
@@ -1,90 +1,72 @@
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/addons/web_kanban/views/web_kanban.xml b/addons/web_kanban/views/web_kanban.xml
index b9fb5dff577..f383254636d 100644
--- a/addons/web_kanban/views/web_kanban.xml
+++ b/addons/web_kanban/views/web_kanban.xml
@@ -3,11 +3,14 @@
-
+
+
+
+
-
+