[IMP] web: use dialog service on action service

Until now, the action container opened/closed an action dialog directly
on the template, and relayed on the bus to communicate with the action service.
Now the action service opens/closes the action dialogs using the action service.
To do this, a new close method is created on the action service.
This commit also separates the code of dialog service and dialog container to
be more standard.

closes odoo-dev/odoo#900

Related: odoo-dev/enterprise#156
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2021-06-18 21:31:30 +02:00
committed by Aaron Bohy
parent 1092475171
commit d73309de28
10 changed files with 173 additions and 186 deletions
@@ -0,0 +1,60 @@
/** @odoo-module **/
import { registry } from "../registry";
import { useService } from "../service_hook";
const { Component, tags } = owl;
class ErrorHandler extends Component {
catchError(error) {
this.props.callback();
throw error;
}
}
ErrorHandler.template = tags.xml`<t t-component="props.dialog.class" t-props="props.dialog.props" />`;
export class DialogContainer extends Component {
setup() {
this.dialogs = {};
const { bus } = useService("dialog");
bus.on("ADD", this, (dialog) => {
this.dialogs[dialog.id] = dialog;
this.render();
});
bus.on("CLOSE", this, (id) => {
this.closeDialog(id);
});
}
onDialogClosed(id) {
this.closeDialog(id);
}
closeDialog(id) {
if (this.dialogs[id].options && this.dialogs[id].options.onCloseCallback) {
this.dialogs[id].options.onCloseCallback();
}
delete this.dialogs[id];
this.render();
}
errorCallBack(id) {
return () => this.closeDialog(id);
}
__destroy() {
this.env.bus.off("ADD", this);
this.env.bus.off("CLOSE", this);
super.__destroy();
}
}
DialogContainer.components = { ErrorHandler };
DialogContainer.template = tags.xml`
<div class="o_dialog_manager">
<t t-foreach="Object.values(dialogs)" t-as="dialog" t-key="dialog.id">
<ErrorHandler dialog="dialog" t-on-dialog-closed="onDialogClosed(dialog.id)" callback="errorCallBack(dialog.id)" />
</t>
</div>
`;
registry.category("main_components").add("DialogContainer", DialogContainer);
@@ -1,76 +1,33 @@
/** @odoo-module **/
import { registry } from "../registry";
import { useService } from "../service_hook";
import { Dialog } from "./dialog";
const { Component, core, tags, useState } = owl;
const { core } = owl;
const { EventBus } = core;
class ErrorHandler extends Component {
catchError(error) {
this.props.callback();
throw error;
}
}
ErrorHandler.template = tags.xml`<t t-component="props.dialog.class" t-props="props.dialog.props" />`;
export class DialogContainer extends Component {
setup() {
this.dialogs = useState({});
this.dialogId = 1;
const { bus } = useService("dialog");
bus.on("UPDATE", this, (dialogClass, props, options) => {
this.addDialog(dialogClass, props, options);
});
}
addDialog(dialogClass, props, options) {
if (!(dialogClass.prototype instanceof Dialog)) {
throw new Error(dialogClass.name + " must be a subclass of Dialog");
}
const id = this.dialogId++;
this.dialogs[id] = {
id,
class: dialogClass,
props,
options,
};
}
onDialogClosed(id) {
this.doCloseDialog(id);
}
doCloseDialog(id) {
if (this.dialogs[id].options && this.dialogs[id].options.onCloseCallback) {
this.dialogs[id].options.onCloseCallback();
}
delete this.dialogs[id];
}
errorCallBack(id) {
return () => this.doCloseDialog(id);
}
}
DialogContainer.components = { ErrorHandler };
DialogContainer.template = tags.xml`
<div class="o_dialog_manager">
<t t-foreach="Object.values(dialogs)" t-as="dialog" t-key="dialog.id">
<ErrorHandler dialog="dialog" t-on-dialog-closed="onDialogClosed(dialog.id)" callback="errorCallBack(dialog.id)" />
</t>
</div>
`;
registry.category("main_components").add("DialogContainer", DialogContainer);
export const dialogService = {
start(env) {
start() {
const bus = new EventBus();
let dialogId = 0;
function open(dialogClass, props, options) {
bus.trigger("UPDATE", dialogClass, props, options);
if (!(dialogClass.prototype instanceof Dialog)) {
throw new Error(dialogClass.name + " must be a subclass of Dialog");
}
const id = ++dialogId;
const dialog = {
id,
class: dialogClass,
props,
options,
};
bus.trigger("ADD", dialog);
return id;
}
return { open, bus };
function close(id) {
bus.trigger("CLOSE", id);
}
return { open, close, bus };
},
};
+55 -70
View File
@@ -4,7 +4,6 @@ import Context from "web.Context";
import core from "web.core";
import { ComponentAdapter } from "web.OwlCompatibility";
import { objectToQuery } from "../core/browser/router_service";
import { useBus } from "../core/bus_hook";
import { useDebugMenu } from "../core/debug/debug_menu";
import { Dialog } from "../core/dialog/dialog";
import { useEffect } from "../core/effect_hook";
@@ -39,34 +38,35 @@ class ActionAdapter extends ComponentAdapter {
// This is protected in legacy (backward compatibility) but should not e supported in Wowl
this.tempQuery = {};
let originalUpdateControlPanel;
useEffect(() => {
this.title.setParts({ action: this.widget.getTitle() });
const query = objectToQuery(this.widget.getState());
Object.assign(query, this.tempQuery);
this.tempQuery = null;
this.__widget = this.widget;
if (!this.wowlEnv.inDialog) {
this.router.pushState(query);
}
this.wowlEnv.bus.on("ACTION_MANAGER:UPDATE", this, (info) => {
if (info.type === "MAIN") {
this.env.bus.trigger("close_dialogs");
useEffect(
() => {
this.title.setParts({ action: this.widget.getTitle() });
const query = objectToQuery(this.widget.getState());
Object.assign(query, this.tempQuery);
this.tempQuery = null;
this.__widget = this.widget;
if (!this.wowlEnv.inDialog) {
this.router.pushState(query);
}
cleanDomFromBootstrap();
});
originalUpdateControlPanel = this.__widget.updateControlPanel.bind(this.__widget);
this.__widget.updateControlPanel = (newProps) => {
this.wowlEnv.bus.on("ACTION_MANAGER:UPDATE", this, () => {
this.env.bus.trigger("close_dialogs");
cleanDomFromBootstrap();
});
originalUpdateControlPanel = this.__widget.updateControlPanel.bind(this.__widget);
this.__widget.updateControlPanel = (newProps) => {
this.trigger("controller-title-updated", this.__widget.getTitle());
return originalUpdateControlPanel(newProps);
};
this.trigger("controller-title-updated", this.__widget.getTitle());
return originalUpdateControlPanel(newProps);
};
this.trigger("controller-title-updated", this.__widget.getTitle());
core.bus.trigger("DOM_updated");
core.bus.trigger("DOM_updated");
return () => {
this.__widget.updateControlPanel = originalUpdateControlPanel;
this.wowlEnv.bus.off("ACTION_MANAGER:UPDATE", this);
};
}, () => []);
return () => {
this.__widget.updateControlPanel = originalUpdateControlPanel;
this.wowlEnv.bus.off("ACTION_MANAGER:UPDATE", this);
};
},
() => []
);
}
_trigger_up(ev) {
@@ -188,34 +188,37 @@ function useMagicLegacyReload() {
let legacyReloadProm = null;
const getReloadProm = () => legacyReloadProm;
let manualReload;
useEffect(() => {
const widget = comp.widget;
const controllerReload = widget.reload;
widget.reload = function (...args) {
manualReload = true;
legacyReloadProm = controllerReload.call(widget, ...args);
return legacyReloadProm.then(() => {
if (manualReload) {
legacyReloadProm = null;
manualReload = false;
}
});
};
const controllerUpdate = widget.update;
widget.update = function (...args) {
const updateProm = controllerUpdate.call(widget, ...args);
const manualUpdate = !manualReload;
if (manualUpdate) {
legacyReloadProm = updateProm;
}
return updateProm.then(() => {
useEffect(
() => {
const widget = comp.widget;
const controllerReload = widget.reload;
widget.reload = function (...args) {
manualReload = true;
legacyReloadProm = controllerReload.call(widget, ...args);
return legacyReloadProm.then(() => {
if (manualReload) {
legacyReloadProm = null;
manualReload = false;
}
});
};
const controllerUpdate = widget.update;
widget.update = function (...args) {
const updateProm = controllerUpdate.call(widget, ...args);
const manualUpdate = !manualReload;
if (manualUpdate) {
legacyReloadProm = null;
legacyReloadProm = updateProm;
}
});
};
widget[magicReloadSymbol] = getReloadProm;
}, () => []);
return updateProm.then(() => {
if (manualUpdate) {
legacyReloadProm = null;
}
});
};
widget[magicReloadSymbol] = getReloadProm;
},
() => []
);
return getReloadProm;
}
@@ -226,7 +229,6 @@ export class ViewAdapter extends ActionAdapter {
this.vm = useService("view");
this.shouldUpdateWidget = true;
this.magicReload = useMagicLegacyReload();
const envWowl = this.env;
useDebugMenu("action", {
action: this.props.viewParams.action,
component: this,
@@ -235,23 +237,6 @@ export class ViewAdapter extends ActionAdapter {
if (this.props.viewInfo.type === "form") {
useDebugMenu("form");
}
if (!envWowl.inDialog) {
useBus(envWowl.bus, "ACTION_MANAGER:UPDATE", (info) => {
switch (info.type) {
case "OPEN_DIALOG": {
// we are a main action, and a dialog is going to open:
// we should not reload
this.shouldUpdateWidget = false;
break;
}
case "CLOSE_DIALOG": {
this.shouldUpdateWidget = false;
info.closingProms.push(() => this.magicReload());
break;
}
}
});
}
this.env = Component.env;
}
@@ -9,36 +9,9 @@ const { Component, tags } = owl;
// -----------------------------------------------------------------------------
export class ActionContainer extends Component {
setup() {
this.main = {};
this.dialog = {};
this.info = {};
this.env.bus.on("ACTION_MANAGER:UPDATE", this, (info) => {
switch (info.type) {
case "MAIN":
this.main = info;
break;
case "OPEN_DIALOG": {
const { onClose } = this.dialog;
this.dialog = {
id: info.id,
props: info.props,
onClose: onClose || info.onClose,
};
break;
}
case "CLOSE_DIALOG": {
let onClose;
if (this.dialog.id) {
onClose = this.dialog.onClose;
} else {
onClose = info.onClose;
}
if (onClose) {
onClose(info.onCloseInfo);
}
this.dialog = {};
break;
}
}
this.info = info;
this.render();
});
}
@@ -47,17 +20,11 @@ export class ActionContainer extends Component {
this.env.bus.off("ACTION_MANAGER:UPDATE", this);
super.destroy();
}
onDialogClosed() {
this.dialog = {};
this.render();
}
}
ActionContainer.components = { ActionDialog };
ActionContainer.template = tags.xml`
<t t-name="web.ActionContainer">
<div class="o_action_manager">
<t t-if="main.Component" t-component="main.Component" t-props="main.componentProps" t-key="main.id"/>
<ActionDialog t-if="dialog.id" t-props="dialog.props" t-key="dialog.id" t-on-dialog-closed="onDialogClosed"/>
<t t-if="info.Component" t-component="info.Component" t-props="info.componentProps" t-key="info.id"/>
</div>
</t>`;
@@ -1,6 +1,7 @@
/** @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";
import { makeContext } from "../../core/context";
@@ -9,6 +10,7 @@ import { evaluateExpr } from "../../core/py_js/py";
import { registry } from "../../core/registry";
import { KeepLast } from "../../core/utils/concurrency";
import { sprintf } from "../../core/utils/strings";
import { ActionDialog } from "./action_dialog";
/**
* @typedef {"current" | "fullscreen" | "new" | "self" | "inline"} ActionMode */
@@ -57,6 +59,7 @@ function makeActionManager(env) {
let controllerStack = [];
let dialogCloseProm;
let actionCache = {};
let dialog = {};
// The state action (or default user action if none) is loaded as soon as possible
// so that the next "doAction" will have its action ready when needed.
@@ -536,6 +539,7 @@ function makeActionManager(env) {
ControllerComponent.Component = controller.Component;
if (action.target === "new") {
cleanDomFromBootstrap();
const actionDialogProps = {
// TODO add size
ActionComponent: ControllerComponent,
@@ -544,12 +548,16 @@ function makeActionManager(env) {
if (action.name) {
actionDialogProps.title = action.name;
}
env.bus.trigger("ACTION_MANAGER:UPDATE", {
type: "OPEN_DIALOG",
id: ++id,
props: actionDialogProps,
onClose: options.onClose,
});
const { onClose } = dialog;
if (dialog.id) {
env.services.dialog.close(dialog.id);
}
const dialogId = env.services.dialog.open(ActionDialog, actionDialogProps);
dialog = {
id: dialogId,
onClose: onClose || options.onClose,
};
return currentActionProm;
}
@@ -571,7 +579,6 @@ function makeActionManager(env) {
controller.props.breadcrumbs = _getBreadcrumbs(nextStack.slice(0, nextStack.length - 1));
const closingProm = _executeCloseAction();
env.bus.trigger("ACTION_MANAGER:UPDATE", {
type: "MAIN",
id: ++id,
Component: ControllerComponent,
componentProps: controller.props,
@@ -903,13 +910,20 @@ function makeActionManager(env) {
}
async function _executeCloseAction(params = {}) {
const closingProms = [];
env.bus.trigger("ACTION_MANAGER:UPDATE", {
type: "CLOSE_DIALOG",
closingProms,
...params,
});
await Promise.all([dialogCloseProm].concat(closingProms.map((fn) => fn())));
cleanDomFromBootstrap();
let onClose;
if (dialog.id) {
onClose = dialog.onClose;
env.services.dialog.close(dialog.id);
} else {
onClose = params.onClose;
}
dialog = {};
if (onClose) {
await onClose(params.onCloseInfo);
}
return dialogCloseProm;
}
// ---------------------------------------------------------------------------
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { DialogContainer, dialogService } from "@web/core/dialog/dialog_service";
import { DialogContainer } from "@web/core/dialog/dialog_container";
import { dialogService } from "@web/core/dialog/dialog_service";
import { ErrorDialog } from "@web/core/errors/error_dialogs";
import { errorService } from "@web/core/errors/error_service";
import { registry } from "@web/core/registry";
@@ -19,6 +19,7 @@ import { effectService } from "@web/webclient/effects/effect_service";
import { hotkeyService } from "@web/webclient/hotkeys/hotkey_service";
import { menuService } from "@web/webclient/menu_service";
import { WebClient } from "@web/webclient/webclient";
import { DialogContainer } from "@web/core/dialog/dialog_container";
// This import is needed because of it's sideeffects, for exemple :
// web.test_utils easyload xml templates at line : 124:130.
// Also it set the autocomplete delay time for the field Many2One at 0 for the tests at line : 132:137
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { DialogContainer } from "@web/core/dialog/dialog_service";
import { DialogContainer } from "@web/core/dialog/dialog_container";
import { registry } from "@web/core/registry";
import { NotificationContainer } from "@web/core/notifications/notification_container";
import testUtils from "web.test_utils";
@@ -8,6 +8,7 @@ import { clearRegistryWithCleanup } from "../../helpers/mock_env";
import { makeFakeNotificationService, makeFakeUserService } from "../../helpers/mock_services";
import { createWebClient, doAction, getActionManagerTestConfig } from "./helpers";
import { mockDownload } from "@web/../tests/helpers/utils";
import { DialogContainer } from "@web/core/dialog/dialog_container";
let testConfig;
@@ -18,6 +19,7 @@ QUnit.module("ActionManager", (hooks) => {
hooks.beforeEach(() => {
testConfig = getActionManagerTestConfig();
clearRegistryWithCleanup(mainComponentRegistry);
mainComponentRegistry.add("DialogContainer", DialogContainer);
});
QUnit.module("Report actions");
@@ -284,7 +286,7 @@ QUnit.module("ActionManager", (hooks) => {
};
const webClient = await createWebClient({ testConfig, mockRPC });
let customHandlerCalled = false;
registry.category("ir.actions.report handlers").add("custom_handler", async action => {
registry.category("ir.actions.report handlers").add("custom_handler", async (action) => {
if (action.id === 7 && !customHandlerCalled) {
customHandlerCalled = true;
assert.step("calling custom handler");
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { debugService } from "@web/core/debug/debug_service";
import { DialogContainer } from "@web/core/dialog/dialog_service";
import { DialogContainer } from "@web/core/dialog/dialog_container";
import { registry } from "@web/core/registry";
import { editView } from "@web/legacy/debug_manager";
import { clearUncommittedChanges } from "@web/webclient/actions/action_service";