summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-09-28 15:01:11 +0200
committerChristophe Simonis <chs@odoo.com>2017-09-28 15:01:11 +0200
commit1512c1b22993ba66a2cfff36bb92b8babca4cc95 (patch)
tree62fe209f48ffc5b7951b307fd7e327d17c8b1165 /addons/web/static/src
parent63a4370afd3377de4308349283b981fac748e36c (diff)
parent9f9a7fdd0d70c1edda94fc546916c9e3516477ad (diff)
[MERGE] forward port branch saas-18 up to 9f9a7fdd0d7
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js2
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js4
-rw-r--r--addons/web/static/src/js/chrome/menu.js3
-rw-r--r--addons/web/static/src/js/chrome/user_menu.js2
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js2
-rw-r--r--addons/web/static/src/js/core/dialog.js2
-rw-r--r--addons/web/static/src/js/core/dom.js10
-rw-r--r--addons/web/static/src/js/core/utils.js38
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js50
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js29
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js33
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js17
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js12
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js3
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js5
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column_progressbar.js64
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js110
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js11
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js172
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js17
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js3
-rw-r--r--addons/web/static/src/js/widgets/domain_selector.js2
-rw-r--r--addons/web/static/src/js/widgets/model_field_selector.js3
-rw-r--r--addons/web/static/src/js/widgets/switch_company_menu.js2
-rw-r--r--addons/web/static/src/less/bootstrap_overridden.less6
-rw-r--r--addons/web/static/src/less/fields.less49
-rw-r--r--addons/web/static/src/less/fields_extra.less2
-rw-r--r--addons/web/static/src/less/form_view_extra.less1
-rw-r--r--addons/web/static/src/less/kanban_column_progressbar.less52
-rw-r--r--addons/web/static/src/less/kanban_view.less62
-rw-r--r--addons/web/static/src/less/kanban_view_mobile.less51
-rw-r--r--addons/web/static/src/less/list_view.less18
-rw-r--r--addons/web/static/src/less/variables.less15
-rw-r--r--addons/web/static/src/less/views.less10
-rw-r--r--addons/web/static/src/less/webclient.less68
-rw-r--r--addons/web/static/src/less/webclient_extra.less13
-rw-r--r--addons/web/static/src/xml/base.xml41
-rw-r--r--addons/web/static/src/xml/kanban.xml19
40 files changed, 722 insertions, 284 deletions
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 8492adc33f6..9a629ec3393 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -245,7 +245,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
},
do_reload: function () {
var self = this;
- return this.session.session_reload().then(function () {
+ return session.session_reload().then(function () {
session.load_modules(true).then(
self.menu.proxy('do_reload'));
});
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index e18a3d60734..194b8e6c8a7 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -912,14 +912,14 @@ var ActionManager = Widget.extend({
action: JSON.stringify(action),
token: new Date().getTime()
};
- var url = self.session.url('/web/report', params);
+ var url = session.url('/web/report', params);
framework.unblockUI();
$('<a href="'+url+'" target="_blank"></a>')[0].click();
return;
}
var c = crash_manager;
return $.Deferred(function (d) {
- self.session.get_file({
+ session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
complete: framework.unblockUI,
diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js
index 5fdfec5fb37..c9d58d89c81 100644
--- a/addons/web/static/src/js/chrome/menu.js
+++ b/addons/web/static/src/js/chrome/menu.js
@@ -5,11 +5,8 @@ var core = require('web.core');
var session = require('web.session');
var Widget = require('web.Widget');
-var QWeb = core.qweb;
-
var Menu = Widget.extend({
init: function() {
- var self = this;
this._super.apply(this, arguments);
this.is_bound = $.Deferred();
this.data = {data:{children:[]}};
diff --git a/addons/web/static/src/js/chrome/user_menu.js b/addons/web/static/src/js/chrome/user_menu.js
index 5e1691c6640..84c16b51f82 100644
--- a/addons/web/static/src/js/chrome/user_menu.js
+++ b/addons/web/static/src/js/chrome/user_menu.js
@@ -23,7 +23,7 @@ var UserMenu = Widget.extend({
start: function () {
var self = this;
var session = this.getSession();
- this.$el.on('click', '.dropdown-menu li a[data-menu]', function (ev) {
+ this.$el.on('click', 'li a[data-menu]', function (ev) {
ev.preventDefault();
var menu = $(this).data('menu');
self['_onMenu' + menu.charAt(0).toUpperCase() + menu.slice(1)]();
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 10b7838b58e..fe44cc503dd 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -359,7 +359,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
var arch = view_descr.fields_view.arch;
var View = this.registry.get(arch.attrs.js_class || view_descr.type);
var params = _.extend({}, view_options, {userContext: this.getSession().user_context});
- if (view_descr.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline') ||
+ if (view_descr.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline' || this.action.target === 'fullscreen') ||
(view_options && (view_options.mode === 'edit' || view_options.context.form_view_initial_mode)))) {
params.mode = params.initial_mode || 'edit';
}
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index 8c75db392fb..0bc370f782a 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -247,7 +247,7 @@ Dialog.confirm = function (owner, message, options) {
*/
Dialog.safeConfirm = function (owner, message, options) {
var $checkbox = dom.renderCheckbox({
- text: options && options.securityMessage || _t("I am sure about this"),
+ text: options && options.securityMessage || _t("I am sure about this."),
}).addClass('mb0');
var $securityCheck = $('<div/>', {
class: 'alert alert-' + (options && options.securityLevel || 'warning') + ' mt8 mb0',
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 6ca13f18e4e..da125aa72af 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -265,19 +265,19 @@ return {
* @returns {jQuery}
*/
renderCheckbox: function (options) {
- var $container = $('<div class="o_checkbox"><input type="checkbox"/><span/></div>');
+ var $container = $('<div class="checkbox o_checkbox"><label><input type="checkbox"/><span/></label></div>');
if (options && options.prop) {
- $container.children('input').prop(options.prop);
+ $container.find('input').prop(options.prop);
}
if (options && options.text) {
- $container = $('<label/>').append(
- $container,
+ $container.find('label').append(
$('<span/>', {
- class: 'ml8',
text: options.text,
+ class: 'o_checkbox_label',
})
);
}
+
return $container;
},
/**
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 9eefc12944c..ac9310dce90 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -86,23 +86,39 @@ var utils = {
return "";
},
/**
- * Returns a human readable number
+ * Returns a human readable number (e.g. 34000 -> 34k).
*
- * @param {Number} number
+ * @param {number} number
+ * @param {integer} [decimals=0]
+ * maximum number of decimals to use in human readable representation
+ * @param {integer} [minDigits=1]
+ * the minimum number of digits to preserve when switching to another
+ * level of thousands (e.g. with a value of '2', 4321 will still be
+ * represented as 4321 otherwise it will be down to one digit (4k))
+ * @param {function} [formatterCallback]
+ * a callback to transform the final number before adding the
+ * thousands symbol (default to adding thousands separators (useful
+ * if minDigits > 1))
+ * @returns {string}
*/
- human_number: function (number, decimals) {
- decimals = decimals | 0;
+ human_number: function (number, decimals, minDigits, formatterCallback) {
number = Math.round(number);
+ decimals = decimals | 0;
+ minDigits = minDigits || 1;
+ formatterCallback = formatterCallback || utils.insert_thousand_seps;
+
var d2 = Math.pow(10, decimals);
- var val = _t("kMGTPE");
- var i = val.length-1, s;
- while( i ) {
- s = Math.pow(10,i--*3);
- if( s <= number ) {
- number = Math.round(number*d2/s)/d2 + val[i];
+ var val = _t('kMGTPE');
+ var symbol = '';
+ for (var i = val.length - 1 ; i > 0 ; i--) {
+ var s = Math.pow(10, i * 3);
+ if (s <= number / Math.pow(10, minDigits - 1)) {
+ number = Math.round(number * d2 / s) / d2;
+ symbol = val[i - 1];
+ break;
}
}
- return number;
+ return formatterCallback('' + number) + symbol;
},
/**
* Returns a human readable size
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 894ee57c813..e25c02f781e 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -457,7 +457,7 @@ var AbstractField = Widget.extend({
this.trigger_up('navigation_move', {direction: 'next_line'});
break;
case $.ui.keyCode.ESCAPE:
- this.trigger_up('navigation_move', {direction: 'cancel'});
+ this.trigger_up('navigation_move', {direction: 'cancel', originalEvent: ev});
break;
case $.ui.keyCode.UP:
ev.stopPropagation();
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 911afea36e5..6392870dc90 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -348,6 +348,49 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
},
});
+
+var LinkButton = AbstractField.extend({
+ events: _.extend({}, AbstractField.prototype.events, {
+ 'click': '_onClick'
+ }),
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Display button
+ * @override
+ * @private
+ */
+ _render: function () {
+ if (this.value) {
+ var className = this.attrs.icon || 'fa-globe';
+
+ this.$el.html("<span />");
+ this.$el.addClass("fa "+ className);
+ this.$el.attr('title', this.value);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Open link button
+ *
+ * @private
+ * @param {MouseEvent} event
+ */
+ _onClick: function (event) {
+ event.stopPropagation();
+ window.open(this.value, '_blank');
+ },
+
+});
+
+
+
var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
@@ -951,6 +994,10 @@ var FieldPhone = FieldEmail.extend({
} else {
this.$el.removeClass('o_form_uri');
}
+ // This class should technically be there in case of a very very long
+ // phone number, but it breaks the o_row mechanism, which is more
+ // important right now.
+ this.$el.removeClass('o_text_overflow');
},
/**
@@ -1006,7 +1053,7 @@ var UrlWidget = InputField.extend({
* @private
*/
_renderReadonly: function () {
- this.$el.text(this.value)
+ this.$el.text(this.attrs.text || this.value)
.addClass('o_form_uri o_text_overflow')
.attr('href', this.value);
}
@@ -2451,6 +2498,7 @@ return {
FieldBooleanButton: FieldBooleanButton,
BooleanToggle: BooleanToggle,
FieldChar: FieldChar,
+ LinkButton: LinkButton,
FieldDate: FieldDate,
FieldDateTime: FieldDateTime,
FieldDomain: FieldDomain,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 61e371d867a..6692663d5f9 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -28,6 +28,7 @@ registry
.add('text', basic_fields.FieldText)
.add('float', basic_fields.FieldFloat)
.add('char', basic_fields.FieldChar)
+ .add('link_button', basic_fields.LinkButton)
.add('handle', basic_fields.HandleWidget)
.add('email', basic_fields.FieldEmail)
.add('phone', basic_fields.FieldPhone)
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 90c2b08e1e5..f518c3df354 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1608,7 +1608,7 @@ var FieldMany2ManyTags = AbstractField.extend({
var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
events: _.extend({}, FieldMany2ManyTags.prototype.events, {
'click .badge': '_onOpenColorPicker',
- 'mousedown .o_colorpicker span': '_onUpdateColor',
+ 'mousedown .o_colorpicker a': '_onUpdateColor',
'focusout .o_colorpicker': '_onCloseColorPicker',
}),
@@ -1681,22 +1681,19 @@ var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
var self = this;
this.$el.empty().addClass('o_field_many2manytags o_kanban_tags');
_.each(this.value.data, function (m2m) {
- var $tag = $('<span>')
- .text(m2m.data.display_name)
- .data('res_id', m2m.res_id)
- .prepend('<span>')
- .appendTo(self.$el);
- if (self.colorField in m2m.data) {
- if (m2m.data[self.colorField] === 10) {
- // 10th color is invisible
- $tag.hide();
- } else {
- $tag.addClass('o_tag o_tag_color_' + m2m.data[self.colorField]);
- }
- } else {
- // display tags in grey by default
- $tag.addClass('o_tag o_tag_color_0');
+ if (self.colorField in m2m.data && !m2m.data[self.colorField]) {
+ // When a color field is specified and that color is the default
+ // one, the kanban tag is not rendered.
+ return;
}
+
+ $('<span>', {
+ class: 'o_tag o_tag_color_' + (m2m.data[self.colorField] || 0),
+ text: m2m.data.display_name,
+ })
+ .data('res_id', m2m.res_id)
+ .prepend('<span>')
+ .appendTo(self.$el);
});
},
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 276fd4df82b..d85f55ddf36 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1645,24 +1645,25 @@ var BasicModel = AbstractModel.extend({
var hasOnchange = false;
var specs = {};
var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
-
- _.each(Object.keys(fieldsInfo), function (name) {
- var field = record.fields[name];
- var fieldInfo = fieldsInfo[name];
- specs[name] = (field.onChange) || "";
- if (field.onChange) {
- hasOnchange = true;
- }
- _.each(fieldInfo.views, function (view) {
- _.each(view.fieldsInfo[view.type], function (field, subname) {
- var onChange = view.fields[subname].onChange;
- specs[name + '.' + subname] = onChange || "";
- if (onChange) {
- hasOnchange = true;
- }
+ generateSpecs(fieldsInfo, record.fields);
+
+ // recursively generates the onchange specs for fields in fieldsInfo,
+ // and their subviews
+ function generateSpecs (fieldsInfo, fields, prefix) {
+ prefix = prefix || '';
+ _.each(Object.keys(fieldsInfo), function (name) {
+ var field = fields[name];
+ var fieldInfo = fieldsInfo[name];
+ var key = prefix + name;
+ specs[key] = (field.onChange) || "";
+ if (field.onChange) {
+ hasOnchange = true;
+ }
+ _.each(fieldInfo.views, function (view) {
+ generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
});
});
- });
+ }
return hasOnchange ? specs : false;
},
/**
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index db2394ddd1d..8d27acb43dd 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -28,6 +28,7 @@ var CalendarController = AbstractController.extend({
changeDate: '_onChangeDate',
changeFilter: '_onChangeFilter',
toggleFullWidth: '_onToggleFullWidth',
+ viewUpdated: '_onViewUpdated',
}),
/**
* @override
@@ -39,6 +40,7 @@ var CalendarController = AbstractController.extend({
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.current_start = null;
+ this.displayName = params.displayName;
this.quickAddPop = params.quickAddPop;
this.disableQuickCreate = params.disableQuickCreate;
this.eventOpenPopup = params.eventOpenPopup;
@@ -317,6 +319,21 @@ var CalendarController = AbstractController.extend({
_onUpdateRecord: function (event) {
this._updateRecord(event.data);
},
+ /**
+ * The internal state of the calendar (mode, period displayed) has changed,
+ * so update the control panel buttons and breadcrumbs accordingly.
+ *
+ * @param {OdooEvent} event
+ */
+ _onViewUpdated: function (event) {
+ this.mode = event.data.mode;
+ if (this.$buttons) {
+ this.$buttons.find('.active').removeClass('active');
+ this.$buttons.find('.o_calendar_button_' + this.mode).addClass('active');
+ }
+ var subtitle = (this.mode === 'week' ? _t('Week ') : '') + event.data.title;
+ this.set({title: this.displayName + ' (' + subtitle + ')'});
+ },
});
return CalendarController;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 584813cba5a..f011a24a32e 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -354,9 +354,19 @@ return AbstractRenderer.extend({
element.find('.fc-content .fc-time').text(display_hour);
},
// Dirty hack to ensure a correct first render
- eventAfterAllRender: function (view) {
+ eventAfterAllRender: function () {
window.dispatchEvent(new Event('resize'));
},
+ viewRender: function (view) {
+ // compute mode from view.name which is either 'month', 'agendaWeek' or 'agendaDay'
+ var mode = view.name === 'month' ? 'month' : (view.name === 'agendaWeek' ? 'week' : 'day');
+ // compute title: in week mode, display the week number
+ var title = mode === 'week' ? view.intervalStart.week() : view.title;
+ self.trigger_up('viewUpdated', {
+ mode: mode,
+ title: title,
+ });
+ },
height: 'parent',
unselectAuto: false,
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 47c16300508..7f58ca55632 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -109,7 +109,7 @@ var CalendarView = AbstractView.extend({
//if quick_add = False, we don't allow quick_add
//if quick_add = not specified in view, we use the default widgets.QuickCreate
- //if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate'
+ //if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate'
this.controllerParams.quickAddPop = (!('quick_add' in attrs) || utils.toBoolElse(attrs.quick_add+'', true));
this.controllerParams.disableQuickCreate = params.disable_quick_create || !this.controllerParams.quickAddPop;
// If this field is set ot true, we don't open the event in form view, but in a popup with the view_id passed by this parameter
@@ -118,6 +118,7 @@ var CalendarView = AbstractView.extend({
this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false);
this.controllerParams.mapping = mapping;
this.controllerParams.context = params.context || {};
+ this.controllerParams.displayName = params.action && params.action.name;
this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index dac28c536dd..ddfc16eaca2 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -99,7 +99,7 @@ var KanbanColumn = Widget.extend({
}
this.$header.find('.o_kanban_header_title').tooltip();
- if (config.device.size_class > config.device.SIZES.XS && this.draggable !== false) {
+ if (!config.isMobile && this.draggable !== false) {
// deactivate sortable in mobile mode. It does not work anyway,
// and it breaks horizontal scrolling in kanban views. Someday, we
// should find a way to use the touch events to make sortable work.
@@ -140,6 +140,7 @@ var KanbanColumn = Widget.extend({
}
});
if (this.barOptions) {
+ this.$el.addClass('o_kanban_has_progressbar');
this.progressBar = new KanbanColumnProgressBar(this, this.barOptions, this.data);
defs.push(this.progressBar.appendTo(this.$header));
}
@@ -238,7 +239,7 @@ var KanbanColumn = Widget.extend({
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);
+ this.$el.toggleClass('o_column_folded', this.folded && !config.isMobile);
var tooltip = this.size + _t(' records');
tooltip = '<p>' + tooltip + '</p>' + this.tooltipInfo;
this.$header.find('.o_kanban_header_title').tooltip({html: true}).attr('data-original-title', tooltip);
diff --git a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
index c4a62989b97..721e300da7b 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js
@@ -32,23 +32,18 @@ var KanbanColumnProgressBar = Widget.extend({
// Previous progressBar state
var state = options.progressBarStates[this.columnID];
- this.groupCount = state ? state.groupCount : 0;
- this.subgroupCounts = state ? state.subgroupCounts : {};
- this.totalCounterValue = state ? state.totalCounterValue : 0;
- this.activeFilter = state ? state.activeFilter : false;
+ if (state) {
+ this.groupCount = state.groupCount;
+ this.subgroupCounts = state.subgroupCounts;
+ this.totalCounterValue = state.totalCounterValue;
+ this.activeFilter = state.activeFilter;
+ }
// Prepare currency (TODO this should be automatic... use a field ?)
- this.counterPrefix = '';
- this.counterSuffix = '';
var sumFieldInfo = this.sumField && columnState.fieldsInfo.kanban[this.sumField];
var currencyField = sumFieldInfo && sumFieldInfo.options && sumFieldInfo.options.currency_field;
if (currencyField && columnState.data.length) {
- var currency = session.currencies[columnState.data[0].data[currencyField].res_id];
- if (currency.position === 'before') {
- this.counterPrefix = currency.symbol + ' ';
- } else {
- this.counterSuffix = ' ' + currency.symbol;
- }
+ this.currency = session.currencies[columnState.data[0].data[currencyField].res_id];
}
},
/**
@@ -62,6 +57,18 @@ var KanbanColumnProgressBar = Widget.extend({
self.$bars[val] = self.$('.bg-' + val + '-full');
});
this.$counter = this.$('.o_kanban_counter_side');
+ this.$number = this.$counter.find('b');
+
+ if (this.currency) {
+ var $currency = $('<span/>', {
+ text: this.currency.symbol,
+ });
+ if (this.currency.position === 'before') {
+ $currency.prependTo(this.$counter);
+ } else {
+ $currency.appendTo(this.$counter);
+ }
+ }
return this._super.apply(this, arguments);
},
@@ -136,7 +143,7 @@ var KanbanColumnProgressBar = Widget.extend({
// Display and animate the progress bars
_.each(self.colors, function (val, key) {
var $bar = self.$bars[val];
- var count = self.subgroupCounts[key];
+ var count = self.subgroupCounts && self.subgroupCounts[key] || 0;
if (!$bar) {
return;
@@ -165,35 +172,30 @@ var KanbanColumnProgressBar = Widget.extend({
});
// Display and animate the counter number
- var humanNumber;
- var suffix = this.counterSuffix;
+ var start = this.prevTotalCounterValue;
var end = this.totalCounterValue;
- if (end >= 10000) {
- humanNumber = utils.human_number(end, 1);
- end = parseFloat(humanNumber.substr(0, humanNumber.length - 1));
- suffix = humanNumber[humanNumber.length - 1] + suffix;
- }
- var start = this.prevTotalCounterValue || 0;
- if (start >= 10000) {
- humanNumber = utils.human_number(start, 1);
- start = parseFloat(humanNumber.substr(0, humanNumber.length - 1));
- }
- if (end > start && this.ANIMATE) {
+ var animationClass = start > 999 ? 'o_kanban_grow' : 'o_kanban_grow_huge';
+
+ if (start !== undefined && end > start && this.ANIMATE) {
$({currentValue: start}).animate({currentValue: end}, {
duration: 1000,
start: function () {
- self.$counter.addClass('o_kanban_grow');
+ self.$counter.addClass(animationClass);
},
step: function () {
- self.$counter.html(self.counterPrefix + Math.round(this.currentValue) + suffix);
+ self.$number.html(_getCounterHTML(this.currentValue));
},
complete: function () {
- self.$counter.html(self.counterPrefix + this.currentValue + suffix);
- self.$counter.removeClass('o_kanban_grow');
+ self.$number.html(_getCounterHTML(this.currentValue));
+ self.$counter.removeClass(animationClass);
},
});
} else {
- this.$counter.html(this.counterPrefix + end + suffix);
+ this.$number.html(_getCounterHTML(end));
+ }
+
+ function _getCounterHTML(value) {
+ return utils.human_number(value, 0, 3);
}
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 8ba0e3fb7c3..fb6df102dd9 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -28,6 +28,7 @@ var KanbanController = BasicController.extend({
kanban_column_resequence: '_onColumnResequence',
kanban_column_archive_records: '_onArchiveRecords',
kanban_load_more: '_onLoadMore',
+ kanban_load_records: '_onLoadColumnRecords',
column_toggle_fold: '_onToggleColumn',
}),
/**
@@ -74,6 +75,7 @@ var KanbanController = BasicController.extend({
/**
* @override method comes from field manager mixin
+ * @private
* @param {string} id local id from the basic record data
* @returns {Deferred}
*/
@@ -90,6 +92,7 @@ var KanbanController = BasicController.extend({
* The column quick create should be displayed in kanban iff grouped by an
* m2o field and group_create action enabled.
*
+ * @private
* @returns {boolean}
*/
_isCreateColumnEnabled: function () {
@@ -108,6 +111,7 @@ var KanbanController = BasicController.extend({
* does not rerender the user interface, because in most case, the
* resequencing operation has already been displayed by the renderer.
*
+ * @private
* @param {string} column_id
* @param {string[]} ids
* @returns {Deferred}
@@ -122,6 +126,7 @@ var KanbanController = BasicController.extend({
* In grouped mode, set 'Create' button as btn-default if there is no column
* (except if we can't create new columns)
*
+ * @private
* @override from abstract controller
*/
_updateButtons: function () {
@@ -144,6 +149,7 @@ var KanbanController = BasicController.extend({
* event bubbles up. When that happens, we need to ask the model to create
* a group and to update the renderer
*
+ * @private
* @param {OdooEvent} event
*/
_onAddColumn: function (event) {
@@ -156,6 +162,7 @@ var KanbanController = BasicController.extend({
});
},
/**
+ * @private
* @param {OdooEvent} event
*/
_onAddRecordToColumn: function (event) {
@@ -177,6 +184,7 @@ var KanbanController = BasicController.extend({
* The interface allows in some case the user to archive a column. This is
* what this handler is for.
*
+ * @private
* @param {OdooEvent} event
*/
_onArchiveRecords: function (event) {
@@ -195,43 +203,7 @@ var KanbanController = BasicController.extend({
}
},
/**
- * @param {OdooEvent} event
- */
- _onColumnResequence: function (event) {
- this._resequenceRecords(event.target.db_id, event.data.ids);
- },
- /**
- * @param {OdooEvent} event
- */
- _onDeleteColumn: function (event) {
- var self = this;
- var column = event.target;
- var state = this.model.get(this.handle, {raw: true});
- var relatedModelName = state.fields[state.groupedBy[0]].relation;
- this.model
- .deleteRecords([column.db_id], relatedModelName)
- .done(function () {
- if (column.isEmpty()) {
- self.renderer.removeWidget(column);
- self._updateButtons();
- } else {
- self.reload();
- }
- });
- },
- /**
- * @param {OdooRevent} event
- */
- _onLoadMore: function (event) {
- var self = this;
- var column = event.target;
- this.model.loadMore(column.db_id).then(function (db_id) {
- var data = self.model.get(db_id);
- self.renderer.updateColumn(db_id, data);
- self._updateEnv();
- });
- },
- /**
+ * @private
* @param {OdooEvent} event
*/
_onButtonClicked: function (event) {
@@ -286,6 +258,65 @@ var KanbanController = BasicController.extend({
}
},
/**
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onColumnResequence: function (event) {
+ this._resequenceRecords(event.target.db_id, event.data.ids);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onDeleteColumn: function (event) {
+ var self = this;
+ var column = event.target;
+ var state = this.model.get(this.handle, {raw: true});
+ var relatedModelName = state.fields[state.groupedBy[0]].relation;
+ this.model
+ .deleteRecords([column.db_id], relatedModelName)
+ .done(function () {
+ if (column.isEmpty()) {
+ self.renderer.removeWidget(column);
+ self._updateButtons();
+ } else {
+ self.reload();
+ }
+ });
+ },
+ /**
+ * Loads the record of a given column (used in mobile, as the columns are
+ * lazy loaded)
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onLoadColumnRecords: function (event) {
+ var self = this;
+ this.model.loadColumnRecords(event.data.columnID).then(function (dbID) {
+ var data = self.model.get(dbID);
+ self.renderer.updateColumn(dbID, data);
+ self._updateEnv();
+ if (event.data.onSuccess) {
+ event.data.onSuccess();
+ }
+ });
+ },
+ /**
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onLoadMore: function (event) {
+ var self = this;
+ var column = event.target;
+ this.model.loadMore(column.db_id).then(function (db_id) {
+ var data = self.model.get(db_id);
+ self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
+ });
+ },
+ /**
+ * @private
* @param {OdooEvent} event
*/
_onQuickCreateRecord: function (event) {
@@ -330,12 +361,14 @@ var KanbanController = BasicController.extend({
}
},
/**
+ * @private
* @param {OdooEvent} event
*/
_onRecordDelete: function (event) {
this._deleteRecords([event.data.id]);
},
/**
+ * @private
* @param {OdooEvent} event
*/
_onResequenceColumn: function (event) {
@@ -347,6 +380,7 @@ var KanbanController = BasicController.extend({
});
},
/**
+ * @private
* @param {OdooEvent} event
*/
_onToggleColumn: function (event) {
@@ -360,6 +394,8 @@ var KanbanController = BasicController.extend({
},
/**
* @todo should simply use field_changed event...
+ *
+ * @private
* @param {OdooEvent} ev
*/
_onUpdateRecord: function (ev) {
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 04ee7012bc4..ca4924b6c65 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -125,6 +125,17 @@ var KanbanModel = BasicModel.extend({
return this._super(params);
},
/**
+ * Opens a given group and loads its <limit> first records
+ *
+ * @param {string} groupID
+ * @returns {Deferred}
+ */
+ loadColumnRecords: function (groupID) {
+ var dataPoint = this.localData[groupID];
+ dataPoint.isOpen = true;
+ return this.reload(groupID);
+ },
+ /**
* Load more records in a group.
*
* @param {string} groupID localID of the group
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
new file mode 100644
index 00000000000..e89d5c4b9a2
--- /dev/null
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
@@ -0,0 +1,172 @@
+odoo.define('web.KanbanRendererMobile', function (require) {
+"use strict";
+
+/**
+ * The purpose of this file is to improve the UX of grouped kanban views in
+ * mobile. It includes the KanbanRenderer (in mobile only) to only display one
+ * column full width, and enables the swipe to browse to the other columns.
+ * Moreover, records in columns are lazy-loaded.
+ */
+
+var config = require('web.config');
+var core = require('web.core');
+var KanbanRenderer = require('web.KanbanRenderer');
+
+var qweb = core.qweb;
+
+if (!config.isMobile) {
+ return;
+}
+
+KanbanRenderer.include({
+ events: _.extend({}, KanbanRenderer.prototype.events, {
+ 'click .o_kanban_mobile_tab': '_onMobileTabClicked',
+ }),
+ ANIMATE: true, // allows to disable animations for the tests
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.activeColumnIndex = 0; // index of the currently displayed column
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Displays the quick create record in the active column
+ */
+ addQuickCreate: function () {
+ this.widgets[this.activeColumnIndex].addQuickCreate();
+ },
+ /**
+ * Overrides to restore the left property and the scrollTop on the updated
+ * column, and to enable the swipe handlers
+ *
+ * @override
+ */
+ updateColumn: function (localID) {
+ var self = this;
+ var index = _.findIndex(this.widgets, {db_id: localID});
+ var $column = this.widgets[index].$el;
+ var left = $column.css('left');
+ var scrollTop = $column.scrollTop();
+ return this._super.apply(this, arguments).then(function () {
+ $column = self.widgets[index].$el;
+ $column.css({left: left});
+ $column.scrollTop(scrollTop); // required when clicking on 'Load More'
+ self._enableSwipe();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Enables swipe event on the current column
+ *
+ * @private
+ * @param {KanbanColumn} column
+ */
+ _enableSwipe: function () {
+ var self = this;
+ var currentColumn = this.widgets[this.activeColumnIndex];
+ currentColumn.$el.swipe({
+ swipeLeft: function () {
+ self._moveToGroup(self.activeColumnIndex + 1, self.ANIMATE);
+ },
+ swipeRight: function () {
+ self._moveToGroup(self.activeColumnIndex - 1, self.ANIMATE);
+ }
+ });
+ },
+ /**
+ * Moves to the given kanban column
+ *
+ * @private
+ * @param {integer} moveToIndex index of the column to move to
+ * @param {boolean} [animate=false] set to true to animate
+ * @returns {Deferred} resolved when the new current group has been loaded
+ * and displayed
+ */
+ _moveToGroup: function (moveToIndex, animate) {
+ var self = this;
+ if (moveToIndex < 0 || moveToIndex >= this.widgets.length) {
+ return $.when();
+ }
+ var def = $.Deferred();
+ this.activeColumnIndex = moveToIndex;
+ var column = this.widgets[this.activeColumnIndex];
+ this.trigger_up('kanban_load_records', {
+ columnID: column.db_id,
+ onSuccess: function () {
+ // update the columns and tabs positions (optionally with an animation)
+ var updateFunc = animate ? 'animate' : 'css';
+ self.$('.o_kanban_mobile_tab').removeClass('o_current');
+ _.each(self.widgets, function (column, index) {
+ var $column = self.$('.o_kanban_group[data-id=' + column.id + ']');
+ var $tab = self.$('.o_kanban_mobile_tab[data-id=' + column.id + ']');
+ if (index === moveToIndex - 1) {
+ $column[updateFunc]({left: '-100%'});
+ $tab[updateFunc]({left: '0%'});
+ } else if (index === moveToIndex + 1) {
+ $column[updateFunc]({left: '100%'});
+ $tab[updateFunc]({left: '100%'});
+ } else if (index === moveToIndex) {
+ $column[updateFunc]({left: '0%'});
+ $tab[updateFunc]({left: '50%'});
+ $tab.addClass('o_current');
+ } else if (index < moveToIndex) {
+ $column.css({left: '-100%'});
+ $tab[updateFunc]({left: '-100%'});
+ } else if (index > moveToIndex) {
+ $column.css({left: '100%'});
+ $tab[updateFunc]({left: '200%'});
+ }
+ });
+ def.resolve();
+ },
+ });
+ return def;
+ },
+ /**
+ * @override
+ * @private
+ */
+ _renderGrouped: function (fragment) {
+ var $tabs = $(qweb.render('KanbanView.MobileTabs', {
+ data: this.state.data,
+ }));
+ $tabs.appendTo(fragment);
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ * @private
+ */
+ _renderView: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ if (self.state.groupedBy.length) {
+ return self._moveToGroup(self.activeColumnIndex);
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {MouseEvent} event
+ */
+ _onMobileTabClicked: function (event) {
+ this._moveToGroup($(event.currentTarget).index(), true);
+ },
+});
+
+});
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 3e808de72e3..adb363695c7 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -3,6 +3,7 @@ odoo.define('web.KanbanView', function (require) {
var BasicView = require('web.BasicView');
var core = require('web.core');
+var config = require('web.config');
var KanbanModel = require('web.KanbanModel');
var KanbanRenderer = require('web.KanbanRenderer');
var KanbanController = require('web.KanbanController');
@@ -12,7 +13,6 @@ var _lt = core._lt;
var KanbanView = BasicView.extend({
accesskey: "k",
- className: "o_kanban",
display_name: _lt("Kanban"),
icon: 'fa-th-large',
mobile_friendly: true,
@@ -21,6 +21,7 @@ var KanbanView = BasicView.extend({
Controller: KanbanController,
Renderer: KanbanRenderer,
},
+ jsLibs: [],
viewType: 'kanban',
/**
@@ -32,7 +33,9 @@ var KanbanView = BasicView.extend({
var arch = viewInfo.arch;
this.loadParams.limit = this.loadParams.limit || 40;
- this.loadParams.openGroupByDefault = true;
+ // in mobile, columns are lazy-loaded, so set 'openGroupByDefault' to
+ // false so that they will won't be loaded by the initial load
+ this.loadParams.openGroupByDefault = config.isMobile ? false : true;
this.loadParams.type = 'list';
this.loadParams.groupBy = arch.attrs.default_group_by ? [arch.attrs.default_group_by] : (params.groupBy || []);
var progressBar;
@@ -59,7 +62,7 @@ var KanbanView = BasicView.extend({
this.rendererParams.column_options = {
editable: activeActions.group_edit,
deletable: activeActions.group_delete,
- group_creatable: activeActions.group_create,
+ group_creatable: activeActions.group_create && !config.isMobile,
quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(viewInfo),
hasProgressBar: !!progressBar,
};
@@ -73,6 +76,10 @@ var KanbanView = BasicView.extend({
this.controllerParams.readOnlyMode = false;
this.controllerParams.hasButtons = true;
+
+ if (config.isMobile) {
+ this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js');
+ }
},
//--------------------------------------------------------------------------
@@ -96,7 +103,9 @@ var KanbanView = BasicView.extend({
return JSON.parse(viewInfo.arch.attrs.quick_create);
}
return true;
- }
+ },
});
+
return KanbanView;
+
});
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 06058069ab6..5bd24044c39 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -729,6 +729,9 @@ ListRenderer.include({
this._moveToNextLine();
break;
case 'cancel':
+ // stop the original event (typically an ESCAPE keydown), to
+ // prevent from closing the potential dialog containing this list
+ ev.data.originalEvent.stopPropagation();
this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID,
});
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js
index 8b273ed5aac..82cca87f4df 100644
--- a/addons/web/static/src/js/widgets/domain_selector.js
+++ b/addons/web/static/src/js/widgets/domain_selector.js
@@ -636,7 +636,7 @@ var DomainLeaf = DomainNode.extend({
// Set list of values according to field type
this.selectionChoices = null;
if (selectedField.type === "boolean") {
- this.selectionChoices = [["1", "set (true)"], ["0", "not set (false)"]];
+ this.selectionChoices = [["1", _t("set (true)")], ["0", _t("not set (false)")]];
} else if (selectedField.type === "selection") {
this.selectionChoices = selectedField.selection;
}
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js
index 6607561a2bb..a8daa781119 100644
--- a/addons/web/static/src/js/widgets/model_field_selector.js
+++ b/addons/web/static/src/js/widgets/model_field_selector.js
@@ -200,7 +200,8 @@ var ModelFieldSelector = Widget.extend({
args: [
false,
["store", "searchable", "type", "string", "relation", "selection", "related"]
- ]
+ ],
+ context: this.getSession().user_context,
})
.then((function (fields) {
modelFieldsCache.cache[model] = sortFields(fields);
diff --git a/addons/web/static/src/js/widgets/switch_company_menu.js b/addons/web/static/src/js/widgets/switch_company_menu.js
index 872ebb54fdb..ac0b6f50738 100644
--- a/addons/web/static/src/js/widgets/switch_company_menu.js
+++ b/addons/web/static/src/js/widgets/switch_company_menu.js
@@ -56,4 +56,6 @@ var SwitchCompanyMenu = Widget.extend({
SystrayMenu.Items.push(SwitchCompanyMenu);
+return SwitchCompanyMenu;
+
});
diff --git a/addons/web/static/src/less/bootstrap_overridden.less b/addons/web/static/src/less/bootstrap_overridden.less
index 25c34229191..0582ec8c384 100644
--- a/addons/web/static/src/less/bootstrap_overridden.less
+++ b/addons/web/static/src/less/bootstrap_overridden.less
@@ -9,12 +9,6 @@
@brand-primary: @odoo-brand-primary;
@gray-lighter: @odoo-color-silver;
-// Define @gray-lighter-dark as a minor @gray-lighter variation used for design
-// purposes only (e.g. cards' borders). We avoid to overwrite the default
-// @gray-light because it is deeply used by bootstrap to define muted elements.
-@gray-lighter-dark: @odoo-color-silver-dark;
-@gray-lighter-darker: @odoo-color-silver-darker;
-
//== Titles
@font-size-h1: @odoo-font-size-base + 13px;
@font-size-h2: @odoo-font-size-base + 7px;
diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less
index 69e1153cc43..de4675d6701 100644
--- a/addons/web/static/src/less/fields.less
+++ b/addons/web/static/src/less/fields.less
@@ -91,19 +91,15 @@
.o-flex-flow(row, wrap);
.o-align-items(baseline);
- .o_tag_color_0 { background-color: @odoo-color-0; }
- .o_tag_color_1 { background-color: @odoo-color-1; }
- .o_tag_color_2 { background-color: @odoo-color-2; }
- .o_tag_color_3 { background-color: @odoo-color-3; }
- .o_tag_color_4 { background-color: @odoo-color-4; }
- .o_tag_color_5 { background-color: @odoo-color-5; }
- .o_tag_color_6 { background-color: @odoo-color-6; }
- .o_tag_color_7 { background-color: @odoo-color-7; }
- .o_tag_color_8 { background-color: @odoo-color-8; }
- .o_tag_color_9 { background-color: @odoo-color-9; }
- .o_tag_color_10 { background-color: @odoo-color-10; }
- .o_tag_color_11 { background-color: @odoo-color-11; }
- .o_tag_color_12 { background-color: @odoo-color-12; }
+ .o-tag-color(@size: 0) when (@size < @odoo-nb-ui-color) {
+ .o_tag_color_@{size} {
+ &, &::after {
+ background-color: ~"@{odoo-color-@{size}}";
+ }
+ }
+ .o-tag-color(@size + 1);
+ }
+ .o-tag-color();
> .o_field_widget {
.o-flex(1, 0, 100px);
@@ -123,30 +119,9 @@
max-width: 200px;
}
- > ul {
- @colored-tag-width: 45px;
- width: 5*@colored-tag-width + 3px;
- > li {
- float: left;
- color: black;
- > span {
- border-radius: 6px;
- width: @colored-tag-width;
- height: 30px;
- display: inline-block;
- border: 2px solid white;
- cursor: pointer;
- padding: 0;
-
- &:hover {
- opacity: 0.8;
- }
- }
- }
- > li:last-child {
- margin-top: 7px;
- cursor: auto;
- }
+ > .o_colorpicker > ul {
+ .o-kanban-colorpicker();
+ white-space: normal;
}
.o_delete {
diff --git a/addons/web/static/src/less/fields_extra.less b/addons/web/static/src/less/fields_extra.less
index 79d9628116d..541fef8ba47 100644
--- a/addons/web/static/src/less/fields_extra.less
+++ b/addons/web/static/src/less/fields_extra.less
@@ -6,7 +6,7 @@
// Required
.o_required_modifier {
&.o_input, .o_input {
- background-color: #D2D2FF;
+ background-color: #D2D2FF!important;
}
}
diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less
index 99903a30db0..2349ee5c0b0 100644
--- a/addons/web/static/src/less/form_view_extra.less
+++ b/addons/web/static/src/less/form_view_extra.less
@@ -68,6 +68,7 @@
// Some buttons only display text without using StatInfo template
.o_stat_info, > span { // contains the value and text
display: inline-block;
+ vertical-align: middle;
max-width: 70%;
line-height: 1.2;
diff --git a/addons/web/static/src/less/kanban_column_progressbar.less b/addons/web/static/src/less/kanban_column_progressbar.less
index c828d5d260f..86fe0853de2 100644
--- a/addons/web/static/src/less/kanban_column_progressbar.less
+++ b/addons/web/static/src/less/kanban_column_progressbar.less
@@ -1,3 +1,4 @@
+@o-kanban-counter-header-height: @o-kanban-header-title-height*0.6;
.o-kanban-css-filter(@class, @accent-color) {
// Provide CSS reordering and highlight
@@ -34,12 +35,22 @@
.o_kanban_record:not(.oe_kanban_card_@{class}) {
order: 3;
- opacity: 0.5;
+ .o-hover-opacity(0.5);
box-shadow: none;
+ }
+ }
+}
- &:hover {
- opacity: 1;
- }
+// If columns has the progressbar, adapt the layout
+.o_kanban_view .o_kanban_group.o_kanban_has_progressbar {
+ > .o_kanban_header .o_kanban_header_title {
+ height: @o-kanban-counter-header-height;
+ margin-top: 5px;
+ }
+
+ &.o_kanban_no_records {
+ .o_kanban_counter {
+ opacity: 0.3;
}
}
}
@@ -48,12 +59,12 @@
position: relative;
.o-flex-display();
.o-align-items(center);
- height: 30px;
+ .o-transition(opacity, 0.3s);
margin-bottom: @o-kanban-record-margin*2;
> .o_kanban_counter_progress {
width: 76%;
- height: @font-size-base;
+ height: @font-size-small;
margin-bottom: 0;
background-color: @gray-lighter-dark;
box-shadow: none;
@@ -79,11 +90,20 @@
width: 21%;
margin-left: 3%;
color: @headings-color;
- font-weight: bold;
text-align: right;
+ .o-transform-origin(right, center);
&.o_kanban_grow {
- .o-animation(grow, 400ms, ease-in, 200ms);
+ .o-animation(grow, 1s, ease);
+ }
+
+ &.o_kanban_grow_huge {
+ .o-animation(grow_huge, 1s, ease);
+ }
+
+ // Target currency icon
+ > span {
+ margin-left: 2px;
}
}
}
@@ -110,8 +130,20 @@
0% {
transform: scale3d(1, 1, 1);
}
- 50% {
- transform: scale3d(1.5, 1.5, 1.5);
+ 30% {
+ transform: scale3d(1.1, 1.1, 1.1);
+ }
+ 100% {
+ transform: scale3d(1, 1, 1);
+ }
+}
+
+@keyframes grow_huge {
+ 0% {
+ transform: scale3d(1, 1, 1);
+ }
+ 30% {
+ transform: scale3d(1.3, 1.3, 1.3);
}
100% {
transform: scale3d(1, 1, 1);
diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less
index 33baa09cc33..b45df8b5a03 100644
--- a/addons/web/static/src/less/kanban_view.less
+++ b/addons/web/static/src/less/kanban_view.less
@@ -4,6 +4,8 @@
@o-kanban-default-record-width: 300px;
@o-kanban-small-record-width: 240px;
+@o-kanban-header-title-height: 50px;
+
@o-kanban-image-width: 64px;
@o-kanban-inside-vgutter: 8px;
@o-kanban-inside-hgutter: 8px;
@@ -15,51 +17,48 @@
// ------- Kanban grouped mixins -------
.o-kanban-icon(@base-opacity: 0.5) {
display: block;
- line-height: 50px;
+ line-height: @o-kanban-header-title-height;
text-align: center;
color: @odoo-main-text-color;
- font-size: 15px;
+ font-size: @font-size-small;
cursor: pointer;
- .o-kanban-hover-effect(@base-opacity);
+ .o-hover-opacity(@base-opacity);
}
-.o-kanban-hover-effect(@base-opacity) when (@base-opacity < 1) {
- opacity: @base-opacity;
- &:hover {
- opacity: 1;
- }
-}
-.o-kanban-tag-color(@size) when (@size >= 0) {
- .o-kanban-tag-color(@size - 1);
+.o-kanban-tag-color(@size: 1) when (@size < @odoo-nb-ui-color) {
+ // Note: the 0 color is not defined as it is the 'no color' for kanban
&.o_tag_color_@{size} span {
background-color: ~"@{odoo-color-@{size}}";
}
+ .o-kanban-tag-color(@size + 1);
}
-.o-kanban-record-color(@size) when (@size > 0) {
- .o-kanban-record-color(@size - 1);
+.o-kanban-record-color(@size: 1) when (@size < @odoo-nb-ui-color) {
+ // Note: the 0 color is not defined as it is the 'no color' for kanban
.oe_kanban_color_@{size}::after {
background-color: ~"@{odoo-color-@{size}}";
}
+ .o-kanban-record-color(@size + 1);
}
.o-kanban-slim-col() {
position: relative;
.o-flex(0, 0, auto);
margin: 0;
- padding: 0 @o-kanban-group-padding * 0.5;
+ padding: 0 floor(@o-kanban-group-padding * 0.7);
cursor: pointer;
}
.o-kanban-header-title() {
.o-flex-display();
- height: 50px;
+ .o-align-items(center);
+ height: @o-kanban-header-title-height;
line-height: 2.2;
color: @headings-color;
}
.o-kanban-v-title() {
- .o-position-absolute(@o-kanban-inside-vgutter * 2, @left: -@o-kanban-inside-vgutter * 1.3);
+ .o-position-absolute(@o-kanban-inside-vgutter * 2, @left: floor(-@o-kanban-inside-vgutter * 1.2));
.o-transform-origin(left, bottom, @z: 0);
.o-transform(rotate(90deg));
overflow: visible;
white-space: nowrap;
- font-size: 16px;
+ font-size: 15px;
}
// ------- Kanban records mixins -------
@@ -99,7 +98,7 @@
}
.o-kanban-colorpicker() {
max-width: 150px;
- padding: 3px @odoo-dropdown-hpadding;
+ padding: 3px (@odoo-dropdown-hpadding - @o-kanban-inner-hmargin) 3px @odoo-dropdown-hpadding;
> li {
display: inline-block;
@@ -130,6 +129,9 @@
.rotate(45deg);
background-color: red;
}
+ &::after {
+ background-color: white;
+ }
}
}
}
@@ -141,7 +143,7 @@
overflow-x: visible;
@media (max-width: @screen-xs-max) {
- padding: 0px;
+ padding: 0px!important;
overflow-x: auto;
}
@@ -164,7 +166,7 @@
margin: 2px 0 8px;
padding: 3px 6px;
color: @headings-color;
- font-weight: bold;
+ font-weight: 500;
}
}
@@ -306,8 +308,8 @@
font-size: 11px;
font-weight: 500;
background-color: transparent;
- .o-kanban-tag-color(12);
- .o-kanban-hover-effect(0.7);
+ .o-kanban-tag-color();
+ .o-hover-opacity(0.7);
span {
display: inline-block;
@@ -449,18 +451,13 @@
// Kanban Grouped Layout - Column default
.o_kanban_group {
.o-flex(0, 0, auto);
- padding: 0 @o-kanban-group-padding @o-kanban-group-padding;
- border: 1px solid @gray-lighter-dark;
- background-color: @gray-lighter;
- border-width: 0 1px;
- margin-left: -1px;
+ padding: 0 @o-kanban-group-padding*2 @o-kanban-group-padding @o-kanban-group-padding;
.o_kanban_header > .o_kanban_header_title {
.o-kanban-header-title();
.o_column_title {
.o-flex(1, 1, auto);
- padding-top: 5px;
.o-text-overflow();
font-size: 16px;
font-weight: 500;
@@ -605,7 +602,7 @@
margin: (@o-kanban-record-margin * 0.5) @o-kanban-record-margin;
@media (max-width: @screen-xs-max) {
- margin: 0 0 @o-kanban-record-margin 0;
+ margin: 0 0 -1px 0;
.o-flex(1, 0, 100%);
}
@@ -633,10 +630,5 @@
}
// Records colours
- .o-kanban-record-color(12);
-}
-
-// Introduced with new views (TODO check)
-.o_kanban {
- height: 100%; // required for grouped kanban views
+ .o-kanban-record-color();
}
diff --git a/addons/web/static/src/less/kanban_view_mobile.less b/addons/web/static/src/less/kanban_view_mobile.less
new file mode 100644
index 00000000000..72ba087e0c2
--- /dev/null
+++ b/addons/web/static/src/less/kanban_view_mobile.less
@@ -0,0 +1,51 @@
+@o-kanban-mobile-tabs-height: 40px;
+
+@media (max-width: @screen-xs-max) {
+ .o_kanban_view.o_kanban_grouped {
+ display: block;
+ position: relative;
+ overflow: hidden;
+
+ .o_kanban_mobile_tabs {
+ height: @o-kanban-mobile-tabs-height;
+ position: relative;
+ background-color: #5E5E5E;
+
+ .o_kanban_mobile_tab {
+ .o-position-absolute(@left: 100%);
+ .o-transform(translateX(-50%));
+ height: @o-kanban-mobile-tabs-height;
+ padding: 10px;
+ font-size: 14px;
+ color: white;
+
+ &.o_current {
+ font-weight: bold;
+ border-bottom: 3px solid @odoo-brand-optional;
+ }
+ .o_column_title {
+ white-space: nowrap;
+ }
+ }
+ }
+
+ &.o_kanban_small_column .o_kanban_group:not(.o_column_folded) {
+ .o-position-absolute(@top: @o-kanban-mobile-tabs-height, @left: 0, @bottom: 0);
+ overflow-x: scroll;
+ width: 100%;
+ padding: 0;
+ margin-left: 0; // override the margin-left: -1px of the desktop mode
+ border: none;
+
+ .o_kanban_header {
+ display: none;
+ }
+ .o_kanban_record, .o_kanban_quick_create {
+ border: none;
+ border-bottom: 1px solid lightgray;
+ padding: 10px 16px;
+ margin: 0;
+ }
+ }
+ }
+}
diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less
index bf3a034f705..d541bc8cfc3 100644
--- a/addons/web/static/src/less/list_view.less
+++ b/addons/web/static/src/less/list_view.less
@@ -130,6 +130,14 @@
.o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) {
position: relative;
+ // The in-edition elements will overflow other elements (the rows below,
+ // the content below the list view, ...). As those might be using
+ // position: relative, the in-edition elements have to define a z-index.
+ // Technically, creating a stacking context for a meaningful parent
+ // (maybe the .o_content) might be required for safety but we should
+ // consider this later if needed.
+ z-index: 1;
+
// Field widgets override
> .o_field_widget {
.o-position-absolute(0, 0, auto, 0);
@@ -139,11 +147,11 @@
white-space: nowrap;
}
- .o_field_widget, .badge { // Many2Many tags
- display: block;
- float: left;
- white-space: normal;
- max-width: 100%; // Fix a chrome bug where badge would not break on multiple lines
+ &.o_field_many2manytags > .badge {
+ &, > .o_badge_text {
+ white-space: normal;
+ max-width: 100%; // Fix a chrome bug where badge would not break on multiple lines
+ }
}
}
> .o_field_translate {
diff --git a/addons/web/static/src/less/variables.less b/addons/web/static/src/less/variables.less
index cc3f5b66ab9..010c972f29b 100644
--- a/addons/web/static/src/less/variables.less
+++ b/addons/web/static/src/less/variables.less
@@ -15,7 +15,14 @@
@odoo-color-silver-dark: #E5E5E5;
@odoo-color-silver-darker: #d9d7d7;
-@odoo-color-pink: #875A7B;
+// Define @gray-lighter-dark as a minor bootstrap @gray-lighter variation used
+// for design purposes only (e.g. cards' borders). We avoid to overwrite the
+// default @gray-light because it is deeply used by bootstrap to define muted
+// elements.
+@gray-lighter-dark: @odoo-color-silver-dark;
+@gray-lighter-darker: @odoo-color-silver-darker;
+
+@odoo-color-pink: #875A7B; // Enterprise @odoo-brand-primary
@odoo-main-color-muted: #a8a8a8;
@odoo-main-text-color: #4c4c4c;
@@ -34,8 +41,9 @@
@odoo-tooltip-color: white;
@odoo-tooltip-arrow-color: black;
-// UI custom colors for tags, kanban records' colors, ...
-@odoo-color-0: #00C7AB;
+// UI custom colors for tags, kanban records' colors, ...)
+@odoo-nb-ui-color: 12;
+@odoo-color-0: @gray-light;
@odoo-color-1: #F06050;
@odoo-color-2: #F4A460;
@odoo-color-3: #F7CD1F;
@@ -47,7 +55,6 @@
@odoo-color-9: #D6145F;
@odoo-color-10: #30C381;
@odoo-color-11: #9365B8;
-@odoo-color-12: #B05048;
// Layout
@screen-xs-min: 475px;
diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less
index 701c8e8b250..ee82aa5d9e5 100644
--- a/addons/web/static/src/less/views.less
+++ b/addons/web/static/src/less/views.less
@@ -44,3 +44,13 @@
background-color: @brand-danger;
}
}
+
+.o_btn-link-as-button {
+ padding: 2px;
+ font-size:12px;
+
+ & > a {
+ margin-bottom: -4px !important;
+ margin-left: 3px;
+ }
+}
diff --git a/addons/web/static/src/less/webclient.less b/addons/web/static/src/less/webclient.less
index fd276e581d7..dfc26b11f5a 100644
--- a/addons/web/static/src/less/webclient.less
+++ b/addons/web/static/src/less/webclient.less
@@ -86,37 +86,53 @@ div.o_boolean_toggle {
width: 32px !important;
height: 18px;
- > input {
- display: none;
- }
+ & > label {
+ & > input {
+ display: none;
+ }
- .slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: #a0a0a0;
- border-radius: 24px;
-
- &:before {
+ & > .slider {
position: absolute;
- content: "";
- height: 12px;
- width: 12px;
- left: 3px;
- bottom: 3px;
- background-color: white;
- border-radius: 50%;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #a0a0a0;
+ border-radius: 24px;
+
+ &:before {
+ position: absolute;
+ content: "";
+ height: 12px;
+ width: 12px;
+ left: 3px;
+ bottom: 3px;
+ background-color: white;
+ border-radius: 50%;
+ }
}
- }
- > input:checked + .slider {
- background-color: @odoo-brand-optional;
+ & > input:checked + .slider {
+ background-color: @odoo-brand-optional;
- &:before {
- .translate(14px; 0px);
+ &:before {
+ .translate(14px; 0px);
+ }
}
}
}
+
+// Full bg colors (bootstrap extension)
+.bg-success-full {
+ background-color: @brand-success;
+}
+.bg-warning-full {
+ background-color: @brand-warning;
+}
+.bg-danger-full {
+ background-color: @brand-danger;
+}
+.bg-info-full {
+ background-color: @brand-info;
+}
diff --git a/addons/web/static/src/less/webclient_extra.less b/addons/web/static/src/less/webclient_extra.less
index 7c337352618..2f4e03e9479 100644
--- a/addons/web/static/src/less/webclient_extra.less
+++ b/addons/web/static/src/less/webclient_extra.less
@@ -4,8 +4,17 @@
div.o_checkbox {
display: inline-block;
vertical-align: sub;
- > input + span {
- display: none;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ input[type="checkbox"] {
+ position: inherit;
+ margin-left: 0;
+ }
+ > label {
+ padding-left: 0px;
+ > input + span {
+ display: none;
+ }
}
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 446ff26c2df..62d40aa62ec 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -821,24 +821,25 @@
</t>
<t t-name="FieldMany2ManyTag">
<t t-foreach="elements" t-as="el">
- <span t-attf-class="badge dropdown o_tag_color_#{el[colorField] || 10}" t-att-data-color="el[colorField] || 10" t-att-data-index="el_index" t-att-data-id="el.id">
+ <t t-set="color" t-value="el[colorField] || 0"/>
+ <span t-attf-class="badge dropdown o_tag_color_#{color}" t-att-data-color="color" t-att-data-index="el_index" t-att-data-id="el.id">
<span class="o_badge_text"><t t-raw="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
</t>
</t>
<t t-name="FieldMany2ManyTag.colorpicker">
- <ul class="o_colorpicker dropdown-menu" role="menu">
- <li t-foreach="10" t-as="color">
- <span t-att-data-id="tag_id" t-att-data-color="color" t-attf-class="o_tag_color_#{color}"/>
- </li>
- <li>
- <span t-att-data-id="tag_id" t-att-data-color="10" class="o_tag_color_10"/>
- </li>
- <li>
- Not shown in kanban
- </li>
- </ul>
+ <div class="o_colorpicker dropdown-menu" role="menu">
+ <ul>
+ <li t-foreach="12" t-as="color">
+ <a href="#"
+ t-att-data-id="tag_id"
+ t-att-data-color="color"
+ t-attf-class="o_tag_color_#{color}"
+ t-att-title="color === 0 ? 'Not shown in kanban' : ''"/>
+ </li>
+ </ul>
+ </div>
</t>
<t t-name="ProgressBar">
<div class="o_progressbar">
@@ -1309,16 +1310,20 @@
<span class="oe_topbar_name"/>
</a>
<ul class="dropdown-menu" role="menu">
- <li><a href="#" data-menu="documentation">Documentation</a></li>
- <li><a href="#" data-menu="support">Support</a></li>
- <li class="divider"/>
- <li><a href="#" data-menu="settings">Preferences</a></li>
- <li><a href="#" data-menu="account">My Odoo.com account</a></li>
- <li><a href="#" data-menu="logout">Log out</a></li>
+ <t t-call="UserMenu.Actions"/>
</ul>
</li>
</t>
+<t t-name="UserMenu.Actions">
+ <li><a href="#" data-menu="documentation">Documentation</a></li>
+ <li><a href="#" data-menu="support">Support</a></li>
+ <li class="divider"/>
+ <li><a href="#" data-menu="settings">Preferences</a></li>
+ <li><a href="#" data-menu="account">My Odoo.com account</a></li>
+ <li><a href="#" data-menu="logout">Log out</a></li>
+</t>
+
<t t-name="SwitchCompanyMenu">
<li class="o_switch_company_menu">
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#">
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index acde7fcc739..1cf5e6d34ec 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -9,7 +9,7 @@
</t>
<t t-name="KanbanView.Group">
- <div class="o_kanban_group" t-att-data-id="widget.id">
+ <div t-attf-class="o_kanban_group#{widget.data_records.length == 0 ? ' o_kanban_no_records' : ''}" t-att-data-id="widget.id">
<div class="o_kanban_header">
<div class="o_kanban_header_title" t-att-title="widget.data_records.length + ' records'" data-delay="500">
<span class="o_column_title"><t t-esc="widget.title"/></span>
@@ -96,14 +96,27 @@
<div class="o_kanban_counter">
<div class="o_kanban_counter_progress progress">
<t t-foreach="widget.colors" t-as="color">
- <t t-set="count" t-value="widget.subgroupCounts[color] || 0"/>
+ <t t-set="count" t-value="widget.subgroupCounts and widget.subgroupCounts[color] or 0"/>
<div t-attf-class="progress-bar transition-off bg-#{color_value}-full#{count ? ' o_bar_has_records' : ''}#{widget.activeFilter ? ' active progress-bar-striped' : ''}"
t-att-data-filter="color"
t-attf-data-original-title="#{count} #{color}"
t-attf-style="width: #{count ? (count * 100 / widget.groupCount) : 0}%;"/>
</t>
</div>
- <div class="o_kanban_counter_side"><t t-esc="widget.totalCounterValue"/></div>
+ <div class="o_kanban_counter_side"><b><t t-esc="widget.totalCounterValue || 0"/></b></div>
</div>
</t>
+
+<!-- Mobile templates -->
+
+<t t-name="KanbanView.MobileTabs">
+ <div class="o_kanban_mobile_tabs">
+ <t t-foreach="data" t-as="group">
+ <div class="o_kanban_mobile_tab" t-att-data-id="group.res_id">
+ <span class="o_column_title"><t t-esc="group.value"/></span>
+ </div>
+ </t>
+ </div>
+</t>
+
</template>