Compare commits

...

12 Commits

Author SHA1 Message Date
Samuel Degueldre e1ff7ac6a4 [FIX] runtime: do not crash when capturing context with getter
When attempting to capture a rendering context that contains an
enumerable getter, there is a crash because we attempt to write a value
on the getter. This commit fixes that by manually climbing the prototype
chain to copy the values instead, and ignoring getters.
2023-06-09 07:34:54 +02:00
Géry Debongnie fbf4c4add2 [FIX] parser: make t-on stricter
Before this commit, the `t-on` directive assumed that the next character
would be a -, and ignored it, so if someone would write `t-onclick` by
mistake, Owl would then add an event handler on the `lick` event,
instead of `click`.

With this commit, we improve the parser to make it stricter and fail if
there is no dash.

closes #1441
2023-05-30 13:30:56 +02:00
Julien Carion (juca) 78d6ff735e [REL] devtools: chrome v1.1.1 2023-05-26 15:15:56 +02:00
Julien Carion (juca) a7f51fa666 [FIX] devtools: fix context menus flickering
This commit makes it so the position of the context menu doesn't require
an additional render to apply so it fixes the flickering.
2023-05-24 15:40:18 +02:00
Julien Carion (juca) 5175f95289 [REL] devtools: chrome v1.1 2023-05-24 14:28:30 +02:00
Julien Carion (juca) 3c9f4a8ae9 [IMP] devtools: Add blacklist to components toggle
This commit adds the functionnality to add components to the toggle
blacklist so that it won't be expanded by default when launching or
reloading the devtools components tree.
2023-05-24 14:28:30 +02:00
Julien Carion (juca) 310730782c [FIX] devtools: Missing global owl
This commit adapts the behavior of the devtools global hook to get
the toRaw and reactive functions from the __OWL_DEVTOOLS__ variable
instead of the global owl when it is missing. Also adds the functions
to the __OWL_DEVTOOLS__ variable for the former to work.
This allows the devtools to work in environments where owl is loaded
through ES modules.
Also quickly fixes iframes detection update on page reload.
2023-05-24 13:34:11 +02:00
Julien Carion (juca) 77a413d750 [FIX] devtools: fix issue when treeElement have same name
This commit fixes the key of the sub tree elements in the details
window to be 100% unique since the name of the elements could not
be unique so it would silently crash and not display. It also removes
the default size property of maps and sets since there's already a
getter available for it.
2023-05-24 11:12:35 +02:00
Julien Carion (juca) fe31f93c94 [FIX] devtools: fix apps patching and reload
This commit fixes issues introduced in the previous fix for the apps
methods patching and also fixes the status of the profiler tracing
on reload.
2023-05-24 11:12:03 +02:00
Michael (mcm) 9cc0f88e02 [IMP] hooks: add types to useEffect to improve DX 2023-05-16 14:26:27 +02:00
Samuel Degueldre 412fda10fd [FIX] gh-page: fix 'unexpected token export' error on landing page
When refactoring the playground to use es-modules, a script that loads
owl as a non-module was forgotten in the head of the page and causes an
error in the console, as the script is not declared as type="module"
despite being an es-module. This script is also useless as owl is loaded
by being imported by the counter component in the page.

The importmap has also been moved to the head because of a bug in
firefox where importmaps can fail to be taken into account if they are
after a script that is not of type="module", see:
https://bugzilla.mozilla.org/show_bug.cgi?id=1833371

Closes #1436
2023-05-16 14:15:10 +02:00
Géry Debongnie aa441274c7 [FIX] compiler: apply translations to t-set text body
Before this commit, the translate function was not applied to text
content inside the body of a t-set (unless that content was itself
inside some html). This is not clearly not intended.

To fix it, we just need to call the translate function at the
appropriate moment.

closes #1434
2023-05-16 10:38:22 +02:00
20 changed files with 394 additions and 66 deletions
+1 -2
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="assets/milligram.css">
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
<link rel="stylesheet" href="assets/main.css">
<script src="./owl.js"></script>
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
</head>
<body>
<header class="container">
@@ -68,7 +68,6 @@
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
</footer>
<script src="assets/highlight.pack.js"></script>
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
<script type="module" src="display_code.js"></script>
<script type="module" src="counter.js"></script>
</body>
+9 -4
View File
@@ -447,6 +447,11 @@ export class CodeGenerator {
.join("");
}
translate(str: string): string {
const match = translationRE.exec(str) as any;
return match[1] + this.translateFn(match[2]) + match[3];
}
/**
* @returns the newly created block name, if any
*/
@@ -527,8 +532,7 @@ export class CodeGenerator {
let value = ast.value;
if (value && ctx.translate !== false) {
const match = translationRE.exec(value) as any;
value = match[1] + this.translateFn(match[2]) + match[3];
value = this.translate(value);
}
if (!ctx.inPreTag) {
value = value.replace(whitespaceRE, " ");
@@ -1095,10 +1099,11 @@ export class CodeGenerator {
} else {
let value: string;
if (ast.defaultValue) {
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
if (ast.value) {
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
value = `withDefault(${expr}, \`${defaultValue}\`)`;
} else {
value = `\`${ast.defaultValue}\``;
value = `\`${defaultValue}\``;
}
} else {
value = expr;
+4 -4
View File
@@ -336,10 +336,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
for (let attr of nodeAttrsNames) {
const value = node.getAttribute(attr)!;
if (attr.startsWith("t-on")) {
if (attr === "t-on") {
throw new OwlError("Missing event name with t-on directive");
}
if (attr === "t-on" || attr === "t-on-") {
throw new OwlError("Missing event name with t-on directive");
}
if (attr.startsWith("t-on-")) {
on = on || {};
on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) {
+5
View File
@@ -7,6 +7,7 @@ import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils";
import { toRaw, reactive } from "./reactivity";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -39,6 +40,8 @@ declare global {
apps: Set<App>;
Fiber: typeof Fiber;
RootFiber: typeof RootFiber;
toRaw: typeof toRaw;
reactive: typeof reactive;
};
}
}
@@ -47,6 +50,8 @@ window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
Fiber: Fiber,
RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
};
export class App<
+13 -6
View File
@@ -59,28 +59,35 @@ export function useChildSubEnv(envExtension: Env) {
// useEffect
// -----------------------------------------------------------------------------
type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
/**
* @param {...any} dependencies the dependencies computed by computeDependencies
* @template T
* @param {...T} dependencies the dependencies computed by computeDependencies
* @returns {void|(()=>void)} a cleanup function that reverses the side
* effects of the effect callback.
*/
type Effect = (...dependencies: any[]) => void | (() => void);
type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() => void);
/**
* This hook will run a callback when a component is mounted and patched, and
* will run a cleanup function before patching and before unmounting the
* the component.
*
* @param {Effect} effect the effect to run on component mount and/or patch
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
* @template T
* @param {Effect<T>} effect the effect to run on component mount and/or patch
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
* dependencies that will decide if the effect needs to be cleaned up and
* run again. If the dependencies did not change, the effect will not run
* again. The default value returns an array containing only NaN because
* NaN !== NaN, which will cause the effect to rerun on every patch.
*/
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
export function useEffect<T extends [...T]>(
effect: Effect<T>,
computeDependencies: () => T = () => [NaN] as never
) {
let cleanup: (() => void) | void;
let dependencies: any[];
let dependencies: T;
onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies);
+14 -2
View File
@@ -8,6 +8,12 @@ import { OwlError } from "./error_handling";
import type { ComponentNode } from "./component_node";
const ObjectCreate = Object.create;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const ObjectDefineProperty = Object.defineProperty;
const ObjectEntries = Object.entries;
const hasOwnProperty = (obj: Object, prop: PropertyKey) =>
Object.prototype.hasOwnProperty.call(obj, prop);
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
@@ -49,8 +55,14 @@ function callSlot(
function capture(ctx: any): any {
const result = ObjectCreate(ctx);
for (let k in ctx) {
result[k] = ctx[k];
let current = ctx;
while (current && current !== Object.prototype) {
for (const [key, descriptor] of ObjectEntries(ObjectGetOwnPropertyDescriptors(current))) {
if (!hasOwnProperty(result, key) && "value" in descriptor) {
ObjectDefineProperty(result, key, descriptor);
}
}
current = ObjectGetPrototypeOf(current);
}
return result;
}
@@ -1,5 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`translation support body of t-sets are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`translated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets inside translation=off are not translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", \`untranslated\`);
return text(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with html content are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block1 = createBlock(\`<div>translated</div>\`);
function value1(ctx, node, key = \\"\\") {
return block1();
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
return safeOutput(ctx['label']);
}
}"
`;
exports[`translation support body of t-sets with text and html content are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
let block3 = createBlock(\`<div>translated</div>\`);
function value1(ctx, node, key = \\"\\") {
const b2 = text(\` translated \`);
const b3 = block3();
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
return safeOutput(ctx['label']);
}
}"
`;
exports[`translation support can set and remove translatable attributes 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -52,6 +126,21 @@ exports[`translation support some attributes are translated 1`] = `
}"
`;
exports[`translation support t-set and falsy t-value: t-body are translated 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { isBoundary, withDefault, setContextValue } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
ctx[isBoundary] = 1
setContextValue(ctx, \\"label\\", withDefault(false, \`translated\`));
return text(ctx['label']);
}
}"
`;
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
"function anonymous(app, bdom, helpers
) {
+24
View File
@@ -1147,6 +1147,24 @@ describe("qweb parser", () => {
});
});
test("t-onclick without dash", async () => {
expect(() => parse(`<button t-onclick="add">Click</button>`)).toThrowError(
"Unknown QWeb directive: 't-onclick'"
);
});
test("t-on without event", async () => {
expect(() => parse(`<button t-on="add">Click</button>`)).toThrowError(
"Missing event name with t-on directive"
);
});
test("t-on- without event", async () => {
expect(() => parse(`<button t-on-="add">Click</button>`)).toThrowError(
"Missing event name with t-on directive"
);
});
// ---------------------------------------------------------------------------
// t-model
// ---------------------------------------------------------------------------
@@ -1275,6 +1293,12 @@ describe("qweb parser", () => {
});
});
test("component with event handler", async () => {
expect(() => parse(`<MyComponent t-onclick="someMethod"/>`)).toThrowError(
"unsupported directive on Component: t-onclick"
);
});
test("component with t-ref", async () => {
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
+70
View File
@@ -100,4 +100,74 @@ describe("translation support", () => {
expect(translateFn).toHaveBeenCalledWith("some word");
expect(fixture.innerHTML).toBe("<div>un mot</div>");
});
test("body of t-sets are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated");
});
test("body of t-sets inside translation=off are not translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-translation="off">
<t t-set="label">untranslated</t>
<t t-esc="label"/>
</t>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("untranslated");
});
test("body of t-sets with html content are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label"><div>untranslated</div></t>
<t t-out="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("<div>translated</div>");
});
test("body of t-sets with text and html content are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label">
some text
<div>untranslated</div>
</t>
<t t-out="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe(" translated <div>translated</div>");
});
test("t-set and falsy t-value: t-body are translated", async () => {
class SomeComponent extends Component {
static template = xml`
<t t-set="label" t-value="false">untranslated</t>
<t t-esc="label"/>`;
}
const translateFn = () => "translated";
await mount(SomeComponent, fixture, { translateFn });
expect(fixture.innerHTML).toBe("translated");
});
});
@@ -1,5 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`t-call component with an enumerable getter, t-call inside slot 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const callTemplate_1 = app.getTemplate(\`sub\`);
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
function slot1(ctx, node, key = \\"\\") {
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['foo'];
return block1([txt1]);
}
}"
`;
exports[`t-call component with an enumerable getter, t-call inside slot 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
return function template(ctx, node, key = \\"\\") {
return callSlot(ctx, node, key, 'default', false, {});
}
}"
`;
exports[`t-call dynamic t-call 1`] = `
"function anonymous(app, bdom, helpers
) {
+22
View File
@@ -425,4 +425,26 @@ describe("t-call", () => {
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
});
test("component with an enumerable getter, t-call inside slot", async () => {
class Child extends Component {
static template = xml`<t t-slot="default"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child><t t-call="sub"/></Child>`;
}
// simulate adding a getter with patch in odoo: getter will be enumarable
Object.defineProperty(Parent.prototype, "foo", {
get() {
return 1;
},
enumerable: true,
});
const app = new App(Parent);
app.addTemplate("sub", `<div t-esc="foo"/>`);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<div>1</div>");
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Owl devtools",
"version": "1.0",
"version": "1.1.1",
"manifest_version": 3,
"description": "Chrome devtools extension for Odoo Owl framework",
"icons": {
@@ -72,7 +72,7 @@
</t>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="store.activeComponent.path.length !== 1">
@@ -30,7 +30,7 @@
</t>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click="() => this.store.logObjectInConsole(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Store as global variable</li>
<t t-if='props.object.contentType == "function"'>
@@ -38,8 +38,8 @@
</t>
</ul>
</div>
<t t-if="props.object.toggled">
<t t-foreach="props.object.children" t-as="child" t-key="child.name">
<t t-if="props.object.toggled" t-key="contextMenuId">
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
<ObjectTreeElement t-else="" object="child"/>
</t>
@@ -1,9 +1,11 @@
/** @odoo-module **/
import { isElementInCenterViewport, minimizeKey } from "../../../../utils";
import { isElementInCenterViewport, minimizeKey, IS_FIREFOX } from "../../../../utils";
import { useStore } from "../../../store/store";
import { HighlightText } from "./highlight_text/highlight_text";
const browserInstance = IS_FIREFOX ? browser : chrome;
const { Component, useRef, useState, useEffect, onMounted } = owl;
export class TreeElement extends Component {
@@ -105,4 +107,30 @@ export class TreeElement extends Component {
this.store.selectComponent(this.props.component.path);
}
}
// Adds the component name to the components toggle blacklist if not already present
// Else, remove it from the blacklist
toggleComponentToBlacklist() {
if (this.store.settings.componentsToggleBlacklist.has(this.props.component.name)) {
if (!this.props.component.toggled) {
this.props.component.toggled = !this.props.component.toggled;
}
this.store.settings.componentsToggleBlacklist.delete(this.props.component.name);
browserInstance.storage.local.set({
owlDevtoolsComponentsToggleBlacklist: Array.from(
this.store.settings.componentsToggleBlacklist
),
});
} else {
if (this.props.component.toggled) {
this.props.component.toggled = !this.props.component.toggled;
}
this.store.settings.componentsToggleBlacklist.add(this.props.component.name);
browserInstance.storage.local.set({
owlDevtoolsComponentsToggleBlacklist: Array.from(
this.store.settings.componentsToggleBlacklist
),
});
}
}
}
@@ -26,7 +26,7 @@
<span t-if="props.component.depth">&gt;</span>
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
</div>
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, true)" class="custom-menu-item py-1 px-4">Expand children</li>
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
@@ -43,6 +43,10 @@
<t t-else="">
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
</t>
<li t-on-click.stop="() => this.toggleComponentToBlacklist()" class="custom-menu-item py-1 px-4">
<t t-if="store.settings.componentsToggleBlacklist.has(props.component.name)">Don't fold component by default</t>
<t t-else="">Fold component by default</t>
</li>
</ul>
</div>
</div>
@@ -43,7 +43,7 @@
</span>
</div>
</t>
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="props.event.path.length !== 1">
@@ -28,14 +28,14 @@
</span>
</div>
</div>
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="nodeContextMenu">
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-ref="nodeContextMenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, true)" class="custom-menu-item py-1 px-4">Expand children</li>
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
<li t-on-click.stop="() => this.store.foldDirectChildren(props.event)" class="custom-menu-item py-1 px-4">Fold direct children</li>
</ul>
</div>
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
<ul class="my-1">
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
<t t-if="props.event.path.length !== 1">
+32 -17
View File
@@ -11,10 +11,9 @@ export const store = reactive({
expandByDefault: true,
toggleOnSelected: false,
darkmode: false,
componentsToggleBlacklist: new Set(),
},
contextMenu: {
top: 0,
left: 0,
id: 0,
activeMenu: -1,
// Opens the context menu corresponding with the given menu html element
@@ -29,9 +28,9 @@ export const store = reactive({
if (y + menuHeight > window.innerHeight) {
y = window.innerHeight - menuHeight;
}
this.left = x + "px";
menu.style.left = x + "px";
// Need 25px offset because of the main navbar from the browser devtools
this.top = y - 25 + "px";
menu.style.top = y - 25 + "px";
},
// Close the currently displayed context menu
close() {
@@ -110,7 +109,7 @@ export const store = reactive({
);
this.apps = apps ? apps : [];
if (!fromOld && this.settings.expandByDefault) {
this.apps.forEach((tree) => expandNodes(tree));
this.apps.forEach((tree) => expandNodes(tree, true));
}
const component = await evalFunctionInWindow(
"getComponentDetails",
@@ -503,13 +502,18 @@ export const store = reactive({
},
// Reset all the relevant data about the page currently stored
resetData() {
async resetData() {
await loadSettings();
this.loadComponentsTree(false);
this.events = [];
this.eventsTree = [];
this.activeRecorder = false;
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
this.traceRenderings = false;
evalFunctionInWindow("toggleTracing", [false]);
this.traceSubscriptions = false;
evalFunctionInWindow("toggleSubscriptionTracing", [false]);
this.updateIFrameList();
},
// Triggers manually the rendering of the selected component
@@ -605,7 +609,7 @@ export const store = reactive({
// Refresh the whole extension
async refreshExtension() {
await loadScripts();
this.resetData();
await this.resetData();
},
// Toggle dark mode in the extension and store result in the storage
@@ -616,7 +620,7 @@ export const store = reactive({
} else {
document.querySelector("html").classList.remove("dark-mode");
}
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
browserInstance.storage.local.set({ owlDevtoolsDarkMode: this.settings.darkMode });
},
openDocumentation() {
@@ -636,6 +640,8 @@ async function init() {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
await loadSettings();
// We want to load the base components tree when the devtools tab is first opened
store.loadComponentsTree(false);
@@ -651,8 +657,6 @@ async function init() {
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
}
loadSettings();
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
// Heartbeat message to test whether the extension context is still valid or not
@@ -677,14 +681,14 @@ browserInstance.runtime.onConnect.addListener((port) => {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
store.resetData();
await store.resetData();
}
}
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
if (msg.type === "FrameReady") {
store.updateIFrameList();
store.owlStatus = true;
store.resetData();
await store.resetData();
}
// We need to reload the components tree when the set of apps in the page is modified
if (msg.type === "RefreshApps") {
@@ -740,11 +744,12 @@ browserInstance.runtime.onConnect.addListener((port) => {
// Load all settings from the chrome sync storage
async function loadSettings() {
let storage = await browserInstance.storage.local.get();
if (storage.owl_devtools_dark_mode === undefined) {
// Darkmode
if (storage.owlDevtoolsDarkMode === undefined) {
// Load dark mode based on the global settings of the chrome devtools
darkMode = browserInstance.devtools.panels.themeName === "dark";
} else {
darkMode = storage.owl_devtools_dark_mode;
darkMode = storage.owlDevtoolsDarkMode;
}
store.settings.darkMode = darkMode;
if (darkMode) {
@@ -752,6 +757,12 @@ async function loadSettings() {
} else {
document.querySelector("html").classList.remove("dark-mode");
}
// Components toggle blacklist
if (storage.owlDevtoolsComponentsToggleBlacklist !== undefined) {
store.settings.componentsToggleBlacklist = new Set(
storage.owlDevtoolsComponentsToggleBlacklist
);
}
}
// Function to handle and store a batch of events coming from the page
@@ -871,10 +882,14 @@ function highlightChildren(component) {
}
// Expand the node given in entry and all of its children
function expandNodes(node) {
node.toggled = true;
function expandNodes(node, blacklist = false) {
if (blacklist && store.settings.componentsToggleBlacklist.has(node.name)) {
node.toggled = false;
} else {
node.toggled = true;
}
for (const child of node.children) {
expandNodes(child);
expandNodes(child, blacklist);
}
}
@@ -11,6 +11,10 @@
this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
// Same but for RootFiber
this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
// This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive
// in __OWL_DEVTOOLS__
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
// Set to keep track of the fibers that are in the flush queue
this.queuedFibers = new WeakSet();
// Set to keep track of the HTML elements we added to the page
@@ -46,6 +50,9 @@
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods();
if (this.apps.size > 0) {
this.patchAppMethods();
}
this.recordEvents = false;
this.traceRenderings = false;
this.traceSubscriptions = false;
@@ -241,13 +248,22 @@
return;
}
let app = this.apps.values().next().value;
if (!app) {
return;
const self = this;
if (app.root) {
this.patchDestroyMethod(app.root);
} else {
const originalMount = app.constructor.prototype.mount;
app.constructor.prototype.mount = async function (...args) {
const result = await originalMount.call(this, ...args);
const root = this.root;
self.patchDestroyMethod(root);
app.constructor.prototype.mount = originalMount;
return result;
};
}
const originalFlush = app.scheduler.constructor.prototype.flush;
let inFlush = false;
let _render = false;
const self = this;
app.scheduler.constructor.prototype.flush = function () {
// Used to know when a render is triggered inside the flush method or not
inFlush = true;
@@ -388,7 +404,7 @@
let targetToKeysToCallbacks;
// Step 1: extract internal values from owl
const obj = owl.reactive({}, () => {});
const obj = self.reactive({}, () => {});
let count = 0;
WeakMap.prototype.get = function () {
count++;
@@ -716,7 +732,7 @@
}
}
if (obj) {
obj = owl.toRaw(obj);
obj = this.toRaw(obj);
}
}
return obj;
@@ -824,7 +840,7 @@
child.contentType = "undefined";
child.hasChildren = false;
} else {
obj = owl.toRaw(obj);
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
child.contentType = "map";
@@ -1000,19 +1016,6 @@
children.push(entries);
index++;
}
const size = this.serializeObjectChild(
obj,
{ type: "item", value: "size", childIndex: children.length },
depth,
objType,
path,
oldBranch.children[index],
oldTree
);
if (size) {
children.push(size);
index++;
}
Reflect.ownKeys(obj).forEach((key) => {
const child = this.serializeObjectChild(
obj,
@@ -1389,7 +1392,7 @@
}
getter.hasChildren = false;
} else {
obj = owl.toRaw(obj);
obj = this.toRaw(obj);
switch (true) {
case obj instanceof Map:
getter.contentType = "map";
@@ -1484,7 +1487,7 @@
return;
}
if (objectType === "subscription") {
owl.reactive(obj)[key] = value;
this.reactive(obj)[key] = value;
} else {
obj[key] = value;
if (objectType === "props" || objectType === "instance") {