From ae583ba1736a09a7d4e62a3a0619cb0575b83822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jun 2015 09:54:13 +0200 Subject: [PATCH] [REF] web_kanban: new redesigned kanban view This commit introduces an almost completely rewritten kanban view. It was necessary in order to be able to render a different DOM to match the new design. It basically has the same features as before, but will render a different DOM. Three main differences, API wise, should be noted: * the way the kanban view components communicate has been changed. It now uses events, and event bubbling. * kanban records do not have access to the view, so every call to this.view should be updated * kanban templates are no longer wrapped in a div, so they should have a single root element for better result. --- addons/web_kanban/static/src/css/kanban.css | 8 +- addons/web_kanban/static/src/css/kanban.sass | 2 +- .../web_kanban/static/src/js/compatibility.js | 10 +- .../web_kanban/static/src/js/kanban_column.js | 245 ++++ .../web_kanban/static/src/js/kanban_common.js | 928 ------------- .../static/src/js/kanban_quick_create.js | 127 ++ .../web_kanban/static/src/js/kanban_record.js | 275 ++++ .../web_kanban/static/src/js/kanban_view.js | 1187 +++++++++-------- .../static/src/js/kanban_widgets.js | 247 ++++ .../static/src/less/kanban_dashboard.less | 49 +- .../static/src/less/kanban_view.less | 500 +++++++ .../web_kanban/static/src/xml/web_kanban.xml | 144 +- addons/web_kanban/views/web_kanban.xml | 7 +- 13 files changed, 2140 insertions(+), 1589 deletions(-) create mode 100644 addons/web_kanban/static/src/js/kanban_column.js delete mode 100644 addons/web_kanban/static/src/js/kanban_common.js create mode 100644 addons/web_kanban/static/src/js/kanban_quick_create.js create mode 100644 addons/web_kanban/static/src/js/kanban_record.js create mode 100644 addons/web_kanban/static/src/js/kanban_widgets.js create mode 100644 addons/web_kanban/static/src/less/kanban_view.less 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 @@