From db2092d8d389fdd285f54e9b34a5a99cc9523d27 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Tue, 15 Jun 2021 15:34:15 +0000 Subject: [PATCH] [REF] web,mail: action_service: doAction passes props to component Before this commit the doAction method of the action_service did not support to pass props directly to the Component. Only a few ad-hoc options were passed though. After this commit, doAction can take a key "props" in options, that will be passed to the Component (a View or a ClientAction) the new API becomes ```ts interface Options { props: { [key: string]: any }; ... } doAction(action: ActionRequest, options: Options); Note that some props (like withFilters) are set by the action service and cannot be set via the key "props". Alongside the change mentioned above, we take the opportunity to align the terminology used in action service to the one used server side: we know use the terms resModel, resId, and resIds instead of model, recordId, and recordIds. ``` closes odoo/odoo#72397 Related: odoo/enterprise#19127 Signed-off-by: Aaron Bohy (aab) Co-authored-by: Lucas Perais Co-authored-by: Mathieu Duckerts-Antoine --- .../static/tests/tools/debug_manager_tests.js | 2 +- .../web/static/src/legacy/action_adapters.js | 10 +- .../src/legacy/legacy_client_actions.js | 17 ++- addons/web/static/src/legacy/legacy_views.js | 12 +- addons/web/static/src/legacy/utils.js | 12 +- .../src/webclient/actions/action_service.js | 137 ++++++++---------- .../webclient/actions/client_action_tests.js | 31 ++++ .../webclient/actions/load_state_tests.js | 4 +- .../webclient/actions/window_action_tests.js | 4 +- 9 files changed, 131 insertions(+), 98 deletions(-) diff --git a/addons/mail/static/tests/tools/debug_manager_tests.js b/addons/mail/static/tests/tools/debug_manager_tests.js index 45b972823c2..d3bde2e0550 100644 --- a/addons/mail/static/tests/tools/debug_manager_tests.js +++ b/addons/mail/static/tests/tools/debug_manager_tests.js @@ -41,7 +41,7 @@ QUnit.test("Manage Messages", async function (assert) { } const wc = await createWebClient({ serverData, mockRPC }); - await doAction(wc, 3, { viewType: "form", resId: 5 }); + await doAction(wc, 3, { viewType: "form", props: { resId: 5 } }); await legacyExtraNextTick(); await click(wc.el, ".o_debug_manager .o_dropdown_toggler"); diff --git a/addons/web/static/src/legacy/action_adapters.js b/addons/web/static/src/legacy/action_adapters.js index e7a1925abdb..095e299031e 100644 --- a/addons/web/static/src/legacy/action_adapters.js +++ b/addons/web/static/src/legacy/action_adapters.js @@ -330,8 +330,8 @@ export class ViewAdapter extends ActionAdapter { const state = ev.target.exportState(); try { await this.actionService.switchView(payload.view_type, { - recordId: payload.res_id, - recordIds: state.resIds, + resId: payload.res_id, + resIds: state.resIds, searchModel: state.searchModel, searchPanel: state.searchPanel, mode: payload.mode, @@ -351,10 +351,10 @@ export class ViewAdapter extends ActionAdapter { buttonContext: buttonContext, context: envContext, close: payload.action_data.close, - model: payload.env.model, + resModel: payload.env.model, name: payload.action_data.name, - recordId: payload.env.currentID || null, - recordIds: payload.env.resIDs, + resId: payload.env.currentID || null, + resIds: payload.env.resIDs, special: payload.action_data.special, type: payload.action_data.type, onClose: payload.on_closed, diff --git a/addons/web/static/src/legacy/legacy_client_actions.js b/addons/web/static/src/legacy/legacy_client_actions.js index 129414b8824..93f2434c68b 100644 --- a/addons/web/static/src/legacy/legacy_client_actions.js +++ b/addons/web/static/src/legacy/legacy_client_actions.js @@ -21,12 +21,17 @@ function registerClientAction(name, action) { super(...arguments); this.controllerRef = hooks.useRef("controller"); this.Widget = action; - this.widgetArgs = [ - this.props.action, - Object.assign({}, this.props.options, { - breadcrumbs: breadcrumbsToLegacy(this.props.breadcrumbs), - }), - ]; + const options = {}; + for (const key in this.props) { + if (key === "action" || key === "actionId") { + continue; + } else if (key === "breadcrumbs") { + options[key] = breadcrumbsToLegacy(this.props[key]); + } else { + options[key] = this.props[key]; + } + } + this.widgetArgs = [this.props.action, options]; this.widget = this.props.state && this.props.state.__legacy_widget__; this.onReverseBreadcrumb = this.props.state && this.props.state.__on_reverse_breadcrumb__; diff --git a/addons/web/static/src/legacy/legacy_views.js b/addons/web/static/src/legacy/legacy_views.js index 16e55b3e783..5f7ae6962d9 100644 --- a/addons/web/static/src/legacy/legacy_views.js +++ b/addons/web/static/src/legacy/legacy_views.js @@ -33,14 +33,14 @@ function registerView(name, LegacyView) { action: this.props.action, // legacy views automatically add the last part of the breadcrumbs breadcrumbs: breadcrumbsToLegacy(this.props.breadcrumbs), - modelName: this.props.model, - currentId: this.props.recordId, + modelName: this.props.resModel, + currentId: this.props.resId, controllerState: { currentId: - "recordId" in this.props - ? this.props.recordId + "resId" in this.props + ? this.props.resId : this.props.state && this.props.state.currentId, - resIds: this.props.recordIds || (this.props.state && this.props.state.resIds), + resIds: this.props.resIds || (this.props.state && this.props.state.resIds), searchModel: this.props.searchModel || (this.props.state && this.props.state.searchModel), @@ -75,7 +75,7 @@ function registerView(name, LegacyView) { async willStart() { const params = { - model: this.props.model, + model: this.props.resModel, views: this.props.views, context: this.props.context, }; diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index 0d4c0748465..16dd561d649 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -16,9 +16,19 @@ export function mapDoActionOptionAPI(legacyOptions) { additionalContext: legacyOptions.additional_context, clearBreadcrumbs: legacyOptions.clear_breadcrumbs, viewType: legacyOptions.view_type, - resId: legacyOptions.res_id, onClose: legacyOptions.on_close, + props: { + resId: legacyOptions.res_id, + }, }); + if (legacyOptions.controllerState) { + if (legacyOptions.controllerState.searchModel) { + legacyOptions.props.searchModel = legacyOptions.controllerState.searchModel; + } + if (legacyOptions.controllerState.searchPanel) { + legacyOptions.props.searchPanel = legacyOptions.controllerState.searchPanel; + } + } delete legacyOptions.additional_context; delete legacyOptions.clear_breadcrumbs; delete legacyOptions.view_type; diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index e5587a091da..8d4b4cf57e7 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -1,6 +1,5 @@ /** @odoo-module **/ -import { KeyNotFoundError } from "@web/core/registry"; import { cleanDomFromBootstrap } from "@web/legacy/utils"; import { browser } from "../../core/browser/browser"; import { useBus } from "../../core/bus_hook"; @@ -12,8 +11,25 @@ import { KeepLast } from "../../core/utils/concurrency"; import { sprintf } from "../../core/utils/strings"; import { ActionDialog } from "./action_dialog"; +/** @typedef {number|false} ActionId */ +/** @typedef {Object} ActionDescription */ +/** @typedef {"current" | "fullscreen" | "new" | "self" | "inline"} ActionMode */ +/** @typedef {string} ActionTag */ +/** @typedef {string} ActionXMLId */ +/** @typedef {Object} Context */ +/** @typedef {Function} CallableFunction */ +/** @typedef {string} ViewType */ + +/** @typedef {ActionId|ActionXMLId|ActionTag|ActionDescription} ActionRequest */ + /** - * @typedef {"current" | "fullscreen" | "new" | "self" | "inline"} ActionMode */ + * @typedef {Object} ActionOptions + * @property {Context} [additionalContext] + * @property {boolean} [clearBreadcrumbs] + * @property {CallableFunction} [onClose] + * @property {Object} [props] + * @property {ViewType} [viewType] + */ const { Component, hooks, tags } = owl; @@ -228,9 +244,11 @@ function makeActionManager(env) { } Object.assign(options, { additionalContext: context, - resId: state.id, viewType: state.view_type, }); + if (state.id) { + options.props = { resId: state.id }; + } } } else if (state.model) { if (state.id) { @@ -260,22 +278,11 @@ function makeActionManager(env) { /** * @param {ClientAction | ActWindowAction} action + * @param {Object} props * @returns {ActionProps} */ - function _getActionProps(action) { - return { - action, - actionId: action.id, - }; - } - - /** - * @param {ClientAction} action - * @param {ActionOptions} options - * @returns {ActionProps} - */ - function _getClientActionProps(action, options) { - return Object.assign({}, _getActionProps(action), { options }); + function _getActionProps(action, props) { + return Object.assign({}, props, { action, actionId: action.id }); } /** @@ -318,11 +325,9 @@ function makeActionManager(env) { currentController.action.type === "ir.actions.act_window" && currentActionId === state.action ) { - const viewOptions = { - recordId: state.id || false, - }; + const props = { resId: state.id || false }; const viewType = state.view_type || currentController.view.type; - return { viewType, viewOptions }; + return { viewType, props }; } } return null; @@ -332,9 +337,10 @@ function makeActionManager(env) { * @param {BaseView} view * @param {ActWindowAction} action * @param {BaseView[]} views + * @param {Object} props * @returns {ViewProps} */ - function _getViewProps(view, action, views, options = {}) { + function _getViewProps(view, action, views, props = {}) { const target = action.target; const viewSwitcherEntries = views .filter((v) => v.multiRecord === view.multiRecord) @@ -348,11 +354,11 @@ function makeActionManager(env) { }; }); const flags = action.flags || {}; - const props = Object.assign({}, _getActionProps(action), { + const viewProps = Object.assign(_getActionProps(action, props), { actionFlags: Object.assign({}, flags, flags[view.type]), context: action.context, domain: action.domain || [], - model: action.res_model, + resModel: action.res_model, type: view.type, views: action.views, viewSwitcherEntries, @@ -360,24 +366,9 @@ function makeActionManager(env) { withFilters: action.views.some((v) => v[1] === "search"), }); if (action.res_id) { - props.recordId = action.res_id; + viewProps.resId = action.res_id; } - if ("recordId" in options) { - props.recordId = options.recordId; - } - if (options.recordIds) { - props.recordIds = options.recordIds; - } - if (options.searchModel) { - props.searchModel = options.searchModel; - } - if (options.searchPanel) { - props.searchPanel = options.searchPanel; - } - if (options.mode) { - props.mode = options.mode; - } - return props; + return viewProps; } /** @@ -666,25 +657,13 @@ function makeActionManager(env) { } } - const viewOptions = {}; - if (options.resId) { - viewOptions.recordId = options.resId; - } - if (options.controllerState) { - if (options.controllerState.searchModel) { - viewOptions.searchModel = options.controllerState.searchModel; - } - if (options.controllerState.searchPanel) { - viewOptions.searchPanel = options.controllerState.searchPanel; - } - } const controller = { jsId: `controller_${++id}`, Component: view, action, view, views, - props: _getViewProps(view, action, views, viewOptions), + props: _getViewProps(view, action, views, options.props), }; action.controllers[view.type] = controller; @@ -741,7 +720,7 @@ function makeActionManager(env) { jsId: `controller_${++id}`, Component: clientAction, action, - props: _getClientActionProps(action, options), + props: _getActionProps(action, options.props), }; return _updateUI(controller, { clearBreadcrumbs: options.clearBreadcrumbs, @@ -847,8 +826,7 @@ function makeActionManager(env) { } function _executeReportClientAction(action, options) { - const clientActionOptions = Object.assign({}, options, { - context: action.context, + const props = Object.assign({}, options.props, { data: action.data, display_name: action.display_name, name: action.name, @@ -856,6 +834,10 @@ function makeActionManager(env) { report_name: action.report_name, report_url: _getReportUrl(action, "html"), }); + const clientActionOptions = Object.assign({}, options, { + context: action.context, + props, + }); return doAction("report.client_action", clientActionOptions); } @@ -1010,7 +992,7 @@ function makeActionManager(env) { action = { type: "ir.actions.act_window_close", infos: { special: true } }; } else if (params.type === "object") { // call a Python Object method, which may return an action to execute - let args = params.recordId ? [[params.recordId]] : [params.recordIds]; + let args = params.resId ? [[params.resId]] : [params.resIds]; if (params.args) { let additionalArgs; try { @@ -1026,7 +1008,7 @@ function makeActionManager(env) { args, kwargs: { context }, method: params.name, - model: params.model, + model: params.resModel, }); action = await keepLast.add(callProm); action = @@ -1035,9 +1017,9 @@ function makeActionManager(env) { : { type: "ir.actions.act_window_close" }; } else if (params.type === "action") { // execute a given action, so load it first - context.active_id = params.recordId || null; - context.active_ids = params.recordIds; - context.active_model = params.model; + context.active_id = params.resId || null; + context.active_ids = params.resIds; + context.active_model = params.resModel; action = await keepLast.add(_loadAction(params.name, context)); } else { throw new InvalidButtonParamsError("Missing type for doActionButton request"); @@ -1051,10 +1033,10 @@ function makeActionManager(env) { currentCtx[key] = params.context[key]; } } - const activeCtx = { active_model: params.model }; - if (params.recordId) { - activeCtx.active_id = params.recordId; - activeCtx.active_ids = [params.recordId]; + const activeCtx = { active_model: params.resModel }; + if (params.resId) { + activeCtx.active_id = params.resId; + activeCtx.active_ids = [params.resId]; } action.context = makeContext(currentCtx, params.buttonContext, activeCtx, action.context); // in case an effect is returned from python and there is already an effect @@ -1075,11 +1057,11 @@ function makeActionManager(env) { * stack. This action must be of type 'ir.actions.act_window'. * * @param {ViewType} viewType - * @param {any} [options={}] + * @param {Object} [props={}] * @throws {ViewNotFoundError} if the viewType is not found on the current action * @returns {Promise} */ - async function switchView(viewType, options = {}) { + async function switchView(viewType, props = {}) { const controller = controllerStack[controllerStack.length - 1]; const view = _getView(viewType); if (!view) { @@ -1103,14 +1085,19 @@ function makeActionManager(env) { // case where a legacy view is reloaded via the view switcher const { __legacy_widget__ } = controller.getState(); const params = {}; - if ("recordId" in options) { - params.currentId = options.recordId; + if ("resId" in props) { + params.currentId = props.resId; } return __legacy_widget__.reload(params); } // END LEGACY CODE COMPATIBILITY - newController.props = _getViewProps(view, controller.action, controller.views, options); + newController.props = _getViewProps( + view, + controller.action, + controller.views, + props + ); controller.action.controllers[viewType] = newController; let index; if (view.multiRecord) { @@ -1165,11 +1152,11 @@ function makeActionManager(env) { const switchViewParams = _getSwitchViewParams(); if (switchViewParams) { // only when we already have an action in dom - const { viewType, viewOptions } = switchViewParams; + const { viewType, props } = switchViewParams; const view = _getView(viewType); if (view) { // Params valid and view found => performs a "switchView" - await switchView(viewType, viewOptions); + await switchView(viewType, props); return true; } } else { @@ -1206,9 +1193,9 @@ function makeActionManager(env) { } if (action.type === "ir.actions.act_window") { const props = controller.props; - newState.model = props.model; + newState.model = props.resModel; newState.view_type = props.type; - newState.id = props.recordId ? `${props.recordId}` : undefined; + newState.id = props.resId ? `${props.resId}` : undefined; } env.services.router.pushState(newState, { replace: true }); } diff --git a/addons/web/static/tests/webclient/actions/client_action_tests.js b/addons/web/static/tests/webclient/actions/client_action_tests.js index 832e38d405e..8eba3d7e52a 100644 --- a/addons/web/static/tests/webclient/actions/client_action_tests.js +++ b/addons/web/static/tests/webclient/actions/client_action_tests.js @@ -340,6 +340,37 @@ QUnit.module("ActionManager", (hooks) => { ); }); + QUnit.test("ClientAction receives arbitrary props from doAction (wowl)", async (assert) => { + assert.expect(1); + class ClientAction extends Component { + setup() { + assert.strictEqual(this.props.division, "bell"); + } + } + ClientAction.template = tags.xml`
`; + actionRegistry.add("OwlClientAction", ClientAction); + const webClient = await createWebClient({ serverData }); + await doAction(webClient, "OwlClientAction", { + props: { division: "bell" }, + }); + }); + + QUnit.test("ClientAction receives arbitrary props from doAction (legacy)", async (assert) => { + assert.expect(1); + const ClientAction = AbstractAction.extend({ + init(parent, action, options) { + assert.strictEqual(options.division, "bell"); + this._super.apply(this, arguments); + }, + }); + core.action_registry.add("ClientAction", ClientAction); + registerCleanup(() => delete core.action_registry.map.ClientAction); + const webClient = await createWebClient({ serverData }); + await doAction(webClient, "ClientAction", { + props: { division: "bell" }, + }); + }); + QUnit.test("test display_notification client action", async function (assert) { assert.expect(6); const webClient = await createWebClient({ serverData }); diff --git a/addons/web/static/tests/webclient/actions/load_state_tests.js b/addons/web/static/tests/webclient/actions/load_state_tests.js index 0e44b4a68b2..657e60e8706 100644 --- a/addons/web/static/tests/webclient/actions/load_state_tests.js +++ b/addons/web/static/tests/webclient/actions/load_state_tests.js @@ -359,7 +359,7 @@ QUnit.module("ActionManager", (hooks) => { "breadcrumbs should contain the display_name of the opened record" ); await doAction(webClient, 3, { - resId: 2, + props: { resId: 2 }, viewType: "form", }); assert.containsN( @@ -815,7 +815,7 @@ QUnit.module("ActionManager", (hooks) => { assert.step(route); }; const webClient = await createWebClient({ serverData, mockRPC }); - await doAction(webClient, 999, { viewType: "form", resId: 2 }); + await doAction(webClient, 999, { viewType: "form", props: { resId: 2 } }); assert.containsOnce(webClient, ".o_form_view"); assert.containsN(webClient, ".breadcrumb-item", 2); assert.strictEqual( diff --git a/addons/web/static/tests/webclient/actions/window_action_tests.js b/addons/web/static/tests/webclient/actions/window_action_tests.js index 797a39bf66f..a14870c5c60 100644 --- a/addons/web/static/tests/webclient/actions/window_action_tests.js +++ b/addons/web/static/tests/webclient/actions/window_action_tests.js @@ -2202,7 +2202,7 @@ QUnit.module("ActionManager", (hooks) => { `; const webClient = await createWebClient({ serverData, mockRPC }); - await doAction(webClient, 3, { viewType: "form", resId: 1 }); + await doAction(webClient, 3, { viewType: "form", props: { resId: 1 } }); await legacyExtraNextTick(); assert.isVisible(webClient.el.querySelector(".o_form_buttons_view .o_form_button_edit")); await click(webClient.el.querySelector(".o_form_buttons_view .o_form_button_edit")); @@ -2287,7 +2287,7 @@ QUnit.module("ActionManager", (hooks) => { const webClient = await createWebClient({ serverData, mockRPC }); await doAction(webClient, 1, { - controllerState: { + props: { searchModel: '{"ControlPanelModelExtension":{"filters":{"1":{"type":"field","description":"Foo","fieldName":"foo","fieldType":"char","groupId":1,"id":1},"2":{"description":"ID is \\"99\\"","domain":"[[\\"id\\",\\"=\\",99]]","type":"filter","groupId":3,"groupNumber":2,"id":2}},"query":[{"groupId":3,"filterId":2}]}}', },