From 53f9b5f235d361b4ff8437bb20208dbb5e19ff5e Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Mon, 27 Sep 2021 07:12:51 +0000 Subject: [PATCH] [REF] *: adapt modules to action and view contexts This commit handles the adaptation of any component using the search model's 'action' and/or 'view' key, now replaced by the environment keys 'actionContext' and 'viewContext'. Affected modules are: - base_import - board - crm - google_spreadsheet - project X-original-commit: 40f1ae87e1436192a70a8a7432eb72d379cfa6dc Part-of: odoo/odoo#77463 --- .../src/import_records/import_records.js | 6 +- .../static/src/add_to_board/add_to_board.js | 41 +- addons/board/static/tests/dashboard_tests.js | 2404 +++++++++-------- .../crm/static/tests/forecast_view_tests.js | 25 +- .../add_to_google_spreadsheet.js | 2 +- .../static/src/js/project_graph_view.js | 4 +- .../static/src/js/project_pivot_view.js | 4 +- .../src/js/project_rating_graph_view.js | 6 +- .../src/js/project_rating_pivot_view.js | 6 +- .../project_control_panel.js | 3 +- 10 files changed, 1310 insertions(+), 1191 deletions(-) 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: '
' + + 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': - '', - }, + "" + + "" + + "", + }); + + 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: `
+ 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": '', + }, + }); + + 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";