[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:
co-authored by
Lucas Perais
parent
a0e3d05df6
commit
db2092d8d3
@@ -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__;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}]}}',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user