[REF] web: refactor debug menu to use context in env instead of service

This makes the debug menu more natural to use, and makes the difference
between the "in-dialog" and "out-of-dialog" debug contexts explicit.

closes odoo/odoo#74346

Related: odoo/enterprise#19967
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
This commit is contained in:
Samuel Degueldre
2021-07-30 06:01:42 +00:00
parent 2c19d865f9
commit 4c7ef96974
18 changed files with 179 additions and 232 deletions
@@ -3,7 +3,6 @@
import { manageMessages } from "@mail/js/tools/debug_manager";
import { click, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient, doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers";
import { debugService } from "@web/core/debug/debug_service";
import { registry } from "@web/core/registry";
QUnit.module("DebugMenu");
@@ -25,8 +24,6 @@ QUnit.test("Manage Messages", async function (assert) {
"mail.message,false,search": `<search/>`,
});
// Activate debug service with "Manage Message" item
registry.category("services").add("debug", debugService);
registry.category("debug").category("form").add("manageMessages", manageMessages);
async function mockRPC(route, args) {
@@ -18,4 +18,4 @@ function runPoSJSTests({ env }) {
};
}
registry.category("debug").add("point_of_sale.runPoSJSTests", runPoSJSTests);
registry.category("debug").category("default").add("point_of_sale.runPoSJSTests", runPoSJSTests);
@@ -0,0 +1,81 @@
/** @odoo-module **/
import { registry } from "../registry";
import { memoize } from "../utils/functions";
import { useEffect } from "../effect_hook";
const { useEnv, useSubEnv } = owl.hooks;
const debugRegistry = registry.category("debug");
const getAccessRights = memoize(async function getAccessRights(orm) {
const rightsToCheck = {
"ir.ui.view": "write",
"ir.rule": "read",
"ir.model.access": "read",
};
const proms = Object.entries(rightsToCheck).map(([model, operation]) => {
return orm.call(model, "check_access_rights", [], {
operation,
raise_exception: false,
});
});
const [canEditView, canSeeRecordRules, canSeeModelAccess] = await Promise.all(proms);
const accessRights = { canEditView, canSeeRecordRules, canSeeModelAccess };
return accessRights;
});
class DebugContext {
constructor(env, defaultCategories) {
this.orm = env.services.orm;
this.categories = new Map(defaultCategories.map((cat) => [cat, [{}]]));
}
activateCategory(category, context) {
const contexts = this.categories.get(category) || new Set();
contexts.add(context);
this.categories.set(category, contexts);
return () => {
contexts.delete(context);
if (contexts.size === 0) {
this.categories.delete(category);
}
};
}
async getItems(env) {
const accessRights = await getAccessRights(this.orm);
return [...this.categories.entries()].flatMap(([category, contexts]) => {
return debugRegistry
.category(category)
.getAll()
.map((factory) => factory(Object.assign({ env, accessRights }, ...contexts)))
.filter(Boolean)
.sort((x, y) => {
const xSeq = x.sequence || 1000;
const ySeq = y.sequence || 1000;
return xSeq - ySeq;
});
});
}
}
const debugContextSymbol = Symbol("debugContext");
export function useOwnDebugContext({ categories = [] } = {}) {
useSubEnv({ [debugContextSymbol]: new DebugContext(useEnv(), categories) });
}
export function useCurrentDebugContext() {
const debugContext = useEnv()[debugContextSymbol];
if (!debugContext) {
throw new Error("There is no debug context available in the current environment.");
}
return debugContext;
}
export function useDebugCategory(category, context = {}) {
const debugContext = useCurrentDebugContext();
useEffect(
() => debugContext.activateCategory(category, context),
() => []
);
}
+5 -83
View File
@@ -1,92 +1,14 @@
/** @odoo-module **/
import { useService } from "../service_hook";
import { useEffect } from "../effect_hook";
import { registry } from "../registry";
const debugRegistry = registry.category("debug");
import { useCurrentDebugContext } from "./debug_context";
const { Component } = owl;
let debugElementsId = 1;
export class DebugMenu extends Component {
setup() {
const globalContext = { env: this.env };
this.activeCategories = new Set();
this.contexts = new Map([["global", globalContext]]);
this.debugService = useService("debug");
this.hasAccessRights = false;
// Defined as arrow to be passed as prop
// @ts-ignore
this.beforeOpenDropdown = async () => {
if (!this.hasAccessRights) {
this.hasAccessRights = true;
const accessRights = await this.debugService.getAccessRights();
Object.assign(globalContext, { accessRights });
}
const debugContext = useCurrentDebugContext();
// Needs to be bound to this for use in template
this.getElements = async () => {
this.elements = await debugContext.getItems(this.env);
};
this.env.bus.on("DEBUG-MANAGER:ADD-CONTEXT", this, (payload) => {
const { category, context, inDialog, itemId } = payload;
if (this.env.inDialog === inDialog) {
this.contexts.set(itemId, context);
this.activeCategories.add(category);
}
});
this.env.bus.on("DEBUG-MANAGER:REMOVE-CONTEXT", this, (payload) => {
const { category, inDialog, itemId } = payload;
if (this.env.inDialog === inDialog) {
this.contexts.delete(itemId);
this.activeCategories.delete(category);
}
});
}
getElements() {
const factories = [];
const context = Object.assign({}, ...this.contexts.values());
// If not in dialog => gets root (generic) factories
if (!this.env.inDialog) {
factories.push(...debugRegistry.getAll());
}
// Retrieves the active categories factories
for (const category of this.activeCategories) {
factories.push(...debugRegistry.category(category).getAll());
}
// Builds, filters and sorts all items
return factories
.map((factory) => factory(context))
.filter(Boolean)
.sort((x, y) => {
const xSeq = x.sequence ? x.sequence : 1000;
const ySeq = y.sequence ? y.sequence : 1000;
return xSeq - ySeq;
});
}
onDropdownItemSelected(ev) {
// items of type "component" don't necessarily have a payload/callback
if (ev.detail.payload && ev.detail.payload.callback) {
ev.detail.payload.callback();
}
}
}
DebugMenu.template = "web.DebugMenu";
export function useDebugMenu(category, context = {}) {
const component = Component.current;
const env = component.env;
const payload = {
category,
context,
inDialog: env.inDialog,
itemId: debugElementsId++,
};
useEffect(
() => {
env.bus.trigger("DEBUG-MANAGER:ADD-CONTEXT", payload);
return () => env.bus.trigger("DEBUG-MANAGER:REMOVE-CONTEXT", payload);
},
() => []
);
}
@@ -3,23 +3,20 @@
<t t-name="web.DebugMenu" owl="1">
<Dropdown class="o_debug_manager"
beforeOpen="beforeOpenDropdown"
t-on-dropdown-item-selected.stop="onDropdownItemSelected"
beforeOpen="getElements"
togglerClass="'btn btn-link'">
<t t-set-slot="toggler">
<span class="fa fa-bug"/>
</t>
<t t-foreach="getElements()" t-as="element" t-key="element_index">
<t t-if="!element.hide">
<t t-if="element.type == 'item'">
<DropdownItem payload="{ callback: element.callback }">
<a t-if="element.href" t-att-href="element.href" t-esc="element.description" t-on-click.prevent=""/>
<span t-else="" t-esc="element.description"/>
</DropdownItem>
</t>
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider"/>
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props"/>
<t t-foreach="elements" t-as="element" t-key="element_index">
<t t-if="element.type == 'item'">
<DropdownItem t-on-dropdown-item-selected.stop="element.callback">
<a t-if="element.href" t-att-href="element.href" t-esc="element.description" t-on-click.prevent=""/>
<span t-else="" t-esc="element.description"/>
</DropdownItem>
</t>
<div t-if="element.type == 'separator'" role="separator" class="dropdown-divider"/>
<t t-if="element.type == 'component'" t-component="element.Component" t-props="element.props"/>
</t>
</Dropdown>
</t>
@@ -126,14 +126,14 @@ export function regenerateAssets({ env }) {
}
function becomeSuperuser({ env }) {
const becomeSuperuserULR = browser.location.origin + "/web/become";
const becomeSuperuserURL = browser.location.origin + "/web/become";
return {
type: "item",
description: env._t("Become Superuser"),
hide: !env.services.user.isAdmin,
href: becomeSuperuserULR,
href: becomeSuperuserURL,
callback: () => {
browser.open(becomeSuperuserULR, "_self");
browser.open(becomeSuperuserURL, "_self");
},
sequence: 440,
};
@@ -154,6 +154,7 @@ function leaveDebugMode({ env }) {
registry
.category("debug")
.category("default")
// Backend
.add("runJSTestsItem", runJSTestsItem)
.add("runJSTestsMobileItem", runJSTestsMobileItem)
@@ -1,74 +0,0 @@
/** @odoo-module **/
import { registry } from "../registry";
import { DebugMenu } from "./debug_menu";
export function editModelDebug(env, title, model, id) {
return env.services.action.doAction({
res_model: model,
res_id: id,
name: title,
type: "ir.actions.act_window",
views: [[false, "form"]],
view_mode: "form",
target: "new",
flags: { action_buttons: true, headless: true },
});
}
export const debugService = {
dependencies: ["orm"],
start(env, { orm }) {
let accessRightsProm;
if (env.debug !== "") {
registry.category("systray").add(
"web.debug_mode_menu",
{
Component: DebugMenu,
},
{ sequence: 100 }
);
}
return {
getAccessRights() {
if (!accessRightsProm) {
accessRightsProm = new Promise((resolve, reject) => {
const accessRights = {
canEditView: false,
canSeeRecordRules: false,
canSeeModelAccess: false,
};
const canEditView = orm
.call("ir.ui.view", "check_access_rights", [], {
operation: "write",
raise_exception: false,
})
.then((result) => (accessRights.canEditView = result));
const canSeeRecordRules = orm
.call("ir.rule", "check_access_rights", [], {
operation: "read",
raise_exception: false,
})
.then((result) => (accessRights.canSeeRecordRules = result));
const canSeeModelAccess = orm
.call("ir.model.access", "check_access_rights", [], {
operation: "read",
raise_exception: false,
})
.then((result) => (accessRights.canSeeModelAccess = result));
Promise.all([canEditView, canSeeRecordRules, canSeeModelAccess])
.then(() => resolve(accessRights))
.catch((error) => {
accessRightsProm = undefined;
reject(error);
});
});
}
return accessRightsProm;
},
};
},
};
registry.category("services").add("debug", debugService);
@@ -71,6 +71,7 @@ const profilingService = {
registry
.category("debug")
.category("default")
.add("profilingSeparator", profilingSeparator)
.add("profilingItem", profilingItem);
@@ -4,7 +4,7 @@ import Context from "web.Context";
import core from "web.core";
import { ComponentAdapter } from "web.OwlCompatibility";
import { objectToQuery } from "../core/browser/router_service";
import { useDebugMenu } from "../core/debug/debug_menu";
import { useDebugCategory } from "../core/debug/debug_context";
import { Dialog } from "../core/dialog/dialog";
import { useEffect } from "../core/effect_hook";
import { useService } from "../core/service_hook";
@@ -163,7 +163,7 @@ class ActionAdapter extends ComponentAdapter {
export class ClientActionAdapter extends ActionAdapter {
setup() {
super.setup();
useDebugMenu("action", { action: this.props.widgetArgs[0] });
useDebugCategory("action", { action: this.props.widgetArgs[0] });
owl.hooks.onMounted(() => {
const action = this.props.widgetArgs[0];
if ("params" in action) {
@@ -258,13 +258,14 @@ export class ViewAdapter extends ActionAdapter {
this.vm = useService("view");
this.shouldUpdateWidget = true;
this.magicReload = useMagicLegacyReload();
useDebugMenu("action", {
const debugContext = {
action: this.props.viewParams.action,
component: this,
});
useDebugMenu("view");
};
useDebugCategory("action", debugContext);
useDebugCategory("view", debugContext);
if (this.props.viewInfo.type === "form") {
useDebugMenu("form");
useDebugCategory("form", debugContext);
}
this.env = Component.env;
}
+19 -2
View File
@@ -1,6 +1,5 @@
/** @odoo-module **/
import { editModelDebug } from "../core/debug/debug_service";
import { Dialog } from "../core/dialog/dialog";
import { formatDateTime, parseDateTime } from "../core/l10n/dates";
import { _lt } from "../core/l10n/translation";
@@ -10,6 +9,19 @@ import { formatMany2one } from "../fields/format";
const { hooks, tags } = owl;
const { useState } = hooks;
function editModelDebug(env, title, model, id) {
return env.services.action.doAction({
res_model: model,
res_id: id,
name: title,
type: "ir.actions.act_window",
views: [[false, "form"]],
view_mode: "form",
target: "new",
flags: { action_buttons: true, headless: true },
});
}
// Action items
function actionSeparator() {
@@ -425,7 +437,12 @@ export function editSearchView({ accessRights, component, env }) {
type: "item",
description,
callback: () => {
editModelDebug(env, description, "ir.ui.view", component.props.viewParams.controlPanelFieldsView.view_id);
editModelDebug(
env,
description,
"ir.ui.view",
component.props.viewParams.controlPanelFieldsView.view_id
);
},
sequence: 360,
};
@@ -2,6 +2,7 @@
import { Dialog } from "../../core/dialog/dialog";
import { DebugMenu } from "../../core/debug/debug_menu";
import { useOwnDebugContext } from "../../core/debug/debug_context";
import { useEffect } from "../../core/effect_hook";
const { hooks } = owl;
@@ -18,6 +19,7 @@ const LEGACY_SIZE_CLASSES = {
class ActionDialog extends Dialog {
setup() {
super.setup();
useOwnDebugContext();
this.actionRef = hooks.useRef("actionRef");
const actionProps = this.props && this.props.actionProps;
const action = actionProps && actionProps.action;
@@ -20,4 +20,4 @@ function runClickTestItem({ env }) {
};
}
registry.category("debug").add("runClickTestItem", runClickTestItem);
registry.category("debug").category("default").add("runClickTestItem", runClickTestItem);
@@ -7,6 +7,9 @@ import { NavBar } from "./navbar/navbar";
import { useEffect } from "@web/core/effect_hook";
import { NotUpdatable } from "../core/utils/components";
import { MainComponentsContainer } from "../core/main_components_container";
import { useOwnDebugContext } from "../core/debug/debug_context";
import { registry } from "@web/core/registry";
import { DebugMenu } from "@web/core/debug/debug_menu";
const { Component, hooks } = owl;
const { useExternalListener } = hooks;
@@ -19,6 +22,16 @@ export class WebClient extends Component {
this.router = useService("router");
this.user = useService("user");
useService("legacy_service_provider");
useOwnDebugContext({ categories: ["default"] });
if (this.env.debug) {
registry.category("systray").add(
"web.debug_mode_menu",
{
Component: DebugMenu,
},
{ sequence: 100 }
);
}
this.title.setParts({ zopenerp: "Odoo" }); // zopenerp is easy to grep
useBus(this.env.bus, "ROUTE_CHANGE", this.loadRouterState);
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", (mode) => {
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { DebugMenu, useDebugMenu } from "@web/core/debug/debug_menu";
import { DebugMenu } from "@web/core/debug/debug_menu";
import { regenerateAssets } from "@web/core/debug/debug_menu_items";
import { registry } from "@web/core/registry";
import { debugService } from "@web/core/debug/debug_service";
import { useDebugCategory, useOwnDebugContext } from "@web/core/debug/debug_context";
import { ormService } from "@web/core/orm_service";
import { uiService } from "@web/core/ui/ui_service";
import { ActionDialog } from "@web/webclient/actions/action_dialog";
@@ -17,8 +17,16 @@ import { createWebClient, doAction, getActionManagerServerData } from "../../web
import { openViewItem } from "@web/core/debug/debug_menu_items";
import { editView, editSearchView } from "@web/legacy/debug_manager";
const { Component, hooks, mount, tags } = owl;
const { useSubEnv } = hooks;
const { Component, mount, tags } = owl;
const { xml } = tags;
export class DebugMenuParent extends Component {
setup() {
useOwnDebugContext({ categories: ["default"] });
}
}
DebugMenuParent.template = xml`<DebugMenu/>`;
DebugMenuParent.components = { DebugMenu };
const debugRegistry = registry.category("debug");
let target;
@@ -32,7 +40,6 @@ QUnit.module("DebugMenu", (hooks) => {
.add("hotkey", hotkeyService)
.add("ui", uiService)
.add("orm", ormService)
.add("debug", debugService)
.add("dialog", makeFakeDialogService());
const mockRPC = async (route, args) => {
if (args.method === "check_access_rights") {
@@ -43,6 +50,7 @@ QUnit.module("DebugMenu", (hooks) => {
});
QUnit.test("can be rendered", async (assert) => {
debugRegistry
.category("default")
.add("item_1", () => {
return {
type: "item",
@@ -79,25 +87,13 @@ QUnit.module("DebugMenu", (hooks) => {
};
})
.add("separator_2", () => {
return {
type: "separator",
sequence: 7,
hide: true,
};
return null;
})
.add("item_4", () => {
return {
type: "item",
description: "Item 4",
callback: () => {
assert.step("callback item_4");
},
hide: true,
sequence: 10,
};
return null;
});
const env = await makeTestEnv(testConfig);
const debugManager = await mount(DebugMenu, { env, target });
const debugManager = await mount(DebugMenuParent, { env, target });
registerCleanup(() => debugManager.destroy());
let debugManagerEl = debugManager.el;
await click(debugManager.el.querySelector("button.o_dropdown_toggler"));
@@ -140,7 +136,7 @@ QUnit.module("DebugMenu", (hooks) => {
"Display the DebugMenu correctly in a ActionDialog if debug mode is enabled",
async (assert) => {
assert.expect(8);
debugRegistry.add("global", () => {
debugRegistry.category("default").add("global", () => {
return {
type: "item",
description: "Global 1",
@@ -173,18 +169,19 @@ QUnit.module("DebugMenu", (hooks) => {
sequence: 20,
};
});
class Parent extends Component {
class WithCustom extends ActionDialog {
setup() {
useSubEnv({ inDialog: true });
useDebugMenu("custom", { customKey: "abc" });
super.setup(...arguments);
useDebugCategory("custom", { customKey: "abc" });
}
close() {}
}
Parent.components = { ActionDialog };
Parent.template = tags.xml`<ActionDialog close="close"/>`;
patchWithCleanup(odoo, { debug: "1" });
const env = await makeTestEnv(testConfig);
const actionDialog = await mount(Parent, { env, target });
const actionDialog = await mount(WithCustom, {
env,
target,
props: { close: () => {} },
});
registerCleanup(() => {
actionDialog.destroy();
});
@@ -232,9 +229,9 @@ QUnit.module("DebugMenu", (hooks) => {
},
});
registry.category("services").add("localization", makeFakeLocalizationService());
debugRegistry.add("regenerateAssets", regenerateAssets);
debugRegistry.category("default").add("regenerateAssets", regenerateAssets);
const env = await makeTestEnv(testConfig);
const debugManager = await mount(DebugMenu, { env, target });
const debugManager = await mount(DebugMenuParent, { env, target });
registerCleanup(() => debugManager.destroy());
await click(debugManager.el.querySelector("button.o_dropdown_toggler"));
assert.containsOnce(debugManager.el, "ul.o_dropdown_menu li.o_dropdown_item");
@@ -258,8 +255,7 @@ QUnit.module("DebugMenu", (hooks) => {
debug: true,
});
registry.category("debug").add("openViewItem", openViewItem);
registry.category("services").add("debug", debugService);
registry.category("debug").category("default").add("openViewItem", openViewItem);
const serverData = getActionManagerServerData();
Object.assign(serverData.models, {
@@ -310,8 +306,7 @@ QUnit.module("DebugMenu", (hooks) => {
debug: true,
});
registry.category("debug").add("editViewItem", editView);
registry.category("services").add("debug", debugService);
registry.category("debug").category("view").add("editViewItem", editView);
const serverData = getActionManagerServerData();
serverData.actions[1234] = {
@@ -353,8 +348,7 @@ QUnit.module("DebugMenu", (hooks) => {
debug: true,
});
registry.category("debug").add("editSearchViewItem", editSearchView);
registry.category("services").add("debug", debugService);
registry.category("debug").category("view").add("editSearchViewItem", editSearchView);
const serverData = getActionManagerServerData();
@@ -12,8 +12,7 @@ import makeTestEnvironment from "web.test_env";
import { ClientActionAdapter } from "@web/legacy/action_adapters";
import { makeLegacyCrashManagerService } from "@web/legacy/utils";
import { useDebugMenu } from "@web/core/debug/debug_menu";
import { debugService } from "@web/core/debug/debug_service";
import { useDebugCategory } from "@web/core/debug/debug_context";
import { ErrorDialog } from "@web/core/errors/error_dialogs";
import ControlPanel from "web.ControlPanel";
@@ -125,7 +124,7 @@ QUnit.module("ActionManager", (hooks) => {
patchWithCleanup(ClientActionAdapter.prototype, {
setup() {
useDebugMenu("custom", { widget: this });
useDebugCategory("custom", { widget: this });
this._super();
},
});
@@ -138,7 +137,6 @@ QUnit.module("ActionManager", (hooks) => {
assert.ok(widget);
return {};
});
registry.category("services").add("debug", debugService);
patchWithCleanup(odoo, { debug: true });
const mockRPC = (route) => {
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { debugService } from "@web/core/debug/debug_service";
import { registry } from "@web/core/registry";
import { editView } from "@web/legacy/debug_manager";
import { clearUncommittedChanges } from "@web/webclient/actions/action_service";
@@ -2276,7 +2275,6 @@ QUnit.module("ActionManager", (hooks) => {
QUnit.test("debugManager is active for (legacy) views", async function (assert) {
assert.expect(2);
serviceRegistry.add("debug", debugService);
registry.category("debug").category("view").add("editView", editView);
patchWithCleanup(odoo, { debug: "1" });
const mockRPC = async (route) => {
@@ -44,5 +44,6 @@ export function startTour({ env }) {
registry
.category("debug")
.category("default")
.add("web_tour.startTour", startTour)
.add("web_tour.disableTours", disableTours);
@@ -2,8 +2,6 @@
import { disableTours } from "@web_tour/debug/debug_manager";
import { DebugMenu } from "@web/core/debug/debug_menu";
import { debugService } from "@web/core/debug/debug_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
@@ -13,6 +11,7 @@ import { click, getFixture } from "@web/../tests/helpers/utils";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { DebugMenuParent } from "@web/../tests/core/debug/debug_manager_tests";
const { mount } = owl;
@@ -30,12 +29,11 @@ QUnit.module("Tours", (hooks) => {
.add("hotkey", hotkeyService)
.add("ui", uiService)
.add("orm", ormService)
.add("debug", debugService)
.add("localization", makeFakeLocalizationService());
});
QUnit.test("can disable tours", async (assert) => {
debugRegistry.add("disableTours", disableTours);
debugRegistry.category("default").add("disableTours", disableTours);
const fakeTourService = {
start(env) {
@@ -60,7 +58,7 @@ QUnit.module("Tours", (hooks) => {
};
const env = await makeTestEnv({ mockRPC });
const debugManager = await mount(DebugMenu, { env, target });
const debugManager = await mount(DebugMenuParent, { env, target });
registerCleanup(() => debugManager.destroy());
await click(debugManager.el.querySelector("button.o_dropdown_toggler"));