diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-09-28 15:01:11 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-09-28 15:01:11 +0200 |
| commit | 1512c1b22993ba66a2cfff36bb92b8babca4cc95 (patch) | |
| tree | 62fe209f48ffc5b7951b307fd7e327d17c8b1165 /addons/web/static/src | |
| parent | 63a4370afd3377de4308349283b981fac748e36c (diff) | |
| parent | 9f9a7fdd0d70c1edda94fc546916c9e3516477ad (diff) | |
[MERGE] forward port branch saas-18 up to 9f9a7fdd0d7
Diffstat (limited to 'addons/web/static/src')
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> |
