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 01/17] [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 @@ From 01c3dfa7d25633d764ff99495271a054e75a7af2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jun 2015 11:05:27 +0200 Subject: [PATCH 02/17] [IMP] web: add pager widget to web client This pager is used by the kanban view. It could be used by the list view, the form view and some others (and it is, in the enterprise edition) --- addons/web/static/src/js/widgets/pager.js | 111 ++++++++++++++++++++++ addons/web/static/src/xml/base.xml | 9 ++ 2 files changed, 120 insertions(+) create mode 100644 addons/web/static/src/js/widgets/pager.js diff --git a/addons/web/static/src/js/widgets/pager.js b/addons/web/static/src/js/widgets/pager.js new file mode 100644 index 00000000000..c195aaf530d --- /dev/null +++ b/addons/web/static/src/js/widgets/pager.js @@ -0,0 +1,111 @@ +odoo.define('web.Pager', function (require) { +"use strict"; + +var Widget = require('web.Widget'); + +var direction = { + previous: -1, + next: 1, +}; + +var Pager = Widget.extend({ + template: "Pager", + + events: { + 'click .o-pager-previous': 'previous', + 'click .o-pager-next': 'next', + }, + + // pager goes from 1 to size (included). + // current value is current_min (if limit === 1) + // or the interval [current_min, current_min + limit[ if limit > 1 + init: function (parent, size, current_min, limit, options) { + this.state = { + size: size, + current_min: current_min, + limit: limit, + }; + Object.defineProperty(this.state, 'current_max', { + get: function() { + return Math.min(this.current_min + this.limit - 1, this.size); + } + }); + this.options = options || {}; + this._super(parent); + }, + + start: function () { + this.$content = this.$('.o-pager-value'); + this._render(); + return this._super(); + }, + + set_state: function (options) { + _.extend(this.state, options); + this._render(); + }, + + previous: function(e) { + this._change_selection(direction.previous); + }, + + next: function() { + this._change_selection(direction.next); + }, + + disable: function() { + this.$('button').prop("disabled", true); + }, + + enable: function() { + this.$('button').prop("disabled", false); + }, + + _render: function () { + var state; + var size = this.state.size; + var current_min = this.state.current_min; + var current_max = this.state.current_max; + var single_page = 1 === current_min && current_max === size; + + if (size === 0 || (single_page && this.options.single_page_hidden)) { + this.$el.hide(); + } else { + this.$el.show(); + + if (single_page) { + this.disable(); + } else { + this.enable(); + } + + if (this.state.limit === 1) { + state = "" + current_min + " / " + size; + } else { + state = "" + current_min + "-" + current_max + " / " + size; + } + this.$content.html(state); + } + }, + + _change_selection: function (direction) { + var size = this.state.size; + var current_min = this.state.current_min; + var limit = this.state.limit; + current_min = (current_min + limit*direction); + if (current_min > size) { + current_min = 1; + } else if ((current_min < 1) && (limit === 1)) { + current_min = size; + } else if ((current_min < 1) && (limit > 1)) { + current_min = size - ((size % limit) || limit) + 1; + } + this.state.current_min = current_min; + this.trigger('pager_changed', _.clone(this.state)); + this._render(); + }, +}); + +return Pager; + +}); \ No newline at end of file diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 00406837a47..27ce7dae6d4 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1946,6 +1946,15 @@ + +
+ + +
+
- +
  • @@ -1116,6 +1116,14 @@
+ + + @@ -1938,8 +1946,10 @@
-
+ + +
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index 42b20752a91..e1a17a245d0 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -307,8 +307,8 @@
    -
  • Edit Opportunity
  • -
  • Delete
  • +
  • Edit Opportunity
  • +
  • Delete
  • Send Email
  • Calls
  • Schedule Meeting
  • diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index ea53ba9f9b0..43091209ed0 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -165,7 +165,7 @@
      - +
    • Delete
      • diff --git a/addons/hr/hr_dashboard.xml b/addons/hr/hr_dashboard.xml index 884e0cbc68c..9567eda3ed7 100644 --- a/addons/hr/hr_dashboard.xml +++ b/addons/hr/hr_dashboard.xml @@ -47,7 +47,7 @@
-
+
  • diff --git a/addons/hr_equipment/views/hr_equipment_views.xml b/addons/hr_equipment/views/hr_equipment_views.xml index ef61997f586..9797223194d 100644 --- a/addons/hr_equipment/views/hr_equipment_views.xml +++ b/addons/hr_equipment/views/hr_equipment_views.xml @@ -92,8 +92,8 @@ @@ -265,8 +265,8 @@ diff --git a/addons/hr_recruitment/hr_recruitment_dashboard.xml b/addons/hr_recruitment/hr_recruitment_dashboard.xml index 75d4a4564a1..e0965de2039 100644 --- a/addons/hr_recruitment/hr_recruitment_dashboard.xml +++ b/addons/hr_recruitment/hr_recruitment_dashboard.xml @@ -123,7 +123,7 @@
    -
    +
  • diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index c3685c5f880..e0b9bcbbba4 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -268,7 +268,7 @@
      -
    • Delete
    • +
    • Delete
    • Schedule Interview
      diff --git a/addons/mass_mailing/views/email_template.xml b/addons/mass_mailing/views/email_template.xml index 73fb508b44b..5367e043383 100644 --- a/addons/mass_mailing/views/email_template.xml +++ b/addons/mass_mailing/views/email_template.xml @@ -50,11 +50,11 @@
      -
        - + diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 6b73230ca18..ce357f7a2f5 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -499,7 +499,7 @@
        @@ -787,10 +787,10 @@
          - +
        • Settings
        • - +
        • Delete
          • diff --git a/addons/project/project_dashboard.xml b/addons/project/project_dashboard.xml index 1e5ccbd13bc..b97e9721187 100644 --- a/addons/project/project_dashboard.xml +++ b/addons/project/project_dashboard.xml @@ -42,7 +42,7 @@ Cumulative Flow
        -
        +
      • diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 5861df6869a..e4e9a752fc6 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -363,8 +363,8 @@ diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index cd4675a1ae3..5b56a2568ff 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -168,8 +168,8 @@ diff --git a/addons/sale_crm/sale_crm_view.xml b/addons/sale_crm/sale_crm_view.xml index 3f63289c592..bd2f5e748d1 100644 --- a/addons/sale_crm/sale_crm_view.xml +++ b/addons/sale_crm/sale_crm_view.xml @@ -35,12 +35,12 @@ context="{'search_default_partner_id': partner_id, 'default_partner_id': partner_id}"> - diff --git a/addons/sales_team/sales_team_dashboard.xml b/addons/sales_team/sales_team_dashboard.xml index fa14cd91284..33e5a7062c5 100644 --- a/addons/sales_team/sales_team_dashboard.xml +++ b/addons/sales_team/sales_team_dashboard.xml @@ -47,7 +47,7 @@
        -
        +
      • diff --git a/addons/stock/stock_dashboard.xml b/addons/stock/stock_dashboard.xml index ddf35c7beb9..002a7fa3f87 100644 --- a/addons/stock/stock_dashboard.xml +++ b/addons/stock/stock_dashboard.xml @@ -100,7 +100,7 @@
        -
        +
      • diff --git a/addons/survey/views/survey_views.xml b/addons/survey/views/survey_views.xml index 284de9fc4a9..df70a6b658e 100644 --- a/addons/survey/views/survey_views.xml +++ b/addons/survey/views/survey_views.xml @@ -184,11 +184,11 @@
        -
        + diff --git a/addons/website/static/src/css/website.backend.css b/addons/website/static/src/css/website.backend.css index 24879793303..91b3e7c01ea 100644 --- a/addons/website/static/src/css/website.backend.css +++ b/addons/website/static/src/css/website.backend.css @@ -1,7 +1,7 @@ -.oe_WidgetWebsiteButton.danger .oe_publish { - display: none; +.oe_stat_button .o_stat_info:not(.published) > .o_stat_text.oe_publish { + display: none; } -.oe_WidgetWebsiteButton.success .oe_unpublish { +.oe_stat_button .o_stat_info.published > .o_stat_text.oe_unpublish { display: none; } .oe_form_field_html_frame iframe { diff --git a/addons/website/static/src/js/website.backend.js b/addons/website/static/src/js/website.backend.js index b1920dc4cab..d1bb6c2f5b0 100644 --- a/addons/website/static/src/js/website.backend.js +++ b/addons/website/static/src/js/website.backend.js @@ -9,10 +9,13 @@ var form_widgets = require('web.form_widgets'); // required to guarantee that var WidgetWebsiteButton = form_common.AbstractField.extend({ template: 'WidgetWebsiteButton', render_value: function() { + // Hack to replace false value by unpublished because falsy form fields + // have classname o_form_field_empty and are hidden + if (!this.get_value()) { + this.set_value('unpublished'); + } this._super(); - this.$() - .toggleClass("success", this.get_value()) - .toggleClass("danger", !this.get_value()); + this.$el.toggleClass("published", this.get_value() === true); if (this.node.attrs.class) { this.$el.addClass(this.node.attrs.class); } diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index 24f8c825816..826e29e261c 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -1,12 +1,10 @@ - -
        - Not
        Published
        - Published -
        -
        +
        + Not Published + Published +
        diff --git a/addons/website_event_track/views/event_backend.xml b/addons/website_event_track/views/event_backend.xml index 7eb6a103d7d..09757cab38b 100644 --- a/addons/website_event_track/views/event_backend.xml +++ b/addons/website_event_track/views/event_backend.xml @@ -18,8 +18,8 @@
        diff --git a/addons/website_forum_doc/views/doc.xml b/addons/website_forum_doc/views/doc.xml index 9a32603fcab..f45243445b8 100644 --- a/addons/website_forum_doc/views/doc.xml +++ b/addons/website_forum_doc/views/doc.xml @@ -41,8 +41,8 @@ From 6c0923f5b41f70ce450f9d5bfdf4b9fcb93f357d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jun 2015 15:04:08 +0200 Subject: [PATCH 05/17] [FIX] account: update to new kanban view --- .../static/src/js/account_journal_dashboard_widget.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/account/static/src/js/account_journal_dashboard_widget.js b/addons/account/static/src/js/account_journal_dashboard_widget.js index 9818309403e..c152a90442d 100644 --- a/addons/account/static/src/js/account_journal_dashboard_widget.js +++ b/addons/account/static/src/js/account_journal_dashboard_widget.js @@ -1,9 +1,9 @@ odoo.define('journal_dashboard', function (require) { 'use strict'; -var kanban_common = require('web_kanban.common'); +var kanban_widgets = require('web_kanban.widgets'); -var JournalDashboardGraph = kanban_common.AbstractField.extend({ +var JournalDashboardGraph = kanban_widgets.AbstractField.extend({ start: function() { var self = this; self.graph_type = self.$node.attr('graph_type') @@ -86,6 +86,6 @@ var JournalDashboardGraph = kanban_common.AbstractField.extend({ }); -kanban_common.registry.add('dashboard_graph', JournalDashboardGraph); +kanban_widgets.registry.add('dashboard_graph', JournalDashboardGraph); }); \ No newline at end of file From a8d102b79caf04f4e1405489c656bf4f8337278c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jun 2015 15:04:27 +0200 Subject: [PATCH 06/17] [FIX] sales_team: update to new kanban view --- addons/sales_team/static/src/js/sales_team.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/sales_team/static/src/js/sales_team.js b/addons/sales_team/static/src/js/sales_team.js index 40f976a465a..0000fbdc7be 100644 --- a/addons/sales_team/static/src/js/sales_team.js +++ b/addons/sales_team/static/src/js/sales_team.js @@ -2,7 +2,7 @@ odoo.define('sales_team.update_kanban', function (require) { "use strict"; var KanbanView = require('web_kanban.KanbanView'); -var kanban_common = require('web_kanban.common'); +var KanbanRecord = require('web_kanban.Record'); var data = require('web.data'); var session = require('web.session'); @@ -40,9 +40,9 @@ KanbanView.include({ }, }); -kanban_common.KanbanRecord.include({ +KanbanRecord.include({ on_card_clicked: function() { - if (this.view.dataset.model === 'crm.team') { + if (this.model === 'crm.team') { this.$('.oe_kanban_crm_salesteams_list a').first().click(); } else { this._super.apply(this, arguments); From b1557efa9ec554f06474aa34a350d7e14a4665d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 11 Jun 2015 15:04:41 +0200 Subject: [PATCH 07/17] [FIX] web_kanban_gauge: update to new kanban view --- addons/web_kanban_gauge/static/src/js/kanban_gauge.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web_kanban_gauge/static/src/js/kanban_gauge.js b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js index 7ffd021af4b..0a9f3a96b44 100644 --- a/addons/web_kanban_gauge/static/src/js/kanban_gauge.js +++ b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js @@ -2,10 +2,10 @@ odoo.define('web_kanban_gauge.widget', function (require) { "use strict"; var core = require('web.core'); -var kanban_common = require('web_kanban.common'); +var kanban_widgets = require('web_kanban.widgets'); -var AbstractField = kanban_common.AbstractField; -var fields_registry = kanban_common.registry; +var AbstractField = kanban_widgets.AbstractField; +var fields_registry = kanban_widgets.registry; var _t = core._t; /** From 05adb7f2352ec7e0331a2b6a0aee7d1272f6332c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 12 Jun 2015 09:51:31 +0200 Subject: [PATCH 08/17] [FIX] various: update res.partner template Update to new design (kanban and form view) --- addons/account/views/partner_view.xml | 41 +++--- addons/base_setup/res_partner_view.xml | 2 +- addons/crm/res_partner_view.xml | 19 +-- addons/crm_claim/res_partner_view.xml | 8 +- addons/project/res_partner_view.xml | 12 +- addons/project_issue/project_issue_view.xml | 18 ++- addons/sale/res_partner_view.xml | 11 +- .../views/res_partner_view.xml | 8 +- openerp/addons/base/res/res_partner_view.xml | 121 +++++++++--------- 9 files changed, 113 insertions(+), 127 deletions(-) diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml index c4299986216..ab6bfcf4118 100644 --- a/addons/account/views/partner_view.xml +++ b/addons/account/views/partner_view.xml @@ -77,7 +77,7 @@ - +
        - +
        @@ -99,12 +102,12 @@ - - - +
        @@ -132,10 +135,10 @@ - + - - - - +
        - diff --git a/addons/crm_claim/res_partner_view.xml b/addons/crm_claim/res_partner_view.xml index 11bf9e5a3ba..a2bcbf7a80f 100644 --- a/addons/crm_claim/res_partner_view.xml +++ b/addons/crm_claim/res_partner_view.xml @@ -8,14 +8,14 @@ - - - +
        - + \ No newline at end of file diff --git a/addons/project/res_partner_view.xml b/addons/project/res_partner_view.xml index ed52b8bcaf7..82448073bf1 100644 --- a/addons/project/res_partner_view.xml +++ b/addons/project/res_partner_view.xml @@ -10,17 +10,15 @@ - - - - - + +
        - + \ No newline at end of file diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 5b56a2568ff..b9b03b0bf60 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -36,7 +36,7 @@
        -
        @@ -278,12 +278,12 @@
      - - - +
      {'invisible': [('use_tasks', '=', False),('use_issues','=',False)]} @@ -331,18 +331,16 @@ - - - - - +
    - + \ No newline at end of file diff --git a/addons/sale/res_partner_view.xml b/addons/sale/res_partner_view.xml index 2f08c015d2a..3c6fb2468f4 100644 --- a/addons/sale/res_partner_view.xml +++ b/addons/sale/res_partner_view.xml @@ -34,9 +34,7 @@ - - Sales - + @@ -48,16 +46,15 @@ - - - +
    - diff --git a/addons/website_partner/views/res_partner_view.xml b/addons/website_partner/views/res_partner_view.xml index 10f0dd0e9de..343733faa7c 100644 --- a/addons/website_partner/views/res_partner_view.xml +++ b/addons/website_partner/views/res_partner_view.xml @@ -10,15 +10,15 @@ - - - +
    - + \ No newline at end of file diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 3f94301bfd6..e4a0df2f374 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -114,8 +114,8 @@ - - + + @@ -128,23 +128,25 @@
    - -
    +
    + +
    -

    - - +
    + + +
    -
    @@ -155,31 +157,30 @@
    @@ -209,4 +209,4 @@ - + \ No newline at end of file diff --git a/openerp/addons/base/static/src/css/modules.css b/openerp/addons/base/static/src/css/modules.css index 9d31e8cf7b1..3df2e344dc9 100644 --- a/openerp/addons/base/static/src/css/modules.css +++ b/openerp/addons/base/static/src/css/modules.css @@ -1,19 +1,26 @@ .oe_module_vignette { padding: 4px; + display: flex; } -.oe_module_icon, .oe_module_desc { - display: inline-block; - vertical-align: top; -} + .oe_module_icon { width: 80px; height: 80px; + flex-shrink: 0; + flex-grow: 0; } .oe_module_desc { - width: 210px; font-size: 13px; padding: 0px 5px; + flex-grow: 1; + position: relative; +} + +.oe_module_desc > button { + position: absolute; + bottom: 0; + right: 0; } .oe_module_desc h4 { @@ -33,7 +40,3 @@ margin: 3px 0 5px; } -.oe_module_desc .oe_button { - min-width: 70px; -} - From efe9e0241f9497aad9b6fd9ec605a3809b202ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 12 Jun 2015 14:13:20 +0200 Subject: [PATCH 12/17] [IMP] hr: update some templates to new design --- addons/hr/hr_view.xml | 84 +++++++------- addons/hr/static/src/less/hr_dashboard.less | 16 ++- addons/hr_recruitment/hr_recruitment_view.xml | 103 +++++++++--------- 3 files changed, 105 insertions(+), 98 deletions(-) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index be7dfecdfdf..3c350723453 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -23,6 +23,7 @@ +
    -
    - -
    - + - + HR - Employess Kanban hr.employee 10 - + @@ -157,40 +155,40 @@ -
    -
    - -
    -
    -

    - () -

    -
      -
    • - - - -
    • -
    • -
    • -
    • -
    • -
    • -
    -
    - - - -
    -
    - - -
    +
    +
    + +
    + + +
    +
    + + () + +
      +
    • + + + +
    • +
    • +
    • +
    • +
    • +
    • +
    +
    + + +
    +
    +
    @@ -352,14 +350,14 @@ +
    -
    - - + +