[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) <aab@odoo.com>
Co-authored-by: Lucas Perais <lpe@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2021-06-21 13:21:53 +00:00
co-authored by Lucas Perais
parent a0e3d05df6
commit db2092d8d3
9 changed files with 131 additions and 98 deletions
@@ -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");
@@ -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,
@@ -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__;
+6 -6
View File
@@ -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,
};
+11 -1
View File
@@ -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;
@@ -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<Number>}
*/
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 });
}
@@ -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`<div class="my_owl_action"></div>`;
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 });
@@ -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(
@@ -2202,7 +2202,7 @@ QUnit.module("ActionManager", (hooks) => {
</sheet>
</form>`;
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}]}}',
},