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}]}}', },