diff --git a/addons/base_import/static/src/import_records/import_records.js b/addons/base_import/static/src/import_records/import_records.js
index 7ffa17660fe..6fcf183eb69 100644
--- a/addons/base_import/static/src/import_records/import_records.js
+++ b/addons/base_import/static/src/import_records/import_records.js
@@ -36,10 +36,10 @@ ImportRecords.template = "base_import.ImportRecords";
const importRecordsItem = {
Component: ImportRecords,
groupNumber: 4,
- isDisplayed: ({ isSmall, searchModel }) =>
+ isDisplayed: ({ config, isSmall }) =>
!isSmall &&
- searchModel.action.type === "ir.actions.act_window" &&
- ["kanban", "list"].includes(searchModel.view.type)
+ config.actionType === "ir.actions.act_window" &&
+ ["kanban", "list"].includes(config.viewType)
// TODO: add arch info to searchModel?
// !!JSON.parse(env.view.arch.attrs.import || "1") &&
// !!JSON.parse(env.view.arch.attrs.create || "1"),
diff --git a/addons/board/static/src/add_to_board/add_to_board.js b/addons/board/static/src/add_to_board/add_to_board.js
index 75fecfb88cf..6ca973f78c7 100644
--- a/addons/board/static/src/add_to_board/add_to_board.js
+++ b/addons/board/static/src/add_to_board/add_to_board.js
@@ -24,7 +24,7 @@ export class AddToBoard extends Component {
setup() {
this.notification = useService("notification");
this.rpc = useService("rpc");
- this.state = useState({ name: this.env.searchModel.displayName });
+ this.state = useState({ name: this.env.config.displayName });
useAutofocus();
}
@@ -34,39 +34,19 @@ export class AddToBoard extends Component {
//---------------------------------------------------------------------
async addToBoard() {
- const {
- action,
- displayName,
- domain,
- context,
- groupBy,
- orderedBy,
- view,
- } = this.env.searchModel;
-
- // Retrieves view context
- const fns = this.env.__getContext__.callbacks;
- const viewContext = Object.assign({}, ...fns.map((fn) => fn()));
-
+ const { context, domain } = this.env.searchModel.getIrFilterValues();
const contextToSave = {
...context,
- group_by: groupBy,
- orderedBy,
+ orderedBy: this.env.searchModel.orderBy,
dashboard_merge_domains_contexts: false,
- ...viewContext,
};
- const detailedComparison = this.env.searchModel.getFullComparison();
- if (detailedComparison) {
- contextToSave.comparison = detailedComparison;
- }
-
const result = await this.rpc("/board/add_to_dashboard", {
- action_id: action.id,
+ action_id: this.env.config.actionId,
context_to_save: contextToSave,
domain,
name: this.state.name,
- view_mode: view.type,
+ view_mode: this.env.config.viewType,
});
if (result) {
@@ -77,12 +57,11 @@ export class AddToBoard extends Component {
type: "warning",
}
);
- this.state.name = displayName;
+ this.state.name = this.env.config.displayName;
} else {
- this.notification.add(
- this.env._t("Could not add filter to dashboard"),
- { type: "danger" }
- );
+ this.notification.add(this.env._t("Could not add filter to dashboard"), {
+ type: "danger",
+ });
}
}
@@ -106,7 +85,7 @@ AddToBoard.template = "board.AddToBoard";
const addToBoardItem = {
Component: AddToBoard,
groupNumber: 4,
- isDisplayed: ({ searchModel }) => searchModel.action.type === "ir.actions.act_window",
+ isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window",
};
favoriteMenuRegistry.add("add-to-board", addToBoardItem, { sequence: 10 });
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index b8b4466279a..7d2129d8a7f 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -1,1192 +1,1332 @@
-odoo.define('board.dashboard_tests', function (require) {
-"use strict";
+odoo.define("board.dashboard_tests", function (require) {
+ "use strict";
-var BoardView = require('board.BoardView');
+ var BoardView = require("board.BoardView");
-var ListController = require('web.ListController');
-var testUtils = require('web.test_utils');
-var ListRenderer = require('web.ListRenderer');
-var pyUtils = require('web.py_utils');
-const { registry } = require("@web/core/registry");
-const { makeFakeUserService } = require("@web/../tests/helpers/mock_services");
-const {
- applyFilter,
- applyGroup,
- editConditionValue,
- toggleAddCustomFilter,
- toggleAddCustomGroup,
- toggleFilterMenu,
- toggleGroupByMenu,
- toggleMenuItem,
- toggleMenuItemOption,
- toggleComparisonMenu,
- toggleFavoriteMenu,
-} = require("@web/../tests/search/helpers");
-const LegacyFavoriteMenu = require("web.FavoriteMenu");
-const LegacyAddToBoard = require("board.AddToBoardMenu");
-const { AddToBoard } = require('@board/add_to_board/add_to_board');
+ var ListController = require("web.ListController");
+ var testUtils = require("web.test_utils");
+ var ListRenderer = require("web.ListRenderer");
+ var pyUtils = require("web.py_utils");
+ const { registry } = require("@web/core/registry");
+ const { makeFakeUserService } = require("@web/../tests/helpers/mock_services");
+ const {
+ applyFilter,
+ applyGroup,
+ editConditionValue,
+ toggleAddCustomFilter,
+ toggleAddCustomGroup,
+ toggleFilterMenu,
+ toggleGroupByMenu,
+ toggleMenuItem,
+ toggleMenuItemOption,
+ toggleComparisonMenu,
+ toggleFavoriteMenu,
+ } = require("@web/../tests/search/helpers");
+ const LegacyFavoriteMenu = require("web.FavoriteMenu");
+ const LegacyAddToBoard = require("board.AddToBoardMenu");
+ const { AddToBoard } = require("@board/add_to_board/add_to_board");
-const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
-var createView = testUtils.createView;
+ const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
+ var createView = testUtils.createView;
-const patchDate = testUtils.mock.patchDate;
-const favoriteMenuRegistry = registry.category("favoriteMenu");
+ const patchDate = testUtils.mock.patchDate;
+ const favoriteMenuRegistry = registry.category("favoriteMenu");
-let serverData;
-QUnit.module('Dashboard', {
- beforeEach: function () {
- this.data = {
- board: {
- fields: {
+ let serverData;
+ QUnit.module("Dashboard", {
+ beforeEach: function () {
+ this.data = {
+ board: {
+ fields: {},
+ records: [],
},
- records: [
- ]
- },
- partner: {
- fields: {
- display_name: {string: "Displayed name", type: "char", searchable: true},
- foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true},
- bar: {string: "Bar", type: "boolean"},
- int_field: {string: "Integer field", type: "integer", group_operator: 'sum'},
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char", searchable: true },
+ foo: {
+ string: "Foo",
+ type: "char",
+ default: "My little Foo Value",
+ searchable: true,
+ },
+ bar: { string: "Bar", type: "boolean" },
+ int_field: {
+ string: "Integer field",
+ type: "integer",
+ group_operator: "sum",
+ },
+ },
+ records: [
+ {
+ id: 1,
+ display_name: "first record",
+ foo: "yop",
+ int_field: 3,
+ },
+ {
+ id: 2,
+ display_name: "second record",
+ foo: "lalala",
+ int_field: 5,
+ },
+ {
+ id: 4,
+ display_name: "aaa",
+ foo: "abc",
+ int_field: 2,
+ },
+ ],
},
- records: [{
- id: 1,
- display_name: "first record",
- foo: "yop",
- int_field: 3,
- }, {
- id: 2,
- display_name: "second record",
- foo: "lalala",
- int_field: 5,
- }, {
- id: 4,
- display_name: "aaa",
- foo: "abc",
- int_field: 2,
- }],
- },
- };
-
- LegacyFavoriteMenu.registry.add('add-to-board-menu', LegacyAddToBoard, 10);
- favoriteMenuRegistry.add("add-to-board", {
- Component: AddToBoard,
- groupNumber: 4,
- isDisplayed: ({ searchModel }) => searchModel.action.type === "ir.actions.act_window",
- }, { sequence: 10 });
- serverData = { models: this.data };
- },
-});
-
-QUnit.test('dashboard basic rendering', async function (assert) {
- assert.expect(4);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '
',
- });
-
- assert.doesNotHaveClass(form.renderer.$el, 'o_dashboard',
- "should not have the o_dashboard css class");
-
- form.destroy();
-
- form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- });
-
- assert.hasClass(form.renderer.$el,'o_dashboard',
- "with a dashboard, the renderer should have the proper css class");
- assert.containsOnce(form, '.o_dashboard .o_view_nocontent',
- "should have a no content helper");
- assert.strictEqual(form.getTitle(), "My Dashboard",
- "should have the correct title");
- form.destroy();
-});
-
-QUnit.test('display the no content helper', async function (assert) {
- assert.expect(1);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- viewOptions: {
- action: {
- help: 'click to add a partner
'
- }
- },
- });
-
- assert.containsOnce(form, '.o_dashboard .o_view_nocontent',
- "should have a no content helper with action help");
- form.destroy();
-});
-
-QUnit.test('basic functionality, with one sub action', async function (assert) {
- assert.expect(26);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route, args) {
- if (route === '/web/action/load') {
- assert.step('load action');
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- if (route === '/web/dataset/search_read') {
- assert.deepEqual(args.domain, [['foo', '!=', 'False']], "the domain should be passed");
- assert.deepEqual(args.context.orderedBy, [{
- 'name': 'foo',
- 'asc': true,
- }],
- 'orderedBy is present in the search read when specified on the custom action'
- );
- }
- if (route === '/web/view/edit_custom') {
- assert.step('edit custom');
- return Promise.resolve(true);
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- },
- });
-
- assert.containsOnce(form, '.oe_dashboard_links',
- "should have rendered a link div");
- assert.containsOnce(form, 'table.oe_dashboard[data-layout="2-1"]',
- "should have rendered a table");
- assert.containsNone(form, 'td.o_list_record_selector',
- "td should not have a list selector");
- assert.strictEqual(form.$('h2 span.oe_header_txt:contains(ABC)').length, 1,
- "should have rendered a header with action string");
- assert.containsN(form, 'tr.o_data_row', 3,
- "should have rendered 3 data rows");
-
- assert.ok(form.$('.oe_content').is(':visible'), "content is visible");
-
- await testUtils.dom.click(form.$('.oe_fold'));
-
- assert.notOk(form.$('.oe_content').is(':visible'), "content is no longer visible");
-
- await testUtils.dom.click(form.$('.oe_fold'));
-
- assert.ok(form.$('.oe_content').is(':visible'), "content is visible again");
- assert.verifySteps(['load action', 'edit custom', 'edit custom']);
-
- assert.strictEqual($('.modal').length, 0, "should have no modal open");
-
- await testUtils.dom.click(form.$('button.oe_dashboard_link_change_layout'));
-
- assert.strictEqual($('.modal').length, 1, "should have opened a modal");
- assert.strictEqual($('.modal li[data-layout="2-1"] i.oe_dashboard_selected_layout').length, 1,
- "should mark currently selected layout");
-
- await testUtils.dom.click($('.modal .oe_dashboard_layout_selector li[data-layout="1-1"]'));
-
- assert.strictEqual($('.modal').length, 0, "should have no modal open");
- assert.containsOnce(form, 'table.oe_dashboard[data-layout="1-1"]',
- "should have rendered a table with correct layout");
-
-
- assert.containsOnce(form, '.oe_action', "should have one displayed action");
- await testUtils.dom.click(form.$('span.oe_close'));
-
- assert.strictEqual($('.modal').length, 1, "should have opened a modal");
-
- // confirm the close operation
- await testUtils.dom.click($('.modal button.btn-primary'));
-
- assert.strictEqual($('.modal').length, 0, "should have no modal open");
- assert.containsNone(form, '.oe_action', "should have no displayed action");
-
- assert.verifySteps(['edit custom', 'edit custom']);
- form.destroy();
-});
-
-QUnit.test('views in the dashboard do not have a control panel', async function (assert) {
- assert.expect(2);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list'], [5, 'form']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- },
- });
-
- assert.containsOnce(form, '.o_action .o_list_view');
- assert.containsNone(form, '.o_action .o_control_panel');
-
- form.destroy();
-});
-
-QUnit.test('can render an action without view_mode attribute', async function (assert) {
- // The view_mode attribute is automatically set to the 'action' nodes when
- // the action is added to the dashboard using the 'Add to dashboard' button
- // in the searchview. However, other dashboard views can be written by hand
- // (see openacademy tutorial), and in this case, we don't want hardcode
- // action's params (like context or domain), as the dashboard can directly
- // retrieve them from the action. Same applies for the view_type, as the
- // first view of the action can be used, by default.
- assert.expect(3);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- archs: {
- 'partner,4,list':
- '',
- },
- mockRPC: function (route, args) {
- if (route === '/board/static/src/img/layout_1-1-1.png') {
- return Promise.resolve();
- }
- if (route === '/web/action/load') {
- return Promise.resolve({
- context: '{"b": 2}',
- domain: '[["foo", "=", "yop"]]',
- res_model: 'partner',
- views: [[4, 'list'], [false, 'form']],
- });
- }
- if (args.method === 'load_views') {
- assert.deepEqual(args.kwargs.context, {a: 1, b: 2},
- "should have mixed both contexts");
- }
- if (route === '/web/dataset/search_read') {
- assert.deepEqual(args.domain, [['foo', '=', 'yop']],
- "should use the domain of the action");
- }
- return this._super.apply(this, arguments);
- },
- });
-
- assert.strictEqual(form.$('.oe_action:contains(ABC) .o_list_view').length, 1,
- "the list view (first view of action) should have been rendered correctly");
-
- form.destroy();
-});
-
-QUnit.test('can sort a sub list', async function (assert) {
- assert.expect(2);
-
- this.data.partner.fields.foo.sortable = true;
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- },
- });
-
- assert.strictEqual($('tr.o_data_row').text(), 'yoplalalaabc',
- "should have correct initial data");
-
- await testUtils.dom.click(form.$('th.o_column_sortable:contains(Foo)'));
-
- assert.strictEqual($('tr.o_data_row').text(), 'abclalalayop',
- "data should have been sorted");
- form.destroy();
-});
-
-QUnit.test('can open a record', async function (assert) {
- assert.expect(1);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- },
- intercepts: {
- do_action: function (event) {
- assert.deepEqual(event.data.action, {
- res_id: 1,
- res_model: 'partner',
- type: 'ir.actions.act_window',
- views: [[false, 'form']],
- }, "should do a do_action with correct parameters");
- },
- },
- });
-
- await testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)'));
- form.destroy();
-});
-
-QUnit.test('can open record using action form view', async function (assert) {
- assert.expect(1);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list'], [5, 'form']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- 'partner,5,form':
- '',
- },
- intercepts: {
- do_action: function (event) {
- assert.deepEqual(event.data.action, {
- res_id: 1,
- res_model: 'partner',
- type: 'ir.actions.act_window',
- views: [[5, 'form']],
- }, "should do a do_action with correct parameters");
- },
- },
- });
-
- await testUtils.dom.click(form.$('tr.o_data_row td:contains(yop)'));
- form.destroy();
-});
-
-QUnit.test('can drag and drop a view', async function (assert) {
- assert.expect(5);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- if (route === '/web/view/edit_custom') {
- assert.step('edit custom');
- return Promise.resolve(true);
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- },
- });
-
- assert.containsOnce(form, 'td.index_0 .oe_action',
- "initial action is in column 0");
-
- await testUtils.dom.dragAndDrop(form.$('.oe_dashboard_column.index_0 .oe_header'),
- form.$('.oe_dashboard_column.index_1'));
- assert.containsNone(form, 'td.index_0 .oe_action',
- "initial action is not in column 0");
- assert.containsOnce(form, 'td.index_1 .oe_action',
- "initial action is in in column 1");
- assert.verifySteps(['edit custom']);
-
- form.destroy();
-});
-
-QUnit.test('twice the same action in a dashboard', async function (assert) {
- assert.expect(2);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list'],[5, 'kanban']],
- });
- }
- if (route === '/web/view/edit_custom') {
- assert.step('edit custom');
- return Promise.resolve(true);
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '',
- 'partner,5,kanban':
- '' +
- '
' +
- '',
- },
- });
-
- var $firstAction = form.$('.oe_action:contains(ABC)');
- assert.strictEqual($firstAction.find('.o_list_view').length, 1,
- "list view should be displayed in 'ABC' block");
- var $secondAction = form.$('.oe_action:contains(DEF)');
- assert.strictEqual($secondAction.find('.o_kanban_view').length, 1,
- "kanban view should be displayed in 'DEF' block");
-
- form.destroy();
-});
-
-QUnit.test('non-existing action in a dashboard', async function (assert) {
- assert.expect(1);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- intercepts: {
- load_views: function () {
- throw new Error('load_views should not be called');
- }
- },
- mockRPC: function (route) {
- if (route === '/board/static/src/img/layout_1-1-1.png') {
- return Promise.resolve();
- }
- if (route === '/web/action/load') {
- // server answer if the action doesn't exist anymore
- return Promise.resolve(false);
- }
- return this._super.apply(this, arguments);
- },
- });
-
- assert.strictEqual(form.$('.oe_action:contains(ABC)').length, 1,
- "there should be a box for the non-existing action");
-
- form.destroy();
-});
-
-QUnit.test('clicking on a kanban\'s button should trigger the action', async function (assert) {
- assert.expect(2);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- archs: {
- 'partner,false,kanban':
- '' +
- '' +
- '' +
- '
' +
- '' +
- '
' +
- '',
- },
- intercepts: {
- execute_action: function (event) {
- var data = event.data;
- assert.strictEqual(data.env.model, 'partner', "should have correct model");
- assert.strictEqual(data.action_data.name, 'sitting_on_a_park_bench',
- "should call correct method");
- }
- },
-
- mockRPC: function (route) {
- if (route === '/board/static/src/img/layout_1-1-1.png') {
- return Promise.resolve();
- }
- if (route === '/web/action/load') {
- return Promise.resolve({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]});
- }
- if (route === '/web/dataset/search_read') {
- return Promise.resolve({records: [{foo: 'aqualung'}]});
- }
- return this._super.apply(this, arguments);
- }
- });
-
- await testUtils.dom.click(form.$('.o_kanban_test').find('button:first'));
-
- form.destroy();
-});
-
-QUnit.test('subviews are aware of attach in or detach from the DOM', async function (assert) {
- assert.expect(2);
-
- // patch list renderer `on_attach_callback` for the test only
- testUtils.mock.patch(ListRenderer, {
- on_attach_callback: function () {
- assert.step('subview on_attach_callback');
- }
- });
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '
',
- },
- });
-
- assert.verifySteps(['subview on_attach_callback']);
-
- // restore on_attach_callback of ListRenderer
- testUtils.mock.unpatch(ListRenderer);
-
- form.destroy();
-});
-
-QUnit.test('dashboard intercepts custom events triggered by sub controllers', async function (assert) {
- assert.expect(1);
-
- // we patch the ListController to force it to trigger the custom events that
- // we want the dashboard to intercept (to stop them or to tweak their data)
- testUtils.mock.patch(ListController, {
- start: function () {
- this.trigger_up('update_filters');
- return this._super.apply(this, arguments);
- },
- });
-
- var board = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({res_model: 'partner', views: [[false, 'list']]});
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,false,list': '',
- },
- intercepts: {
- update_filters: assert.step.bind(assert, 'update_filters'),
- },
- });
-
- assert.verifySteps([]);
-
- testUtils.mock.unpatch(ListController);
- board.destroy();
-});
-
-QUnit.test("save actions to dashboard", async function (assert) {
- assert.expect(6);
-
- testUtils.mock.patch(ListController, {
- getOwnedQueryParams: function () {
- var result = this._super.apply(this, arguments);
- result.context = {
- fire: "on the bayou",
};
- return result;
- },
- });
- serverData.models.partner.fields.foo.sortable = true;
-
- serverData.views = {
- "partner,false,list": '
',
- "partner,false,search": "",
- };
-
- const mockRPC = (route, args) => {
- if (route === "/board/add_to_dashboard") {
- assert.deepEqual(
- args.context_to_save.group_by,
- ["foo"],
- "The group_by should have been saved"
+ LegacyFavoriteMenu.registry.add("add-to-board-menu", LegacyAddToBoard, 10);
+ favoriteMenuRegistry.add(
+ "add-to-board",
+ {
+ Component: AddToBoard,
+ groupNumber: 4,
+ isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window",
+ },
+ { sequence: 10 }
);
- assert.deepEqual(
- args.context_to_save.orderedBy,
- [
- {
- name: "foo",
- asc: true,
- },
- ],
- "The orderedBy should have been saved"
- );
- assert.strictEqual(
- args.context_to_save.fire,
- "on the bayou",
- "The context of a controller should be passed and flattened"
- );
- assert.strictEqual(args.action_id, 1, "should save the correct action");
- assert.strictEqual(args.view_mode, "list", "should save the correct view type");
- return Promise.resolve(true);
- }
- };
-
- const webClient = await createWebClient({ serverData, mockRPC });
-
- await doAction(webClient, {
- id: 1,
- res_model: "partner",
- type: "ir.actions.act_window",
- views: [[false, "list"]],
+ serverData = { models: this.data };
+ },
});
- assert.containsOnce(webClient, ".o_list_view", "should display the list view");
+ QUnit.test("dashboard basic rendering", async function (assert) {
+ assert.expect(4);
- // Sort the list
- await testUtils.dom.click($(".o_column_sortable"));
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch: '",
+ });
- // Group It
- await toggleGroupByMenu(webClient);
- await toggleAddCustomGroup(webClient);
- await applyGroup(webClient);
+ assert.doesNotHaveClass(
+ form.renderer.$el,
+ "o_dashboard",
+ "should not have the o_dashboard css class"
+ );
- // add this action to dashboard
- await toggleFavoriteMenu(webClient);
+ form.destroy();
- await testUtils.dom.click($(".o_add_to_board button.o_dropdown_toggler"));
- await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
- await testUtils.dom.click($(".o_add_to_board .o_dropdown_menu button"));
-
- testUtils.mock.unpatch(ListController);
-});
-
-QUnit.test("save two searches to dashboard", async function (assert) {
- // the second search saved should not be influenced by the first
- assert.expect(2);
-
- serverData.views = {
- "partner,false,list": '
',
- "partner,false,search": "",
- };
-
- const mockRPC = (route, args) => {
- if (route === "/board/add_to_dashboard") {
- if (filter_count === 0) {
- assert.deepEqual(
- args.domain,
- [["display_name", "ilike", "a"]],
- "the correct domain should be sent"
- );
- }
- if (filter_count === 1) {
- assert.deepEqual(
- args.domain,
- [["display_name", "ilike", "b"]],
- "the correct domain should be sent"
- );
- }
-
- filter_count += 1;
- return Promise.resolve(true);
- }
- };
-
- const webClient = await createWebClient({ serverData, mockRPC });
-
- await doAction(webClient, {
- id: 1,
- res_model: "partner",
- type: "ir.actions.act_window",
- views: [[false, "list"]],
- });
-
- var filter_count = 0;
- // Add a first filter
- await toggleFilterMenu(webClient);
- await toggleAddCustomFilter(webClient);
- await editConditionValue(webClient, 0, "a");
- await applyFilter(webClient);
-
- // Add it to dashboard
- await toggleFavoriteMenu(webClient);
- await testUtils.dom.click($(".o_add_to_board button.o_dropdown_toggler"));
- await testUtils.dom.click($(".o_add_to_board .o_dropdown_menu button"));
-
- // Remove it
- await testUtils.dom.click(webClient.el.querySelector(".o_facet_remove"));
-
- // Add the second filter
- await toggleFilterMenu(webClient);
- await toggleAddCustomFilter(webClient);
- await editConditionValue(webClient, 0, "b");
- await applyFilter(webClient);
- // Add it to dashboard
- await toggleFavoriteMenu(webClient);
- await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board button.o_dropdown_toggler"));
- await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board .o_dropdown_menu button"));
-});
-
-QUnit.test("save a action domain to dashboard", async function (assert) {
- // View domains are to be added to the dashboard domain
- assert.expect(1);
-
- var view_domain = ["display_name", "ilike", "a"];
- var filter_domain = ["display_name", "ilike", "b"];
-
- // The filter domain already contains the view domain, but is always added by dashboard..,
- var expected_domain = ["&", view_domain, "&", view_domain, filter_domain];
-
- serverData.views = {
- "partner,false,list": '
',
- "partner,false,search": "",
- };
-
- const mockRPC = (route, args) => {
- if (route === "/board/add_to_dashboard") {
- assert.deepEqual(args.domain, expected_domain, "the correct domain should be sent");
- return Promise.resolve(true);
- }
- };
-
- const webClient = await createWebClient({ serverData, mockRPC });
-
- await doAction(webClient, {
- id: 1,
- res_model: "partner",
- type: "ir.actions.act_window",
- views: [[false, "list"]],
- domain: [view_domain],
- });
-
- // Add a filter
- await toggleFilterMenu(webClient);
- await toggleAddCustomFilter(webClient);
- await editConditionValue(webClient, 0, "b");
- await applyFilter(webClient);
- // Add it to dashboard
- await toggleFavoriteMenu(webClient);
- await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board button.o_dropdown_toggler"));
- // add
- await testUtils.dom.click(webClient.el.querySelector(".o_add_to_board .o_dropdown_menu button"));
-});
-
-QUnit.test("Views should be loaded in the user's language", async function (assert) {
- assert.expect(2);
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- session: {user_context: {lang: 'fr_FR'}},
- arch: '',
- mockRPC: function (route, args) {
- if (args.method === 'load_views') {
- assert.deepEqual(pyUtils.eval('context', args.kwargs.context), {lang: 'fr_FR'},
- 'The views should be loaded with the correct context');
- }
- if (route === "/web/dataset/search_read") {
- assert.equal(args.context.lang, 'fr_FR',
- 'The data should be loaded with the correct context');
- }
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '
',
- },
+ "" +
+ "" +
+ "",
+ });
+
+ assert.hasClass(
+ form.renderer.$el,
+ "o_dashboard",
+ "with a dashboard, the renderer should have the proper css class"
+ );
+ assert.containsOnce(
+ form,
+ ".o_dashboard .o_view_nocontent",
+ "should have a no content helper"
+ );
+ assert.strictEqual(form.getTitle(), "My Dashboard", "should have the correct title");
+ form.destroy();
});
- form.destroy();
-});
-
-QUnit.test("Dashboard should use correct groupby", async function (assert) {
- assert.expect(1);
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route, args) {
- if (args.method === 'web_read_group') {
- assert.deepEqual(args.kwargs.groupby, ['bar'],
- 'user defined groupby should have precedence on action groupby');
- }
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- context: {
- group_by: 'some_field',
- },
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '
',
- },
- });
-
- form.destroy();
-});
-
-QUnit.test("Dashboard should use correct groupby when defined as a string of one field", async function (assert) {
- assert.expect(1);
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route, args) {
- if (args.method === 'web_read_group') {
- assert.deepEqual(args.kwargs.groupby, ['bar'],
- 'user defined groupby should have precedence on action groupby');
- }
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- context: {
- group_by: 'some_field',
- },
- views: [[4, 'list']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,list':
- '
',
- },
- });
-
- form.destroy();
-});
-
-QUnit.test('click on a cell of pivot view inside dashboard', async function (assert) {
- assert.expect(3);
-
- var form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: '',
- mockRPC: function (route) {
- if (route === '/web/action/load') {
- return Promise.resolve({
- res_model: 'partner',
- views: [[4, 'pivot']],
- });
- }
- return this._super.apply(this, arguments);
- },
- archs: {
- 'partner,4,pivot': '',
- },
- intercepts: {
- do_action: function () {
- assert.step('do action');
- },
- },
- });
-
- assert.verifySteps([]);
-
- await testUtils.dom.click(form.$('.o_legacy_pivot .o_pivot_cell_value'));
-
- assert.verifySteps(['do action']);
-
- form.destroy();
-});
-
-// TODO: The button "Add to my dashboard" is not yet developped on the new control panel search view
-QUnit.skip(
- "correctly save the time ranges of a reporting view in comparison mode",
- async function (assert) {
+ QUnit.test("display the no content helper", async function (assert) {
assert.expect(1);
- const unpatchDate = patchDate(2020, 6, 1, 11, 0, 0);
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ viewOptions: {
+ action: {
+ help: 'click to add a partner
',
+ },
+ },
+ });
- serverData.models.partner.fields.date = {
- string: "Date",
- type: "date",
- sortable: true,
- };
+ assert.containsOnce(
+ form,
+ ".o_dashboard .o_view_nocontent",
+ "should have a no content helper with action help"
+ );
+ form.destroy();
+ });
+
+ QUnit.test("basic functionality, with one sub action", async function (assert) {
+ assert.expect(26);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route, args) {
+ if (route === "/web/action/load") {
+ assert.step("load action");
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ if (route === "/web/dataset/search_read") {
+ assert.deepEqual(
+ args.domain,
+ [["foo", "!=", "False"]],
+ "the domain should be passed"
+ );
+ assert.deepEqual(
+ args.context.orderedBy,
+ [
+ {
+ name: "foo",
+ asc: true,
+ },
+ ],
+ "orderedBy is present in the search read when specified on the custom action"
+ );
+ }
+ if (route === "/web/view/edit_custom") {
+ assert.step("edit custom");
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ },
+ });
+
+ assert.containsOnce(form, ".oe_dashboard_links", "should have rendered a link div");
+ assert.containsOnce(
+ form,
+ 'table.oe_dashboard[data-layout="2-1"]',
+ "should have rendered a table"
+ );
+ assert.containsNone(
+ form,
+ "td.o_list_record_selector",
+ "td should not have a list selector"
+ );
+ assert.strictEqual(
+ form.$("h2 span.oe_header_txt:contains(ABC)").length,
+ 1,
+ "should have rendered a header with action string"
+ );
+ assert.containsN(form, "tr.o_data_row", 3, "should have rendered 3 data rows");
+
+ assert.ok(form.$(".oe_content").is(":visible"), "content is visible");
+
+ await testUtils.dom.click(form.$(".oe_fold"));
+
+ assert.notOk(form.$(".oe_content").is(":visible"), "content is no longer visible");
+
+ await testUtils.dom.click(form.$(".oe_fold"));
+
+ assert.ok(form.$(".oe_content").is(":visible"), "content is visible again");
+ assert.verifySteps(["load action", "edit custom", "edit custom"]);
+
+ assert.strictEqual($(".modal").length, 0, "should have no modal open");
+
+ await testUtils.dom.click(form.$("button.oe_dashboard_link_change_layout"));
+
+ assert.strictEqual($(".modal").length, 1, "should have opened a modal");
+ assert.strictEqual(
+ $('.modal li[data-layout="2-1"] i.oe_dashboard_selected_layout').length,
+ 1,
+ "should mark currently selected layout"
+ );
+
+ await testUtils.dom.click($('.modal .oe_dashboard_layout_selector li[data-layout="1-1"]'));
+
+ assert.strictEqual($(".modal").length, 0, "should have no modal open");
+ assert.containsOnce(
+ form,
+ 'table.oe_dashboard[data-layout="1-1"]',
+ "should have rendered a table with correct layout"
+ );
+
+ assert.containsOnce(form, ".oe_action", "should have one displayed action");
+ await testUtils.dom.click(form.$("span.oe_close"));
+
+ assert.strictEqual($(".modal").length, 1, "should have opened a modal");
+
+ // confirm the close operation
+ await testUtils.dom.click($(".modal button.btn-primary"));
+
+ assert.strictEqual($(".modal").length, 0, "should have no modal open");
+ assert.containsNone(form, ".oe_action", "should have no displayed action");
+
+ assert.verifySteps(["edit custom", "edit custom"]);
+ form.destroy();
+ });
+
+ QUnit.test("views in the dashboard do not have a control panel", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ "",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [
+ [4, "list"],
+ [5, "form"],
+ ],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ },
+ });
+
+ assert.containsOnce(form, ".o_action .o_list_view");
+ assert.containsNone(form, ".o_action .o_control_panel");
+
+ form.destroy();
+ });
+
+ QUnit.test("can render an action without view_mode attribute", async function (assert) {
+ // The view_mode attribute is automatically set to the 'action' nodes when
+ // the action is added to the dashboard using the 'Add to dashboard' button
+ // in the searchview. However, other dashboard views can be written by hand
+ // (see openacademy tutorial), and in this case, we don't want hardcode
+ // action's params (like context or domain), as the dashboard can directly
+ // retrieve them from the action. Same applies for the view_type, as the
+ // first view of the action can be used, by default.
+ assert.expect(3);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ archs: {
+ "partner,4,list": '',
+ },
+ mockRPC: function (route, args) {
+ if (route === "/board/static/src/img/layout_1-1-1.png") {
+ return Promise.resolve();
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ context: '{"b": 2}',
+ domain: '[["foo", "=", "yop"]]',
+ res_model: "partner",
+ views: [
+ [4, "list"],
+ [false, "form"],
+ ],
+ });
+ }
+ if (args.method === "load_views") {
+ assert.deepEqual(
+ args.kwargs.context,
+ { a: 1, b: 2 },
+ "should have mixed both contexts"
+ );
+ }
+ if (route === "/web/dataset/search_read") {
+ assert.deepEqual(
+ args.domain,
+ [["foo", "=", "yop"]],
+ "should use the domain of the action"
+ );
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(
+ form.$(".oe_action:contains(ABC) .o_list_view").length,
+ 1,
+ "the list view (first view of action) should have been rendered correctly"
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test("can sort a sub list", async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.sortable = true;
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ },
+ });
+
+ assert.strictEqual(
+ $("tr.o_data_row").text(),
+ "yoplalalaabc",
+ "should have correct initial data"
+ );
+
+ await testUtils.dom.click(form.$("th.o_column_sortable:contains(Foo)"));
+
+ assert.strictEqual(
+ $("tr.o_data_row").text(),
+ "abclalalayop",
+ "data should have been sorted"
+ );
+ form.destroy();
+ });
+
+ QUnit.test("can open a record", async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ },
+ intercepts: {
+ do_action: function (event) {
+ assert.deepEqual(
+ event.data.action,
+ {
+ res_id: 1,
+ res_model: "partner",
+ type: "ir.actions.act_window",
+ views: [[false, "form"]],
+ },
+ "should do a do_action with correct parameters"
+ );
+ },
+ },
+ });
+
+ await testUtils.dom.click(form.$("tr.o_data_row td:contains(yop)"));
+ form.destroy();
+ });
+
+ QUnit.test("can open record using action form view", async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [
+ [4, "list"],
+ [5, "form"],
+ ],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ "partner,5,form": '',
+ },
+ intercepts: {
+ do_action: function (event) {
+ assert.deepEqual(
+ event.data.action,
+ {
+ res_id: 1,
+ res_model: "partner",
+ type: "ir.actions.act_window",
+ views: [[5, "form"]],
+ },
+ "should do a do_action with correct parameters"
+ );
+ },
+ },
+ });
+
+ await testUtils.dom.click(form.$("tr.o_data_row td:contains(yop)"));
+ form.destroy();
+ });
+
+ QUnit.test("can drag and drop a view", async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ if (route === "/web/view/edit_custom") {
+ assert.step("edit custom");
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ },
+ });
+
+ assert.containsOnce(form, "td.index_0 .oe_action", "initial action is in column 0");
+
+ await testUtils.dom.dragAndDrop(
+ form.$(".oe_dashboard_column.index_0 .oe_header"),
+ form.$(".oe_dashboard_column.index_1")
+ );
+ assert.containsNone(form, "td.index_0 .oe_action", "initial action is not in column 0");
+ assert.containsOnce(form, "td.index_1 .oe_action", "initial action is in in column 1");
+ assert.verifySteps(["edit custom"]);
+
+ form.destroy();
+ });
+
+ QUnit.test("twice the same action in a dashboard", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [
+ [4, "list"],
+ [5, "kanban"],
+ ],
+ });
+ }
+ if (route === "/web/view/edit_custom") {
+ assert.step("edit custom");
+ return Promise.resolve(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '',
+ "partner,5,kanban":
+ '' +
+ '
' +
+ "",
+ },
+ });
+
+ var $firstAction = form.$(".oe_action:contains(ABC)");
+ assert.strictEqual(
+ $firstAction.find(".o_list_view").length,
+ 1,
+ "list view should be displayed in 'ABC' block"
+ );
+ var $secondAction = form.$(".oe_action:contains(DEF)");
+ assert.strictEqual(
+ $secondAction.find(".o_kanban_view").length,
+ 1,
+ "kanban view should be displayed in 'DEF' block"
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test("non-existing action in a dashboard", async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ intercepts: {
+ load_views: function () {
+ throw new Error("load_views should not be called");
+ },
+ },
+ mockRPC: function (route) {
+ if (route === "/board/static/src/img/layout_1-1-1.png") {
+ return Promise.resolve();
+ }
+ if (route === "/web/action/load") {
+ // server answer if the action doesn't exist anymore
+ return Promise.resolve(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(
+ form.$(".oe_action:contains(ABC)").length,
+ 1,
+ "there should be a box for the non-existing action"
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test("clicking on a kanban's button should trigger the action", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ archs: {
+ "partner,false,kanban":
+ '' +
+ "" +
+ '' +
+ "
" +
+ '' +
+ "
" +
+ "",
+ },
+ intercepts: {
+ execute_action: function (event) {
+ var data = event.data;
+ assert.strictEqual(data.env.model, "partner", "should have correct model");
+ assert.strictEqual(
+ data.action_data.name,
+ "sitting_on_a_park_bench",
+ "should call correct method"
+ );
+ },
+ },
+
+ mockRPC: function (route) {
+ if (route === "/board/static/src/img/layout_1-1-1.png") {
+ return Promise.resolve();
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ view_mode: "kanban",
+ views: [[false, "kanban"]],
+ });
+ }
+ if (route === "/web/dataset/search_read") {
+ return Promise.resolve({ records: [{ foo: "aqualung" }] });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$(".o_kanban_test").find("button:first"));
+
+ form.destroy();
+ });
+
+ QUnit.test("subviews are aware of attach in or detach from the DOM", async function (assert) {
+ assert.expect(2);
+
+ // patch list renderer `on_attach_callback` for the test only
+ testUtils.mock.patch(ListRenderer, {
+ on_attach_callback: function () {
+ assert.step("subview on_attach_callback");
+ },
+ });
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '
',
+ },
+ });
+
+ assert.verifySteps(["subview on_attach_callback"]);
+
+ // restore on_attach_callback of ListRenderer
+ testUtils.mock.unpatch(ListRenderer);
+
+ form.destroy();
+ });
+
+ QUnit.test(
+ "dashboard intercepts custom events triggered by sub controllers",
+ async function (assert) {
+ assert.expect(1);
+
+ // we patch the ListController to force it to trigger the custom events that
+ // we want the dashboard to intercept (to stop them or to tweak their data)
+ testUtils.mock.patch(ListController, {
+ start: function () {
+ this.trigger_up("update_filters");
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var board = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({ res_model: "partner", views: [[false, "list"]] });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,false,list": '',
+ },
+ intercepts: {
+ update_filters: assert.step.bind(assert, "update_filters"),
+ },
+ });
+
+ assert.verifySteps([]);
+
+ testUtils.mock.unpatch(ListController);
+ board.destroy();
+ }
+ );
+
+ QUnit.test("save actions to dashboard", async function (assert) {
+ assert.expect(6);
+
+ testUtils.mock.patch(ListController, {
+ getOwnedQueryParams: function () {
+ var result = this._super.apply(this, arguments);
+ result.context = {
+ fire: "on the bayou",
+ };
+ return result;
+ },
+ });
+
+ serverData.models.partner.fields.foo.sortable = true;
serverData.views = {
- "partner,false,pivot": '',
- "partner,false,search": '',
+ "partner,false,list": '
',
+ "partner,false,search": "",
};
const mockRPC = (route, args) => {
if (route === "/board/add_to_dashboard") {
- assert.deepEqual(args.context_to_save.comparison, {
- comparisonId: "previous_period",
- fieldName: "date",
- fieldDescription: "Date",
- rangeDescription: "July 2020",
- range: ["&", ["date", ">=", "2020-07-01"], ["date", "<=", "2020-07-31"]],
- comparisonRange: [
- "&",
- ["date", ">=", "2020-06-01"],
- ["date", "<=", "2020-06-30"],
+ assert.deepEqual(
+ args.context_to_save.group_by,
+ ["foo"],
+ "The group_by should have been saved"
+ );
+ assert.deepEqual(
+ args.context_to_save.orderedBy,
+ [
+ {
+ name: "foo",
+ asc: true,
+ },
],
- comparisonRangeDescription: "June 2020",
- });
+ "The orderedBy should have been saved"
+ );
+ assert.strictEqual(
+ args.context_to_save.fire,
+ "on the bayou",
+ "The context of a controller should be passed and flattened"
+ );
+ assert.strictEqual(args.action_id, 1, "should save the correct action");
+ assert.strictEqual(args.view_mode, "list", "should save the correct view type");
return Promise.resolve(true);
}
};
- registry.category("services").add("user", makeFakeUserService());
-
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, {
id: 1,
res_model: "partner",
type: "ir.actions.act_window",
- views: [[false, "pivot"]],
+ views: [[false, "list"]],
});
- // filter on July 2020
- await toggleFilterMenu(webClient);
- await toggleMenuItem(webClient, "Date");
- await toggleMenuItemOption(webClient, "Date", "July");
+ assert.containsOnce(webClient, ".o_list_view", "should display the list view");
- // compare July 2020 to June 2020
- await toggleComparisonMenu(webClient);
- await toggleMenuItem(webClient, 0);
+ // Sort the list
+ await testUtils.dom.click($(".o_column_sortable"));
- // add the view to the dashboard
+ // Group It
+ await toggleGroupByMenu(webClient);
+ await toggleAddCustomGroup(webClient);
+ await applyGroup(webClient);
+
+ // add this action to dashboard
await toggleFavoriteMenu(webClient);
await testUtils.dom.click($(".o_add_to_board button.o_dropdown_toggler"));
await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
- await testUtils.dom.click($(".o_add_to_board div button"));
+ await testUtils.dom.click($(".o_add_to_board .o_dropdown_menu button"));
- unpatchDate();
- }
-);
+ testUtils.mock.unpatch(ListController);
+ });
-QUnit.test('correctly display the time range descriptions of a reporting view in comparison mode', async function (assert) {
- assert.expect(1);
+ QUnit.test("save two searches to dashboard", async function (assert) {
+ // the second search saved should not be influenced by the first
+ assert.expect(2);
- this.data.partner.fields.date = { string: 'Date', type: 'date', sortable: true };
- this.data.partner.records[0].date = '2020-07-15';
+ serverData.views = {
+ "partner,false,list": '
',
+ "partner,false,search": "",
+ };
- const form = await createView({
- View: BoardView,
- model: 'board',
- data: this.data,
- arch: `",
+ mockRPC: function (route, args) {
+ if (args.method === "load_views") {
+ assert.deepEqual(
+ pyUtils.eval("context", args.kwargs.context),
+ { lang: "fr_FR" },
+ "The views should be loaded with the correct context"
+ );
+ }
+ if (route === "/web/dataset/search_read") {
+ assert.equal(
+ args.context.lang,
+ "fr_FR",
+ "The data should be loaded with the correct context"
+ );
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '
',
+ },
+ });
+
+ form.destroy();
+ });
+
+ QUnit.test("Dashboard should use correct groupby", async function (assert) {
+ assert.expect(1);
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route, args) {
+ if (args.method === "web_read_group") {
+ assert.deepEqual(
+ args.kwargs.groupby,
+ ["bar"],
+ "user defined groupby should have precedence on action groupby"
+ );
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ context: {
+ group_by: "some_field",
+ },
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '
',
+ },
+ });
+
+ form.destroy();
+ });
+
+ QUnit.test(
+ "Dashboard should use correct groupby when defined as a string of one field",
+ async function (assert) {
+ assert.expect(1);
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ '",
+ mockRPC: function (route, args) {
+ if (args.method === "web_read_group") {
+ assert.deepEqual(
+ args.kwargs.groupby,
+ ["bar"],
+ "user defined groupby should have precedence on action groupby"
+ );
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ context: {
+ group_by: "some_field",
+ },
+ views: [[4, "list"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,list": '
',
+ },
+ });
+
+ form.destroy();
+ }
+ );
+
+ QUnit.test("click on a cell of pivot view inside dashboard", async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch:
+ "",
+ mockRPC: function (route) {
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ res_model: "partner",
+ views: [[4, "pivot"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ "partner,4,pivot": '',
+ },
+ intercepts: {
+ do_action: function () {
+ assert.step("do action");
+ },
+ },
+ });
+
+ assert.verifySteps([]);
+
+ await testUtils.dom.click(form.$(".o_legacy_pivot .o_pivot_cell_value"));
+
+ assert.verifySteps(["do action"]);
+
+ form.destroy();
+ });
+
+ // TODO: The button "Add to my dashboard" is not yet developped on the new control panel search view
+ QUnit.skip(
+ "correctly save the time ranges of a reporting view in comparison mode",
+ async function (assert) {
+ assert.expect(1);
+
+ const unpatchDate = patchDate(2020, 6, 1, 11, 0, 0);
+
+ serverData.models.partner.fields.date = {
+ string: "Date",
+ type: "date",
+ sortable: true,
+ };
+
+ serverData.views = {
+ "partner,false,pivot": '',
+ "partner,false,search": '',
+ };
+
+ const mockRPC = (route, args) => {
+ if (route === "/board/add_to_dashboard") {
+ assert.deepEqual(args.context_to_save.comparison, {
+ comparisonId: "previous_period",
+ fieldName: "date",
+ fieldDescription: "Date",
+ rangeDescription: "July 2020",
+ range: ["&", ["date", ">=", "2020-07-01"], ["date", "<=", "2020-07-31"]],
+ comparisonRange: [
+ "&",
+ ["date", ">=", "2020-06-01"],
+ ["date", "<=", "2020-06-30"],
+ ],
+ comparisonRangeDescription: "June 2020",
+ });
+ return Promise.resolve(true);
+ }
+ };
+
+ registry.category("services").add("user", makeFakeUserService());
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, {
+ id: 1,
+ res_model: "partner",
+ type: "ir.actions.act_window",
+ views: [[false, "pivot"]],
+ });
+
+ // filter on July 2020
+ await toggleFilterMenu(webClient);
+ await toggleMenuItem(webClient, "Date");
+ await toggleMenuItemOption(webClient, "Date", "July");
+
+ // compare July 2020 to June 2020
+ await toggleComparisonMenu(webClient);
+ await toggleMenuItem(webClient, 0);
+
+ // add the view to the dashboard
+ await toggleFavoriteMenu(webClient);
+
+ await testUtils.dom.click($(".o_add_to_board button.o_dropdown_toggler"));
+ await testUtils.fields.editInput($(".o_add_to_board input"), "a name");
+ await testUtils.dom.click($(".o_add_to_board div button"));
+
+ unpatchDate();
+ }
+ );
+
+ QUnit.test(
+ "correctly display the time range descriptions of a reporting view in comparison mode",
+ async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.date = { string: "Date", type: "date", sortable: true };
+ this.data.partner.records[0].date = "2020-07-15";
+
+ const form = await createView({
+ View: BoardView,
+ model: "board",
+ data: this.data,
+ arch: ``,
- archs: {
- 'partner,1,pivot':
- '',
- },
- mockRPC: function (route, args) {
- if (route === '/board/static/src/img/layout_1-1-1.png') {
- return Promise.resolve();
- }
- if (route === '/web/action/load') {
- return Promise.resolve({
- context: JSON.stringify({ comparison: {
- comparisonId: "previous_period",
- fieldName: "date",
- fieldDescription: "Date",
- rangeDescription: "July 2020",
- range: ["&",["date", ">=", "2020-07-01"], ["date", "<=", "2020-07-31"]],
- comparisonRange: ["&", ["date", ">=", "2020-06-01"], ["date", "<=", "2020-06-30"]],
- comparisonRangeDescription: "June 2020",
- }}),
- domain: '[]',
- res_model: 'partner',
- views: [[1, 'pivot']],
- });
- }
- return this._super.apply(this, arguments);
- },
- });
+ archs: {
+ "partner,1,pivot": '',
+ },
+ mockRPC: function (route, args) {
+ if (route === "/board/static/src/img/layout_1-1-1.png") {
+ return Promise.resolve();
+ }
+ if (route === "/web/action/load") {
+ return Promise.resolve({
+ context: JSON.stringify({
+ comparison: {
+ comparisonId: "previous_period",
+ fieldName: "date",
+ fieldDescription: "Date",
+ rangeDescription: "July 2020",
+ range: [
+ "&",
+ ["date", ">=", "2020-07-01"],
+ ["date", "<=", "2020-07-31"],
+ ],
+ comparisonRange: [
+ "&",
+ ["date", ">=", "2020-06-01"],
+ ["date", "<=", "2020-06-30"],
+ ],
+ comparisonRangeDescription: "June 2020",
+ },
+ }),
+ domain: "[]",
+ res_model: "partner",
+ views: [[1, "pivot"]],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
- assert.deepEqual(
- [...form.el.querySelectorAll('div.o_legacy_pivot th.o_pivot_origin_row')].map(el => el.innerText),
- ['June 2020', 'July 2020', 'Variation']
+ assert.deepEqual(
+ [...form.el.querySelectorAll("div.o_legacy_pivot th.o_pivot_origin_row")].map(
+ (el) => el.innerText
+ ),
+ ["June 2020", "July 2020", "Variation"]
+ );
+
+ form.destroy();
+ }
);
-
- form.destroy();
-});
});
diff --git a/addons/crm/static/tests/forecast_view_tests.js b/addons/crm/static/tests/forecast_view_tests.js
index b3a05e29150..7d2992142e7 100644
--- a/addons/crm/static/tests/forecast_view_tests.js
+++ b/addons/crm/static/tests/forecast_view_tests.js
@@ -1,24 +1,23 @@
/** @odoo-module **/
-import { _lt } from "@web/core/l10n/translation";
-import AbstractView from "web.AbstractView";
-import AbstractModel from "web.AbstractModel";
-import { controlPanel as cpHelpers } from "web.test_utils";
-import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
-import { dialogService } from "@web/core/dialog/dialog_service";
import { legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
-import legacyViewRegistry from "web.view_registry";
-import { makeView } from "@web/../tests/views/helpers";
-import { mock } from "web.test_utils";
-import { registry } from "@web/core/registry";
import {
setupControlPanelServiceRegistry,
switchView,
toggleFilterMenu,
toggleGroupByMenu,
toggleMenuItem,
- toggleMenuItemOption,
+ toggleMenuItemOption
} from "@web/../tests/search/helpers";
+import { makeView } from "@web/../tests/views/helpers";
+import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
+import { dialogService } from "@web/core/dialog/dialog_service";
+import { _lt } from "@web/core/l10n/translation";
+import { registry } from "@web/core/registry";
+import AbstractModel from "web.AbstractModel";
+import AbstractView from "web.AbstractView";
+import { controlPanel as cpHelpers, mock } from "web.test_utils";
+import legacyViewRegistry from "web.view_registry";
const patchDate = mock.patchDate;
@@ -88,7 +87,7 @@ QUnit.module("Views", (hooks) => {
const forecastGraph = await makeView({
resModel: "foo",
- type: "forecast_graph",
+ type: "graph",
serverData,
searchViewId: false,
context: {
@@ -133,7 +132,7 @@ QUnit.module("Views", (hooks) => {
await makeView({
resModel: "foo",
- type: "forecast_graph",
+ type: "graph",
serverData,
searchViewId: false,
context: {
diff --git a/addons/google_spreadsheet/static/src/add_to_google_spreadsheet/add_to_google_spreadsheet.js b/addons/google_spreadsheet/static/src/add_to_google_spreadsheet/add_to_google_spreadsheet.js
index 49c0e988001..970b6e83ff1 100644
--- a/addons/google_spreadsheet/static/src/add_to_google_spreadsheet/add_to_google_spreadsheet.js
+++ b/addons/google_spreadsheet/static/src/add_to_google_spreadsheet/add_to_google_spreadsheet.js
@@ -45,7 +45,7 @@ AddToGoogleSpreadsheet.template = "google_spreadsheet.AddToGoogleSpreadsheet";
const addToGoogleSpreadsheetItem = {
Component: AddToGoogleSpreadsheet,
groupNumber: 4,
- isDisplayed: ({ searchModel }) => searchModel.action.type === "ir.actions.act_window",
+ isDisplayed: ({ config }) => config.actionType === "ir.actions.act_window",
};
favoriteMenuRegistry.add("add-to-google-spreadsheet", addToGoogleSpreadsheetItem, { sequence: 20 });
diff --git a/addons/project/static/src/js/project_graph_view.js b/addons/project/static/src/js/project_graph_view.js
index 40cf831393f..066193f4b15 100644
--- a/addons/project/static/src/js/project_graph_view.js
+++ b/addons/project/static/src/js/project_graph_view.js
@@ -1,12 +1,12 @@
/** @odoo-module **/
-import { GraphView } from "@web/views/graph/graph_view";
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
+import { GraphView } from "@web/views/graph/graph_view";
const viewRegistry = registry.category("views");
class ProjectGraphView extends GraphView {}
-ProjectGraphView.components = { ...GraphView.components, ControlPanel: ProjectControlPanel };
+ProjectGraphView.ControlPanel = ProjectControlPanel;
viewRegistry.add("project_graph", ProjectGraphView);
diff --git a/addons/project/static/src/js/project_pivot_view.js b/addons/project/static/src/js/project_pivot_view.js
index 2a28361e861..a221d933405 100644
--- a/addons/project/static/src/js/project_pivot_view.js
+++ b/addons/project/static/src/js/project_pivot_view.js
@@ -1,12 +1,12 @@
/** @odoo-module **/
-import { PivotView } from "@web/views/pivot/pivot_view";
import { ProjectControlPanel } from "@project/project_control_panel/project_control_panel";
import { registry } from "@web/core/registry";
+import { PivotView } from "@web/views/pivot/pivot_view";
const viewRegistry = registry.category("views");
class ProjectPivotView extends PivotView {}
-ProjectPivotView.components = { ...PivotView.components, ControlPanel: ProjectControlPanel };
+ProjectPivotView.ControlPanel = ProjectControlPanel;
viewRegistry.add("project_pivot", ProjectPivotView);
diff --git a/addons/project/static/src/js/project_rating_graph_view.js b/addons/project/static/src/js/project_rating_graph_view.js
index 4c98d62a001..380ef3ffb2b 100644
--- a/addons/project/static/src/js/project_rating_graph_view.js
+++ b/addons/project/static/src/js/project_rating_graph_view.js
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { _lt } from "@web/core/l10n/translation";
-import { GraphView } from "@web/views/graph/graph_view";
-import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
import { registry } from "@web/core/registry";
+import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
+import { GraphView } from "@web/views/graph/graph_view";
const viewRegistry = registry.category("views");
@@ -29,6 +29,6 @@ class ProjectRatingArchParser extends GraphArchParser {
// Would it be not better achiedved by using a proper arch directly?
class ProjectRatingGraphView extends GraphView {}
-ProjectRatingGraphView.archParser = ProjectRatingArchParser;
+ProjectRatingGraphView.ArchParser = ProjectRatingArchParser;
viewRegistry.add("project_rating_graph", ProjectRatingGraphView);
diff --git a/addons/project/static/src/js/project_rating_pivot_view.js b/addons/project/static/src/js/project_rating_pivot_view.js
index 5d54afceb19..e5dae9a110a 100644
--- a/addons/project/static/src/js/project_rating_pivot_view.js
+++ b/addons/project/static/src/js/project_rating_pivot_view.js
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { _lt } from "@web/core/l10n/translation";
-import { PivotView } from "@web/views/pivot/pivot_view";
-import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
import { registry } from "@web/core/registry";
+import { PivotArchParser } from "@web/views/pivot/pivot_arch_parser";
+import { PivotView } from "@web/views/pivot/pivot_view";
const viewRegistry = registry.category("views");
@@ -29,6 +29,6 @@ class ProjectRatingArchParser extends PivotArchParser {
// Would it be not better achiedved by using a proper arch directly?
class ProjectRatingPivotView extends PivotView {}
-ProjectRatingPivotView.archParser = ProjectRatingArchParser;
+ProjectRatingPivotView.ArchParser = ProjectRatingArchParser;
viewRegistry.add("project_rating_pivot", ProjectRatingPivotView);
diff --git a/addons/project/static/src/project_control_panel/project_control_panel.js b/addons/project/static/src/project_control_panel/project_control_panel.js
index 6d415a547ff..29e12668102 100644
--- a/addons/project/static/src/project_control_panel/project_control_panel.js
+++ b/addons/project/static/src/project_control_panel/project_control_panel.js
@@ -9,7 +9,7 @@ export class ProjectControlPanel extends ControlPanel {
this.orm = useService("orm");
this.user = useService("user");
const { active_id, show_project_update } = this.env.searchModel.globalContext;
- this.showProjectUpdate = this.env.searchModel.view.type === "form" || show_project_update;
+ this.showProjectUpdate = this.env.config.viewType === "form" || show_project_update;
this.projectId = this.showProjectUpdate ? active_id : false;
}
@@ -48,4 +48,5 @@ export class ProjectControlPanel extends ControlPanel {
});
}
}
+
ProjectControlPanel.template = "project.ProjectControlPanel";
diff --git a/addons/web/static/src/legacy/legacy_views.js b/addons/web/static/src/legacy/legacy_views.js
index f0c02eafaf4..48876eed3e2 100644
--- a/addons/web/static/src/legacy/legacy_views.js
+++ b/addons/web/static/src/legacy/legacy_views.js
@@ -51,10 +51,11 @@ function registerView(name, LegacyView) {
searchPanel = globalState.searchPanel;
}
- this.viewParams = Object.assign({}, this.props.actionFlags, {
+ const { actionFlags, breadcrumbs = [] } = this.env.config;
+ this.viewParams = Object.assign({}, actionFlags, {
action: this.props.action,
// legacy views automatically add the last part of the breadcrumbs
- breadcrumbs: breadcrumbsToLegacy(this.props.breadcrumbs),
+ breadcrumbs: breadcrumbsToLegacy(breadcrumbs),
modelName: this.props.resModel,
currentId: this.props.resId,
controllerState: {
@@ -99,7 +100,7 @@ function registerView(name, LegacyView) {
context: this.props.context,
};
const options = {
- actionId: this.props.action.id,
+ actionId: this.env.config.actionId,
loadActionMenus: this.props.loadActionMenus,
loadIrFilters: this.props.loadIrFilters,
};
@@ -120,10 +121,11 @@ function registerView(name, LegacyView) {
viewFields: result.fields_views.search.fields,
});
}
+ const { viewSwitcherEntries = [] } = this.env.config;
const views = this.viewParams.action.views
- .filter(([vid, vtype]) => vtype !== "search")
+ .filter(([, vtype]) => vtype !== "search")
.map(([vid, vtype]) => {
- const view = this.props.viewSwitcherEntries.find((v) => v.type === vtype);
+ const view = viewSwitcherEntries.find((v) => v.type === vtype);
if (view) {
return Object.assign({}, view, { viewID: vid });
} else {
diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js
index dac80643c39..28603350257 100644
--- a/addons/web/static/src/search/control_panel/control_panel.js
+++ b/addons/web/static/src/search/control_panel/control_panel.js
@@ -1,11 +1,11 @@
/** @odoo-module **/
+import { useService } from "@web/core/utils/hooks";
import { ComparisonMenu } from "../comparison_menu/comparison_menu";
import { FavoriteMenu } from "../favorite_menu/favorite_menu";
import { FilterMenu } from "../filter_menu/filter_menu";
import { GroupByMenu } from "../group_by_menu/group_by_menu";
import { SearchBar } from "../search_bar/search_bar";
-import { useService } from "@web/core/utils/hooks";
const { Component } = owl;
@@ -21,6 +21,44 @@ export class ControlPanel extends Component {
this.actionService = useService("action");
}
+ /**
+ * !!! What follows is a hack, do not copy it !!!
+ *
+ * Duplicates the slots defined for the parent s.t. they are also available
+ * for the current control panel.
+ *
+ * This hack is necessary since Owl does not support manual slots
+ * assignment/transfer yet. This must be removed as soon as Owl implements
+ * such a system.
+ *
+ * @strongly_discouraged_override
+ */
+ __render() {
+ const { slots } = this.env.qweb.constructor;
+ const { __owl__ } = this;
+ const originalSlots = {};
+ const transferredSlotNames = [
+ "control-panel-top-left",
+ "control-panel-top-right",
+ "control-panel-bottom-left",
+ "control-panel-bottom-right",
+ ];
+ for (const slotName of transferredSlotNames) {
+ const parentSlotkey = `${__owl__.parent.__owl__.slotId}_${slotName}`;
+ if (parentSlotkey in slots) {
+ const cpSlotKey = `${__owl__.slotId}_${slotName}`;
+ originalSlots[cpSlotKey] = slots[cpSlotKey];
+ slots[cpSlotKey] = function (scope, extra) {
+ slots[parentSlotkey].call(this, __owl__.parent.__owl__.scope, extra);
+ };
+ }
+ }
+ const res = super.__render(...arguments);
+ // Clean up
+ Object.assign(slots, originalSlots);
+ return res;
+ }
+
/**
* @returns {Object}
*/
@@ -78,25 +116,3 @@ export class ControlPanel extends Component {
ControlPanel.components = { ComparisonMenu, FavoriteMenu, FilterMenu, GroupByMenu, SearchBar };
ControlPanel.template = "web.ControlPanel";
-// ControlPanel.props = {
-// breadcrumbs: { type: Array, element: { jsId: String, name: String }, optional: true },
-// display: { type: Object, optional: true },
-// displayName: { type: String, optional: true },
-// viewSwitcherEntries: {
-// type: Array,
-// element: {
-// type: Object,
-// shape: {
-// active: { type: Boolean, optional: true },
-// icon: String,
-// multiRecord: { type: Boolean, optional: true },
-// name: [Object, String],
-// type: String,
-// },
-// },
-// optional: true,
-// },
-// };
-ControlPanel.defaultProps = {
- breadcrumbs: [],
-};
diff --git a/addons/web/static/src/search/control_panel/control_panel.xml b/addons/web/static/src/search/control_panel/control_panel.xml
index 617c77b2d98..a374b9f0a19 100644
--- a/addons/web/static/src/search/control_panel/control_panel.xml
+++ b/addons/web/static/src/search/control_panel/control_panel.xml
@@ -30,9 +30,9 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js
index 2a7bb73c24f..3af0cbfb574 100644
--- a/addons/web/static/src/views/view.js
+++ b/addons/web/static/src/views/view.js
@@ -2,21 +2,22 @@
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
-import { useService } from "@web/core/utils/hooks";
import { KeepLast } from "@web/core/utils/concurrency";
+import { useService } from "@web/core/utils/hooks";
+import { deepCopy } from "@web/core/utils/objects";
import { WithSearch } from "@web/search/with_search/with_search";
import { useActionLinks } from "@web/views/helpers/view_hook";
+import { extractLayoutComponents } from "@web/views/layout";
const viewRegistry = registry.category("views");
-const { Component } = owl;
+const { Component, hooks } = owl;
+const { useSubEnv } = hooks;
/** @typedef {Object} ViewProps
* @property {string} resModel
* @property {string} type
*
- * @property {Array[]} [views]
- *
* @property {string} [arch] if given, fields must be given too /\ no post processing is done (evaluation of "groups" attribute,...)
* @property {Object} [fields] if given, arch must be given too
* @property {number|false} [viewId]
@@ -35,18 +36,11 @@ const { Component } = owl;
* @property {string[]} [groupBy]
* @property {string[]} [orderBy]
*
- * @property {Object} [action={}]
- * @property {Object} [actionFlags={}]
- * @property {string} [displayName]
- *
* @property {boolean} [useSampleModel]
* @property {string} [noContentHelp]
*
* @property {Object} [display={}] to rework
*
- * @property {Object[]} [breadcrumbs]
- * @property {Object[]} [viewSwitcherEntries]
- *
* manipulated by withSearch
*
* @property {boolean} [activateFavorite]
@@ -55,12 +49,10 @@ const { Component } = owl;
* @property {Object} [globalState]
*/
-const STANDARD_PROPS = new Set([
+const STANDARD_PROPS = [
"resModel",
"type",
- "views",
-
"arch",
"fields",
"viewId",
@@ -79,17 +71,10 @@ const STANDARD_PROPS = new Set([
"groupBy",
"orderBy",
- "action",
- "actionFlags",
- "displayName",
-
"useSampleModel",
"noContentHelp",
- "breadcrumbs",
"display",
- "viewSwitcherEntries",
-
"globalState",
"activateFavorite",
@@ -97,7 +82,7 @@ const STANDARD_PROPS = new Set([
// LEGACY: remove this later (clean when mappings old state <-> new state are established)
"searchPanel",
"searchModel",
-]);
+];
export class View extends Component {
setup() {
@@ -117,11 +102,11 @@ export class View extends Component {
}
this.viewService = useService("view");
-
this.withSearchProps = null;
- owl.hooks.useSubEnv({
+ useSubEnv({
keepLast: new KeepLast(),
+ config: { ...(this.env.config || {}) },
});
useActionLinks({ resModel });
}
@@ -132,16 +117,16 @@ export class View extends Component {
const type = ViewClass.type;
// determine views for which descriptions should be obtained
- let { views, viewId, searchViewId } = this.props;
+ let { viewId, searchViewId } = this.props;
- views = JSON.parse(JSON.stringify(views));
-
- const view = views.find((v) => v[1] === type);
- if (view) {
+ const views = deepCopy(this.env.config.views || []);
+ const view = views.find((v) => v[1] === type) || [];
+ if (view.length) {
view[0] = viewId !== undefined ? viewId : view[0];
viewId = view[0];
} else {
- views.push([viewId || false, type]); // viewId will remain undefined if not specified and loadView=false
+ view.push(viewId || false, type);
+ views.push(view); // viewId will remain undefined if not specified and loadView=false
}
const searchView = views.find((v) => v[1] === "search");
@@ -154,26 +139,27 @@ export class View extends Component {
// searchViewId will remains undefined if loadSearchView=false
// prepare view description
- const { action, context, resModel, loadActionMenus, loadIrFilters } = this.props;
+ const { context, resModel, loadActionMenus, loadIrFilters } = this.props;
let { arch, fields, searchViewArch, searchViewFields, irFilters, actionMenus } = this.props;
let loadView = !arch || (!actionMenus && loadActionMenus);
let loadSearchView =
(searchViewId !== undefined && !searchViewArch) || (!irFilters && loadIrFilters);
- let viewDescription = { resModel, type };
+ let viewDescription = { viewId, resModel, type };
let searchViewDescription;
if (loadView || loadSearchView) {
// view description (or search view description if required) is incomplete
// a loadViews is done to complete the missing information
const viewDescriptions = await this.viewService.loadViews(
{ context, resModel, views },
- { actionId: action.id, loadActionMenus, loadIrFilters }
+ { actionId: this.env.config.actionId, loadActionMenus, loadIrFilters }
);
// Note: if this.props.views is different from views, the cached descriptions
// will certainly not be reused! (but for the standard flow this will work as
// before)
viewDescription = viewDescriptions[type];
+ view[0] = viewDescription.viewId;
searchViewDescription = viewDescriptions.search;
if (loadSearchView) {
if (!searchViewArch) {
@@ -184,6 +170,7 @@ export class View extends Component {
irFilters = searchViewDescription.irFilters;
}
}
+ this.env.config.views = views;
}
if (!arch) {
@@ -197,33 +184,28 @@ export class View extends Component {
const parser = new DOMParser();
const xml = parser.parseFromString(arch, "text/xml");
const rootNode = xml.documentElement;
- const rootAttrs = {};
- for (const attrName of rootNode.getAttributeNames()) {
- rootAttrs[attrName] = rootNode.getAttribute(attrName);
- }
+
+ const subType = rootNode.getAttribute("js_class");
+ const bannerRoute = rootNode.getAttribute("banner_route");
+ const sample = rootNode.getAttribute("sample");
// determine ViewClass to instantiate (if not already done)
- if (rootAttrs.js_class) {
- ViewClass = viewRegistry.get(rootAttrs.js_class);
+ if (subType) {
+ ViewClass = viewRegistry.get(subType);
}
- const bannerRoute = rootAttrs.banner_route;
+ Object.assign(this.env.config, {
+ viewId: viewDescription.viewId,
+ viewType: type,
+ viewSubType: subType,
+ bannerRoute,
+ ...extractLayoutComponents(ViewClass),
+ });
// prepare the view props
- let viewProps = {
- info: {
- actionFlags: this.props.actionFlags,
- action: this.props.action,
- viewId: viewDescription.viewId,
- views,
- mode: this.props.display.mode,
- actionMenus,
- breadcrumbs: this.props.breadcrumbs,
- viewSwitcherEntries: this.props.viewSwitcherEntries,
- displayName: this.props.displayName,
- },
+ const viewProps = {
+ info: { actionMenus, mode: this.props.display.mode },
arch,
- bannerRoute,
fields,
resModel,
useSampleModel: false,
@@ -234,12 +216,12 @@ export class View extends Component {
if ("useSampleModel" in this.props) {
viewProps.useSampleModel = this.props.useSampleModel;
- } else if (rootAttrs.sample) {
- viewProps.useSampleModel = Boolean(evaluateExpr(rootAttrs.sample));
+ } else if (sample) {
+ viewProps.useSampleModel = Boolean(evaluateExpr(sample));
}
for (const key in this.props) {
- if (!STANDARD_PROPS.has(key)) {
+ if (!STANDARD_PROPS.includes(key)) {
viewProps[key] = this.props[key];
}
}
@@ -254,13 +236,11 @@ export class View extends Component {
}
// prepare the WithSearh component props
- this.withSearchProps = { ...this.props };
-
- Object.assign(this.withSearchProps, {
+ this.withSearchProps = {
+ ...this.props,
Component: ViewClass,
componentProps: viewProps,
- view: { id: viewId, type },
- });
+ };
if (searchViewId !== undefined) {
this.withSearchProps.searchViewId = searchViewId;
@@ -298,13 +278,10 @@ export class View extends Component {
View.template = "web.View";
View.components = { WithSearch };
View.defaultProps = {
- action: { id: false, views: [] },
display: {},
context: {},
- actionFlags: {},
loadActionMenus: false,
loadIrFilters: false,
- views: [],
};
View.searchMenuTypes = ["filter", "groupBy", "favorite"];
diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js
index a6bf736bfbe..2b53fca6f62 100644
--- a/addons/web/static/src/webclient/actions/action_service.js
+++ b/addons/web/static/src/webclient/actions/action_service.js
@@ -1,18 +1,18 @@
/** @odoo-module **/
-import { cleanDomFromBootstrap } from "@web/legacy/utils";
import { browser } from "@web/core/browser/browser";
-import { useBus } from "@web/core/utils/hooks";
import { makeContext } from "@web/core/context";
+import { useDebugCategory } from "@web/core/debug/debug_context";
import { download } from "@web/core/network/download";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { KeepLast } from "@web/core/utils/concurrency";
+import { useBus } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
-import { useDebugCategory } from "@web/core/debug/debug_context";
+import { cleanDomFromBootstrap } from "@web/legacy/utils";
+import { View } from "@web/views/view";
import { ActionDialog } from "./action_dialog";
import { CallbackRecorder } from "./action_hook";
-import { View } from "@web/views/view";
const { Component, hooks, tags } = owl;
const { useRef, useSubEnv } = hooks;
@@ -307,10 +307,19 @@ function makeActionManager(env) {
/**
* @param {ClientAction | ActWindowAction} action
* @param {Object} props
- * @returns {ActionProps}
+ * @returns {{ props: ActionProps, config: Config }}
*/
- function _getActionProps(action, props) {
- return Object.assign({}, props, { action, actionId: action.id });
+ function _getActionInfo(action, props) {
+ return {
+ props: Object.assign({}, props, { action, actionId: action.id }),
+ config: {
+ actionId: action.id,
+ actionType: action.type,
+ actionFlags: action.flags,
+ displayName: action.display_name || action.name || "",
+ views: action.views,
+ },
+ };
}
/**
@@ -366,9 +375,9 @@ function makeActionManager(env) {
* @param {ActWindowAction} action
* @param {BaseView[]} views
* @param {Object} props
- * @returns {ViewProps}
+ * @returns {{ props: ViewProps, config: Config }}
*/
- function _getViewProps(view, action, views, props = {}) {
+ function _getViewInfo(view, action, views, props = {}) {
const target = action.target;
const viewSwitcherEntries = views
.filter((v) => v.multiRecord === view.multiRecord)
@@ -385,25 +394,15 @@ function makeActionManager(env) {
return viewSwitcherEntry;
});
const context = action.context || {};
- const flags = action.flags || {};
const viewProps = Object.assign({}, props, {
- actionFlags: Object.assign({}, flags, flags[view.type]), // review system
- action: {
- id: action.id || false,
- type: action.type || false,
- views: action.views || [],
- },
context,
display: { mode: target === "new" ? "inDialog" : target },
- displayName: action.display_name || action.name,
domain: action.domain || [],
groupBy: action.context.group_by || [],
loadActionMenus: target !== "new" && target !== "inline",
loadIrFilters: action.views.some((v) => v[1] === "search"),
resModel: action.res_model,
type: view.type,
- views: action.views,
- viewSwitcherEntries,
});
if (target === "inline") {
@@ -433,11 +432,25 @@ function makeActionManager(env) {
// LEGACY CODE COMPATIBILITY: remove when all views will be written in owl
if (view.isLegacy) {
const legacyActionInfo = { ...action, ...viewProps.action };
- Object.assign(viewProps, { action: legacyActionInfo, View: view });
+ Object.assign(viewProps, {
+ action: legacyActionInfo,
+ View: view,
+ views: action.views,
+ });
}
// END LEGACY CODE COMPATIBILITY
- return viewProps;
+ return {
+ props: viewProps,
+ config: {
+ actionId: action.id,
+ actionType: action.type,
+ actionFlags: action.flags,
+ displayName: action.display_name || action.name || "",
+ views: action.views,
+ viewSwitcherEntries,
+ },
+ };
}
/**
@@ -502,11 +515,24 @@ function makeActionManager(env) {
});
const action = controller.action;
+ // Compute breadcrumbs
+ const index = _computeStackIndex(options);
+ const controllerArray = [controller];
+ if (options.lazyController) {
+ controllerArray.unshift(options.lazyController);
+ }
+ const nextStack = controllerStack.slice(0, index).concat(controllerArray);
+ controller.config.breadcrumbs = _getBreadcrumbs(nextStack.slice(0, -1));
+ if (controller.Component.isLegacy) {
+ controller.props.breadcrumbs = controller.config.breadcrumbs;
+ }
+
class ControllerComponent extends Component {
setup() {
this.Component = controller.Component;
this.componentRef = useRef("component");
useDebugCategory("action", { action });
+ useSubEnv({ config: controller.config });
if (action.target !== "new") {
this.__beforeLeave__ = new CallbackRecorder();
this.__getGlobalState__ = new CallbackRecorder();
@@ -679,13 +705,6 @@ function makeActionManager(env) {
controller.props.globalState = controller.action.globalState;
}
- const index = _computeStackIndex(options);
- const controllerArray = [controller];
- if (options.lazyController) {
- controllerArray.unshift(options.lazyController);
- }
- const nextStack = controllerStack.slice(0, index).concat(controllerArray);
- controller.props.breadcrumbs = _getBreadcrumbs(nextStack.slice(0, nextStack.length - 1));
const closingProm = _executeCloseAction();
controller.__info__ = {
@@ -772,7 +791,7 @@ function makeActionManager(env) {
action,
view,
views,
- props: _getViewProps(view, action, views, options.props),
+ ..._getViewInfo(view, action, views, options.props),
};
action.controllers[view.type] = controller;
@@ -789,7 +808,7 @@ function makeActionManager(env) {
action,
view: lazyView,
views,
- props: _getViewProps(lazyView, action, views),
+ ..._getViewInfo(lazyView, action, views),
};
}
@@ -829,7 +848,7 @@ function makeActionManager(env) {
jsId: `controller_${++id}`,
Component: clientAction,
action,
- props: _getActionProps(action, options.props),
+ ..._getActionInfo(action, options.props),
};
return _updateUI(controller, {
clearBreadcrumbs: options.clearBreadcrumbs,
@@ -1197,7 +1216,10 @@ function makeActionManager(env) {
}
// END LEGACY CODE COMPATIBILITY
- newController.props = _getViewProps(view, controller.action, controller.views, props);
+ Object.assign(
+ newController,
+ _getViewInfo(view, controller.action, controller.views, props)
+ );
controller.action.controllers[viewType] = newController;
let index;
if (view.multiRecord) {
@@ -1235,7 +1257,10 @@ function makeActionManager(env) {
}
const controller = controllerStack[index];
if (controller.action.type === "ir.actions.act_window") {
- controller.props = _getViewProps(controller.view, controller.action, controller.views);
+ Object.assign(
+ controller,
+ _getViewInfo(controller.view, controller.action, controller.views)
+ );
}
await clearUncommittedChanges(env);
return _updateUI(controller, { index });
diff --git a/addons/web/static/tests/helpers/mock_env.js b/addons/web/static/tests/helpers/mock_env.js
index 1d01c82ee7d..867646a2b27 100644
--- a/addons/web/static/tests/helpers/mock_env.js
+++ b/addons/web/static/tests/helpers/mock_env.js
@@ -110,6 +110,7 @@ export async function makeTestEnv(config = {}) {
});
const env = makeEnv();
+ env.config = config.config || {};
await startServices(env);
env.qweb.addTemplates(window.__ODOO_TEMPLATES__);
return env;
diff --git a/addons/web/static/tests/legacy/views/state_mapping_tests.js b/addons/web/static/tests/legacy/views/state_mapping_tests.js
index 571688ec0de..04189dd873d 100644
--- a/addons/web/static/tests/legacy/views/state_mapping_tests.js
+++ b/addons/web/static/tests/legacy/views/state_mapping_tests.js
@@ -26,6 +26,7 @@ import legacyViewRegistry from "web.view_registry";
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");
+const searchModelRegistry = registry.category("search_models");
let serverData;
QUnit.module("Views", (hooks) => {
@@ -81,7 +82,7 @@ QUnit.module("Views", (hooks) => {
ToyView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
ToyView.template = owl.tags.xml`
-
+
`;
ToyView.type = "toy";
diff --git a/addons/web/static/tests/search/control_panel.js b/addons/web/static/tests/search/control_panel.js
index 589ccd875cf..12c71cab63f 100644
--- a/addons/web/static/tests/search/control_panel.js
+++ b/addons/web/static/tests/search/control_panel.js
@@ -57,7 +57,7 @@ QUnit.module("Search", (hooks) => {
serverData,
resModel: "foo",
Component: ControlPanel,
- componentProps: {
+ config: {
breadcrumbs: [{ jsId: "controller_7", name: "Previous" }],
displayName: "Current",
},
@@ -83,7 +83,7 @@ QUnit.module("Search", (hooks) => {
serverData,
resModel: "foo",
Component: ControlPanel,
- componentProps: {
+ config: {
viewSwitcherEntries: [
{ type: "list", active: true, icon: "fa-list-ul", name: "List" },
{ type: "kanban", icon: "fa-th-large", name: "Kanban" },
diff --git a/addons/web/static/tests/search/custom_favorite_item_tests.js b/addons/web/static/tests/search/custom_favorite_item_tests.js
index 67de6d3eeef..a63dc326a2b 100644
--- a/addons/web/static/tests/search/custom_favorite_item_tests.js
+++ b/addons/web/static/tests/search/custom_favorite_item_tests.js
@@ -75,7 +75,9 @@ QUnit.module("Search", (hooks) => {
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
- displayName: "Action Name",
+ config: {
+ displayName: "Action Name",
+ },
});
await toggleFavoriteMenu(controlPanel);
@@ -288,7 +290,7 @@ QUnit.module("Search", (hooks) => {
if (args.model === "ir.filters" && args.method === "create_or_replace") {
const irFilter = args.args[0];
assert.deepEqual(irFilter, {
- action_id: false,
+ action_id: undefined,
context: { group_by: [] },
domain: "[]",
is_default: false,
diff --git a/addons/web/static/tests/search/favorite_menu_tests.js b/addons/web/static/tests/search/favorite_menu_tests.js
index 7708736cc9d..0851aded1f7 100644
--- a/addons/web/static/tests/search/favorite_menu_tests.js
+++ b/addons/web/static/tests/search/favorite_menu_tests.js
@@ -67,7 +67,9 @@ QUnit.module("Search", (hooks) => {
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
- displayName: "Action Name",
+ config: {
+ displayName: "Action Name",
+ },
});
assert.containsOnce(controlPanel, "div.o_favorite_menu > button i.fa.fa-star");
@@ -98,7 +100,9 @@ QUnit.module("Search", (hooks) => {
Component: ControlPanel,
searchMenuTypes: ["favorite"],
searchViewId: false,
- displayName: "Action Name",
+ config: {
+ displayName: "Action Name",
+ },
});
assert.containsOnce(controlPanel, "div.o_favorite_menu > button i.fa.fa-star");
diff --git a/addons/web/static/tests/search/helpers.js b/addons/web/static/tests/search/helpers.js
index b36a557deeb..6d9ba334435 100644
--- a/addons/web/static/tests/search/helpers.js
+++ b/addons/web/static/tests/search/helpers.js
@@ -4,10 +4,10 @@ import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { notificationService } from "@web/core/notifications/notification_service";
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
+import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
import { WithSearch } from "@web/search/with_search/with_search";
import { viewService } from "@web/views/view_service";
import { actionService } from "@web/webclient/actions/action_service";
-import { CustomFavoriteItem } from "@web/search/favorite_menu/custom_favorite_item";
import { registerCleanup } from "../helpers/cleanup";
import { makeTestEnv } from "../helpers/mock_env";
import { click, getFixture, triggerEvent } from "../helpers/utils";
@@ -34,16 +34,25 @@ export const setupControlPanelFavoriteMenuRegistry = () => {
};
export const makeWithSearch = async (params) => {
- const serverData = params.serverData || undefined;
- const mockRPC = params.mockRPC || undefined;
const props = { ...params };
+
+ const serverData = props.serverData || undefined;
+ const mockRPC = props.mockRPC || undefined;
+ const config = props.config || {};
+
delete props.serverData;
delete props.mockRPC;
- const env = await makeTestEnv({ serverData, mockRPC });
+ delete props.config;
+
+ const env = await makeTestEnv({ serverData, mockRPC, config });
+
const target = getFixture();
const withSearch = await mount(WithSearch, { env, props, target });
+
registerCleanup(() => withSearch.destroy());
+
const component = Object.values(withSearch.__owl__.children)[0];
+
return component;
};
diff --git a/addons/web/static/tests/search/search_model_tests.js b/addons/web/static/tests/search/search_model_tests.js
index cbc6ff6f3e7..986677d4fa3 100644
--- a/addons/web/static/tests/search/search_model_tests.js
+++ b/addons/web/static/tests/search/search_model_tests.js
@@ -401,7 +401,7 @@ QUnit.module("Search", (hooks) => {
`,
- view: { type: "kanban" },
+ config: { viewType: "kanban" },
});
assert.deepEqual(model.getSections(), []);
});
@@ -418,7 +418,7 @@ QUnit.module("Search", (hooks) => {
`,
resModel: "partner",
- view: { type: "kanban" },
+ config: { viewType: "kanban" },
});
const sections = model.getSections();
for (const section of sections) {
@@ -488,7 +488,7 @@ QUnit.module("Search", (hooks) => {
`,
resModel: "partner",
- view: { type: "kanban" },
+ config: { viewType: "kanban" },
});
const sections = model.getSections();
for (const section of sections) {
diff --git a/addons/web/static/tests/search/search_panel_tests.js b/addons/web/static/tests/search/search_panel_tests.js
index 77b594b590e..67aa1f8bbc5 100644
--- a/addons/web/static/tests/search/search_panel_tests.js
+++ b/addons/web/static/tests/search/search_panel_tests.js
@@ -101,7 +101,7 @@ const makeTestComponent = ({ onWillStart, onWillUpdateProps } = {}) => {
TestComponent.components = { FilterMenu, GroupByMenu, SearchPanel };
TestComponent.template = xml`
-
+
`;
@@ -282,7 +282,29 @@ QUnit.module("Search", (hooks) => {
searchViewId: false,
display: { searchPanel: false },
});
- assert.doesNotHaveClass(comp.el, "o_component_with_search_panel");
+ assert.containsNone(comp, ".o_search_panel");
+ assert.deepEqual(getDomain(), []); // initial domain
+ });
+
+ QUnit.test("basic rendering of a component with empty search panel", async (assert) => {
+ assert.expect(2);
+
+ serverData.views["partner,false,search"] = ``;
+
+ const { TestComponent, getDomain } = makeTestComponent();
+ const comp = await makeWithSearch({
+ serverData,
+ async mockRPC(route, { method, model }) {
+ if (/search_panel_/.test(method || route)) {
+ assert.step(`${method || route} on ${model}`);
+ }
+ },
+ Component: TestComponent,
+ resModel: "partner",
+ searchViewId: false,
+ });
+
+ assert.containsNone(comp, ".o_search_panel");
assert.deepEqual(getDomain(), []); // initial domain
});
@@ -299,7 +321,6 @@ QUnit.module("Search", (hooks) => {
Component: TestComponent,
resModel: "partner",
searchViewId: false,
- view: { type: "kanban" },
});
assert.containsOnce(comp, ".o_search_panel");
@@ -361,7 +382,7 @@ QUnit.module("Search", (hooks) => {
Component: TestComponent,
resModel: "partner",
searchViewId: false,
- view: { type: "toy" },
+ config: { viewType: "toy" },
});
const sectionHeaderIcons = comp.el.querySelectorAll(".o_search_panel_section_header i");
@@ -397,7 +418,7 @@ QUnit.module("Search", (hooks) => {
Component: TestComponent,
resModel: "partner",
searchViewId: false,
- view: { type: "kanban" },
+ config: { viewType: "kanban" },
});
assert.containsOnce(comp, ".o_search_panel_section");
@@ -422,7 +443,7 @@ QUnit.module("Search", (hooks) => {
Component: TestComponent,
resModel: "partner",
searchViewId: false,
- view: { type: "kanban" },
+ config: { viewType: "kanban" },
});
const headers = comp.el.getElementsByClassName("o_search_panel_section_header");
diff --git a/addons/web/static/tests/views/graph_view_tests.js b/addons/web/static/tests/views/graph_view_tests.js
index c5649280149..1670d9c6c74 100644
--- a/addons/web/static/tests/views/graph_view_tests.js
+++ b/addons/web/static/tests/views/graph_view_tests.js
@@ -1,13 +1,7 @@
/** @odoo-module **/
-import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors";
-import { dialogService } from "@web/core/dialog/dialog_service";
-import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
-import { registry } from "@web/core/registry";
-import { makeView } from "./helpers";
-import { click, makeDeferred, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
-import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
+import { click, makeDeferred, nextTick, triggerEvent } from "@web/../tests/helpers/utils";
import {
editFavoriteName,
saveFavorite,
@@ -22,6 +16,12 @@ import {
toggleMenuItemOption,
toggleSaveFavorite,
} from "@web/../tests/search/helpers";
+import { makeView } from "@web/../tests/views/helpers";
+import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
+import { dialogService } from "@web/core/dialog/dialog_service";
+import { registry } from "@web/core/registry";
+import { BORDER_WHITE, DEFAULT_BG } from "@web/views/graph/colors";
+import { GraphArchParser } from "@web/views/graph/graph_arch_parser";
const serviceRegistry = registry.category("services");
@@ -30,7 +30,8 @@ function getGraphModelMetaData(graph) {
}
export function getGraphRenderer(graph) {
- return Object.values(graph.__owl__.children).find((c) => c.chart);
+ const layout = Object.values(graph.__owl__.children)[0];
+ return Object.values(layout.__owl__.children).find((c) => c.chart);
}
function getChart(graph) {
@@ -2031,26 +2032,9 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
noContentHelp: 'This helper should not be displayed in graph views
',
- views: [[false, "search"]],
- });
- assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
- assert.containsNone(graph, "div.o_view_nocontent");
- assert.containsNone(graph, ".abc");
- await toggleFilterMenu(graph);
- await toggleMenuItem(graph, "False Domain");
- assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
- assert.containsNone(graph, "div.o_view_nocontent");
- assert.containsNone(graph, ".abc");
- });
-
- QUnit.test("no content helper after update", async function (assert) {
- assert.expect(6);
- const graph = await makeView({
- serverData,
- type: "graph",
- resModel: "foo",
- noContentHelp: 'This helper should not be displayed in graph views
',
- views: [[false, "search"]],
+ config: {
+ views: [[false, "search"]],
+ },
});
assert.containsOnce(graph, "div.o_graph_canvas_container canvas");
assert.containsNone(graph, "div.o_view_nocontent");
@@ -2719,7 +2703,9 @@ QUnit.module("Views", (hooks) => {
resModel: "foo",
groupBy: ["date:year", "product_id", "date", "date:quarter"],
arch: ``,
- views: [[false, "search"]],
+ config: {
+ views: [[false, "search"]],
+ },
});
checkLabels(assert, graph, ["January 2016", "March 2016", "May 2016", "April 2016"]);
// mockReadGroup does not always sort groups -> May 2016 is before April 2016 for that reason.
@@ -2748,7 +2734,9 @@ QUnit.module("Views", (hooks) => {
serverData,
type: "graph",
resModel: "foo",
- displayName: "Glou glou",
+ config: {
+ displayName: "Glou glou",
+ },
});
assert.strictEqual(
graph.el.querySelector(".o_control_panel .breadcrumb-item.active").innerText,
@@ -2851,10 +2839,12 @@ QUnit.module("Views", (hooks) => {
`,
- views: [
- [364, "list"],
- [29, "form"],
- ],
+ config: {
+ views: [
+ [364, "list"],
+ [29, "form"],
+ ],
+ },
});
checkModeIs(assert, graph, "pie");
checkDatasets(assert, graph, ["domains"], {
@@ -3093,7 +3083,7 @@ QUnit.module("Views", (hooks) => {
type: "graph",
resModel: "foo",
arch: `
-
+
diff --git a/addons/web/static/tests/views/helpers.js b/addons/web/static/tests/views/helpers.js
index 4f8cb1f69b7..f961f479bb8 100644
--- a/addons/web/static/tests/views/helpers.js
+++ b/addons/web/static/tests/views/helpers.js
@@ -1,9 +1,9 @@
/** @odoo-module **/
-import { View } from "@web/views/view";
-import { getFixture } from "@web/../tests/helpers/utils";
-import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
+import { makeTestEnv } from "@web/../tests/helpers/mock_env";
+import { getFixture } from "@web/../tests/helpers/utils";
+import { View } from "@web/views/view";
import { _fieldsViewGet } from "../helpers/mock_server";
import { addLegacyMockEnvironment } from "../webclient/helpers";
@@ -21,20 +21,26 @@ const { mount } = owl;
/**
* @param {MakeViewParams} params
+ * @param {Object} [options={}]
+ * @param {boolean} [options.noFields] Do not add default fields
* @returns {owl.Component}
*/
-export async function makeView(params) {
- const serverData = params.serverData;
- const mockRPC = params.mockRPC;
- const legacyParams = params.legacyParams || {};
- const props = Object.assign({}, params);
+export const makeView = async (params, options = {}) => {
+ const props = { ...params };
+ const serverData = props.serverData;
+ const mockRPC = props.mockRPC;
+ const config = props.config || {};
+ const legacyParams = props.legacyParams || {};
+
delete props.serverData;
delete props.mockRPC;
delete props.legacyParams;
+ delete props.config;
- const env = await makeTestEnv({ serverData, mockRPC });
- const defaultFields = serverData.models[props.resModel].fields;
- if (props.arch) {
+ const env = await makeTestEnv({ serverData, mockRPC, config });
+
+ if (!options.noFields && props.arch) {
+ const defaultFields = serverData.models[props.resModel].fields;
if (!props.fields) {
props.fields = Object.assign({}, defaultFields);
// write the field name inside the field description (as done by fields_get)
@@ -81,4 +87,4 @@ export async function makeView(params) {
const concreteView = Object.values(withSearch.__owl__.children)[0];
return concreteView;
-}
+};
diff --git a/addons/web/static/tests/views/layout_tests.js b/addons/web/static/tests/views/layout_tests.js
new file mode 100644
index 00000000000..8be75bc2c4b
--- /dev/null
+++ b/addons/web/static/tests/views/layout_tests.js
@@ -0,0 +1,273 @@
+/** @odoo-module **/
+
+import { getFixture } from "@web/../tests/helpers/utils";
+import { makeWithSearch, setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
+import { dialogService } from "@web/core/dialog/dialog_service";
+import { registry } from "@web/core/registry";
+import { Layout } from "@web/views/layout";
+import { makeTestEnv } from "@web/../tests/helpers/mock_env";
+
+const { Component, hooks, mount, tags } = owl;
+const { xml } = tags;
+const { useSubEnv } = hooks;
+
+const serviceRegistry = registry.category("services");
+
+let serverData;
+
+QUnit.module("Views", (hooks) => {
+ hooks.beforeEach(() => {
+ serverData = {
+ models: {
+ foo: {
+ fields: {
+ aaa: {
+ type: "selection",
+ selection: [
+ ["a", "A"],
+ ["b", "B"],
+ ],
+ },
+ },
+ records: [],
+ },
+ },
+ views: {
+ "foo,false,search": /* xml */ `
+
+
+
+
+ `,
+ },
+ };
+
+ setupControlPanelServiceRegistry();
+ serviceRegistry.add("dialog", dialogService);
+ });
+
+ QUnit.module("Layout");
+
+ QUnit.test("Simple rendering", async (assert) => {
+ assert.expect(5);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ const env = await makeTestEnv({ config: {} });
+ const comp = await mount(ToyComponent, { target: getFixture(), env });
+
+ assert.hasClass(comp.el, "o_toy_view o_view_sample_data");
+ assert.containsNone(comp, ".o_control_panel");
+ assert.containsNone(comp, ".o_component_with_search_panel");
+ assert.containsNone(comp, ".o_search_panel");
+ assert.containsOnce(comp, ".o_content > .toy_content");
+ });
+
+ QUnit.test("Simple rendering: with search", async (assert) => {
+ assert.expect(6);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyComponent,
+ resModel: "foo",
+ searchViewId: false,
+ });
+
+ assert.hasClass(root.el, "o_toy_view");
+ assert.doesNotHaveClass(root.el, "o_view_sample_data");
+ assert.containsOnce(root, ".o_control_panel .o_cp_top_right .toy_search_bar");
+ assert.containsOnce(root, ".o_component_with_search_panel .o_search_panel");
+ assert.containsNone(root, ".o_cp_searchview");
+ assert.containsOnce(root, ".o_content > .toy_content");
+ });
+
+ QUnit.test("Nested layouts", async (assert) => {
+ assert.expect(10);
+
+ // Component C: bottom (no control panel)
+
+ class ToyC extends Component {
+ setup() {
+ useSubEnv({
+ searchModel: {
+ display: {
+ controlPanel: false,
+ searchPanel: true,
+ },
+ },
+ });
+ }
+ }
+ ToyC.template = xml`
+
+
+ `;
+ ToyC.components = { Layout };
+
+ // Component B: center (with custom search panel)
+
+ class SearchPanel extends Component {}
+ SearchPanel.template = xml``;
+
+ class ToyB extends Component {
+ setup() {
+ useSubEnv({ config: { SearchPanel } });
+ }
+ }
+ ToyB.template = xml`
+
+
+
+
+
+ `;
+ ToyB.components = { Layout, ToyC };
+
+ // Component A: top
+
+ class ToyA extends Component {}
+ ToyA.template = xml`
+
+
+
+
+
+ `;
+ ToyA.components = { Layout, ToyB };
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyA,
+ resModel: "foo",
+ searchViewId: false,
+ });
+
+ assert.hasClass(root.el, "o_toy_a_view");
+ assert.doesNotHaveClass(root.el, "o_view_sample_data");
+ assert.containsOnce(root, ".o_content .o_toy_b_view .o_content .o_toy_c_view .o_content"); // Full chain of contents
+ assert.containsN(root, ".o_control_panel", 2); // Component C has hidden its control panel
+ assert.containsN(root, ".o_content.o_component_with_search_panel", 3);
+ assert.containsOnce(root, ".o_search_panel"); // Standard search panel
+ assert.containsN(root, ".o_toy_search_panel", 2); // Custom search panels
+ assert.containsOnce(root, ".toy_a_search");
+ assert.containsOnce(root, ".toy_b_breadcrumbs");
+ assert.containsOnce(root, ".toy_c_content");
+ });
+
+ QUnit.test("Custom control panel", async (assert) => {
+ assert.expect(3);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ class ControlPanel extends Component {}
+ ControlPanel.template = xml``;
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyComponent,
+ resModel: "foo",
+ searchViewId: false,
+ config: { ControlPanel },
+ });
+
+ assert.containsOnce(root, ".o_toy_content");
+ assert.containsOnce(root, ".o_toy_search_panel");
+ assert.containsNone(root, ".o_control_panel");
+ });
+
+ QUnit.test("Custom search panel", async (assert) => {
+ assert.expect(3);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ class SearchPanel extends Component {}
+ SearchPanel.template = xml``;
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyComponent,
+ resModel: "foo",
+ searchViewId: false,
+ config: { SearchPanel },
+ });
+
+ assert.containsOnce(root, ".o_toy_content");
+ assert.containsOnce(root, ".o_toy_search_panel");
+ assert.containsNone(root, ".o_search_panel");
+ });
+
+ QUnit.test("Custom banner: no bannerRoute in env", async (assert) => {
+ assert.expect(2);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ class Banner extends Component {}
+ Banner.template = xml``;
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyComponent,
+ resModel: "foo",
+ searchViewId: false,
+ config: { Banner },
+ });
+
+ assert.containsOnce(root, ".o_toy_content");
+ assert.containsNone(root, ".o_toy_banner");
+ });
+
+ QUnit.test("Custom banner: with bannerRoute in env", async (assert) => {
+ assert.expect(2);
+
+ class ToyComponent extends Component {}
+ ToyComponent.template = xml`
+
+
+ `;
+ ToyComponent.components = { Layout };
+
+ class Banner extends Component {}
+ Banner.template = xml``;
+
+ const root = await makeWithSearch({
+ serverData,
+ Component: ToyComponent,
+ resModel: "foo",
+ searchViewId: false,
+ config: { Banner, bannerRoute: "toy/banner/route" },
+ });
+
+ assert.containsOnce(root, ".o_toy_content");
+ assert.containsOnce(root, ".o_toy_banner");
+ });
+});
diff --git a/addons/web/static/tests/views/pivot_view_tests.js b/addons/web/static/tests/views/pivot_view_tests.js
index 6aeb591dfaf..d511b682fa8 100644
--- a/addons/web/static/tests/views/pivot_view_tests.js
+++ b/addons/web/static/tests/views/pivot_view_tests.js
@@ -6,19 +6,21 @@ import { session } from "@web/session";
import { makeFakeLocalizationService, makeFakeUserService } from "../helpers/mock_services";
import {
click,
- nextTick,
legacyExtraNextTick,
makeDeferred,
mockDownload,
+ nextTick,
patchDate,
patchWithCleanup,
triggerEvent,
triggerEvents,
} from "../helpers/utils";
-import { makeView } from "./helpers";
import {
applyGroup,
editFavoriteName,
+ removeFacet,
+ saveFavorite,
+ selectGroup,
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
toggleAddCustomGroup,
@@ -26,15 +28,13 @@ import {
toggleFavoriteMenu,
toggleFilterMenu,
toggleGroupByMenu,
+ toggleMenu,
toggleMenuItem,
toggleMenuItemOption,
toggleSaveFavorite,
- saveFavorite,
- selectGroup,
- removeFacet,
- toggleMenu,
} from "../search/helpers";
import { createWebClient, doAction } from "../webclient/helpers";
+import { makeView } from "./helpers";
const serviceRegistry = registry.category("services");
@@ -438,12 +438,14 @@ QUnit.module("Views", (hooks) => {
`,
context: { someKey: true, search_default_test: 3 },
- views: [
- [2, "form"],
- [5, "kanban"],
- [false, "list"],
- [false, "pivot"],
- ],
+ config: {
+ views: [
+ [2, "form"],
+ [5, "kanban"],
+ [false, "list"],
+ [false, "pivot"],
+ ],
+ },
});
assert.hasClass(pivot.el.querySelector("table"), "o_enable_linking");
@@ -4689,7 +4691,9 @@ QUnit.module("Views", (hooks) => {
serverData,
context: { search_default_small_than_0: true },
noContentHelp: 'click to add a foo
',
- views: [[false, "search"]],
+ config: {
+ views: [[false, "search"]],
+ },
});
assert.containsOnce(pivot, ".o_view_nocontent .abc");
@@ -4720,7 +4724,9 @@ QUnit.module("Views", (hooks) => {
serverData,
context: { search_default_small_than_0: true },
noContentHelp: 'click to add a foo
',
- views: [[false, "search"]],
+ config: {
+ views: [[false, "search"]],
+ },
});
assert.hasClass(pivot.el, "o_view_sample_data");
@@ -4753,7 +4759,9 @@ QUnit.module("Views", (hooks) => {
resModel: "partner",
serverData,
noContentHelp: 'click to add a foo
',
- views: [[false, "search"]],
+ config: {
+ views: [[false, "search"]],
+ },
});
assert.doesNotHaveClass(pivot.el, "o_view_sample_data");
diff --git a/addons/web/static/tests/views/view_tests.js b/addons/web/static/tests/views/view_tests.js
index 911f7845625..9501d281e51 100644
--- a/addons/web/static/tests/views/view_tests.js
+++ b/addons/web/static/tests/views/view_tests.js
@@ -1,14 +1,18 @@
/** @odoo-module **/
-import { getFixture, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
-import { ormService } from "@web/core/orm_service";
-import { registerCleanup } from "@web/../tests/helpers/cleanup";
+import {
+ click,
+ getFixture,
+ makeDeferred,
+ nextTick,
+ patchWithCleanup,
+} from "@web/../tests/helpers/utils";
+import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
+import { makeView } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
-import { View } from "@web/views/view";
import { OnboardingBanner } from "@web/views/onboarding_banner";
-import { viewService } from "@web/views/view_service";
-import { click, makeDeferred } from "../helpers/utils";
+import { View } from "@web/views/view";
import { actionService } from "@web/webclient/actions/action_service";
const { Component, mount, hooks, tags } = owl;
@@ -18,28 +22,6 @@ const { xml } = tags;
const serviceRegistry = registry.category("services");
const viewRegistry = registry.category("views");
-async function makeView(params) {
- const serverData = params.serverData;
- const mockRPC = params.mockRPC;
- const env = await makeTestEnv({ serverData, mockRPC });
-
- // we don't want "fields" to be added here !!!
- const props = Object.assign({}, params);
- delete props.serverData;
- delete props.mockRPC;
-
- const target = getFixture();
-
- const view = await mount(View, { env, props, target });
-
- registerCleanup(() => view.destroy());
-
- const withSearch = Object.values(view.__owl__.children)[0];
- const concreteView = Object.values(withSearch.__owl__.children)[0];
-
- return concreteView;
-}
-
let serverData;
QUnit.module("Views", (hooks) => {
@@ -55,6 +37,7 @@ QUnit.module("Views", (hooks) => {
selection: [
["omnivorous", "Omnivorous"],
["herbivorous", "Herbivorous"],
+
["carnivorous", "Carnivorous"],
],
store: true,
@@ -109,8 +92,7 @@ QUnit.module("Views", (hooks) => {
viewRegistry.add("toy", ToyView);
viewRegistry.add("toy_imp", ToyViewImp);
- serviceRegistry.add("orm", ormService);
- serviceRegistry.add("view", viewService);
+ setupControlPanelServiceRegistry();
const fakeActionService = {
name: "action",
@@ -140,7 +122,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, false);
+ assert.strictEqual(this.env.config.viewId, false);
},
});
@@ -174,7 +156,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, 1);
+ assert.strictEqual(this.env.config.viewId, 1);
},
});
@@ -207,13 +189,14 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, 1);
+ assert.strictEqual(this.env.config.viewId, 1);
},
});
const view = await makeView({
serverData,
mockRPC: (_, args) => {
+ console.log(_);
assert.deepEqual(args.kwargs.views, [[1, "toy"]]);
assert.deepEqual(args.kwargs.options, {
action_id: false,
@@ -223,7 +206,9 @@ QUnit.module("Views", (hooks) => {
},
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.hasClass(view.el, "o_toy_view");
assert.strictEqual(view.el.innerHTML, serverData.views["animal,1,toy"]);
@@ -242,7 +227,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, false);
+ assert.strictEqual(this.env.config.viewId, false);
},
});
@@ -261,7 +246,9 @@ QUnit.module("Views", (hooks) => {
},
resModel: "animal",
type: "toy",
- views: [[false, "other"]],
+ config: {
+ views: [[false, "other"]],
+ },
});
assert.hasClass(view.el, "o_toy_view");
assert.strictEqual(view.el.innerHTML, serverData.views["animal,false,toy"]);
@@ -279,7 +266,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, 1);
+ assert.strictEqual(this.env.config.viewId, 1);
},
});
@@ -299,10 +286,12 @@ QUnit.module("Views", (hooks) => {
resModel: "animal",
type: "toy",
viewId: 1,
- views: [
- [3, "toy"],
- [false, "other"],
- ],
+ config: {
+ views: [
+ [3, "toy"],
+ [false, "other"],
+ ],
+ },
});
assert.hasClass(view.el, "o_toy_view");
assert.strictEqual(view.el.innerHTML, serverData.views["animal,1,toy"]);
@@ -319,7 +308,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, `Specific arch content`);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
- assert.strictEqual(info.viewId, undefined);
+ assert.strictEqual(this.env.config.viewId, undefined);
},
});
@@ -348,7 +337,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, serverData.models.animal.fields);
assert.deepEqual(info.actionMenus, {});
- assert.strictEqual(info.viewId, false);
+ assert.strictEqual(this.env.config.viewId, false);
},
});
@@ -384,7 +373,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, `Specific arch content`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
- assert.strictEqual(info.viewId, false);
+ assert.strictEqual(this.env.config.viewId, false);
},
});
@@ -423,7 +412,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(arch, `Specific arch content`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
- assert.strictEqual(info.viewId, undefined);
+ assert.strictEqual(this.env.config.viewId, undefined);
},
});
@@ -714,7 +703,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.containsOnce(toy, "a");
@@ -757,7 +748,9 @@ QUnit.module("Views", (hooks) => {
serverData,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.containsOnce(toy, "a");
@@ -807,7 +800,9 @@ QUnit.module("Views", (hooks) => {
serverData,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.containsOnce(toy, "a");
@@ -818,7 +813,7 @@ QUnit.module("Views", (hooks) => {
assert.expect(3);
serverData.views["animal,1,toy"] = `
-
+
`;
const mockRPC = (route) => {
@@ -833,7 +828,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.verifySteps(["/mybody/isacage"]);
@@ -844,7 +841,7 @@ QUnit.module("Views", (hooks) => {
assert.expect(7);
serverData.views["animal,1,toy"] = `
-
+
`;
const bannerArch = `
@@ -893,7 +890,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.verifySteps(["/mybody/isacage", "js loaded", "css loaded"]);
@@ -910,7 +909,7 @@ QUnit.module("Views", (hooks) => {
serverData.views["animal,1,toy"] = `
-
+
`;
const banners = [
@@ -938,7 +937,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.verifySteps(["/mybody/isacage"]);
@@ -954,7 +955,7 @@ QUnit.module("Views", (hooks) => {
assert.expect(5);
serverData.views["animal,1,toy"] = `
-
+
`;
const bannerArch = `
@@ -974,7 +975,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
assert.verifySteps(["/mybody/isacage"]);
@@ -1010,7 +1013,7 @@ QUnit.module("Views", (hooks) => {
serverData.views["animal,1,toy"] = `
-
+
`;
@@ -1040,7 +1043,9 @@ QUnit.module("Views", (hooks) => {
serverData,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
await click(toy.el.querySelector("a[data-method='setTheControl']"));
@@ -1054,7 +1059,7 @@ QUnit.module("Views", (hooks) => {
serverData.views["animal,1,toy"] = `
-
+
`;
const bannerArch = `
@@ -1104,7 +1109,9 @@ QUnit.module("Views", (hooks) => {
mockRPC,
resModel: "animal",
type: "toy",
- views: [[1, "toy"]],
+ config: {
+ views: [[1, "toy"]],
+ },
});
const prom = new Promise((resolve) => {
@@ -1252,7 +1259,7 @@ QUnit.module("Views", (hooks) => {
QUnit.test("'resModel' must be passed as prop", async function (assert) {
assert.expect(2);
try {
- await makeView({ serverData });
+ await makeView({ serverData }, { noFields: true });
} catch (error) {
assert.step(error.message);
}
@@ -1272,7 +1279,10 @@ QUnit.module("Views", (hooks) => {
QUnit.test("'arch' cannot be passed as prop alone", async function (assert) {
assert.expect(2);
try {
- await makeView({ serverData, resModel: "animal", type: "toy", arch: "" });
+ await makeView(
+ { serverData, resModel: "animal", type: "toy", arch: "" },
+ { noFields: true }
+ );
} catch (error) {
assert.step(error.message);
}
diff --git a/addons/web/static/tests/webclient/actions/client_action_tests.js b/addons/web/static/tests/webclient/actions/client_action_tests.js
index b6c74bf4b11..9dd9f8349a2 100644
--- a/addons/web/static/tests/webclient/actions/client_action_tests.js
+++ b/addons/web/static/tests/webclient/actions/client_action_tests.js
@@ -313,9 +313,9 @@ QUnit.module("ActionManager", (hooks) => {
class ClientAction extends Component {
setup() {
this.breadcrumbTitle = "myOwlAction";
- const breadCrumbs = this.props.breadcrumbs;
- assert.strictEqual(breadCrumbs.length, 1);
- assert.strictEqual(breadCrumbs[0].name, "Favorite Ponies");
+ const { breadcrumbs } = this.env.config;
+ assert.strictEqual(breadcrumbs.length, 1);
+ assert.strictEqual(breadcrumbs[0].name, "Favorite Ponies");
}
mounted() {
this.trigger("controller-title-updated", this.breadcrumbTitle);
@@ -422,10 +422,12 @@ QUnit.module("ActionManager", (hooks) => {
title: "title",
message: "message %s ",
sticky: true,
- links: [{
- label: "test ",
- url: '#action={action.id}&id={order.id}&model=purchase.order',
- }],
+ links: [
+ {
+ label: "test ",
+ url: "#action={action.id}&id={order.id}&model=purchase.order",
+ },
+ ],
},
});
const notificationSelector = ".o_notification_manager .o_notification";
@@ -460,10 +462,12 @@ QUnit.module("ActionManager", (hooks) => {
params: {
message: "message %s ",
sticky: true,
- links: [{
- label: "test ",
- url: '#action={action.id}&id={order.id}&model=purchase.order',
- }],
+ links: [
+ {
+ label: "test ",
+ url: "#action={action.id}&id={order.id}&model=purchase.order",
+ },
+ ],
},
});
assert.containsOnce(
@@ -473,7 +477,8 @@ QUnit.module("ActionManager", (hooks) => {
);
notificationElement = document.body.querySelector(notificationSelector);
assert.containsNone(
- notificationElement, ".o_notification_title",
+ notificationElement,
+ ".o_notification_title",
"the notification should not have title"
);
});
diff --git a/addons/web/static/tests/webclient/actions/concurrency_tests.js b/addons/web/static/tests/webclient/actions/concurrency_tests.js
index 3303f41fbb0..9dcb108e520 100644
--- a/addons/web/static/tests/webclient/actions/concurrency_tests.js
+++ b/addons/web/static/tests/webclient/actions/concurrency_tests.js
@@ -652,7 +652,7 @@ QUnit.module("ActionManager", (hooks) => {
ToyView.searchMenuTypes = ["filter"];
ToyView.template = owl.tags.xml`
-
+
`;
ToyView.type = "toy";