From 6dd7efed589d79b63b1dbaddda1d4fd7eb1e74be Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Tue, 14 Sep 2021 12:16:01 +0000 Subject: [PATCH 1/5] [IMP] web: Only display search panel with sections Before this commit, the search model would consider displaying a search panel regardless of whether it had any sections. Now, the search panel is considered non-displayable when there is no sections. X-original-commit: d3b191600b50fb052a956d1199730453789674e2 Part-of: odoo/odoo#77463 --- addons/web/static/src/search/search_model.js | 1 + .../static/tests/search/search_panel_tests.js | 27 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index 9576b143ab2..d367210c042 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -1384,6 +1384,7 @@ export class SearchModel extends EventBus { return { controlPanel: "controlPanel" in display ? display.controlPanel : {}, searchPanel: + this.sections.size && (!this.view.type || viewTypes.includes(this.view.type)) && ("searchPanel" in display ? display.searchPanel : true), }; diff --git a/addons/web/static/tests/search/search_panel_tests.js b/addons/web/static/tests/search/search_panel_tests.js index 77b594b590e..8f4b42a25c7 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"); From 5bcf16311a6dc77bfc58c379dd9cb48ab0b4b37e Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Thu, 9 Sep 2021 12:02:23 +0000 Subject: [PATCH 2/5] [IMP] web: View layout This commit introduces a new component: the layout component. Its purpose is to make abstraction of most generic components and classes when writing a view template or an action. To make use of this component, we also needed a new system to pass slot from the parent of the layout to the control panel. This was done in a hacky way and is meant to be replaced as soon as Owl provides a built-in way to pass slots. The control panel has also been slightly tweaked to default its props from an environment's newly introduced key: the 'config'. This has been done to reduce the amount of props passed to the layout and search subcomponents. X-original-commit: 3b29dd230acd961a72fb0cb971f9363958f65b60 Part-of: odoo/odoo#77463 Co-authored-by: Aaron Bohy Co-authored-by: Julien Mougenot --- addons/web/static/src/legacy/legacy_views.js | 12 +- .../src/search/control_panel/control_panel.js | 62 ++-- .../search/control_panel/control_panel.xml | 8 +- .../favorite_menu/custom_favorite_item.js | 4 +- addons/web/static/src/search/search_model.js | 21 +- .../src/search/with_search/with_search.js | 55 +--- addons/web/static/src/views/debug_items.js | 5 +- addons/web/static/src/views/layout.js | 32 ++ addons/web/static/src/views/layout.xml | 17 ++ .../web/static/src/views/onboarding_banner.js | 6 +- addons/web/static/src/views/view.js | 105 +++---- .../src/webclient/actions/action_service.js | 91 +++--- addons/web/static/tests/helpers/mock_env.js | 1 + .../tests/legacy/views/state_mapping_tests.js | 3 +- .../web/static/tests/search/control_panel.js | 4 +- .../search/custom_favorite_item_tests.js | 6 +- .../tests/search/favorite_menu_tests.js | 8 +- addons/web/static/tests/search/helpers.js | 17 +- .../static/tests/search/search_model_tests.js | 6 +- .../static/tests/search/search_panel_tests.js | 6 +- addons/web/static/tests/views/helpers.js | 30 +- addons/web/static/tests/views/layout_tests.js | 273 ++++++++++++++++++ addons/web/static/tests/views/view_tests.js | 134 +++++---- .../webclient/actions/client_action_tests.js | 29 +- .../webclient/actions/concurrency_tests.js | 2 +- 25 files changed, 636 insertions(+), 301 deletions(-) create mode 100644 addons/web/static/src/views/layout.js create mode 100644 addons/web/static/src/views/layout.xml create mode 100644 addons/web/static/tests/views/layout_tests.js 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 @@
- +
' + - '', - }, - 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";