mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64de716b91 | |||
| 1b1597c49e | |||
| ef5e4a0637 | |||
| a9323c3fcd | |||
| ce61e90135 | |||
| 8893e026d3 | |||
| 0024f33fa1 |
+1
-1
@@ -28,4 +28,4 @@ release-notes.md
|
||||
.rpt2_cache
|
||||
|
||||
# useful in some cases
|
||||
/temp
|
||||
/temp
|
||||
|
||||
@@ -113,6 +113,7 @@ Are you new to Owl? This is the place to start!
|
||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||
- [Owl devtools extension](doc/tools/devtools.md)
|
||||
|
||||
## Installing Owl
|
||||
|
||||
@@ -121,8 +122,28 @@ Owl is available on `npm` and can be installed with the following command:
|
||||
```
|
||||
npm install @odoo/owl
|
||||
```
|
||||
|
||||
If you want to use a simple `<script>` tag, the last release can be downloaded here:
|
||||
|
||||
- [owl](https://github.com/odoo/owl/releases/latest)
|
||||
|
||||
## Installing Owl devtools
|
||||
|
||||
The Owl devtools browser extension is also available in the [release](https://github.com/odoo/owl/releases/latest):
|
||||
Unzip the owl-devtools.zip file and follow the instructions depending on your browser:
|
||||
|
||||
### Chrome
|
||||
|
||||
Go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the devtools-chrome folder and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
### Firefox
|
||||
Go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file in the devtools-firefox folder and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ not work with other iterables, such as `Set`. However, it is only a matter of
|
||||
using the `...` javascript operator. For example:
|
||||
|
||||
```xml
|
||||
<t t-foreach="...items" t-as="item">...</t>
|
||||
<t t-foreach="[...items]" t-as="item">...</t>
|
||||
```
|
||||
|
||||
The `...` operator will convert the `Set` (or any other iterables) into a list,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Owl Devtools Browser extension
|
||||
|
||||
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
|
||||
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
|
||||
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
|
||||
the components' lifecycle and be able to trace their origin.
|
||||
|
||||
## Install the extension
|
||||
|
||||
In the owl root folder:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
For chrome:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-chrome
|
||||
```
|
||||
|
||||
For firefox:
|
||||
|
||||
```bash
|
||||
npm run build:devtools-firefox
|
||||
```
|
||||
|
||||
You can also run:
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-chrome
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
npm run dev:devtools-firefox
|
||||
```
|
||||
|
||||
to avoid recompiling owl and gain time if it has already been done.
|
||||
|
||||
To run the extension:
|
||||
|
||||
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||
Select the output folder (dist/devtools) and that's it, your extension is active!
|
||||
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||
|
||||
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
|
||||
Here, you can use the reload button to refresh the extension.
|
||||
|
||||
Note that you may have to open another window or reload your tab to see the extension working.
|
||||
Also note that the extension will only be active on pages that have a sufficient version of owl.
|
||||
Generated
+490
-216
File diff suppressed because it is too large
Load Diff
+12
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.0.8",
|
||||
"version": "2.1.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -16,6 +16,11 @@
|
||||
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
||||
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
||||
"build": "npm run build:bundle",
|
||||
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
|
||||
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
|
||||
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
|
||||
"build:devtools-chrome": "NODE_ENV=production npm run dev:devtools-chrome",
|
||||
"build:devtools-firefox": "NODE_ENV=production npm run dev:devtools-firefox",
|
||||
"test": "jest",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -23,8 +28,8 @@
|
||||
"playground": "npm run build && npm run playground:serve",
|
||||
"preplayground:watch": "npm run build",
|
||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||
"publish": "npm run build && npm publish",
|
||||
"release": "node tools/release.js",
|
||||
@@ -56,7 +61,11 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-copy": "^3.3.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.2.2",
|
||||
"rollup-plugin-execute": "^1.1.1",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"source-map-support": "^0.5.10",
|
||||
|
||||
@@ -1135,7 +1135,7 @@ export class CodeGenerator {
|
||||
name = _name;
|
||||
switch (suffix) {
|
||||
case "bind":
|
||||
value = `${value}.bind(this)`;
|
||||
value = `(${value}).bind(this)`;
|
||||
break;
|
||||
case "alike":
|
||||
break;
|
||||
|
||||
@@ -63,7 +63,7 @@ export function onMounted(fn: () => void | any) {
|
||||
node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
|
||||
}
|
||||
|
||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||
export function onWillPatch(fn: () => any | void) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
|
||||
@@ -75,13 +75,13 @@ export function onPatched(fn: () => void | any) {
|
||||
node.patched.push(decorate(fn.bind(node.component), "onPatched"));
|
||||
}
|
||||
|
||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||
export function onWillUnmount(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
|
||||
}
|
||||
|
||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
||||
export function onWillDestroy(fn: () => void | any) {
|
||||
const node = getCurrent();
|
||||
const decorate = node.app.dev ? wrapError : (fn: any) => fn;
|
||||
node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This value is updated by the release script.
|
||||
export const version = "2.0.8";
|
||||
export const version = "2.1.0";
|
||||
|
||||
@@ -349,7 +349,7 @@ exports[`bound functions are considered 'alike' 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(ctx['state'].val);
|
||||
const b3 = comp1({fn: ctx['someFunction'].bind(this)}, key + \`__1\`, node, this, null);
|
||||
const b3 = comp1({fn: (ctx['someFunction']).bind(this)}, key + \`__1\`, node, this, null);
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
@@ -373,7 +373,7 @@ exports[`bound functions is not referentially equal after update 1`] = `
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"val\\"]);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({val: ctx['state'].val,fn: ctx['someFunction'].bind(this)}, key + \`__1\`, node, this, null);
|
||||
return comp1({val: ctx['state'].val,fn: (ctx['someFunction']).bind(this)}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -396,7 +396,7 @@ exports[`can bind function prop with bind suffix 1`] = `
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({doSomething: ctx['doSomething'].bind(this)}, key + \`__1\`, node, this, null);
|
||||
return comp1({doSomething: (ctx['doSomething']).bind(this)}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -411,3 +411,27 @@ exports[`can bind function prop with bind suffix 2`] = `
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`do not crash when binding anonymous function prop with bind suffix 1`] = `
|
||||
"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 = \\"\\") {
|
||||
const v1 = ctx['this'];
|
||||
return comp1({doSomething: ((_val)=>v1.doSomething(_val)).bind(this)}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`do not crash when binding anonymous function prop with bind suffix 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`child\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -90,7 +90,7 @@ exports[`slots can define and call slots with bound params 1`] = `
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp1({slots: markRaw({'abc': {__render: slot1.bind(this), __ctx: ctx1, getValue: ctx['getValue'].bind(this)}})}, key + \`__1\`, node, this, null);
|
||||
return comp1({slots: markRaw({'abc': {__render: slot1.bind(this), __ctx: ctx1, getValue: (ctx['getValue']).bind(this)}})}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -1516,7 +1516,7 @@ exports[`slots simple default slot with params and bound function 2`] = `
|
||||
let { callSlot } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return callSlot(ctx, node, key, 'default', false, {fn: ctx['getValue'].bind(this)});
|
||||
return callSlot(ctx, node, key, 'default', false, {fn: (ctx['getValue']).bind(this)});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
@@ -556,7 +556,7 @@ exports[`t-call t-call-context: ComponentNode is not looked up in the context 2`
|
||||
let ref1 = (el) => this.__owl__.setRef((\`myRef\`), el);
|
||||
const b2 = block2([ref1]);
|
||||
const ctx1 = capture(ctx);
|
||||
const b6 = comp1({prop: ctx['method'].bind(this),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__1\`, node, this, null);
|
||||
const b6 = comp1({prop: (ctx['method']).bind(this),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__1\`, node, this, null);
|
||||
return multi([b2, b6]);
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -200,6 +200,31 @@ test("can bind function prop with bind suffix", async () => {
|
||||
expect(fixture.innerHTML).toBe("child");
|
||||
});
|
||||
|
||||
test("do not crash when binding anonymous function prop with bind suffix", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`child`;
|
||||
setup() {
|
||||
this.props.doSomething(123);
|
||||
}
|
||||
}
|
||||
|
||||
let boundedThing: any = null;
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child doSomething.bind="(val) => this.doSomething(val)"/>`;
|
||||
static components = { Child };
|
||||
|
||||
doSomething(val: number) {
|
||||
expect(val).toBe(123);
|
||||
boundedThing = this;
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(boundedThing).toBe(parent);
|
||||
expect(fixture.innerHTML).toBe("child");
|
||||
});
|
||||
|
||||
test("bound functions is not referentially equal after update", async () => {
|
||||
let isEqual = false;
|
||||
class Child extends Component {
|
||||
|
||||
@@ -85,7 +85,7 @@ async function compileTemplates(files) {
|
||||
const fnName = slugify(name);
|
||||
try {
|
||||
const fn = compile(template).toString().replace('anonymous', fnName);
|
||||
templates.push(`owl.App.registerTemplate("${name}", ${fn});\n`);
|
||||
templates.push(`"${name}": ${fn},\n`);
|
||||
} catch (e) {
|
||||
errors.push({ name, fileName, e });
|
||||
}
|
||||
@@ -99,7 +99,7 @@ async function compileTemplates(files) {
|
||||
}
|
||||
console.log(`${templates.length} templates compiled`);
|
||||
|
||||
return templates.join("\n");
|
||||
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||
}
|
||||
|
||||
const templatesPath = process.argv[2];
|
||||
|
||||
Binary file not shown.
+7
File diff suppressed because one or more lines are too long
+4
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.0",
|
||||
"manifest_version": 3,
|
||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||
"icons": {
|
||||
"128": "assets/icon128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"128": "assets/icon_disabled128.png"
|
||||
},
|
||||
"default_title": "Owl devtools",
|
||||
"default_popup": "popup_app/popup.html"
|
||||
},
|
||||
"permissions": ["scripting", "storage"],
|
||||
"host_permissions": ["http://*/*", "https://*/*"],
|
||||
"content_security_policy": {
|
||||
"script-src": "self",
|
||||
"object-src": "self"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"devtools_page": "devtools_app/devtools.html",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.0",
|
||||
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||
"manifest_version": 2,
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "@owl-devtools",
|
||||
"strict_min_version": "74.0"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"128": "assets/icon128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"128": "assets/icon_disabled128.png"
|
||||
},
|
||||
"default_title": "Owl devtools",
|
||||
"default_popup": "popup_app/popup.html"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"popup_app/popup.html",
|
||||
"devtools_app/devtools.html",
|
||||
"devtools_app/components_panel.html"
|
||||
],
|
||||
"permissions": ["storage", "scripting", "<all_urls>"],
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"devtools_page": "devtools_app/devtools.html",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import terser from "rollup-plugin-terser";
|
||||
import copy from "rollup-plugin-copy";
|
||||
import execute from "rollup-plugin-execute";
|
||||
import del from "rollup-plugin-delete";
|
||||
import { string } from "rollup-plugin-string";
|
||||
|
||||
export default ({ "config-browser": browser }) => {
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const isChrome = browser === "chrome";
|
||||
const filesToMove = [
|
||||
{ src: "tools/devtools/assets/**/*", dest: "dist/devtools/assets/" },
|
||||
{
|
||||
src: "tools/devtools/src/devtools_app/devtools.html",
|
||||
dest: "dist/devtools/devtools_app",
|
||||
},
|
||||
{
|
||||
src: "tools/devtools/src/devtools_app/devtools_panel.html",
|
||||
dest: "dist/devtools/devtools_app",
|
||||
},
|
||||
{
|
||||
src: "tools/devtools/src/page_scripts/owl_devtools_global_hook.js",
|
||||
dest: "dist/devtools/page_scripts",
|
||||
},
|
||||
{ src: "tools/devtools/src/fonts/*", dest: "dist/devtools/fonts/" },
|
||||
{ src: "tools/devtools/src/popup_app/popup.html", dest: "dist/devtools/popup_app" },
|
||||
{ src: "tools/devtools/src/background.html", dest: "dist/devtools" },
|
||||
{ src: "tools/devtools/src/main.css", dest: "dist/devtools/popup_app" },
|
||||
{ src: "tools/devtools/src/main.css", dest: "dist/devtools/devtools_app" },
|
||||
{
|
||||
src: isChrome
|
||||
? "tools/devtools/manifest-chrome.json"
|
||||
: "tools/devtools/manifest-firefox.json",
|
||||
dest: "dist/devtools",
|
||||
rename: "manifest.json",
|
||||
},
|
||||
];
|
||||
|
||||
function generateRule(input, format = "esm") {
|
||||
return {
|
||||
input: input,
|
||||
output: [
|
||||
{
|
||||
format: format,
|
||||
file: input.replace("tools/devtools/src", "dist/devtools"),
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
string({
|
||||
include: "**/page_scripts/owl_devtools_global_hook.js",
|
||||
}),
|
||||
isProduction && terser.terser(),
|
||||
],
|
||||
};
|
||||
}
|
||||
const commands = new Array(2);
|
||||
commands[1] =
|
||||
"npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
|
||||
const firstRule = generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js");
|
||||
if (isProduction)
|
||||
commands[0] =
|
||||
"npm run build && cp dist/owl.iife.js tools/devtools/assets/owl.js && npm run build:compiler";
|
||||
else commands[0] = "cp dist/owl.iife.js tools/devtools/assets/owl.js";
|
||||
firstRule.plugins.push(execute(commands, true));
|
||||
const secondRule = generateRule("tools/devtools/src/content.js");
|
||||
secondRule.plugins.push(copy({ targets: filesToMove }));
|
||||
const lastRule = generateRule("tools/devtools/src/background.js");
|
||||
lastRule.plugins.push(del({ targets: "tools/devtools/assets/*.js" }));
|
||||
|
||||
return [
|
||||
firstRule,
|
||||
secondRule,
|
||||
generateRule("tools/devtools/src/devtools_app/devtools.js"),
|
||||
generateRule("tools/devtools/src/utils.js"),
|
||||
generateRule("tools/devtools/src/devtools_app/devtools_panel.js"),
|
||||
generateRule("tools/devtools/src/popup_app/popup.js"),
|
||||
lastRule,
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<script type="module" src="./background.js"></script>
|
||||
@@ -0,0 +1,103 @@
|
||||
import { IS_FIREFOX, getActiveTabURL } from "./utils";
|
||||
|
||||
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();
|
||||
|
||||
// Load the devtools global hook this way when running on manifest v3 chrome
|
||||
if (!IS_FIREFOX) {
|
||||
chrome.scripting.registerContentScripts([
|
||||
{
|
||||
id: "owlDevtoolsGLobalHook",
|
||||
matches: ["<all_urls>"],
|
||||
js: ["page_scripts/owl_devtools_global_hook.js"],
|
||||
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Update the owlStatus variable and the extension icon accordingly
|
||||
function setOwlStatus(status) {
|
||||
owlStatus = status;
|
||||
if (IS_FIREFOX) {
|
||||
browser.browserAction.setIcon({
|
||||
path: owlStatus === 2 ? "assets/icon128.png" : "assets/icon_disabled128.png",
|
||||
});
|
||||
} else {
|
||||
browserInstance.action.setIcon({
|
||||
path: owlStatus === 2 ? "assets/icon128.png" : "assets/icon_disabled128.png",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from the set when the tab is closed
|
||||
browserInstance.tabs.onRemoved.addListener((tabId) => {
|
||||
activePanels.delete(tabId);
|
||||
});
|
||||
|
||||
// Check owl status on tab update
|
||||
browserInstance.tabs.onUpdated.addListener((tab) => {
|
||||
browserInstance.tabs.get(tab, (tabData) => {
|
||||
if (tabData.status === "complete") {
|
||||
setOwlStatus(0);
|
||||
checkOwlStatus(tabData.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check owl status on tab activation
|
||||
browserInstance.tabs.onActivated.addListener((activeInfo) => {
|
||||
setOwlStatus(0);
|
||||
checkOwlStatus(activeInfo.tabId);
|
||||
});
|
||||
|
||||
// send a message to the window which will be intercepted by the page script and will result in a response of type owlStatus
|
||||
function checkOwlStatus(tabId) {
|
||||
browserInstance.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: () => {
|
||||
window.postMessage({
|
||||
source: "owl-devtools-background",
|
||||
type: "checkOwlStatus",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Messages handler for the background script
|
||||
browserInstance.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
|
||||
// Send back the owl status to the sender
|
||||
if (message.type === "getOwlStatus") {
|
||||
sendResponse({ result: owlStatus });
|
||||
return true;
|
||||
} else if (message.type === "owlStatus") {
|
||||
setOwlStatus(message.data);
|
||||
// Dummy message to test if the extension context is still valid
|
||||
} else if (message.type === "keepAlive") {
|
||||
return;
|
||||
// Relay the received message to the devtools app
|
||||
} else if (message.type === "newDevtoolsPanel") {
|
||||
const tab = await getActiveTabURL();
|
||||
activePanels.add(tab);
|
||||
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
|
||||
} else if (message.type === "getActiveTabURL") {
|
||||
getActiveTabURL().then((tab) => {
|
||||
sendResponse({ result: tab });
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
const tab = await getActiveTabURL();
|
||||
if (!activePanels.has(tab)) {
|
||||
return;
|
||||
}
|
||||
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 }
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import globalHook from "./page_scripts/owl_devtools_global_hook";
|
||||
import { IS_FIREFOX } from "./utils";
|
||||
|
||||
// Relays the owlDevtools__... type top window messages to the background script so that it can relay it to the devtools app
|
||||
window.addEventListener(
|
||||
"message",
|
||||
function (event) {
|
||||
if (event.data.type && event.data.source === "owl-devtools") {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
event.data.data
|
||||
? { type: event.data.type, data: event.data.data, origin: event.data.origin }
|
||||
: { type: event.data.type, origin: event.data.origin }
|
||||
);
|
||||
} catch (e) {
|
||||
// Extension context invalidated, cannot be handled here since the whole communication system
|
||||
// inside the extension is dead in this case.
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Load the devtools global hook this way when running on firefox
|
||||
if (IS_FIREFOX) {
|
||||
const script = document.createElement("script");
|
||||
script.textContent = globalHook;
|
||||
document.documentElement.appendChild(script);
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<script src="devtools.js"></script>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IS_FIREFOX } from "../utils";
|
||||
|
||||
let created = false;
|
||||
let browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Try to load the owl panel each 1000 ms in case it (re)appears on the page later on
|
||||
const checkInterval = setInterval(createPanelsIfOwl, 1000);
|
||||
|
||||
createPanelsIfOwl();
|
||||
|
||||
// Create the owl devtools panel if owl on the page is available at a sufficient version
|
||||
function createPanelsIfOwl() {
|
||||
if (created) {
|
||||
clearInterval(checkInterval);
|
||||
return;
|
||||
}
|
||||
browserInstance.devtools.inspectedWindow.eval(
|
||||
"window.__OWL_DEVTOOLS__?.Fiber !== undefined;",
|
||||
async (hasOwl) => {
|
||||
if (!hasOwl || created) {
|
||||
return;
|
||||
}
|
||||
clearInterval(checkInterval);
|
||||
created = true;
|
||||
browserInstance.devtools.panels.create(
|
||||
"Owl",
|
||||
"../../assets/icon128.png",
|
||||
IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html"
|
||||
);
|
||||
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel" });
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="../assets/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="../assets/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="../assets/owl.js"></script>
|
||||
<script src="devtools_panel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { DevtoolsWindow } from "./devtools_window/devtools_window";
|
||||
const { mount } = owl;
|
||||
import { templates } from "../../assets/templates.js";
|
||||
|
||||
for (const template in templates) {
|
||||
owl.App.registerTemplate(template, templates[template]);
|
||||
}
|
||||
mount(DevtoolsWindow, document.body, { dev: true });
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ComponentSearchBar extends Component {
|
||||
static template = "devtools.ComponentSearchBar";
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
updateSearch(event) {
|
||||
if (event.key !== "Enter") {
|
||||
this.store.updateSearch(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Go to the next search result repeatedly while enter is pressed
|
||||
onSearchKeyDown(event) {
|
||||
if (event.key === "Enter") {
|
||||
this.store.componentSearch.getNextSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ComponentSearchBar" owl="1">
|
||||
<div class="pointer-icon ms-1 px-2 py-1" t-on-click.stop='() => this.store.toggleSelector()'>
|
||||
<i title="Select an element in the page to inspect the corresponding component" class="fa fa-mouse-pointer" t-attf-style="color: {{store.componentSearch.activeSelector ? 'var(--active-icon)' : 'var(--text-color)'}};"></i>
|
||||
</div>
|
||||
<div class="icons-separator"/>
|
||||
<div class="d-flex align-items-center ms-2 flex-grow-1">
|
||||
<i class="fa fa-search search-icon" aria-hidden="true"></i>
|
||||
<input type="text" class="search-input ms-1 w-100 border-0 h-100" placeholder="Search" t-on-keyup.stop="updateSearch" t-on-keydown.stop="onSearchKeyDown" t-att-value="store.componentSearch.search"/>
|
||||
<t t-if="store.componentSearch.search.length > 0">
|
||||
<t t-esc="store.componentSearch.searchResults.length ? store.componentSearch.searchIndex + 1 : 0"/>|<t t-esc="store.componentSearch.searchResults.length"/>
|
||||
<i class="fa fa-angle-up lg-icon utility-icon ms-1 p-1" t-on-click.stop="() => this.store.componentSearch.getPrevSearch()"></i>
|
||||
<i class="fa fa-angle-down lg-icon utility-icon p-1" t-on-click.stop="() => this.store.componentSearch.getNextSearch()"></i>
|
||||
<i class="fa fa-times lg-icon utility-icon p-1 me-2" t-on-click.stop='() => this.store.updateSearch("")'></i>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,67 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, onWillUnmount, useExternalListener } = owl;
|
||||
import { TreeElement } from "./tree_element/tree_element";
|
||||
import { DetailsWindow } from "./details_window/details_window";
|
||||
import { ComponentSearchBar } from "./component_search_bar/component_search_bar";
|
||||
import { useStore } from "../../store/store";
|
||||
|
||||
export class ComponentsTab extends Component {
|
||||
static template = "devtools.ComponentsTab";
|
||||
|
||||
static components = { TreeElement, DetailsWindow, ComponentSearchBar };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.flushRendersTimeout = false;
|
||||
useExternalListener(document, "keydown", this.onKeyboardEvent);
|
||||
|
||||
onWillUnmount(() => {
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
window.removeEventListener("mouseup", this.onMouseUp);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply the right action depending on which arrow key is pressed (on keydown)
|
||||
onKeyboardEvent(event) {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
this.store.toggleOrSelectPrevElement(true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.store.toggleOrSelectPrevElement(false);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.store.toggleOrSelectNextElement(true);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.store.toggleOrSelectNextElement(false);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown = () => {
|
||||
// Add event listeners for mouse move and mouse up events
|
||||
// to allow the user to drag the split screen border
|
||||
window.addEventListener("mousemove", this.onMouseMove);
|
||||
window.addEventListener("mouseup", this.onMouseUp);
|
||||
};
|
||||
|
||||
// Adjust the position of the split between the left and right right window of the components tab
|
||||
onMouseMove = (event) => {
|
||||
this.store.splitPosition = Math.max(
|
||||
Math.min((event.clientX / window.innerWidth) * 100, 85),
|
||||
15
|
||||
);
|
||||
};
|
||||
|
||||
onMouseUp = () => {
|
||||
// Remove the event listeners when the user releases the mouse button
|
||||
window.removeEventListener("mousemove", this.onMouseMove);
|
||||
window.removeEventListener("mouseup", this.onMouseUp);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ComponentsTab" owl="1">
|
||||
<t t-if="store.apps.length === 0">
|
||||
<div class="status-message d-flex justify-content-center align-items-center">
|
||||
There are no apps currently running.
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="position-relative overflow-hidden d-flex flex-row h-100">
|
||||
<div class="split-screen-left d-flex flex-column" t-attf-style="width:{{store.splitPosition}}%;">
|
||||
<div class="panel-top d-flex align-items-center">
|
||||
<ComponentSearchBar/>
|
||||
</div>
|
||||
<div class="overflow-auto h-100 font-monospace">
|
||||
<div id="tree-wrapper">
|
||||
<t t-foreach="store.apps" t-as="app" t-key="app_index">
|
||||
<TreeElement component="app"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="split-screen-border user-select-none"
|
||||
t-on-mousedown="onMouseDown"
|
||||
/>
|
||||
<div class="split-screen-right d-flex flex-column font-monospace" t-attf-style="width:{{100 - store.splitPosition}}%;">
|
||||
<DetailsWindow t-if="store.activeComponent"/>
|
||||
<t t-else="">
|
||||
<div class="status-message d-flex justify-content-center align-items-center">
|
||||
There was an error while processing this component.
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
import { useStore } from "../../../store/store";
|
||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||
import { Subscriptions } from "./subscriptions/subscriptions";
|
||||
|
||||
export class DetailsWindow extends Component {
|
||||
static template = "devtools.DetailsWindow";
|
||||
static components = { ObjectTreeElement, Subscriptions };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent;
|
||||
// Open the context menu when the ids match
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
}
|
||||
|
||||
toggleCategory(ev, category) {
|
||||
this.store.activeComponent[category].toggled = !this.store.activeComponent[category].toggled;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.DetailsWindow" owl="1">
|
||||
<div class="panel-top d-flex align-items-center">
|
||||
<div class="ms-1 p-1 text-truncate" style="width: 100%;">
|
||||
<span t-if="store.activeComponent.path.length > 1"><</span>
|
||||
<b style="color: var(--component-color); cursor: pointer;" t-on-mouseover.stop="() => this.store.highlightComponent(this.store.activeComponent.path)" t-on-click.stop="() => this.store.focusSelectedComponent()" t-on-contextmenu.prevent="openMenu" t-esc="store.activeComponent.name"/>
|
||||
<span t-if="store.activeComponent.path.length > 1">></span>
|
||||
<span class="version" t-else="">owl=<t t-esc="store.activeComponent.version"/></span>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.path.length > 1">
|
||||
<i title="Inspect component in the Elements tab" class="fa fa-eye utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('DOM')"></i>
|
||||
<i title="Store component as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole()"></i>
|
||||
<i title="Inspect source code of component" class="fa fa-file-code-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('source')"></i>
|
||||
<i title="Trigger rerender of the component" class="fa fa-refresh utility-icon p-1" t-on-click.stop="() => this.store.refreshComponent()"></i>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i title="Store app as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole()"></i>
|
||||
<i title="Inspect source code of the app" class="fa fa-file-code-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('source')"></i>
|
||||
</t>
|
||||
</div>
|
||||
<div class="details-container">
|
||||
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
t-att-class="{'fa-caret-right': !store.activeComponent.env.toggled, 'fa-caret-down': store.activeComponent.env.toggled}"
|
||||
/><b>env</b>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.env.toggled" t-foreach="store.activeComponent.env.children" t-as="env" t-key="env_index">
|
||||
<ObjectTreeElement object="env"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.props.children.length > 0" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'props')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
t-att-class="{'fa-caret-right': !store.activeComponent.props.toggled, 'fa-caret-down': store.activeComponent.props.toggled}"
|
||||
/><b>props</b>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.props.toggled" t-foreach="store.activeComponent.props.children" t-as="prop" t-key="prop_index">
|
||||
<ObjectTreeElement object="prop"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.subscriptions.children.length > 0" id="subscriptions" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex">
|
||||
<div title="Shows all the targets that will trigger a re-render of the component when one of its associated keys is modified" class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'subscriptions')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
t-att-class="{'fa-caret-right': !store.activeComponent.subscriptions.toggled, 'fa-caret-down': store.activeComponent.subscriptions.toggled}"
|
||||
/><b>observed state</b>
|
||||
</div>
|
||||
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
|
||||
</div>
|
||||
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
t-att-class="{'fa-caret-right': !store.activeComponent.instance.toggled, 'fa-caret-down': store.activeComponent.instance.toggled}"
|
||||
/><b>instance</b>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.path.length > 1">
|
||||
<i title="Inspect compiled template" class="fa fa-hashtag utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('compiled template')"></i>
|
||||
<i title="Send raw template to console" class="fa fa-file-word-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('raw template')"></i>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.instance.toggled" t-foreach="store.activeComponent.instance.children" t-as="instance" t-key="instance_index">
|
||||
<ObjectTreeElement object="instance"/>
|
||||
</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">
|
||||
<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">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { useStore } from "../../../../store/store";
|
||||
|
||||
const { Component, useState, useEffect, useRef } = owl;
|
||||
|
||||
export class ObjectTreeElement extends Component {
|
||||
static template = "devtools.ObjectTreeElement";
|
||||
|
||||
static components = { ObjectTreeElement };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
editMode: false,
|
||||
menuTop: 0,
|
||||
menuLeft: 0,
|
||||
});
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
const inputRef = useRef("input");
|
||||
this.store = useStore();
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
useEffect(
|
||||
(editMode) => {
|
||||
// Focus on the input when it is created
|
||||
if (editMode) {
|
||||
inputRef.el.select();
|
||||
}
|
||||
},
|
||||
() => [this.state.editMode]
|
||||
);
|
||||
}
|
||||
|
||||
get pathAsString() {
|
||||
return JSON.stringify(this.props.object.path);
|
||||
}
|
||||
|
||||
get objectName() {
|
||||
return this.props.object.name;
|
||||
}
|
||||
|
||||
get objectLineClass() {
|
||||
// Prototype items will be dyed down to appear less important
|
||||
if (this.pathAsString.includes('{"type":"prototype",')) {
|
||||
return { attenuate: true };
|
||||
}
|
||||
// Same for subscription items which are not present in the keys while the keys will be bold
|
||||
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
|
||||
if (this.props.keys.includes(this.props.object.name.toString())) {
|
||||
return { "fw-bolder": true };
|
||||
}
|
||||
return { attenuate: true };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
get objectPadding() {
|
||||
return this.props.object.depth * 0.8 + 0.3;
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
}
|
||||
|
||||
setupEditMode() {
|
||||
if (!this.state.editMode) {
|
||||
if (!this.props.object.hasChildren) {
|
||||
this.state.editMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editObject(ev) {
|
||||
let value = ev.target.value;
|
||||
if (ev.keyCode === 13 && value !== "") {
|
||||
this.store.editObjectTreeElement(this.props.object.path, value, this.props.object.objectType);
|
||||
this.state.editMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||
t-att-class="objectLineClass"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
>
|
||||
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
|
||||
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="objectName"/>
|
||||
<t t-if="props.object.content.length > 0">: </t>
|
||||
<t t-if="props.object.contentType == 'getter'">
|
||||
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||
<t t-esc="props.object.content"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="object-content" t-att-class="objectLineClass" t-on-dblclick.stop="setupEditMode">
|
||||
<t t-if="state.editMode">
|
||||
<input t-attf-id="objectEditionInput/{{pathAsString}}" t-ref="input" type="text" placeholder="" t-att-value="props.object.content" t-on-keydown.stop="editObject"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="props.object.content"/>
|
||||
</t>
|
||||
</span>
|
||||
</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">
|
||||
<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"'>
|
||||
<li t-on-click="() => this.store.inspectFunctionSource(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Inspect function source code</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.object.toggled">
|
||||
<t t-foreach="props.object.children" t-as="child" t-key="child.name">
|
||||
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
|
||||
<ObjectTreeElement t-else="" object="child"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
const { Component } = owl;
|
||||
import { useStore } from "../../../../store/store";
|
||||
import { ObjectTreeElement } from "../object_tree_element/object_tree_element";
|
||||
|
||||
export class Subscriptions extends Component {
|
||||
static template = "devtools.Subscriptions";
|
||||
|
||||
static components = { ObjectTreeElement };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
// Used to display the keys in a compact way
|
||||
keysContent(index) {
|
||||
const keys = this.store.activeComponent.subscriptions.children[index].keys;
|
||||
let content = JSON.stringify(keys);
|
||||
const maxLength = 50;
|
||||
content = content.replace(/,/g, ", ");
|
||||
if (content.length > maxLength) {
|
||||
content = content.slice(0, content.lastIndexOf(",", maxLength - 5)) + ", ...]";
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
expandKeys(event, index) {
|
||||
this.store.activeComponent.subscriptions.children[index].keysExpanded =
|
||||
!this.store.activeComponent.subscriptions.children[index].keysExpanded;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Subscriptions" owl="1">
|
||||
<div id="subscriptionsPanel">
|
||||
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
||||
<div class="my-2">
|
||||
<div class="my-0 p-0 object-line" t-on-click.stop="(ev) => this.expandKeys(ev, subscription_index)">
|
||||
<span class="ps-1 text-nowrap">
|
||||
<i class="fa fa-caret-right ms-1" t-attf-style="cursor: pointer;{{subscription.keysExpanded ? 'transform: rotate(90deg);' : ''}}"></i>
|
||||
keys: <span class="key-name"><t t-esc="this.keysContent(subscription_index)"/></span>
|
||||
</span>
|
||||
</div>
|
||||
<div t-foreach="subscription.keys" t-as="key" t-key="key_index" class="my-0 p-0 object-line" t-attf-style="display: {{subscription.keysExpanded ? 'flex' : 'none'}}">
|
||||
<div style="transform: translateX(calc(1.1rem))" class="key-content">
|
||||
<i class="fa fa-caret-right mx-1" t-attf-style="cursor: pointer; visibility: hidden;"></i>
|
||||
<t t-esc="key"/>
|
||||
</div>
|
||||
</div>
|
||||
<ObjectTreeElement object="subscription.target" keys="subscription.keys"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
const { Component, onWillRender } = owl;
|
||||
|
||||
export class HighlightText extends Component {
|
||||
setup() {
|
||||
onWillRender(() => {
|
||||
const splitText = this.splitFuzzySearch(this.props.originalText, this.props.searchValue);
|
||||
this.splitText =
|
||||
this.props.searchValue.length && splitText.length > 1
|
||||
? splitText
|
||||
: [this.props.originalText];
|
||||
});
|
||||
}
|
||||
|
||||
// Logic to split the text to highlight it according to a fuzzy search pattern
|
||||
splitFuzzySearch(text, search) {
|
||||
if (!search || search.length === 0) {
|
||||
return [text];
|
||||
}
|
||||
let splits = [""];
|
||||
let searchIndex = 0;
|
||||
for (const letter of text) {
|
||||
if (
|
||||
!(searchIndex >= search.length) &&
|
||||
(letter === search[searchIndex] || letter === search[searchIndex].toUpperCase())
|
||||
) {
|
||||
if (splits.length % 2) {
|
||||
splits.push(letter);
|
||||
} else {
|
||||
splits[splits.length - 1] += letter;
|
||||
}
|
||||
searchIndex++;
|
||||
} else {
|
||||
if (splits.length % 2) {
|
||||
splits[splits.length - 1] += letter;
|
||||
} else {
|
||||
splits.push(letter);
|
||||
}
|
||||
}
|
||||
}
|
||||
return splits;
|
||||
}
|
||||
}
|
||||
HighlightText.template = "utils.HighlightText";
|
||||
HighlightText.props = {
|
||||
originalText: String,
|
||||
searchValue: String,
|
||||
};
|
||||
HighlightText.highlightClass = "highlight-search";
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="utils.HighlightText" owl="1">
|
||||
<t t-foreach="splitText" t-as="name" t-key="name_index">
|
||||
<b t-if="name_index % 2" t-esc="name" t-att-class="constructor.highlightClass"/>
|
||||
<t t-else="" t-esc="name"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { isElementInCenterViewport, minimizeKey } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
import { HighlightText } from "./highlight_text/highlight_text";
|
||||
|
||||
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
||||
|
||||
export class TreeElement extends Component {
|
||||
static template = "devtools.TreeElement";
|
||||
|
||||
static components = { TreeElement, HighlightText };
|
||||
|
||||
setup() {
|
||||
this.state = useState({
|
||||
searched: false,
|
||||
});
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
this.element = useRef("element");
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent;
|
||||
this.stringifiedPath = JSON.stringify(this.props.component.path);
|
||||
// Scroll to the selected element when it changes
|
||||
onMounted(() => {
|
||||
if (this.props.component.selected) {
|
||||
this.element.el.scrollIntoView({ block: "center", behavior: "auto" });
|
||||
}
|
||||
});
|
||||
useEffect(
|
||||
(selected) => {
|
||||
if (selected) {
|
||||
if (!isElementInCenterViewport(this.element.el)) {
|
||||
this.element.el.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
this.store.selectedElement = this.element.el;
|
||||
},
|
||||
() => [this.props.component.selected]
|
||||
);
|
||||
// Open the context menu when the ids match
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
// Effect to apply a short highlight effect to the component when it is rendered
|
||||
useEffect(
|
||||
() => {
|
||||
if (this.store.renderPaths.has(this.stringifiedPath)) {
|
||||
const treeElement = this.element.el;
|
||||
treeElement.classList.add("render-highlight");
|
||||
setTimeout(() => {
|
||||
treeElement.classList.add("highlight-fade");
|
||||
treeElement.classList.remove("render-highlight");
|
||||
setTimeout(() => {
|
||||
treeElement.classList.remove("highlight-fade");
|
||||
this.blockHighlight = false;
|
||||
}, 500);
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
() => [this.store.renderPaths.size]
|
||||
);
|
||||
// Used to know when the component is in the search bar results
|
||||
useEffect(
|
||||
(searchResults) => {
|
||||
if (searchResults.includes(this.props.component.path)) {
|
||||
this.state.searched = true;
|
||||
} else {
|
||||
this.state.searched = false;
|
||||
}
|
||||
},
|
||||
() => [this.store.componentSearch.searchResults]
|
||||
);
|
||||
}
|
||||
|
||||
get componentPadding() {
|
||||
return this.props.component.depth * 0.8;
|
||||
}
|
||||
|
||||
get minimizedKey() {
|
||||
return minimizeKey(this.props.component.key);
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
}
|
||||
|
||||
// Expand/fold the component node
|
||||
toggleDisplay() {
|
||||
this.props.component.toggled = !this.props.component.toggled;
|
||||
}
|
||||
|
||||
// Used to select the component node
|
||||
toggleComponent() {
|
||||
if (this.store.settings.toggleOnSelected) {
|
||||
this.toggleDisplay();
|
||||
}
|
||||
if (!this.props.component.selected) {
|
||||
this.store.selectComponent(this.props.component.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.TreeElement" owl="1">
|
||||
<div t-ref="element"
|
||||
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
|
||||
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
||||
t-on-click.stop="toggleComponent"
|
||||
>
|
||||
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
|
||||
t-on-click.stop="toggleDisplay"
|
||||
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
|
||||
/>
|
||||
<span t-if="props.component.depth"><</span>
|
||||
<span style="color: var(--component-color);">
|
||||
<HighlightText originalText="props.component.name" searchValue="state.searched ? store.componentSearch.search : ''"/>
|
||||
<t t-if="minimizedKey.length > 0">
|
||||
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||
<t t-esc="minimizedKey"/>
|
||||
</span>
|
||||
</t>
|
||||
</span>
|
||||
<span t-if="props.component.depth">></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">
|
||||
<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>
|
||||
<li t-on-click.stop="() => this.store.foldDirectChildren(props.component)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', props.component.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="props.component.path.length !== 1">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.component.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.component.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.component.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.component.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="props.component.toggled">
|
||||
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
|
||||
<TreeElement component="child_value"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,19 @@
|
||||
const { Component } = owl;
|
||||
import { ComponentsTab } from "./components_tab/components_tab";
|
||||
import { Tab } from "./tab/tab";
|
||||
import { ProfilerTab } from "./profiler_tab/profiler_tab";
|
||||
import { useStore } from "../store/store";
|
||||
|
||||
export class DevtoolsWindow extends Component {
|
||||
static props = [];
|
||||
static template = "devtools.DevtoolsWindow";
|
||||
static components = { ComponentsTab, Tab, ProfilerTab };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
selectFrame(ev) {
|
||||
const val = ev.target.value;
|
||||
this.store.selectFrame(val);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.DevtoolsWindow" owl="1">
|
||||
<div id="container" class="d-flex w-100 h-100 flex-column position-absolute overflow-hidden" t-on-mouseover.stop="() => this.store.removeHighlights()" t-on-mouseout.stop="() => this.store.removeHighlights()">
|
||||
<t t-if="!store.extensionContextStatus">
|
||||
<div class="status-message d-flex justify-content-center align-items-center">
|
||||
Extension context is invalid. Please close the devtools and reload the page.
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="store.owlStatus">
|
||||
<div class="panel-top d-flex align-items-center custom-navbar">
|
||||
<Tab tabName="'ComponentsTab'"/>
|
||||
<Tab tabName="'ProfilerTab'"/>
|
||||
<select t-if="store.frameUrls.length > 1" class="form-select form-select-sm custom-select navbar-select border-0" t-on-change="selectFrame">
|
||||
<t t-foreach="store.frameUrls" t-as="frame" t-key="frame_index">
|
||||
<option t-att-value="frame"><t t-esc="frame"/></option>
|
||||
</t>
|
||||
</select>
|
||||
<i class="ms-auto p-1 me-1 lg-icon fa pointer-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||
<i class="p-1 me-1 lg-icon fa fa-repeat pointer-icon" title="Refresh extension" t-on-click.stop="() => this.store.refreshExtension()"></i>
|
||||
</div>
|
||||
<ComponentsTab t-if="store.page === 'ComponentsTab'"/>
|
||||
<ProfilerTab t-if="store.page === 'ProfilerTab'"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="status-message d-flex justify-content-center align-items-center">
|
||||
Owl is not loaded on this page.
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,66 @@
|
||||
import { minimizeKey } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component, useEffect, useRef } = owl;
|
||||
|
||||
export class Event extends Component {
|
||||
static template = "devtools.Event";
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.componentContextMenu = useRef("componentContextmenu");
|
||||
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.componentContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
}
|
||||
|
||||
// Formatting for displaying the key of the component
|
||||
get minimizedKey() {
|
||||
return minimizeKey(this.props.event.key);
|
||||
}
|
||||
|
||||
// Same for the origin component
|
||||
get originMinimizedKey() {
|
||||
return minimizeKey(this.props.event.origin.key);
|
||||
}
|
||||
|
||||
get renderTime() {
|
||||
if (Number.isInteger(this.props.event.time)) {
|
||||
if (this.props.event.time === 0) {
|
||||
return "<1";
|
||||
} else {
|
||||
return this.props.event.time;
|
||||
}
|
||||
} else {
|
||||
if (this.props.event.time < 0.1) {
|
||||
return "<0.1";
|
||||
} else {
|
||||
return this.props.event.time.toFixed(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/fold the event
|
||||
toggleDisplay() {
|
||||
if (this.props.event.origin) {
|
||||
this.props.event.toggled = !this.props.event.toggled;
|
||||
}
|
||||
}
|
||||
|
||||
openComponentMenu(ev) {
|
||||
if (this.props.event.type === "destroy") {
|
||||
return;
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Event" owl="1">
|
||||
<div class="event-container">
|
||||
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
||||
<div class="ps-2 text-nowrap">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||
t-attf-style="visibility: {{props.event.origin ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="props.event.type"/>:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu="openComponentMenu"
|
||||
t-esc="props.event.component"
|
||||
/>
|
||||
<t t-if="minimizedKey.length > 0">
|
||||
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||
<t t-esc="minimizedKey"/>
|
||||
</span>
|
||||
</t>>
|
||||
<span>
|
||||
(<t t-esc="renderTime"/>ms)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="props.event.toggled">
|
||||
<div class="my-0 pt-1 object-line">
|
||||
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
|
||||
<span>
|
||||
origin:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
||||
t-esc="props.event.origin.component"
|
||||
/>
|
||||
<t t-if="originMinimizedKey.length > 0">
|
||||
<span style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||
<t t-esc="originMinimizedKey"/>
|
||||
</span>
|
||||
</t>>
|
||||
</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">
|
||||
<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">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,78 @@
|
||||
import { minimizeKey } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
|
||||
export class EventNode extends Component {
|
||||
static template = "devtools.EventNode";
|
||||
|
||||
static components = { EventNode };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.nodeContextMenu = useRef("nodeContextMenu");
|
||||
this.nodeContextMenuId = this.store.contextMenu.id++;
|
||||
this.componentContextMenu = useRef("componentContextmenu");
|
||||
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.nodeContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.nodeContextMenu.el);
|
||||
}
|
||||
if (menuId === this.componentContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
}
|
||||
|
||||
get eventPadding() {
|
||||
return this.props.event.depth * 0.8 + 0.3;
|
||||
}
|
||||
|
||||
openNodeMenu(ev) {
|
||||
if (this.props.event.children.length) {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.nodeContextMenuId;
|
||||
}
|
||||
}
|
||||
|
||||
openComponentMenu(ev) {
|
||||
if (this.props.event.type === "destroy") {
|
||||
return;
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/fold the event node
|
||||
toggleDisplay() {
|
||||
this.props.event.toggled = !this.props.event.toggled;
|
||||
}
|
||||
|
||||
// Formatting for displaying the key of the component
|
||||
get minimizedKey() {
|
||||
return minimizeKey(this.props.event.key);
|
||||
}
|
||||
|
||||
get renderTime() {
|
||||
if (Number.isInteger(this.props.event.time)) {
|
||||
if (this.props.event.time === 0) {
|
||||
return "<1";
|
||||
} else {
|
||||
return this.props.event.time;
|
||||
}
|
||||
} else {
|
||||
if (this.props.event.time < 1.0) {
|
||||
return "<1";
|
||||
} else {
|
||||
return this.props.event.time.toFixed(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.EventNode" owl="1">
|
||||
<div class="my-0 p-0 object-line"
|
||||
t-on-click.stop="toggleDisplay"
|
||||
t-on-contextmenu="openNodeMenu"
|
||||
>
|
||||
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
|
||||
/>
|
||||
<span>
|
||||
<t t-esc="props.event.type"/>:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu.stop="openComponentMenu"
|
||||
t-esc="props.event.component"/>
|
||||
<t t-if="minimizedKey.length > 0">
|
||||
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||
<t t-esc="minimizedKey"/>
|
||||
</span>
|
||||
</t>>
|
||||
<span>
|
||||
(<t t-esc="renderTime"/>ms)
|
||||
</span>
|
||||
</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">
|
||||
<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">
|
||||
<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">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.event.toggled">
|
||||
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
|
||||
<EventNode event="child"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class EventSearchBar extends Component {
|
||||
static template = "devtools.EventSearchBar";
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
// On keyup
|
||||
updateSearch(event) {
|
||||
if (!(event.keyCode === 13)) {
|
||||
const search = event.target.value;
|
||||
this.store.updateSearch(search);
|
||||
}
|
||||
}
|
||||
|
||||
clearSearch() {
|
||||
this.store.updateSearch("");
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.EventSearchBar" owl="1">
|
||||
<div class="search-bar-wrapper">
|
||||
<i class="fa fa-search search-icon" aria-hidden="true"></i>
|
||||
<input type="text" class="search-input" placeholder="Search" t-on-keyup.stop="updateSearch" t-att-value="store.eventSearch.search"/>
|
||||
<t t-if="store.eventSearch.search.length > 0">
|
||||
<i class="fa fa-times lg-icon utility-icon pe-2" t-on-click.stop="clearSearch"></i>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,29 @@
|
||||
const { Component } = owl;
|
||||
import { useStore } from "../../store/store";
|
||||
import { Event } from "./event/event";
|
||||
import { EventNode } from "./event_node/event_node";
|
||||
import { EventSearchBar } from "./event_search_bar/event_search_bar";
|
||||
|
||||
export class ProfilerTab extends Component {
|
||||
static template = "devtools.ProfilerTab";
|
||||
|
||||
static components = { Event, EventNode, EventSearchBar };
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
showHelp() {
|
||||
return this.store.events.length < 1 && !this.store.activeRecorder;
|
||||
}
|
||||
|
||||
selectDisplayMode(ev) {
|
||||
const val = ev.target.value;
|
||||
if (val === "Tree") {
|
||||
this.store.buildEventsTree();
|
||||
this.store.eventsTreeView = true;
|
||||
} else {
|
||||
this.store.eventsTreeView = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ProfilerTab" owl="1">
|
||||
<div class="position-relative overflow-hidden d-flex flex-column h-100">
|
||||
<div class="panel-top d-flex align-items-center">
|
||||
<i title="Start/Stop Recording" class="fa fa-circle pointer-icon ms-1 p-1" t-attf-style="color: {{store.activeRecorder ? 'var(--active-recorder)' : 'var(--text-color)'}};" t-on-click.stop="() => this.store.toggleRecording()" aria-hidden="true"></i>
|
||||
<i title="Clear events" class="fa fa-ban pointer-icon p-1 px-2" t-on-click.stop="() => this.store.clearEventsConsole()" aria-hidden="true"></i>
|
||||
<div class="icons-separator mx-1"/>
|
||||
<select class="form-select form-select-sm custom-select border-0" t-on-change="selectDisplayMode">
|
||||
<option t-att-selected="store.eventsTreeView" value="Tree">Tree view</option>
|
||||
<option t-att-selected="!store.eventsTreeView" value="List">Events log</option>
|
||||
</select>
|
||||
<i title="Collapse All" class="fa fa-list me-1" t-on-click="() => this.store.collapseAll()"></i>
|
||||
<div class="icons-separator"/>
|
||||
<label class="mx-2 form-check-label pointer-icon" title="Trace renderings in console">
|
||||
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceRenderings" t-on-input="() => this.store.toggleTracing()"/> Trace Renderings
|
||||
</label>
|
||||
<label class="mx-2 form-check-label pointer-icon" title="Trace subscriptions in console (warning: it is VERY verbose)">
|
||||
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceSubscriptions" t-on-input="() => this.store.toggleSubscriptionTracing()"/> Trace Subscriptions
|
||||
</label>
|
||||
<!-- <EventSearchBar/> -->
|
||||
</div>
|
||||
<div class="events-container h-100 font-monospace">
|
||||
<t t-if="showHelp()">
|
||||
<div class="status-message d-flex justify-content-center align-items-center">
|
||||
<div>
|
||||
Click on the <i class="fa fa-circle" style="font-size: 0.8em;"></i> button to start recording events.
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="store.eventsTreeView">
|
||||
<t t-foreach="store.eventsTree" t-as="event" t-key="event.id">
|
||||
<EventNode event="event"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="store.events" t-as="event" t-key="event_index">
|
||||
<Event event="event"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useStore } from "../../store/store";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class Tab extends Component {
|
||||
static props = ["tabName"];
|
||||
|
||||
static template = "devtools.Tab";
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this.props.tabName === this.store.page;
|
||||
}
|
||||
|
||||
get name() {
|
||||
switch (this.props.tabName) {
|
||||
case "ComponentsTab":
|
||||
return "Components";
|
||||
case "ProfilerTab":
|
||||
return "Profiler";
|
||||
}
|
||||
}
|
||||
|
||||
selectTab(ev) {
|
||||
this.store.switchTab(this.props.tabName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Tab" owl="1">
|
||||
<div t-on-click="selectTab" class="navbar-btn d-block" t-att-class="active
|
||||
? 'btn-selected'
|
||||
: ''">
|
||||
<t t-esc="name" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,983 @@
|
||||
const { reactive, useState, toRaw } = owl;
|
||||
import { fuzzySearch, IS_FIREFOX, getActiveTabURL } from "../../utils";
|
||||
import globalHook from "../../page_scripts/owl_devtools_global_hook";
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
// Main store which contains all states that needs to be maintained throughout all components in the devtools app
|
||||
export const store = reactive({
|
||||
devtoolsId: 0,
|
||||
settings: {
|
||||
expandByDefault: true,
|
||||
toggleOnSelected: false,
|
||||
darkmode: false,
|
||||
},
|
||||
contextMenu: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
id: 0,
|
||||
activeMenu: -1,
|
||||
// Opens the context menu corresponding with the given menu html element
|
||||
open(event, menu) {
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const menuHeight = menu.offsetHeight;
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x = window.innerWidth - menuWidth;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight;
|
||||
}
|
||||
this.left = x + "px";
|
||||
// Need 25px offset because of the main navbar from the browser devtools
|
||||
this.top = y - 25 + "px";
|
||||
},
|
||||
// Close the currently displayed context menu
|
||||
close() {
|
||||
this.activeMenu = -1;
|
||||
},
|
||||
},
|
||||
isFirefox: IS_FIREFOX,
|
||||
frameUrls: ["top"],
|
||||
activeFrame: "top",
|
||||
page: "ComponentsTab",
|
||||
events: [],
|
||||
eventsTreeView: true,
|
||||
eventsTree: [],
|
||||
activeRecorder: false,
|
||||
owlStatus: true,
|
||||
extensionContextStatus: true,
|
||||
splitPosition: window.innerWidth > window.innerHeight ? 45 : 60,
|
||||
apps: [],
|
||||
traceRenderings: false,
|
||||
traceSubscriptions: false,
|
||||
activeComponent: {
|
||||
path: ["0"],
|
||||
name: "App",
|
||||
subscriptions: { toggled: true, children: [] },
|
||||
props: { toggled: true, children: [] },
|
||||
env: { toggled: false, children: [] },
|
||||
instance: { toggled: true, children: [] },
|
||||
version: "1.0",
|
||||
},
|
||||
selectedElement: null,
|
||||
componentSearch: {
|
||||
search: "",
|
||||
searchResults: [],
|
||||
searchIndex: 0,
|
||||
activeSelector: false,
|
||||
getNextSearch() {
|
||||
if (this.searchIndex > -1 && this.searchIndex < this.searchResults.length - 1) {
|
||||
store.setSearchIndex(this.searchIndex + 1);
|
||||
} else if (this.searchIndex === this.searchResults.length - 1) {
|
||||
store.setSearchIndex(0);
|
||||
}
|
||||
},
|
||||
getPrevSearch() {
|
||||
if (this.searchIndex > 0) {
|
||||
store.setSearchIndex(this.searchIndex - 1);
|
||||
} else if (this.searchIndex === 0) {
|
||||
store.setSearchIndex(this.searchResults.length - 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
// eventSearch: {
|
||||
// search: "",
|
||||
// searchResults: [],
|
||||
// filters: [],
|
||||
// },
|
||||
renderPaths: new Set(),
|
||||
|
||||
// Used to navigate between the Components tab and the Events tab
|
||||
switchTab(componentName) {
|
||||
this.page = componentName;
|
||||
this.componentSearch.activeSelector = false;
|
||||
evalFunctionInWindow("disableHTMLSelector", [], this.activeFrame);
|
||||
},
|
||||
|
||||
// Load all data related to the components tree using the global hook loaded on the page
|
||||
// Use fromOld to specify if we want to keep most of the toggled/selected data of the old tree
|
||||
// when generating the new one
|
||||
async loadComponentsTree(fromOld) {
|
||||
if (IS_FIREFOX) {
|
||||
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
||||
}
|
||||
const apps = await evalFunctionInWindow(
|
||||
"getComponentsTree",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.apps = apps ? apps : [];
|
||||
if (!fromOld && this.settings.expandByDefault) {
|
||||
this.apps.forEach((tree) => expandNodes(tree));
|
||||
}
|
||||
const component = await evalFunctionInWindow(
|
||||
"getComponentDetails",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = component;
|
||||
},
|
||||
|
||||
// Select a component by retrieving its details from the page based on its path
|
||||
async selectComponent(path) {
|
||||
// Deselect all components
|
||||
this.apps.forEach((app) => {
|
||||
app.selected = false;
|
||||
app.highlighted = false;
|
||||
app.children.forEach((child) => {
|
||||
deselectComponent(child);
|
||||
});
|
||||
});
|
||||
let component;
|
||||
// element is the app here
|
||||
if (path.length === 1) {
|
||||
component = this.apps[path[0]];
|
||||
// the second element in the path is always the root of the app so no need to check
|
||||
} else {
|
||||
component = this.apps[path[0]].children[0];
|
||||
}
|
||||
for (let i = 2; i < path.length; i++) {
|
||||
component.toggled = true;
|
||||
const result = component.children.find((child) => child.key === path[i]);
|
||||
if (result) {
|
||||
component = result;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
component.selected = true;
|
||||
highlightChildren(component);
|
||||
const details = await evalFunctionInWindow(
|
||||
"getComponentDetails",
|
||||
[component.path],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = details;
|
||||
if (!this.activeComponent) {
|
||||
await this.loadComponentsTree(false);
|
||||
}
|
||||
if (this.page !== "ComponentsTab") {
|
||||
this.switchTab("ComponentsTab");
|
||||
}
|
||||
},
|
||||
|
||||
// Update the search state value with the current search string and trigger the search
|
||||
updateSearch(search) {
|
||||
this.componentSearch.search = search;
|
||||
this.componentSearch.searchResults = [];
|
||||
this.apps.forEach((app) => this.getComponentSearchResults(search, app));
|
||||
if (this.componentSearch.searchResults.length > 0) {
|
||||
this.componentSearch.searchIndex = 0;
|
||||
this.selectComponent(this.componentSearch.searchResults[0]);
|
||||
this.apps.forEach((app) => foldNodes(app));
|
||||
for (const result of this.componentSearch.searchResults) {
|
||||
this.toggleComponentParents(result);
|
||||
}
|
||||
} else {
|
||||
this.componentSearch.searchIndex = -1;
|
||||
}
|
||||
},
|
||||
|
||||
// Search for results in the components tree given the current search string (in a fuzzy way)
|
||||
getComponentSearchResults(search, node) {
|
||||
if (search.length < 1) {
|
||||
return;
|
||||
}
|
||||
if (fuzzySearch(node.name, search)) {
|
||||
this.componentSearch.searchResults.push(node.path);
|
||||
}
|
||||
if (node.children) {
|
||||
node.children.forEach((child) => this.getComponentSearchResults(search, child));
|
||||
}
|
||||
},
|
||||
|
||||
// Same but only record the component names for events
|
||||
// getComponentNameSearchResults(search, node) {
|
||||
// if (search.length < 1) return;
|
||||
// if (fuzzySearch(node.name, search)) {
|
||||
// if(!this.eventSearch.searchResults.includes(node.name))
|
||||
// this.eventSearch.searchResults.push(node.name);
|
||||
// }
|
||||
// if (node.children) {
|
||||
// node.children.forEach((child) => this.getComponentNameSearchResults(search, child));
|
||||
// }
|
||||
// },
|
||||
|
||||
// updateEventSearch(search){
|
||||
// this.eventSearch.search = search;
|
||||
// this.eventSearch.searchResults = [];
|
||||
// this.apps.forEach((app) => this.getComponentNameSearchResults(search, app));
|
||||
// [""]
|
||||
// },
|
||||
|
||||
// Toggle all parent components of the specified one to make sure it is visible in the tree
|
||||
toggleComponentParents(path) {
|
||||
let cp = path.slice(2);
|
||||
this.apps[path[0]].toggled = true;
|
||||
let component = this.apps[path[0]].children[0];
|
||||
for (const key of cp) {
|
||||
component.toggled = true;
|
||||
component = component.children.find((child) => child.key === key);
|
||||
}
|
||||
},
|
||||
|
||||
// Returns access to the specified component in the tree
|
||||
getComponentByPath(path) {
|
||||
let component;
|
||||
if (path.length < 2) {
|
||||
component = this.apps[path[0]];
|
||||
} else {
|
||||
component = this.apps[path[0]].children[0];
|
||||
}
|
||||
let cp = path.slice(2);
|
||||
for (const key of cp) {
|
||||
component = component.children.find((child) => child.key === key);
|
||||
}
|
||||
return component;
|
||||
},
|
||||
|
||||
// expand/fold the component and its children based on toggle
|
||||
toggleComponentAndChildren(component, toggle) {
|
||||
if (toggle) {
|
||||
expandNodes(component);
|
||||
} else {
|
||||
foldNodes(component);
|
||||
}
|
||||
},
|
||||
|
||||
foldDirectChildren(element) {
|
||||
for (const child of element.children) {
|
||||
child.toggled = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Action related to the left(toggle)/up(not toggle) arrow keys for navigation purpose
|
||||
// The resulting behaviour is the same as in the Elements tab of the chrome devtools
|
||||
toggleOrSelectPrevElement(toggle) {
|
||||
if (toggle) {
|
||||
const component = this.getComponentByPath(this.activeComponent.path);
|
||||
if (component.children.length > 0 && component.toggled) {
|
||||
component.toggled = false;
|
||||
} else if (this.activeComponent.path.length > 1) {
|
||||
this.selectComponent(this.activeComponent.path.slice(0, -1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const parentPath = [...this.activeComponent.path];
|
||||
const key = parentPath.pop();
|
||||
// If component is an app, find descendant with the highest successive children indexes of the app above
|
||||
// or do nothing if there is no app above
|
||||
if (parentPath.length === 0) {
|
||||
const parent = this.apps;
|
||||
const index = Number(key);
|
||||
if (index > 0) {
|
||||
let sibling = parent[index - 1];
|
||||
while (sibling.toggled && sibling.children.length) {
|
||||
sibling = sibling.children[sibling.children.length - 1];
|
||||
}
|
||||
this.selectComponent(sibling.path);
|
||||
}
|
||||
} else {
|
||||
const parent = this.getComponentByPath(parentPath);
|
||||
const index = parent.children.findIndex((child) => child.key === key);
|
||||
if (index > 0) {
|
||||
let sibling = parent.children[index - 1];
|
||||
while (sibling.toggled && sibling.children.length) {
|
||||
sibling = sibling.children[sibling.children.length - 1];
|
||||
}
|
||||
this.selectComponent(sibling.path);
|
||||
} else {
|
||||
this.selectComponent(parent.path);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Action related to the right(toggle)/down(not toggle) arrow keys for navigation purpose
|
||||
// The resulting behaviour is the same as in the Elements tab of the chrome devtools
|
||||
toggleOrSelectNextElement(toggle) {
|
||||
let component = this.getComponentByPath(this.activeComponent.path);
|
||||
if (toggle) {
|
||||
if (!component.children.length) {
|
||||
return;
|
||||
} else if (!component.toggled) {
|
||||
component.toggled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If component has children and is toggled, select its first child
|
||||
if (component.toggled && component.children.length) {
|
||||
this.selectComponent(component.children[0].path);
|
||||
} else {
|
||||
const parentPath = [...this.activeComponent.path];
|
||||
while (true) {
|
||||
const key = parentPath.pop();
|
||||
if (parentPath.length === 0) {
|
||||
const index = Number(key);
|
||||
if (index < this.apps.length - 1) {
|
||||
this.selectComponent(this.apps[index + 1].path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const parent = this.getComponentByPath(parentPath);
|
||||
const index = parent.children.findIndex((child) => child.key === key);
|
||||
if (index < parent.children.length - 1 && index > -1) {
|
||||
this.selectComponent(parent.children[index + 1].path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Set the search index to the provided one in order to select the current searched component
|
||||
setSearchIndex(index) {
|
||||
this.componentSearch.searchIndex = index;
|
||||
this.selectComponent(this.componentSearch.searchResults[index]);
|
||||
},
|
||||
|
||||
// Replace the (...) content of a getter with the value returned by the corresponding get method
|
||||
async loadGetterContent(obj) {
|
||||
const result = await evalFunctionInWindow("loadGetterContent", [obj], this.activeFrame);
|
||||
Object.keys(obj).forEach((key) => {
|
||||
obj[key] = result[key];
|
||||
});
|
||||
obj.children = [];
|
||||
},
|
||||
|
||||
// Expand the children of the input object property and load it from page if necessary
|
||||
async toggleObjectTreeElementsDisplay(obj) {
|
||||
if (!obj.hasChildren) {
|
||||
return;
|
||||
}
|
||||
// Since it is sometimes impossible (and always ineffective) to load all descendants of a property
|
||||
// when the component details are loaded, we need to populate the children of a property only when
|
||||
// it is first expanded. Do note that it has a limit to how deep we can expand (when reaching a circular dependancy)
|
||||
if (obj.hasChildren && obj.children.length === 0) {
|
||||
const children = await evalFunctionInWindow(
|
||||
"loadObjectChildren",
|
||||
[obj.path, obj.depth, obj.contentType, obj.objectType, this.activeComponent],
|
||||
this.activeFrame
|
||||
);
|
||||
obj.children = children;
|
||||
}
|
||||
obj.toggled = !obj.toggled;
|
||||
},
|
||||
|
||||
// Toggle the selector tool which is used to select a component based on the hovered Dom element
|
||||
toggleSelector() {
|
||||
this.componentSearch.activeSelector = !this.componentSearch.activeSelector;
|
||||
evalFunctionInWindow(
|
||||
this.componentSearch.activeSelector ? "enableHTMLSelector" : "disableHTMLSelector",
|
||||
[],
|
||||
this.activeFrame
|
||||
);
|
||||
},
|
||||
|
||||
// Update the value of the given object with the new provided one
|
||||
editObjectTreeElement(path, value, objectType) {
|
||||
evalFunctionInWindow("editObject", [path, value, objectType], this.activeFrame);
|
||||
},
|
||||
|
||||
// toggle the tracing mode which will record all root render events and send their trace in the console
|
||||
async toggleTracing() {
|
||||
this.traceRenderings = await evalFunctionInWindow(
|
||||
"toggleTracing",
|
||||
[!this.traceRenderings],
|
||||
this.activeFrame
|
||||
);
|
||||
},
|
||||
|
||||
// toggle subscriptions tracing mode which will record all new subscriptions
|
||||
async toggleSubscriptionTracing() {
|
||||
this.traceSubscriptions = await evalFunctionInWindow(
|
||||
"toggleSubscriptionTracing",
|
||||
[!this.traceSubscriptions],
|
||||
this.activeFrame
|
||||
);
|
||||
},
|
||||
|
||||
// Checks for all iframes in the page, register it and load the scripts inside if not already done
|
||||
async updateIFrameList() {
|
||||
const frames = await evalFunctionInWindow("getIFrameUrls");
|
||||
this.frameUrls = ["top"];
|
||||
if (this.activeFrame !== "top") {
|
||||
this.selectFrame("top");
|
||||
}
|
||||
for (const frame of frames) {
|
||||
const hasOwl = await evalInWindow("window.__OWL_DEVTOOLS__?.Fiber !== undefined;", frame);
|
||||
if (hasOwl) {
|
||||
const scriptsLoaded = await evalInWindow(
|
||||
"window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;",
|
||||
frame
|
||||
);
|
||||
if (!scriptsLoaded) {
|
||||
await loadScripts(frame);
|
||||
}
|
||||
evalInWindow(
|
||||
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
|
||||
store.devtoolsId
|
||||
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
|
||||
frame
|
||||
);
|
||||
if (!this.frameUrls.includes(frame)) {
|
||||
this.frameUrls = [...this.frameUrls, frame];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Remove all the highlight boxes created above the html elements in the page to show components
|
||||
removeHighlights() {
|
||||
if (this.owlStatus && !this.invalidContext) {
|
||||
evalFunctionInWindow("removeHighlights", [], this.activeFrame);
|
||||
}
|
||||
},
|
||||
|
||||
// Reset the context of all values in the devtools tab and load the one from the given frame
|
||||
selectFrame(frame) {
|
||||
this.removeHighlights();
|
||||
evalFunctionInWindow("toggleEventsRecording", [false, 0], this.activeFrame);
|
||||
evalFunctionInWindow("toggleTracing", [false], this.activeFrame);
|
||||
evalFunctionInWindow("toggleSubscriptionTracing", [false], this.activeFrame);
|
||||
this.events = [];
|
||||
this.eventsTree = [];
|
||||
this.activeFrame = frame;
|
||||
store.loadComponentsTree(false);
|
||||
evalFunctionInWindow(
|
||||
"toggleEventsRecording",
|
||||
[this.activeRecorder, this.events.length],
|
||||
this.activeFrame
|
||||
);
|
||||
evalFunctionInWindow("toggleTracing", [this.traceRenderings], this.activeFrame);
|
||||
evalFunctionInWindow("toggleSubscriptionTracing", [this.traceSubscriptions], this.activeFrame);
|
||||
},
|
||||
|
||||
// Constructs the tree that represents the currently recorded events to see them as a tree instead of a temporally accurate list
|
||||
buildEventsTree() {
|
||||
let tree = [];
|
||||
for (const event of this.events) {
|
||||
let eventNode = Object.assign({}, event);
|
||||
eventNode.children = [];
|
||||
eventNode.toggled = true;
|
||||
if (!eventNode.origin) {
|
||||
eventNode.depth = 0;
|
||||
tree.push(eventNode);
|
||||
} else {
|
||||
// This is litteraly an array.find but which starts searching from the end of the array
|
||||
for (let i = tree.length - 1; i >= 0; i--) {
|
||||
if (arraysEqual(eventNode.origin.path, tree[i].path)) {
|
||||
// path from the origin event (root render) to the direct parent of the current event
|
||||
const relativePath = eventNode.path.slice(
|
||||
tree[i].path.length,
|
||||
eventNode.path.length - 1
|
||||
);
|
||||
let parent = tree[i];
|
||||
// find the direct parent in the tree
|
||||
for (const key of relativePath) {
|
||||
parent = parent.children.find((child) => child.key === key);
|
||||
}
|
||||
eventNode.depth = parent.depth + 1;
|
||||
parent.children.push(eventNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.eventsTree = tree;
|
||||
},
|
||||
|
||||
// expand/fold the event tree node based on toggle
|
||||
toggleEventAndChildren(event, toggle) {
|
||||
if (toggle) {
|
||||
expandNodes(event);
|
||||
} else {
|
||||
foldNodes(event);
|
||||
}
|
||||
},
|
||||
|
||||
collapseAll() {
|
||||
for (let event of this.eventsTree) {
|
||||
event.toggled = false;
|
||||
}
|
||||
},
|
||||
|
||||
// Reset all the relevant data about the page currently stored
|
||||
resetData() {
|
||||
this.loadComponentsTree(false);
|
||||
this.events = [];
|
||||
this.eventsTree = [];
|
||||
this.activeRecorder = false;
|
||||
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
||||
evalFunctionInWindow("toggleTracing", [false]);
|
||||
},
|
||||
|
||||
// Triggers manually the rendering of the selected component
|
||||
refreshComponent(path = this.activeComponent.path) {
|
||||
evalFunctionInWindow("refreshComponent", [path], this.activeFrame);
|
||||
},
|
||||
|
||||
// Allows to log any object in the console, defaults to the active component (or app)
|
||||
logObjectInConsole(path) {
|
||||
if (!path) {
|
||||
if (this.activeComponent.path.length > 1) {
|
||||
path = [...this.activeComponent.path, { type: "item", value: "component" }];
|
||||
} else {
|
||||
path = this.activeComponent.path;
|
||||
}
|
||||
}
|
||||
evalFunctionInWindow("sendObjectToConsole", [path], this.activeFrame);
|
||||
},
|
||||
|
||||
// inspect the source code of the object given by its path
|
||||
async inspectFunctionSource(path) {
|
||||
await evalFunctionInWindow("inspectFunctionSource", [path], this.activeFrame);
|
||||
if (IS_FIREFOX) {
|
||||
await evalInWindow("inspect(window.$temp);", this.activeFrame);
|
||||
}
|
||||
},
|
||||
|
||||
// Inspect the given component's data based on the given type
|
||||
async inspectComponent(type, path = this.activeComponent.path) {
|
||||
switch (type) {
|
||||
case "DOM":
|
||||
await evalFunctionInWindow("inspectComponentDOM", [path], this.activeFrame);
|
||||
break;
|
||||
case "source":
|
||||
if (path.length > 1) {
|
||||
await evalFunctionInWindow(
|
||||
"inspectFunctionSource",
|
||||
[
|
||||
[
|
||||
...path,
|
||||
{ type: "item", value: "component" },
|
||||
{ type: "item", value: "constructor" },
|
||||
],
|
||||
],
|
||||
this.activeFrame
|
||||
);
|
||||
} else {
|
||||
await evalFunctionInWindow(
|
||||
"inspectFunctionSource",
|
||||
[[...path, { type: "item", value: "constructor" }]],
|
||||
this.activeFrame
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "compiled template":
|
||||
await evalFunctionInWindow("inspectComponentCompiledTemplate", [path], this.activeFrame);
|
||||
break;
|
||||
case "raw template":
|
||||
await evalFunctionInWindow("inspectComponentRawTemplate", [path], this.activeFrame);
|
||||
break;
|
||||
}
|
||||
if (IS_FIREFOX && type !== "raw template") {
|
||||
await evalInWindow("inspect(window.$temp);", this.activeFrame);
|
||||
}
|
||||
},
|
||||
|
||||
// Trigger the highlight on the component in the page
|
||||
highlightComponent(path) {
|
||||
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
|
||||
},
|
||||
|
||||
// Center the view around the currently selected component
|
||||
focusSelectedComponent() {
|
||||
this.selectedElement.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||
},
|
||||
|
||||
// Toggle the recording of events in the page
|
||||
async toggleRecording() {
|
||||
this.activeRecorder = await evalFunctionInWindow(
|
||||
"toggleEventsRecording",
|
||||
[!this.activeRecorder, this.events.length],
|
||||
this.activeFrame
|
||||
);
|
||||
},
|
||||
|
||||
// Reset all events data
|
||||
clearEventsConsole() {
|
||||
this.events = [];
|
||||
this.eventsTree = [];
|
||||
evalFunctionInWindow("resetEvents", [], this.activeFrame);
|
||||
},
|
||||
|
||||
// Refresh the whole extension
|
||||
async refreshExtension() {
|
||||
await loadScripts();
|
||||
this.resetData();
|
||||
},
|
||||
|
||||
// Toggle dark mode in the extension and store result in the storage
|
||||
toggleDarkMode() {
|
||||
this.settings.darkMode = !this.settings.darkMode;
|
||||
if (this.settings.darkMode) {
|
||||
document.querySelector("html").classList.add("dark-mode");
|
||||
} else {
|
||||
document.querySelector("html").classList.remove("dark-mode");
|
||||
}
|
||||
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
|
||||
},
|
||||
});
|
||||
|
||||
// Instantiate the store
|
||||
export function useStore() {
|
||||
return useState(store);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
store.devtoolsId = await getTabURL();
|
||||
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
|
||||
// We want to load the base components tree when the devtools tab is first opened
|
||||
store.loadComponentsTree(false);
|
||||
|
||||
// We also want to detect the different iframes at first loading of the devtools tab
|
||||
store.updateIFrameList();
|
||||
|
||||
// Global listeners to close the currently shown context menu when the user clicks or opens another
|
||||
document.addEventListener("click", () => store.contextMenu.close(), { capture: true });
|
||||
document.addEventListener("contextmenu", () => store.contextMenu.close(), { capture: true });
|
||||
|
||||
// Make sure the events recorder is at its initial state in every frame
|
||||
for (const frame of store.frameUrls) {
|
||||
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
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;
|
||||
}
|
||||
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||
if (store.owlStatus) {
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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) {
|
||||
// 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;
|
||||
}
|
||||
store.settings.darkMode = darkMode;
|
||||
if (darkMode) {
|
||||
document.querySelector("html").classList.add("dark-mode");
|
||||
} else {
|
||||
document.querySelector("html").classList.remove("dark-mode");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle and store a batch of events coming from the page
|
||||
function loadEvents(events) {
|
||||
if (!Array.isArray(events)) {
|
||||
return;
|
||||
}
|
||||
for (const event of events) {
|
||||
// Check if the event data has the right shape for security purpose
|
||||
if (
|
||||
!isObjectWithShape(event, {
|
||||
type: "string",
|
||||
component: "string",
|
||||
key: "string",
|
||||
path: "object",
|
||||
time: "number",
|
||||
id: "number",
|
||||
}) ||
|
||||
!(Array.isArray(event.path) && event.path.every((val) => typeof val === "string"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.origin = null;
|
||||
event.toggled = false;
|
||||
// Logic to retrace the origin of the event if it is not a root render event
|
||||
if (!event.type.includes("render")) {
|
||||
for (let i = store.events.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
!store.events[i].origin &&
|
||||
event.path.join("/").includes(store.events[i].path.join("/"))
|
||||
) {
|
||||
event.origin = toRaw(store.events[i]);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
store.events[i].origin &&
|
||||
event.path.join("/").includes(store.events[i].origin.path.join("/"))
|
||||
) {
|
||||
event.origin = toRaw(store.events[i].origin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add the event to the events tree immediatly when the events view is in tree mode
|
||||
if (store.eventsTreeView) {
|
||||
let eventNode = Object.assign({}, event);
|
||||
eventNode.children = [];
|
||||
eventNode.toggled = true;
|
||||
if (!eventNode.origin) {
|
||||
eventNode.depth = 0;
|
||||
store.eventsTree.push(eventNode);
|
||||
} else {
|
||||
// Similar to when we're constructing the whole tree
|
||||
for (let i = store.eventsTree.length - 1; i >= 0; i--) {
|
||||
if (eventNode.origin.path.join("/") === store.eventsTree[i].path.join("/")) {
|
||||
const relativePath = eventNode.path.slice(
|
||||
store.eventsTree[i].path.length,
|
||||
eventNode.path.length - 1
|
||||
);
|
||||
let parent = store.eventsTree[i];
|
||||
for (const key of relativePath) {
|
||||
parent = parent.children.find((child) => child.key === key);
|
||||
}
|
||||
eventNode.depth = parent.depth + 1;
|
||||
parent.children.push(eventNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Make sure we add the event while keeping the whole list ordered by id
|
||||
addEventSorted(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect component and remove highlight on all children
|
||||
function deselectComponent(component) {
|
||||
component.selected = false;
|
||||
component.highlighted = false;
|
||||
for (const child of component.children) {
|
||||
deselectComponent(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Used to check if the given object has the right shape
|
||||
function isObjectWithShape(obj, shape) {
|
||||
if (typeof obj !== "object" || Array.isArray(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.keys(shape).every(
|
||||
(key) => obj.hasOwnProperty(key) && typeof obj[key] === shape[key]
|
||||
);
|
||||
}
|
||||
|
||||
// A binary search algorith to efficiently add an event in the events array while keeping it sorted
|
||||
function addEventSorted(item) {
|
||||
let low = 0;
|
||||
let high = store.events.length;
|
||||
while (low < high) {
|
||||
let mid = Math.floor((low + high) / 2);
|
||||
if (store.events[mid].id < item.id) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
store.events.splice(low, 0, item);
|
||||
}
|
||||
|
||||
// Apply highlight recursively to all children of a selected component
|
||||
function highlightChildren(component) {
|
||||
component.children.forEach((child) => {
|
||||
child.highlighted = true;
|
||||
highlightChildren(child);
|
||||
});
|
||||
}
|
||||
|
||||
// Expand the node given in entry and all of its children
|
||||
function expandNodes(node) {
|
||||
node.toggled = true;
|
||||
for (const child of node.children) {
|
||||
expandNodes(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Fold the node given in entry and all of its children
|
||||
function foldNodes(node) {
|
||||
node.toggled = false;
|
||||
for (const child of node.children) {
|
||||
foldNodes(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Load the scripts in the specified frame
|
||||
async function loadScripts(frameUrl) {
|
||||
return await evalInWindow(globalHook, frameUrl);
|
||||
}
|
||||
|
||||
// Shallow array equality
|
||||
function arraysEqual(arr1, arr2) {
|
||||
if (arr1.length !== arr2.length) {
|
||||
// Check if the arrays are of the same length
|
||||
return false;
|
||||
}
|
||||
return arr1.every((val, i) => val === arr2[i]); // Compare each element of the arrays
|
||||
}
|
||||
|
||||
async function getTabURL() {
|
||||
if (IS_FIREFOX) {
|
||||
// This happens in firefox when the method is called inside devtools so we ask the background to execute it instead
|
||||
browserInstance.runtime.sendMessage({ type: "getActiveTabURL" }).then((response) => {
|
||||
return response.result;
|
||||
});
|
||||
} else {
|
||||
return await getActiveTabURL();
|
||||
}
|
||||
}
|
||||
|
||||
// General method for executing functions from the loaded scripts in the right frame of the page
|
||||
// using the __OWL__DEVTOOLS_GLOBAL_HOOK__. Take the function's args as an array.
|
||||
async function evalFunctionInWindow(fn, args = [], frameUrl = "top") {
|
||||
const stringifiedArgs = [...args].map((arg) => {
|
||||
arg = JSON.stringify(arg);
|
||||
return arg;
|
||||
});
|
||||
const argsString = "(" + stringifiedArgs.join(", ") + ");";
|
||||
let script = `__OWL__DEVTOOLS_GLOBAL_HOOK__.${fn}${argsString}`;
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (frameUrl !== "top") {
|
||||
browserInstance.devtools.inspectedWindow.eval(
|
||||
script,
|
||||
{ frameURL: frameUrl },
|
||||
(result, isException) => {
|
||||
if (!isException) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(script);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
browserInstance.devtools.inspectedWindow.eval(script, (result, isException) => {
|
||||
if (!isException) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(script);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// General method for executing code in the window using chrome.devtools.inspectedWindow.eval.
|
||||
async function evalInWindow(code, frameUrl = "top") {
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (frameUrl !== "top") {
|
||||
browserInstance.devtools.inspectedWindow.eval(
|
||||
code,
|
||||
{ frameURL: frameUrl },
|
||||
(result, isException) => {
|
||||
if (!isException) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(code);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
browserInstance.devtools.inspectedWindow.eval(code, (result, isException) => {
|
||||
if (!isException) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,291 @@
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
src: url('../assets/FiraMono-Medium.ttf') format('truetype');
|
||||
}
|
||||
|
||||
:root {
|
||||
--border-color: lightgray;
|
||||
--navbar-selected: #5f9cf0;
|
||||
--component-hover: #d4e3f2;
|
||||
--bs-font-monospace: "Fira Mono";
|
||||
--text-color: #494949;
|
||||
--object-color: rgb(119, 34, 255);
|
||||
--prototype-color: rgb(179 133 255);
|
||||
--text-proto-color: #818181;
|
||||
--highlight-color: yellow;
|
||||
--component-color: rgb(48, 74, 219);
|
||||
--key-name: rgb(192 166 118);
|
||||
--key-content: rgb(141 112 188 / 59%);
|
||||
--subscription-key: rgb(34, 49, 255);
|
||||
--component-highlighted: #f0f8fb;
|
||||
--component-selected: #d4e3f2;
|
||||
--active-icon: rgb(41, 134, 255);
|
||||
--active-recorder: rgb(255, 31, 31);
|
||||
--navbar-bg: #f1f3f4;
|
||||
--background-color: white;
|
||||
--text-selected: white;
|
||||
--menu-highlight-bg: rgb(201, 201, 201);
|
||||
--version-bg: teal;
|
||||
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23444444' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
--border-color: #838383;
|
||||
--navbar-selected: #33a3ff;
|
||||
--component-hover: #597179;
|
||||
--text-color: #c9c9c9;
|
||||
--object-color: rgb(188 147 255);
|
||||
--prototype-color: rgb(134 105 180);
|
||||
--text-proto-color: #818181;
|
||||
--highlight-color: yellow;
|
||||
--component-color: rgb(135 153 255);
|
||||
--key-name: rgb(171 212 253 / 51%);
|
||||
--key-content: rgb(227 213 252 / 60%);
|
||||
--subscription-key: rgb(154 161 255);
|
||||
--component-highlighted: #525d61;
|
||||
--component-selected: #287e99;
|
||||
--active-icon: rgb(93 163 255);
|
||||
--active-recorder: rgb(255, 31, 31);
|
||||
--navbar-bg: #484a4b;
|
||||
--background-color: rgb(45 45 45);
|
||||
--text-selected: #c9c9c9;
|
||||
--menu-highlight-bg: rgb(65, 65, 65);
|
||||
--version-bg: #805900ad;
|
||||
color-scheme: dark;
|
||||
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23c9c9c9' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.caret {
|
||||
width: 0.9rem;
|
||||
}
|
||||
|
||||
#container {
|
||||
font-size: 0.75em;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
min-height: 70vh;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
font-size: 12px;
|
||||
padding: 0 !important;
|
||||
background: var(--navbar-bg);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
.navbar-btn {
|
||||
height: 23px;
|
||||
padding: 0.5rem;
|
||||
padding-top: 0.2em;
|
||||
color: var(--text-color);
|
||||
border-right: solid 1px lightgrey;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-btn:hover {
|
||||
color: var(--navbar-selected);
|
||||
}
|
||||
|
||||
.btn-selected {
|
||||
background-color: var(--navbar-selected);
|
||||
color: var(--text-selected);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-selected:hover{
|
||||
color: var(--text-selected);
|
||||
}
|
||||
|
||||
#tree-wrapper {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tree-component:hover {
|
||||
background-color: var(--component-hover);
|
||||
}
|
||||
|
||||
.object-line.attenuate {
|
||||
color: var(--text-proto-color);
|
||||
}
|
||||
|
||||
.object-content {
|
||||
color: var(--object-color);
|
||||
}
|
||||
|
||||
.object-content.attenuate {
|
||||
color: var(--prototype-color);
|
||||
}
|
||||
|
||||
.event-container {
|
||||
border-bottom: 1px solid rgb(240, 238, 238);
|
||||
padding-top: 2px!important;
|
||||
padding-bottom: 2px!important;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.getter-content:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.render-highlight {
|
||||
background-color: var(--highlight-color) !important;
|
||||
}
|
||||
|
||||
.highlight-fade {
|
||||
transition: background-color 0.5s ease-out;
|
||||
}
|
||||
|
||||
.highlight-search {
|
||||
background-color: var(--highlight-color);
|
||||
}
|
||||
|
||||
.key-name {
|
||||
color: var(--key-content);
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
max-width: max-content;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-color) !important;
|
||||
background-color: var(--background-color);
|
||||
background-image: var(--select-icon);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.navbar-select {
|
||||
font-size: .7rem !important;
|
||||
padding-top: 0.35rem !important;
|
||||
border-right: solid 1px var(--border-color) !important;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.details-panel:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.component-highlighted {
|
||||
background-color: var(--component-highlighted);
|
||||
}
|
||||
|
||||
.component-selected {
|
||||
background-color: var(--component-selected);
|
||||
}
|
||||
|
||||
.split-screen-border {
|
||||
width: 10px;
|
||||
cursor: col-resize;
|
||||
margin: 0px -5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blank-space {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.key-content {
|
||||
color: var(--subscription-key);
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-left: 5px;
|
||||
background-color: var(--version-bg);
|
||||
color: white;
|
||||
padding: 2px 3px;
|
||||
border-radius: 4px;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.panel-top {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.details-container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.events-container {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.split-screen-right {
|
||||
margin: 0;
|
||||
border-left: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.utility-icon {
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.lg-icon {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.fa-caret-right {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.icons-separator {
|
||||
width: 1px;
|
||||
height: 17px;
|
||||
background-color: var(--border-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pointer-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-menu {
|
||||
position: absolute;
|
||||
color: var(--text-color);
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid gray;
|
||||
z-index: 1;
|
||||
box-shadow: 1px 2px 5px #888;
|
||||
font-family: var(--bs-font-sans-serif);
|
||||
}
|
||||
|
||||
.custom-menu ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.custom-menu-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-menu-item:hover {
|
||||
background-color: var(--menu-highlight-bg);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="../assets/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="main.css" />
|
||||
</head>
|
||||
<body>
|
||||
<script src="../assets/owl.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { templates } from "../../assets/templates.js";
|
||||
import { getOwlStatus } from "../utils";
|
||||
const { Component, useState, onWillStart, mount, App } = owl;
|
||||
|
||||
class PopUpApp extends Component {
|
||||
static template = "popup.PopUpApp";
|
||||
|
||||
setup() {
|
||||
this.state = useState({ status: 0 });
|
||||
onWillStart(async () => {
|
||||
try {
|
||||
this.state.status = await getOwlStatus();
|
||||
} catch (e) {
|
||||
this.state.status = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const template in templates) {
|
||||
App.registerTemplate(template, templates[template]);
|
||||
}
|
||||
mount(PopUpApp, document.body, { dev: true });
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="popup.PopUpApp">
|
||||
<div class="container m-0 p-4 text-white bg-dark" style="width: 370px">
|
||||
<p t-if="state.status === 1">
|
||||
Owl is detected on this page but the version seems to be outdated.
|
||||
Please upgrade to a newer version in order to use the devtools.
|
||||
</p>
|
||||
<p t-elif="state.status === 2">
|
||||
Owl is detected on this page.
|
||||
Open DevTools and look for the Owl panel.
|
||||
</p>
|
||||
<p t-else="">
|
||||
Owl is not detected on this page.
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,58 @@
|
||||
export const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1;
|
||||
|
||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||
|
||||
export async function getOwlStatus() {
|
||||
const response = await browserInstance.runtime.sendMessage({ type: "getOwlStatus" });
|
||||
return response.result;
|
||||
}
|
||||
|
||||
export async function getActiveTabURL() {
|
||||
const [tab] = await browserInstance.tabs.query({ active: true, lastFocusedWindow: true });
|
||||
return tab.id;
|
||||
}
|
||||
|
||||
// inspired from https://www.tutorialspoint.com/fuzzy-search-algorithm-in-javascript
|
||||
// Check if the query matches with the base in a fuzzy search way
|
||||
export function fuzzySearch(baseString, queryString) {
|
||||
const base = baseString.toLowerCase();
|
||||
const query = queryString.toLowerCase();
|
||||
let queryIndex = 0;
|
||||
let baseIndex = -1;
|
||||
let character;
|
||||
// Loop through each character in the query string
|
||||
while ((character = query[queryIndex++])) {
|
||||
// Find the index of the character in the base string, starting from the previous index plus 1
|
||||
baseIndex = base.indexOf(character, baseIndex + 1);
|
||||
// If the character is not found, return false
|
||||
if (baseIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// All characters in the query string were found in the base string, so return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the given element is vertically centered in the user view (between 25 and 75% of the height)
|
||||
export function isElementInCenterViewport(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.bottom >= 0.25 * (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.bottom <= 0.75 * (window.innerHeight || document.documentElement.clientHeight)
|
||||
);
|
||||
}
|
||||
|
||||
// Formatting for displaying the key of the component
|
||||
export function minimizeKey(key) {
|
||||
if (key.startsWith("__")) {
|
||||
const split = key.split("__");
|
||||
if (split.length > 2) {
|
||||
key = key.substring(4 + split[1].length, key.length);
|
||||
} else {
|
||||
key = "";
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
+16
-2
@@ -98,13 +98,27 @@ async function startRelease() {
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
log(`Step 5/${STEPS}: building owl...`);
|
||||
log(`Step 5/${STEPS}: building owl and devtools...`);
|
||||
await execCommand("rm -rf dist/");
|
||||
const buildResult = await execCommand("npm run build");
|
||||
if (buildResult !== 0) {
|
||||
logError("Build failed. Aborting.");
|
||||
return;
|
||||
}
|
||||
const chromeResult = await execCommand("npm run build:devtools-chrome");
|
||||
if (chromeResult !== 0) {
|
||||
logError("Build devtools chrome failed. Aborting.");
|
||||
return;
|
||||
}
|
||||
await execCommand("mv dist/devtools dist/devtools-chrome");
|
||||
const firefoxResult = await execCommand("npm run build:devtools-firefox");
|
||||
if (firefoxResult !== 0) {
|
||||
logError("Build devtools firefox failed. Aborting.");
|
||||
return;
|
||||
}
|
||||
await execCommand("mv dist/devtools dist/devtools-firefox");
|
||||
await execCommand("cd dist && zip -r owl-devtools.zip devtools-chrome devtools-firefox && cd ..");
|
||||
await execCommand("rm -r dist/devtools-chrome dist/devtools-firefox && rm dist/compiler.js");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
log(`Step 6/${STEPS}: pushing on github...`);
|
||||
@@ -117,7 +131,7 @@ async function startRelease() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
log(`Step 7/${STEPS}: Creating the release...`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js ${draft} -F ${file}`);
|
||||
const relaseResult = await execCommand(`gh release create v${next} dist/*.js dist/*.zip ${draft} -F ${file}`);
|
||||
if (relaseResult !== 0) {
|
||||
logError("github release failed. Aborting.");
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user