diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index 98d169c11e2..3c40524e12c 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -20,7 +20,6 @@ FormView.include({ */ init: function (viewInfo) { this._super.apply(this, arguments); - this.rendererParams.noContentHelp = this.controllerParams.noContentHelp; this.controllerParams.viewID = viewInfo.view_id; }, }); diff --git a/addons/crm/static/tests/sales_team_dashboard_tests.js b/addons/crm/static/tests/sales_team_dashboard_tests.js index d4da44e16a7..a4430d29a52 100644 --- a/addons/crm/static/tests/sales_team_dashboard_tests.js +++ b/addons/crm/static/tests/sales_team_dashboard_tests.js @@ -133,4 +133,39 @@ QUnit.test('dashboard: click on a button to execute an action', function (assert kanban.destroy(); }); +QUnit.test('dashboard should be displayed even if there is no content', function (assert) { + assert.expect(2); + + var dashboardData = this.dashboard_data; + var kanban = createView({ + View: view_registry.get('sales_team_dashboard'), + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + domain: [['id', '=', 239]], // no record will match this domain + mockRPC: function (route, args) { + if (args.method === 'retrieve_sales_dashboard') { + return $.when(dashboardData); + } + return this._super(route, args); + }, + viewOptions: { + action: { + help: '

A help message

', + }, + }, + }); + + assert.strictEqual(kanban.$('div.o_sales_dashboard').length, 1, + "should render the dashboard"); + assert.strictEqual(kanban.$('.oe_view_nocontent:contains(A help message)').length, 1, + "should correctly render the nocontent helper"); + + kanban.destroy(); +}); + }); diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index b84f6ec1951..3bdf8ab08ca 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -32,7 +32,6 @@ var AbstractController = Widget.extend({ * @param {string} params.modelName * @param {any} [params.handle] a handle that will be given to the model (some id) * @param {any} params.initialState the initialState - * @param {string} [params.noContentHelp] */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); @@ -42,7 +41,6 @@ var AbstractController = Widget.extend({ this.handle = params.handle; this.activeActions = params.activeActions; this.initialState = params.initialState; - this.noContentHelp = params.noContentHelp; }, /** * Simply renders and updates the url. @@ -223,31 +221,6 @@ var AbstractController = Widget.extend({ _pushState: function (state) { this.trigger_up('push_state', state || {}); }, - /** - * Hide or show the nocontent helper. For this, it also remove the renderer - * from the dom, and reattach it when necessary. - * - * This method is a helper for controllers that want to display a help - * message when no content is available. It is suggested to override - * _update to call this method. - * - * @private - * @param {boolean} hasNoContent - */ - _toggleNoContentHelper: function (hasNoContent) { - if (hasNoContent && this.noContentHelp) { - this.renderer.$el.detach(); - var $msg = $('
') - .addClass('oe_view_nocontent') - .html(this.noContentHelp); - this.$el.html($msg); - } else { - if (!document.contains(this.renderer.el)) { - this.$('div.oe_view_nocontent').remove(); - this.$el.append(this.renderer.$el); - } - } - }, /** * This method is called after each update or when the start method is * completed. diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js index 7d10f05eab8..d7eb5002f2d 100644 --- a/addons/web/static/src/js/views/abstract_renderer.js +++ b/addons/web/static/src/js/views/abstract_renderer.js @@ -15,11 +15,13 @@ return Widget.extend({ * @param {Widget} parent * @param {any} state * @param {Object} params + * @param {string} [params.noContentHelp] */ init: function (parent, state, params) { this._super(parent); this.state = state; this.arch = params.arch; + this.noContentHelp = params.noContentHelp; }, /** * The rendering can be asynchronous (but it is not encouraged). The start diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index e14e064ee33..74e1e1fce18 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -77,6 +77,7 @@ var AbstractView = Class.extend({ init: function (viewInfo, params) { this.rendererParams = { arch: viewInfo.arch, + noContentHelp: params.action && params.action.help, }; this.controllerParams = { @@ -87,7 +88,6 @@ var AbstractView = Class.extend({ delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true, duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true, }, - noContentHelp: params.action && params.action.help, }; this.loadParams = { diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index cc72816a85d..54c8ccfbf51 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -361,17 +361,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { model: this.modelName, }; }, - /** - * Used by list and kanban views to determine whether or not to display - * the no content helper (if there is no data in the state to display) - * - * @private - * @param {Object} state - * @returns {boolean} - */ - _hasContent: function (state) { - return state.count !== 0; - }, /** * Helper function to display a warning that some fields have an invalid * value. This is used when a save operation cannot be completed. diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 8bbf9ae45a4..e040a5fa7f0 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -317,6 +317,16 @@ var BasicRenderer = AbstractRenderer.extend({ _getModifiersData: function (node) { return _.findWhere(this.allModifiersData, {node: node}); }, + /** + * Used by list and kanban renderers to determine whether or not to display + * the no content helper (if there is no data in the state to display) + * + * @private + * @returns {boolean} + */ + _hasContent: function () { + return this.state.count !== 0; + }, /** * Registers or updates the modifiers data associated to the given node. * This method is quiet complex as it handles all the needs of the basic @@ -486,6 +496,20 @@ var BasicRenderer = AbstractRenderer.extend({ return widget; }, + /** + * Renders the nocontent helper. + * + * This method is a helper for renderers that want to display a help + * message when no content is available. + * + * @private + */ + _renderNoContentHelper: function () { + var $msg = $('
') + .addClass('oe_view_nocontent') + .html(this.noContentHelp); + this.$el.html($msg); + }, /** * Actual rendering. Supposed to be overridden by concrete renderers. * The basic responsabilities of _renderView are: 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 2592e3d96ec..2cf6a94285e 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -80,19 +80,6 @@ var KanbanController = BasicController.extend({ _confirmSave: function (id) { return this.renderer.updateRecord(this.model.get(id)); }, - /** - * The nocontent helper should be displayed in kanban: - * - ungrouped: if there is no records - * - grouped: if there is no groups and no column quick create - * - * @override - * @private - */ - _hasContent: function (state) { - return this._super.apply(this, arguments) || - this.createColumnEnabled || - (state.groupedBy.length && state.data.length); - }, /** * The column quick create should be displayed in kanban iff grouped by an * m2o field and group_create action enabled. @@ -138,17 +125,6 @@ var KanbanController = BasicController.extend({ .toggleClass('btn-default', createMuted); } }, - /** - * @override - * @param {Object} state - * @returns {Deferred} - */ - _update: function (state) { - var hasNoContent = !this._hasContent(state); - this.$el.toggleClass('o_kanban_nocontent', hasNoContent); - this._toggleNoContentHelper(hasNoContent); - return this._super.apply(this, arguments); - }, //-------------------------------------------------------------------------- // Handlers 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 ea2f0270017..0bbc7aa2698 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -100,6 +100,8 @@ var KanbanRenderer = BasicRenderer.extend({ viewType: 'kanban', }); this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb }); + + this._setState(state); }, //-------------------------------------------------------------------------- @@ -167,11 +169,31 @@ var KanbanRenderer = BasicRenderer.extend({ record.update(recordState); } }, + /** + * @override + */ + updateState: function (state) { + this._setState(state); + return this._super.apply(this, arguments); + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** + * The nocontent helper should be displayed in kanban: + * - ungrouped: if there is no records + * - grouped: if there is no groups and no column quick create + * + * @override + * @private + */ + _hasContent: function () { + return this._super.apply(this, arguments) || + this.createColumnEnabled || + (this.state.groupedBy.length && this.state.data.length); + }, /** * Renders empty invisible divs in a document fragment. * @@ -193,28 +215,6 @@ var KanbanRenderer = BasicRenderer.extend({ */ _renderGrouped: function (fragment) { var self = this; - var groupByFieldAttrs = this.state.fields[this.state.groupedBy[0]]; - var groupByFieldInfo = this.state.fieldsInfo.kanban[this.state.groupedBy[0]]; - // Deactivate the drag'n'drop if the groupedBy field: - // - is a date or datetime since we group by month or - // - is readonly - var draggable = true; - if (groupByFieldAttrs) { - if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") { - draggable = false; - } else if (groupByFieldAttrs.readonly !== undefined) { - draggable = !(groupByFieldAttrs.readonly); - } - } - var groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one'); - var grouped_by_field = groupedByM2O && groupByFieldAttrs.relation; - var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip; - this.columnOptions = _.extend(this.columnOptions, { - draggable: draggable, - group_by_tooltip: groupByTooltip, - grouped_by_m2o: groupedByM2O, - relation: grouped_by_field, - }); // Render columns _.each(this.state.data, function (group) { @@ -228,7 +228,7 @@ var KanbanRenderer = BasicRenderer.extend({ } }); - if (groupedByM2O) { + if (this.groupedByM2O) { // Enable column sorting this.$el.sortable({ axis: 'x', @@ -249,7 +249,7 @@ var KanbanRenderer = BasicRenderer.extend({ }); // Enable column quickcreate - if (this.columnOptions.group_creatable) { + if (this.createColumnEnabled) { this.quickCreate = new ColumnQuickCreate(this); this.quickCreate.appendTo(fragment); } @@ -280,20 +280,62 @@ var KanbanRenderer = BasicRenderer.extend({ _renderView: function () { var oldWidgets = this.widgets; this.widgets = []; - var isGrouped = !!this.state.groupedBy.length; - this.$el.toggleClass('o_kanban_grouped', isGrouped); - this.$el.toggleClass('o_kanban_ungrouped', !isGrouped); this.$el.empty(); - var fragment = document.createDocumentFragment(); - if (isGrouped) { - this._renderGrouped(fragment); + var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp; + this.$el.toggleClass('o_kanban_nocontent', displayNoContentHelper); + if (displayNoContentHelper) { + // display the no content helper if there is no data to display + this._renderNoContentHelper(); } else { - this._renderUngrouped(fragment); + var isGrouped = !!this.state.groupedBy.length; + this.$el.toggleClass('o_kanban_grouped', isGrouped); + this.$el.toggleClass('o_kanban_ungrouped', !isGrouped); + var fragment = document.createDocumentFragment(); + // render the kanban view + if (isGrouped) { + this._renderGrouped(fragment); + } else { + this._renderUngrouped(fragment); + } + this.$el.append(fragment); } - this.$el.append(fragment); + return this._super.apply(this, arguments).then(_.invoke.bind(_, oldWidgets, 'destroy')); }, + /** + * Sets the current state and updates some internal attributes accordingly. + * + * @private + * @param {Object} state + */ + _setState: function (state) { + this.state = state; + + var groupByFieldAttrs = state.fields[state.groupedBy[0]]; + var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]]; + // Deactivate the drag'n'drop if the groupedBy field: + // - is a date or datetime since we group by month or + // - is readonly + var draggable = true; + if (groupByFieldAttrs) { + if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") { + draggable = false; + } else if (groupByFieldAttrs.readonly !== undefined) { + draggable = !(groupByFieldAttrs.readonly); + } + } + this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one'); + var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation; + var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip; + this.columnOptions = _.extend(this.columnOptions, { + draggable: draggable, + group_by_tooltip: groupByTooltip, + grouped_by_m2o: this.groupedByM2O, + relation: grouped_by_field, + }); + this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable; + }, }); return KanbanRenderer; diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 2ec9cf3181e..3b74f140e4d 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -188,7 +188,6 @@ var ListController = BasicController.extend({ position: self.editable, }); }).then(function (recordID) { - self._toggleNoContentHelper(false); var state = self.model.get(self.handle); self.renderer.updateState(state, {}); self.renderer.editRecord(recordID); @@ -283,12 +282,10 @@ var ListController = BasicController.extend({ }, /** * @override - * @param {Object} state * @returns {Deferred} */ - _update: function (state) { + _update: function () { this.selectedRecords = []; - this._toggleNoContentHelper(!this._hasContent(state)); this._toggleSidebar(); return this._super.apply(this, arguments); }, diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 802f2f03a09..295266db276 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -30,7 +30,6 @@ var FIELD_CLASSES = { }; var ListRenderer = BasicRenderer.extend({ - className: 'table-responsive', events: { 'click tbody tr': '_onRowClicked', 'click tbody .o_list_record_selector': '_onSelectRecord', @@ -577,12 +576,24 @@ var ListRenderer = BasicRenderer.extend({ _renderView: function () { var self = this; + this.$el + .removeClass('table-responsive') + .empty(); + // destroy the previously instantiated pagers, if any _.invoke(this.pagers, 'destroy'); this.pagers = []; + // display the no content helper if there is no data to display + if (!this._hasContent() && this.noContentHelp) { + this._renderNoContentHelper(); + return this._super(); + } + var $table = $('').addClass('o_list_view table table-condensed table-striped'); - this.$el.empty().append($table); + this.$el + .addClass('table-responsive') + .append($table); var is_grouped = !!this.state.groupedBy.length; this._computeAggregates(); $table.toggleClass('o_list_view_grouped', is_grouped); diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index 78640a97124..731183e7efe 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -170,21 +170,8 @@ var PivotController = AbstractController.extend({ /** * @private */ - _update: function (state) { + _update: function () { this._updateButtons(); - var hasNoContent = !state.has_data || !state.measures.length; - - if (hasNoContent) { - if (!this.$('.oe_view_nocontent').length) { - this.renderer.$el.detach(); - this.$el.append(QWeb.render('PivotView.nodata')); - } - } else { - if (!document.contains(this.renderer.el)) { - this.$('div.oe_view_nocontent').remove(); - this.$el.append(this.renderer.$el); - } - } return this._super.apply(this, arguments); }, /** diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index a37e65b104b..d279d59d4b1 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -2,8 +2,11 @@ odoo.define('web.PivotRenderer', function (require) { "use strict"; var AbstractRenderer = require('web.AbstractRenderer'); +var core = require('web.core'); var field_utils = require('web.field_utils'); +var QWeb = core.qweb; + var PivotRenderer = AbstractRenderer.extend({ tagName: 'table', className: 'table-hover table-condensed table-bordered', @@ -15,15 +18,32 @@ var PivotRenderer = AbstractRenderer.extend({ // Private //-------------------------------------------------------------------------- + /** + * Used to determine whether or not to display the no content helper. + * + * @private + * @returns {boolean} + */ + _hasContent: function () { + return this.state.has_data && this.state.measures.length; + }, /** * @override * @private * @returns {Deferred} */ _render: function () { - if (!this.state.has_data) { + if (!this._hasContent()) { + // display the nocontent helper + this.replaceElement(QWeb.render('PivotView.nodata')); return this._super.apply(this, arguments); } + + if (!this.$el.is('table')) { + // coming from the no content helper, so the root element has to be + // re-rendered before rendering and appending its content + this.renderElement(); + } var $fragment = $(document.createDocumentFragment()); var $table = $('
').appendTo($fragment); var $thead = $('').appendTo($table); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 7aac83f26f8..712cce02067 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -953,7 +953,7 @@ QUnit.module('Views', { }, }); - assert.ok(kanban.$el.hasClass('o_kanban_nocontent'), + assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_nocontent'), "$el should have correct no content class"); assert.strictEqual(kanban.$('.oe_view_nocontent').length, 1,