diff options
Diffstat (limited to 'addons/web/static/src')
25 files changed, 337 insertions, 155 deletions
diff --git a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js index dc65d2ea610..4913d5a6a80 100644 --- a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js +++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js @@ -45,13 +45,21 @@ odoo.define('web.KeyboardNavigationMixin', function (require) { var accesskeyElements = $(document).find('[accesskey]').filter(':visible'); _.each(accesskeyElements, function (elem) { var overlay = $(_.str.sprintf("<div class='o_web_accesskey_overlay'>%s</div>", $(elem).attr('accesskey').toUpperCase())); + + var $overlayParent; if (elem.tagName.toUpperCase() === "INPUT") { - // special case for the search input that has an access key defined. We cannot set the overlay on the input itself, only on its parent - overlay.appendTo($(elem).parent().css('position', 'relative')); + // special case for the search input that has an access key + // defined. We cannot set the overlay on the input itself, + // only on its parent. + $overlayParent = $(elem).parent(); + } else { + $overlayParent = $(elem); } - else { - overlay.appendTo($(elem).css('position', 'relative')); + + if ($overlayParent.css('position') !== 'absolute') { + $overlayParent.css('position', 'relative'); } + overlay.appendTo($overlayParent); }); }, /** diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js index 7f60e2ea44f..8722724c9a6 100644 --- a/addons/web/static/src/js/chrome/menu.js +++ b/addons/web/static/src/js/chrome/menu.js @@ -38,7 +38,7 @@ var Menu = Widget.extend({ }); // Bus event - core.bus.on('change_menu_section', this, this._onChangeMenuSection); + core.bus.on('change_menu_section', this, this.change_menu_section); }, start: function () { var self = this; @@ -79,6 +79,33 @@ var Menu = Widget.extend({ return this._super.apply(this, arguments); }, + change_menu_section: function (primary_menu_id) { + if (!this.$menu_sections[primary_menu_id]) { + this._updateMenuBrand(); + return; // unknown menu_id + } + + if (this.current_primary_menu === primary_menu_id) { + return; // already in that menu + } + + if (this.current_primary_menu) { + this.$menu_sections[this.current_primary_menu].detach(); + } + + // Get back the application name + for (var i = 0; i < this.menu_data.children.length; i++) { + if (this.menu_data.children[i].id === primary_menu_id) { + this._updateMenuBrand(this.menu_data.children[i].name); + break; + } + } + + this.$menu_sections[primary_menu_id].appendTo(this.$section_placeholder); + this.current_primary_menu = primary_menu_id; + + core.bus.trigger('resize'); + }, _trigger_menu_clicked: function (menu_id, action_id) { this.trigger_up('menu_clicked', { id: menu_id, @@ -86,6 +113,22 @@ var Menu = Widget.extend({ previous_menu_id: this.current_secondary_menu || this.current_primary_menu, }); }, + /** + * Updates the name of the app in the menu to the value of brandName. + * If brandName is falsy, hides the menu and its sections. + * + * @private + * @param {brandName} string + */ + _updateMenuBrand: function (brandName) { + if (brandName) { + this.$menu_brand_placeholder.text(brandName).show(); + this.$section_placeholder.show(); + } else { + this.$menu_brand_placeholder.hide() + this.$section_placeholder.hide(); + } + }, _on_secondary_menu_click: function (menu_id, action_id) { var self = this; @@ -172,36 +215,6 @@ var Menu = Widget.extend({ }, /** * @private - * @param {integer} primaryMenuID - */ - _onChangeMenuSection: function (primaryMenuID) { - if (!this.$menu_sections[primaryMenuID]) { - return; // unknown menu_id - } - - if (this.current_primary_menu === primaryMenuID) { - return; // already in that menu - } - - if (this.current_primary_menu) { - this.$menu_sections[this.current_primary_menu].detach(); - } - - // Get back the application name - for (var i = 0; i < this.menu_data.children.length; i++) { - if (this.menu_data.children[i].id === primaryMenuID) { - this.$menu_brand_placeholder.text(this.menu_data.children[i].name); - break; - } - } - - this.$menu_sections[primaryMenuID].appendTo(this.$section_placeholder); - this.current_primary_menu = primaryMenuID; - - core.bus.trigger('resize'); - }, - /** - * @private * @param {MouseEvent} ev */ _onMouseOverMenu: function (ev) { diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index 8b725423d0f..0688adc362f 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -151,9 +151,7 @@ return AbstractWebClient.extend({ var action = self.action_manager.getCurrentAction(); if (action) { var menu_id = self.menu.action_id_to_primary_menu_id(action.id); - if (menu_id) { - core.bus.trigger('change_menu_section', menu_id); - } + core.bus.trigger('change_menu_section', menu_id); } } }); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 12b7b8f302c..b324d6f2efa 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -500,6 +500,10 @@ var FieldDate = InputField.extend({ this._super.apply(this, arguments); // use the session timezone when formatting dates this.formatOptions.timezone = true; + this.datepickerOptions = _.defaults( + this.nodeOptions.datepicker || {}, + {defaultDate: this.value} + ); }, /** * In edit mode, instantiates a DateWidget datepicker and listen to changes. @@ -583,13 +587,7 @@ var FieldDate = InputField.extend({ * @private */ _makeDatePicker: function () { - return new datepicker.DateWidget( - this, - _.defaults( - this.nodeOptions.datepicker || {}, - {defaultDate: this.value} - ) - ); + return new datepicker.DateWidget(this, this.datepickerOptions); }, /** @@ -607,6 +605,17 @@ var FieldDate = InputField.extend({ var FieldDateTime = FieldDate.extend({ supportedFieldTypes: ['datetime'], + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + if (this.value) { + var offset = this.getSession().getTZOffset(this.value); + var displayedValue = this.value.clone().add(offset, 'minutes'); + this.datepickerOptions.defaultDate = displayedValue; + } + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -637,10 +646,8 @@ var FieldDateTime = FieldDate.extend({ * @private */ _makeDatePicker: function () { - var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes'); - return new datepicker.DateTimeWidget(this, {defaultDate: value}); + return new datepicker.DateTimeWidget(this, this.datepickerOptions); }, - /** * Set the datepicker to the right value rather than the default one. * @@ -1171,6 +1178,9 @@ var HandleWidget = AbstractField.extend({ var FieldEmail = InputField.extend({ className: 'o_field_email', + events: _.extend({}, InputField.prototype.events, { + 'click': '_onClick', + }), prefix: 'mailto', supportedFieldTypes: ['char'], @@ -1211,7 +1221,21 @@ var FieldEmail = InputField.extend({ this.$el.text(this.value) .addClass('o_form_uri o_text_overflow') .attr('href', this.prefix + ':' + this.value); - } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Prevent the URL click from opening the record (when used on a list). + * + * @private + * @param {MouseEvent} ev + */ + _onClick: function (ev) { + ev.stopPropagation(); + }, }); var FieldPhone = FieldEmail.extend({ @@ -1238,6 +1262,9 @@ var FieldPhone = FieldEmail.extend({ var UrlWidget = InputField.extend({ className: 'o_field_url', + events: _.extend({}, InputField.prototype.events, { + 'click': '_onClick', + }), supportedFieldTypes: ['char'], /** @@ -1279,7 +1306,21 @@ var UrlWidget = InputField.extend({ .addClass('o_form_uri o_text_overflow') .attr('target', '_blank') .attr('href', this.value); - } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Prevent the URL click from opening the record (when used on a list). + * + * @private + * @param {MouseEvent} ev + */ + _onClick: function (ev) { + ev.stopPropagation(); + }, }); var CopyClipboard = { @@ -1363,7 +1404,7 @@ var AbstractFieldBinary = AbstractField.extend({ 'click .o_select_file_button': function () { this.$('.o_input_file').click(); }, - 'click .o_clear_file_button': 'on_clear', + 'click .o_clear_file_button': '_onClearClick', }), init: function (parent, name, record) { this._super.apply(this, arguments); @@ -1449,11 +1490,33 @@ var AbstractFieldBinary = AbstractField.extend({ }); } }, - on_clear: function () { + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Clear the file input + * + * @private + */ + _clearFile: function (){ this.set_filename(''); this._setValue(false); this._render(); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * On "clear file" button click + * + * @param {MouseEvent} ev + * @private + */ + _onClearClick: function (ev) { + this._clearFile(); + }, }); var FieldBinaryImage = AbstractFieldBinary.extend({ @@ -1510,7 +1573,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ this.$('> img').remove(); this.$el.prepend($img); $img.on('error', function () { - self.on_clear(); + self._clearFile(); $img.attr('src', self.placeholder); self.do_warn(_t("Image"), _t("Could not display the selected image.")); }); @@ -1623,19 +1686,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({ }, /** * @private + * @param {string} [fileURI] file URI if specified * @returns {string} the pdf viewer URI */ - _getURI: function () { - var queryObj = { - model: this.model, - field: this.name, - id: this.res_id, - }; + _getURI: function (fileURI) { var page = this.recordData[this.name + '_page'] || 1; - var queryString = $.param(queryObj); - var url = encodeURIComponent('/web/image?' + queryString); + if (!fileURI) { + var queryObj = { + model: this.model, + field: this.name, + id: this.res_id, + }; + var queryString = $.param(queryObj); + fileURI = '/web/image?' + queryString + } + fileURI = encodeURIComponent(fileURI); var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file='; - return viewerURL + url + '#page=' + page; + return viewerURL + fileURI + '#page=' + page; }, /** * @private @@ -1679,14 +1746,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({ */ on_file_change: function (ev) { this._super.apply(this, arguments); + var files = ev.target.files; + if (!files || files.length === 0) { + return; + } + // TOCheck: is there requirement to fallback on FileReader if browser don't support URL + var fileURI = URL.createObjectURL(files[0]); if (this.PDFViewerApplication) { - var files = ev.target.files; - if (!files || files.length === 0) { - return; - } - var file = files[0]; - // TOCheck: is there requirement to fallback on FileReader if browser don't support URL - this.PDFViewerApplication.open(URL.createObjectURL(file), 0); + this.PDFViewerApplication.open(fileURI, 0); + } else { + this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI)); } }, /** diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index e27128a1e3b..bf46c538c23 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -2584,10 +2584,12 @@ var FieldSelectionBadge = FieldSelection.extend({ * a FieldMany2one for its value. * Its intern representation is similar to the many2one (a datapoint with a * `name_get` as data). + * Note that there is some logic to support char field because of one use in our + * codebase, but this use should be removed along with this note. */ var FieldReference = FieldMany2One.extend({ specialData: "_fetchSpecialReference", - supportedFieldTypes: ['char', 'reference'], + supportedFieldTypes: ['reference'], template: 'FieldReference', events: _.extend({}, FieldMany2One.prototype.events, { 'change select': '_onSelectionChange', diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js index cc87305a6a2..a0d2720604b 100644 --- a/addons/web/static/src/js/services/notification_service.js +++ b/addons/web/static/src/js/services/notification_service.js @@ -58,6 +58,8 @@ var NotificationService = AbstractService.extend({ * Note that this method does not wait for the appendTo method to complete. * * @param {Object} params + * @param {function} [params.Notification] javascript class of a notification + * to instantiate by default use 'web.Notification' * @param {string} params.title notification title * @param {string} params.message notification main message * @param {string} params.type 'notification' or 'warning' @@ -78,7 +80,8 @@ var NotificationService = AbstractService.extend({ this.$el = $('<div class="o_notification_manager"/>'); this.$el.prependTo('body'); } - var notification = this.notifications[++id] = new Notification(this, params); + var NotificationWidget = params.Notification || Notification; + var notification = this.notifications[++id] = new NotificationWidget(this, params); notification.appendTo(this.$el); return id; }, diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 7705ee5972e..f3277d394a3 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -308,3 +308,55 @@ var AbstractView = Class.extend({ return AbstractView; }); + +odoo.define('web.viewUtils', function () { +"use strict"; + +/** + * FIXME: move this module to its own file in master + */ + +var utils = { + /** + * Returns the value of a group dataPoint, i.e. the value of the groupBy + * field for the records in that group. + * + * @param {Object} group dataPoint of type list, corresponding to a group + * @param {string} groupByField the name of the groupBy field + * @returns {string | integer | false} + */ + getGroupValue: function (group, groupByField) { + var groupedByField = group.fields[groupByField]; + switch (groupedByField.type) { + case 'many2one': + return group.res_id || false; + case 'selection': + var descriptor = _.findWhere(groupedByField.selection, group.value); + return descriptor && descriptor[0]; + default: + return group.value; + } + }, + /** + * States whether or not the quick create feature is available for the given + * datapoint, depending on its groupBy field. + * + * @param {Object} list dataPoint of type list + * @returns {Boolean} true iff the kanban quick create feature is available + */ + isQuickCreateEnabled: function (list) { + var groupByField = list.groupedBy[0] && list.groupedBy[0].split(':')[0]; + if (!groupByField) { + return false; + } + var availableTypes = ['char', 'boolean', 'many2one', 'selection']; + if (!_.contains(availableTypes, list.fields[groupByField].type)) { + return false; + } + return true; + }, +}; + +return utils; + +}); 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 f6bf46aed68..de879018ad5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -398,8 +398,14 @@ var BasicModel = AbstractModel.extend({ // by 1 (at least until there is a gap in the numbering). // We don't do it now because it's not an important case. + // However, we can for sure increment by 1 if we are on the last page. + var handleFieldValue = computedList.data[index].data[handleField]; - handleFieldValue = position !== 'top' ? handleFieldValue : handleFieldValue - 1; + if (position === 'top') { + handleFieldValue--; + } else if (list.count <= list.offset + list.limit - (list.tempLimitIncrement || 0)) { + handleFieldValue++; + } return { field: handleField, @@ -1010,27 +1016,51 @@ var BasicModel = AbstractModel.extend({ route: '/web/dataset/resequence', params: params, }) - .then(function () { - var offset = options.offset ? options.offset : 0; - var old_data = data.data.slice(); - data.data = _.sortBy(data.data, function (d) { - if (_.contains(resIDs, self.localData[d].res_id)) { - return _.indexOf(resIDs, self.localData[d].res_id) + offset; - } else { - return _.indexOf(old_data, d); - } - }); - data.res_ids = []; - _.each(data.data, function (d) { - var dataPoint = self.localData[d]; - if (dataPoint.type === 'record') { - data.res_ids.push(dataPoint.res_id); - } else { - data.res_ids = data.res_ids.concat(dataPoint.res_ids); + .then(function (wasResequenced) { + if (!wasResequenced) { + // the field on which the resequence was triggered does not + // exist, so no resequence happened server-side + return $.when(); + } + var field = params.field ? params.field : 'sequence'; + + return self._rpc({ + model: modelName, + method: 'read', + args: [resIDs, [field]], + }).then(function (records) { + if (data.data.length) { + var dataType = self.localData[data.data[0]].type; + if (dataType === 'record') { + _.each(data.data, function (dataPoint) { + var recordData = self.localData[dataPoint].data; + var inRecords = _.findWhere(records, {id: recordData.id}); + if (inRecords) { + recordData[field] = inRecords[field]; + } + }); + data.data = _.sortBy(data.data, function (d) { + return self.localData[d].data[field]; + }); + } + if (dataType === 'list') { + data.data = _.sortBy(data.data, function (d) { + return _.indexOf(resIDs, self.localData[d].res_id) + }); + } } - }); - self._updateParentResIDs(data); - return parentID; + data.res_ids = []; + _.each(data.data, function (d) { + var dataPoint = self.localData[d]; + if (dataPoint.type === 'record') { + data.res_ids.push(dataPoint.res_id); + } else { + data.res_ids = data.res_ids.concat(dataPoint.res_ids); + } + }); + self._updateParentResIDs(data); + return parentID; + }) }); }, /** diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 7bc5e95bfc8..9e1cde2aada 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -225,7 +225,7 @@ var BasicView = AbstractView.extend({ attrs.options = attrs.options ? pyUtils.py_eval(attrs.options) : {}; } - if (attrs.on_change && !field.onChange) { + if (attrs.on_change && attrs.on_change !== "0" && !field.onChange) { field.onChange = "1"; } @@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({ if (!(dependency_name in fields)) { fields[dependency_name] = dependency_dict; } + + if (fv.fields && !(dependency_name in fv.fields)) { + fv.fields[dependency_name] = dependency_dict; + } } } return false; 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 e76a487b73b..c2c90ea7e62 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -77,9 +77,9 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, { self.model.get(recordID), { mode: 'edit', - can_create: false, attrs: { - 'placeholder': _.str.sprintf(_t("Add %s"), self.title), + placeholder: _.str.sprintf(_t("Add %s"), self.title), + can_create: false }, }); }); diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index 190fbe88ad2..c7a32b72ba9 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -995,10 +995,7 @@ var FormRenderer = BasicRenderer.extend({ * @param {OdooEvent} ev */ _onNavigationMove: function (ev) { - if (ev.data.direction !== "cancel") { - ev.stopPropagation(); - } - + ev.stopPropagation(); var index; if (ev.data.direction === "next") { index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target); diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index f923ade149b..0517e14c876 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -341,7 +341,6 @@ return AbstractRenderer.extend({ var data = []; var ticksLabels = []; - var tickValues = []; var measure = this.state.fields[this.state.measure].string; var values; @@ -363,18 +362,6 @@ return AbstractRenderer.extend({ key: measure + ' (compare)', color: '#ff7f0e', }); - - if (this.state.comparisonData.length > graphData.length) { - tickValues = this.state.comparisonData.map(function (d, i) { - return i; - }); - } - } - - if (!tickValues.length) { - tickValues = graphData.map(function (d, i) { - return i; - }); } for (i = 0; i < graphData.length; i++) { @@ -403,7 +390,6 @@ return AbstractRenderer.extend({ for (var i = 0; i < this.state.data.length; i++) { if (graphData[i].labels[0] !== tickLabel) { tickLabel = this.state.data[i].labels[0]; - tickValues.push(tick); ticksLabels.push(tickLabel); tick++; } @@ -438,7 +424,6 @@ return AbstractRenderer.extend({ }); chart.forceY([0]); chart.xAxis - .tickValues(tickValues) .tickFormat(function (d) { return ticksLabels[d]; }); @@ -491,19 +476,6 @@ return AbstractRenderer.extend({ $('.nv-y .tick > line').attr('x2', function (i, value) { return Math.abs(value); }); - - // We don't need to show all labels - $('.o_graph_svg_container svg .nv-x g.tick > text').show(); - var $ticksText = $('svg .nv-x g.tick:not(.zero) > text'); - var ticksLength = $ticksText.length; - var tickTextMargin = 5; - if (ticksLength) { - var tickWidth = $ticksText[0].getBBox().width + tickTextMargin; - var svgWidth = $('.o_graph_svg_container').width(); - var keepOneOf = Math.ceil(ticksLength / (svgWidth / tickWidth)); - // FIXME: should work with two line charts - $('.o_graph_svg_container svg .nv-x g.tick:not(:nth-child(' + keepOneOf + 'n+1)) > text').hide(); - } }) chartResize(chart); 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 ef75caca8c0..ffb63ad9083 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column.js +++ b/addons/web/static/src/js/views/kanban/kanban_column.js @@ -7,6 +7,7 @@ var Dialog = require('web.Dialog'); var KanbanRecord = require('web.KanbanRecord'); var RecordQuickCreate = require('web.kanban_record_quick_create'); var view_dialogs = require('web.view_dialogs'); +var viewUtils = require('web.viewUtils'); var Widget = require('web.Widget'); var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); @@ -201,7 +202,7 @@ var KanbanColumn = Widget.extend({ this.trigger_up('close_quick_create'); // close other quick create widgets this.trigger_up('start_quick_create'); var context = this.data.getContext(); - context['default_' + this.groupedBy] = this.id; + context['default_' + this.groupedBy] = viewUtils.getGroupValue(this.data, this.groupedBy); this.quickCreateWidget = new RecordQuickCreate(this, { context: context, formViewRef: this.quickCreateView, 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 9599d29db26..e195b33068c 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -12,6 +12,7 @@ var Context = require('web.Context'); var core = require('web.core'); var Domain = require('web.Domain'); var view_dialogs = require('web.view_dialogs'); +var viewUtils = require('web.viewUtils'); var _t = core._t; var qweb = core.qweb; @@ -35,12 +36,15 @@ var KanbanController = BasicController.extend({ /** * @override * @param {Object} params + * @param {boolean} params.quickCreateEnabled set to false to disable the + * quick create feature */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.on_create = params.on_create; this.hasButtons = params.hasButtons; + this.quickCreateEnabled = params.quickCreateEnabled; }, //-------------------------------------------------------------------------- @@ -268,8 +272,8 @@ var KanbanController = BasicController.extend({ */ _onButtonNew: function () { var state = this.model.get(this.handle, {raw: true}); - var hasColumns = state.groupedBy.length > 0 && state.data.length > 0; - if (hasColumns && this.on_create === 'quick_create') { + var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state); + if (this.on_create === 'quick_create' && quickCreateEnabled) { // Activate the quick create in the first column this.renderer.addQuickCreate(); } else if (this.on_create && this.on_create !== 'quick_create') { @@ -396,10 +400,11 @@ var KanbanController = BasicController.extend({ var columnState = self.model.get(column.db_id, {raw: true}); var context = columnState.getContext(); var state = self.model.get(self.handle, {raw: true}); - context['default_' + state.groupedBy[0]] = columnState.res_id; + var groupedBy = state.groupedBy[0]; + context['default_' + groupedBy] = viewUtils.getGroupValue(columnState, groupedBy); new view_dialogs.FormViewDialog(self, { res_model: state.model, - context: _.extend({default_name: name}, context), + context: _.extend({default_name: values.name || values.display_name}, context), title: _t("Create"), disable_multiple_selection: true, on_saved: function (record) { 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 905ea05ac1e..fd21ed4df86 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -7,6 +7,7 @@ odoo.define('web.KanbanModel', function (require) { */ var BasicModel = require('web.BasicModel'); +var viewUtils = require('web.viewUtils'); var KanbanModel = BasicModel.extend({ @@ -105,7 +106,8 @@ var KanbanModel = BasicModel.extend({ var group = this.localData[groupID]; var context = this._getContext(group); var parent = this.localData[group.parentID]; - context['default_' + parent.groupedBy[0]] = group.res_id; + var groupedBy = parent.groupedBy; + context['default_' + groupedBy] = viewUtils.getGroupValue(group, groupedBy); var def; if (Object.keys(values).length === 1 && 'display_name' in values) { // only 'display_name is given, perform a 'name_create' @@ -132,7 +134,7 @@ var KanbanModel = BasicModel.extend({ }, /** * Add the following (kanban specific) keys when performing a `get`: - * + * * - tooltipData * - progressBarValues * - isGroupedByM2ONoColumn diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js index 7013464ea87..d9d7ac77fe2 100644 --- a/addons/web/static/src/js/views/kanban/kanban_record.js +++ b/addons/web/static/src/js/views/kanban/kanban_record.js @@ -509,10 +509,12 @@ var KanbanRecord = Widget.extend({ ischild = false; } var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== 'bs.tooltip'); + var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href; if (ischild) { children.push(elem); - if (test_event) { - // do not trigger global click if one child has a click event registered + if (test_event || testLinkWithHref) { + // Do not trigger global click if one child has a click + // event registered (or it is a link with href) trigger = false; } } diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index c51ac945de1..9356311d51c 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -9,6 +9,7 @@ var ColumnQuickCreate = require('web.kanban_column_quick_create'); var QWeb = require('web.QWeb'); var session = require('web.session'); var utils = require('web.utils'); +var viewUtils = require('web.viewUtils'); var qweb = core.qweb; @@ -98,6 +99,9 @@ var KanbanRenderer = BasicRenderer.extend({ /** * @override + * @param {Object} params + * @param {boolean} params.quickCreateEnabled set to false to disable the + * quick create feature */ init: function (parent, state, params) { this._super.apply(this, arguments); @@ -116,6 +120,7 @@ var KanbanRenderer = BasicRenderer.extend({ if (this.columnOptions.hasProgressBar) { this.columnOptions.progressBarStates = {}; } + this.quickCreateEnabled = params.quickCreateEnabled; this._setState(state); }, /** @@ -453,6 +458,7 @@ var KanbanRenderer = BasicRenderer.extend({ groupedBy: groupByField, grouped_by_m2o: this.groupedByM2O, relation: relation, + quick_create: this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state), }); this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable; }, 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 791e3664668..523f0ef1c57 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -63,7 +63,6 @@ var KanbanView = BasicView.extend({ deletable: activeActions.group_delete, archivable: this.arch.attrs.archivable ? JSON.parse(this.arch.attrs.archivable) : true, group_creatable: activeActions.group_create && !config.device.isMobile, - quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(), quickCreateView: this.arch.attrs.quick_create_view || null, hasProgressBar: !!progressBar, }; @@ -72,6 +71,7 @@ var KanbanView = BasicView.extend({ deletable: activeActions.delete, read_only_mode: params.readOnlyMode, }; + this.rendererParams.quickCreateEnabled = this._isQuickCreateEnabled(); var examples = this.arch.attrs.examples; if (examples) { this.rendererParams.examples = kanbanExamplesRegistry.get(examples); @@ -80,6 +80,7 @@ var KanbanView = BasicView.extend({ this.controllerParams.on_create = this.arch.attrs.on_create; this.controllerParams.readOnlyMode = false; this.controllerParams.hasButtons = true; + this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled; if (config.device.isMobile) { this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js'); @@ -93,13 +94,10 @@ var KanbanView = BasicView.extend({ /** * @private * @param {Object} viewInfo + * @returns {boolean} true iff the quick create feature is not explicitely + * disabled (with create="False" or quick_create="False" in the arch) */ _isQuickCreateEnabled: function () { - var groupBy = this.loadParams.groupBy[0]; - groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined; - if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], this.fields[groupBy].type)) { - return false; - } if (!this.controllerParams.activeActions.create) { return false; } diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 109a5c958c4..7ba3010a3b5 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -93,8 +93,6 @@ var DateWidget = Widget.extend({ } } if (hasChanged) { - // The condition is strangely written; this is because the - // values can be false/undefined if (this.options.warn_future) { this._warnFuture(newValue); } @@ -182,7 +180,7 @@ var DateWidget = Widget.extend({ this.$warning.attr('title', title); this.$input.after(this.$warning); } - if (currentDate.isAfter(moment())) { + if (currentDate && currentDate.isAfter(moment())) { this.$warning.show(); } else { this.$warning.hide(); diff --git a/addons/web/static/src/scss/bootstrap_review.scss b/addons/web/static/src/scss/bootstrap_review.scss new file mode 100644 index 00000000000..4efc59da2cf --- /dev/null +++ b/addons/web/static/src/scss/bootstrap_review.scss @@ -0,0 +1,10 @@ +/// +/// This file regroups the CSS rules made to fix/extend bootstrap in all places +/// where it is used in Odoo (backend / frontend / reports / ...) +/// + +.alert { + // Alerts are block elements with relative positioning. + // They would go over floating elements, which is never what we want. + clear: both; +} diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index 0ebd5044806..6171762ed1f 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -207,7 +207,7 @@ border-left: 1px solid gray('300'); } - &:before, &:not(:first-child):after { + &:not(:first-child):before, &:not(:first-child):after { content: " "; display: block; @include o-position-absolute(0, -$o-statusbar-arrow-width + 1); @@ -231,7 +231,7 @@ } } - &:before { + &:not(:first-child):before { right: -$o-statusbar-arrow-width; border-left-color: gray('300'); } diff --git a/addons/web/static/src/scss/form_view_extra.scss b/addons/web/static/src/scss/form_view_extra.scss index ad7e8600cb3..277537beab4 100644 --- a/addons/web/static/src/scss/form_view_extra.scss +++ b/addons/web/static/src/scss/form_view_extra.scss @@ -33,6 +33,7 @@ width: 400px; text-align: right; float: right; + margin-bottom: 4px; &, & + .oe_avatar { + .oe_title { @@ -40,6 +41,12 @@ } } + // TODO remove me in master (this rule is a copy of the one in the + // bootstrap_review.scss file) + + .alert { + clear: both; + } + .oe_stat_button { color: $o-main-text-color; text-align: left; diff --git a/addons/web/static/src/scss/layout_background.scss b/addons/web/static/src/scss/layout_background.scss index 9f4aed173f4..ff2d0cef3b0 100644 --- a/addons/web/static/src/scss/layout_background.scss +++ b/addons/web/static/src/scss/layout_background.scss @@ -3,18 +3,17 @@ color: gray('700'); } .o_background_header { + min-width: 900px; border-bottom: 1px solid gray('200'); - .company_address { - min-width: 300px; - } img { max-height: 96px; - max-width: 300px; + max-width: 200px; margin-right: 16px; } h3 { color: $o-brand-odoo; font-weight: 700; + font-size: 1.25rem; max-width: 300px; } } diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 4ccf7de9c2b..770508ac1b8 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -23,6 +23,9 @@ border-bottom: 1px solid #cacaca; > tr > th { border-left: 1px solid #dfdfdf; + @include media-breakpoint-down(sm) { + white-space: nowrap; + } &:not(.o_column_sortable):hover { cursor: default; } @@ -80,9 +83,10 @@ .o_list_record_remove button { padding: 0px; - background-style: none; + background: none; border-style: none; display: table-cell; + cursor: pointer; } // Contextual classes @@ -93,8 +97,10 @@ // Grouped list views tbody > tr.o_group_header { - height: 31px; - + > th, > td { + padding-top: ($table-cell-padding-sm*2); + padding-bottom: ($table-cell-padding-sm*2); + } .o_group_name { white-space: nowrap; } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 907a02c0237..1eef5047f46 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -810,7 +810,7 @@ <t t-name="FieldMany2One"> <t t-if="widget.mode === 'readonly'"> - <a t-if="!widget.nodeOptions.no_open" class="o_form_uri" href="#"/> + <a t-if="!widget.nodeOptions.no_open" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/> <span t-if="widget.nodeOptions.no_open"/> </t> <div t-if="widget.mode === 'edit'" class="o_field_widget o_field_many2one" aria-atomic="true"> |
