mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[ADD] tools: create owl-devtools extension
This commit adds the code of the owl devtools extension. This extension can be added on chrome or on firefox for developpers to be able to inspect the contents of all owl applications that are present on any web page, see all the components, interract with them and their content to some extend and perform a bit of profiling with their renders.
This commit is contained in:
committed by
Géry Debongnie
parent
a9323c3fcd
commit
ef5e4a0637
+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>
|
||||
Reference in New Issue
Block a user