Compare commits

..

5 Commits

Author SHA1 Message Date
Géry Debongnie ba20267151 [REL] v2.1.3
# v2.1.3

 - [FIX] components: properly differentiate t-call subcomponents
 - [REF] devtools: Better messages forwarding
 - [FIX] devtools: Fix app methods patching
 - [DOC] Fix a code bug in the example of slots
2023-04-29 09:46:05 +02:00
Géry Debongnie aabb7559b8 [FIX] components: properly differentiate t-call subcomponents
Before this commit, when using a dynamic t-call, it was possible to have
a collision between subcomponent keys. This would cause hard to
understand owl crash, because owl would incorrectly use the same
component instance for two different template location. From
owl point of view, one of these component has to be destroyed and the
other one should be mounted, so a crash would occur.

The fix is to simply properly key sub components.
2023-04-28 14:31:34 +02:00
Julien Carion (juca) 606d14a399 [REF] devtools: Better messages forwarding
This commit improves how messages are relayed from the page to the
devtools by using a specific port for each instance of the owl devtools
panel and refreshing it through the heartbeat message. This allows
to prevent sending messages to every devtools instances and sorting
the messages inside.
2023-04-25 09:31:53 +02:00
Julien Carion (juca) c049022798 [FIX] devtools: Fix app methods patching
This commit fixes how the apps methods are patched by decoupling
the patching of the destroy method from the rest because we need
to have a valid reference to a component node to do so (the root
node in this case). The previous behavior was to only patch methods
when an app with a root node is added but this was not working
when the app has no root node initially and gets one dynamically.
There is now a listener to patch the destroy method as soon as a
root node is set on any of the apps if it is not already the case.
2023-04-25 09:28:57 +02:00
myearn4 cae32c5bd4 [DOC] Fix a code bug in the example of slots
The value of activeTab should be tab_index rather than tab.
2023-04-25 09:24:31 +02:00
12 changed files with 238 additions and 145 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ class Notebook extends Component {
<div class="notebook">
<div class="tabs">
<t t-foreach="tabNames" t-as="tab" t-key="tab_index">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab">
<span t-att-class="{active:tab_index === activeTab}" t-on-click="() => state.activeTab=tab_index">
<t t-esc="props.slots[tab].title"/>
</span>
</t>
+4 -4
View File
@@ -3221,7 +3221,7 @@ class TemplateSet {
}
callTemplate(owner, subTemplate, ctx, parent, key) {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
}
}
// -----------------------------------------------------------------------------
@@ -5506,7 +5506,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.1.1";
const version = "2.1.2";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5920,6 +5920,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2023-04-24T14:29:32.376Z';
__info__.hash = 'f9d810a';
__info__.date = '2023-04-29T07:45:54.333Z';
__info__.hash = 'aabb755';
__info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.2",
"version": "2.1.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.1.2",
"version": "2.1.3",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
+1 -1
View File
@@ -125,7 +125,7 @@ export class TemplateSet {
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
const template = this.getTemplate(subTemplate);
return toggler(subTemplate, template.call(owner, ctx, parent, key));
return toggler(subTemplate, template.call(owner, ctx, parent, key + subTemplate));
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.1.2";
export const version = "2.1.3";
@@ -42,6 +42,56 @@ exports[`t-call dynamic t-call 3`] = `
}"
`;
exports[`t-call dynamic t-call with same sub component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const call = app.callTemplate.bind(app);
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['current'].template);
const template1 = (ctx['current'].template);
const b3 = call(this, template1, ctx, node, key + \`__1\`);
return multi([b2, b3]);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`t-call dynamic t-call with same sub component 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
return function template(ctx, node, key = \\"\\") {
return comp1({}, key + \`__1\`, node, this, null);
}
}"
`;
exports[`t-call dynamic t-call: key is propagated 1`] = `
"function anonymous(app, bdom, helpers
) {
+27
View File
@@ -398,4 +398,31 @@ describe("t-call", () => {
});
expect(fixture.innerHTML).toBe("");
});
test("dynamic t-call with same sub component", async () => {
class Child extends Component {
static template = xml`child`;
}
class Root extends Component {
static template = xml`
<t t-esc="current.template"/>
<t t-call="{{current.template}}"/>`;
static components = { Child };
current = useState({ template: "A" });
}
const root = await mount(Root, fixture, {
templates: `
<templates>
<t t-name="A"><Child/></t>
<t t-name="B"><Child/></t>
</templates>`,
});
expect(fixture.innerHTML).toBe("Achild");
root.current.template = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
});
});
+32 -14
View File
@@ -5,7 +5,7 @@ let owlStatus = 0;
const browserInstance = IS_FIREFOX ? browser : chrome;
// Used to keep track of the tabs where the owl devtools have been opened
const activePanels = new Set();
const activePanels = new Map();
// Load the devtools global hook this way when running on manifest v3 chrome
if (!IS_FIREFOX) {
@@ -75,8 +75,23 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
return true;
} else if (message.type === "owlStatus") {
setOwlStatus(message.data);
// Dummy message to test if the extension context is still valid
// Refresh panel connection timeout
} else if (message.type === "keepAlive") {
const panel = activePanels.get(message.id);
if (panel) {
clearTimeout(panel.expirationTimeout);
panel.expirationTimeout = setTimeout(() => {
activePanels.delete(message.id);
panel.port.disconnect();
}, 750);
} else {
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + message.id });
const expirationTimeout = setTimeout(() => {
activePanels.delete(message.id);
port.disconnect();
}, 750);
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
}
return;
// Open the devtools documentation in a new tab
} else if (message.type === "openDoc") {
@@ -87,10 +102,15 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
}
);
return;
// Relay the received message to the devtools app
// Register a new port for the devtools panel
} else if (message.type === "newDevtoolsPanel") {
const tab = await getActiveTabURL();
activePanels.add(tab);
const id = message.id;
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort_" + id });
const expirationTimeout = setTimeout(() => {
activePanels.delete(id);
port.disconnect();
}, 750);
activePanels.set(message.id, { port: port, expirationTimeout: expirationTimeout });
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
} else if (message.type === "getActiveTabURL") {
getActiveTabURL().then((tab) => {
@@ -98,15 +118,13 @@ browserInstance.runtime.onMessage.addListener(async (message, sender, sendRespon
});
return true;
} else {
const tab = await getActiveTabURL();
if (!activePanels.has(tab)) {
return;
const destinationPanel = activePanels.get(sender.tab.id);
if (destinationPanel) {
destinationPanel.port.postMessage(
message.data
? { type: message.type, data: message.data, origin: message.origin }
: { type: message.type, origin: message.origin }
);
}
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort" });
port.postMessage(
message.data
? { type: message.type, data: message.data, origin: message.origin }
: { type: message.type, origin: message.origin }
);
}
});
@@ -27,7 +27,6 @@ function createPanelsIfOwl() {
"../../assets/icon128.png",
IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html"
);
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel" });
}
);
}
+68 -82
View File
@@ -652,103 +652,89 @@ async function init() {
}
loadSettings();
}
// Heartbeat message to test whether the extension context is still valid or not
setInterval(() => {
if (store.extensionContextStatus) {
try {
browserInstance.runtime.sendMessage({ type: "keepAlive" });
} catch (e) {
store.extensionContextStatus = false;
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
// Heartbeat message to test whether the extension context is still valid or not
setInterval(() => {
if (store.extensionContextStatus) {
try {
browserInstance.runtime.sendMessage({ type: "keepAlive", id: store.devtoolsId });
} catch (e) {
store.extensionContextStatus = false;
}
}
}
}, 1000);
}, 500);
}
let flushRendersTimeout = false;
// Connect to the port to communicate to the background script
browserInstance.runtime.onConnect.addListener((port) => {
if (!port.name === "OwlDevtoolsPort") {
return;
}
port.onMessage.addListener(async (msg) => {
// Reload the tree after checking if the scripts are loaded when this message is received
if (msg.type === "Reload") {
const tab = await getTabURL();
// Since this message is sent to all devtools windows, only take it into account when this is the active tab
if (tab !== store.devtoolsId) {
return;
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
port.onMessage.addListener(async (msg) => {
// Reload the tree after checking if the scripts are loaded when this message is received
if (msg.type === "Reload") {
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
store.resetData();
}
}
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
if (store.owlStatus) {
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
// 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();
}
}
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
if (msg.type === "FrameReady") {
const tab = await getTabURL();
// Same as for the reload message
if (tab !== store.devtoolsId) {
return;
// We need to reload the components tree when the set of apps in the page is modified
if (msg.type === "RefreshApps") {
store.loadComponentsTree(true);
}
store.updateIFrameList();
store.owlStatus = true;
store.resetData();
}
// We need to reload the components tree when the set of apps in the page is modified
if (msg.type === "RefreshApps") {
store.loadComponentsTree(true);
}
// Filter out the messages that are not destined to this devtools tab. The messages above may be sent before
// the devtoolsId is set
if (msg.origin.id !== store.devtoolsId) {
return;
}
// When message of type Flush is received, overwrite the component tree with the new one from page
// A flush message is sent everytime a component is rendered on the page
if (msg.type === "Flush") {
if (msg.origin.frame !== store.activeFrame) {
return;
// When message of type Flush is received, overwrite the component tree with the new one from page
// A flush message is sent everytime a component is rendered on the page
if (msg.type === "Flush") {
if (msg.origin.frame !== store.activeFrame) {
return;
}
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
}
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(flushRendersTimeout);
flushRendersTimeout = setTimeout(() => {
store.renderPaths.clear();
}, 100);
store.loadComponentsTree(true);
}
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
// Select the component based on the path received with the SelectElement message
if (msg.type === "SelectElement") {
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
}
store.selectComponent(msg.data);
}
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
store.renderPaths.add(JSON.stringify(msg.data));
clearTimeout(flushRendersTimeout);
flushRendersTimeout = setTimeout(() => {
store.renderPaths.clear();
}, 100);
store.loadComponentsTree(true);
}
// Select the component based on the path received with the SelectElement message
if (msg.type === "SelectElement") {
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
return;
// Stop the DOM element selector tool upon receiving the StopSelector message
if (msg.type === "StopSelector") {
store.componentSearch.activeSelector = false;
}
store.selectComponent(msg.data);
}
// Stop the DOM element selector tool upon receiving the StopSelector message
if (msg.type === "StopSelector") {
store.componentSearch.activeSelector = false;
}
// Logic for recording an event when the event message is received
if (msg.type === "Event") {
let events = msg.data;
loadEvents(events);
}
// If we know a new iframe has been added to the page, load scripts into it and update the
// frames list if it has been directly loaded.
if (msg.type === "NewIFrame") {
const isLoaded = await loadScripts(msg.data);
if (isLoaded) {
store.updateIFrameList();
// Logic for recording an event when the event message is received
if (msg.type === "Event") {
let events = msg.data;
loadEvents(events);
}
}
});
// If we know a new iframe has been added to the page, load scripts into it and update the
// frames list if it has been directly loaded.
if (msg.type === "NewIFrame") {
const isLoaded = await loadScripts(msg.data);
if (isLoaded) {
store.updateIFrameList();
}
}
});
}
});
// Load all settings from the chrome sync storage
@@ -17,8 +17,6 @@
this.addedElements = [];
// To keep track of the succession order of the render events
this.eventId = 0;
// Will be reset as soon as a new devtools owl tab is opened. Allows to avoid sending messages to the wrong devtools tab later on
this.devtoolsId = 0;
// Set to keep track of the frame on which this script is loaded
this.frame = "top";
// Allows to launch a message each time an iframe html element is added to the page
@@ -38,7 +36,6 @@
source: "owl-devtools",
type: "NewIFrame",
data: addedNode.contentDocument.location.href,
origin: { id: self.devtoolsId },
});
}
}
@@ -47,10 +44,8 @@
});
iFrameObserver.observe(document.body, { subtree: true, childList: true });
this.appsPatched = false;
this.destroyPatched = false;
this.patchAppsSetMethods();
setTimeout(() => {
this.patchAppMethods();
}, 200);
this.recordEvents = false;
this.traceRenderings = false;
this.traceSubscriptions = false;
@@ -177,6 +172,26 @@
const self = this;
this.apps.add = function () {
originalAdd.call(this, ...arguments);
if (!self.destroyPatched) {
const newApp = arguments[0];
// It is not a given that apps have a root node at creation so we need to wait
if (newApp.root) {
self.patchDestroyMethod(newApp.root);
} else {
let root = null;
Object.defineProperty(newApp, "root", {
get() {
return root;
},
set(value) {
root = value;
if (!self.destroyPatched) {
self.patchDestroyMethod(root);
}
},
});
}
}
self.patchAppMethods();
window.top.postMessage({
source: "owl-devtools",
@@ -192,18 +207,40 @@
};
}
patchDestroyMethod(root) {
if (!this.destroyPatched) {
// Signals when a component is destroyed
const originalDestroy = root.constructor.prototype._destroy;
const self = this;
root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.destroyPatched = true;
}
}
// Modify methods of each app so that it triggers messages on each flush and component render
patchAppMethods() {
if (this.appsPatched) {
return;
}
let app;
for (const appItem of this.apps) {
if (appItem.root) {
app = appItem;
}
}
// We don't want to bother patching the apps methods if none have components inside
let app = this.apps.values().next().value;
if (!app) {
return;
}
@@ -223,7 +260,7 @@
source: "owl-devtools",
type: "Flush",
data: path,
origin: { id: self.devtoolsId, frame: self.frame },
origin: { frame: self.frame },
});
}
});
@@ -332,32 +369,10 @@
source: "owl-devtools",
type: "Event",
data: self.eventsBatch,
origin: { id: self.devtoolsId },
});
self.eventsBatch = [];
}
};
// Signals when a component is destroyed
const originalDestroy = app.root.constructor.prototype._destroy;
app.root.constructor.prototype._destroy = function () {
if (self.recordEvents) {
const path = self.getComponentPath(this);
const event = {
type: "destroy",
component: this.name,
key: this.parentKey,
path: path,
time: 0,
id: self.eventId++,
};
self.eventsBatch.push(event);
const before = performance.now();
originalDestroy.call(this, ...arguments);
event.time = performance.now() - before;
} else {
originalDestroy.call(this, ...arguments);
}
};
this.appsPatched = true;
}
@@ -609,7 +624,6 @@
source: "owl-devtools",
type: "SelectElement",
data: path,
origin: { id: this.devtoolsId },
});
}
};
@@ -637,7 +651,6 @@
window.top.postMessage({
source: "owl-devtools",
type: "StopSelector",
origin: { id: this.devtoolsId },
});
};
@@ -1748,6 +1761,6 @@
false
);
checkOwlStatus();
// Indicates whether the scripts loaded successfully or not (useful when loaded with eval in iframes only)
// Indicates whether the scripts loaded successfully or not (only useful when loaded with eval in iframes)
return window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;
})();