Bug 1831149 - [devtools] Fold ManagedTree into SourcesTree. r=bomsy

This help avoid this unecessary indirection which is ManagedTree.
And hopefully this may help simplify "expanded" and "focused" data
which is currently stored in redux but rather looks like something
that belongs to this React component's state.

Differential Revision: https://phabricator.services.mozilla.com/D177066
This commit is contained in:
Alexandre Poirot 2023-05-22 16:09:34 +00:00
parent 1b54639cdc
commit 3093557b91
10 changed files with 107 additions and 793 deletions

View File

@ -30,7 +30,7 @@
grid-area: custom-root;
}
.sources-list :is(.managed-tree, .no-sources-message) {
.sources-list :is(.tree, .no-sources-message) {
grid-area: sources-tree-or-empty-message;
}
@ -68,27 +68,60 @@
/* Sources tree */
/*****************/
.sources-list .managed-tree {
display: flex;
.sources-list .tree {
flex-grow: 1;
overflow: auto;
}
.sources-list .managed-tree .tree {
padding: 4px 0;
user-select: none;
white-space: nowrap;
overflow: auto;
min-width: 100%;
display: grid;
grid-template-columns: 1fr;
align-content: start;
line-height: 1.4em;
}
.sources-list .managed-tree .tree .node {
.sources-list .tree .node {
display: flex;
align-items: center;
width: 100%;
padding: 3px 8px 3px 6px;
padding-block: 8px;
padding-inline: 6px 8px;
}
.sources-list .managed-tree .tree .tree-node:not(.focused):hover {
.sources-list .tree .tree-node:not(.focused):hover {
background: var(--theme-toolbar-background-hover);
}
.sources-list .tree button {
display: block;
}
.sources-list .tree .node {
padding: 2px 3px;
position: relative;
}
.sources-list .tree .node.focused {
color: var(--theme-selection-color);
background-color: var(--theme-selection-background);
}
html:not([dir="rtl"]) .sources-list .tree .node > div {
margin-left: 10px;
}
html[dir="rtl"] .sources-list .tree .node > div {
margin-right: 10px;
}
.sources-list .tree-node button {
position: fixed;
}
.sources-list .img {
margin-inline-end: 4px;
}

View File

@ -28,13 +28,13 @@ import actions from "../../actions";
// Components
import SourcesTreeItem from "./SourcesTreeItem";
import AccessibleImage from "../shared/AccessibleImage";
import ManagedTree from "../shared/ManagedTree";
// Utils
import { getRawSourceURL } from "../../utils/source";
import { createLocation } from "../../utils/location";
const classnames = require("devtools/client/shared/classnames.js");
const Tree = require("devtools/client/shared/components/Tree");
function shouldAutoExpand(item, mainThreadHost) {
// There is only one case where we want to force auto expand,
@ -43,17 +43,18 @@ function shouldAutoExpand(item, mainThreadHost) {
}
/**
* Get the one directory item where the given source is meant to be displayed in the SourceTree.
* Get the SourceItem displayed in the SourceTree for a given "tree location".
*
* @param {Object} treeLocation
* An object containing the Source coming from the sources.js reducer and the source actor
* See getTreeLocation().
* @param {object} rootItems
* Result of getSourcesTreeSources selector, containing all sources sorted in a tree structure.
* items to be displayed in the source tree.
* @return {SourceItem}
* The directory source item where the given source is displayed.
*/
function getDirectoryForSource(treeLocation, rootItems) {
function getSourceItemForTreeLocation(treeLocation, rootItems) {
// Sources without URLs are not visible in the SourceTree
const { source, sourceActor } = treeLocation;
@ -131,7 +132,7 @@ class SourcesTree extends Component {
const { selectedTreeLocation } = this.props;
// We might fail to find the source if its thread is registered late,
// so that we should re-search the selected source if highlightItems is empty.
// so that we should re-search the selected source if state.focused is null.
if (
nextProps.selectedTreeLocation?.source &&
(nextProps.selectedTreeLocation.source != selectedTreeLocation?.source ||
@ -139,20 +140,23 @@ class SourcesTree extends Component {
selectedTreeLocation?.source &&
nextProps.selectedTreeLocation.sourceActor !=
selectedTreeLocation?.sourceActor) ||
!this.state.highlightItems?.length)
!this.props.focused)
) {
let parentDirectory = getDirectoryForSource(
const sourceItem = getSourceItemForTreeLocation(
nextProps.selectedTreeLocation,
this.props.rootItems
);
// As highlightItems has to contains *all* the expanded items,
// walk up the tree to put all ancestor items up to the root of the tree.
const highlightItems = [];
while (parentDirectory) {
highlightItems.push(parentDirectory);
parentDirectory = this.getParent(parentDirectory);
if (sourceItem) {
// Walk up the tree to expand all ancestor items up to the root of the tree.
const expanded = new Set(this.props.expanded);
let parentDirectory = sourceItem;
while (parentDirectory) {
expanded.add(this.getKey(parentDirectory));
parentDirectory = this.getParent(parentDirectory);
}
this.props.setExpandedState(expanded);
this.onFocus(sourceItem);
}
this.setState({ highlightItems });
}
}
@ -170,12 +174,45 @@ class SourcesTree extends Component {
}
};
onExpand = (item, expandedState) => {
this.props.setExpandedState(expandedState);
onExpand = (item, shouldIncludeChildren) => {
this.setExpanded(item, true, shouldIncludeChildren);
};
onCollapse = (item, expandedState) => {
this.props.setExpandedState(expandedState);
onCollapse = (item, shouldIncludeChildren) => {
this.setExpanded(item, false, shouldIncludeChildren);
};
setExpanded = (item, isExpanded, shouldIncludeChildren) => {
const { expanded } = this.props;
let changed = false;
const expandItem = i => {
const key = this.getKey(i);
if (isExpanded) {
changed |= !expanded.has(key);
expanded.add(key);
} else {
changed |= expanded.has(key);
expanded.delete(key);
}
};
expandItem(item);
if (shouldIncludeChildren) {
let parents = [item];
while (parents.length) {
const children = [];
for (const parent of parents) {
for (const child of this.getChildren(parent)) {
expandItem(child);
children.push(child);
}
}
parents = children;
}
}
if (changed) {
this.props.setExpandedState(expanded);
}
};
isEmpty() {
@ -194,7 +231,7 @@ class SourcesTree extends Component {
return this.props.rootItems;
};
getPath = item => {
getKey = item => {
// As this is used as React key in Tree component,
// we need to update the key when switching to a new project root
// otherwise these items won't be updated and will have a buggy padding start.
@ -323,7 +360,7 @@ class SourcesTree extends Component {
);
}
renderItem = (item, depth, focused, _, expanded, { setExpanded }) => {
renderItem = (item, depth, focused, _, expanded) => {
const { mainThreadHost, projectRoot } = this.props;
return (
<SourcesTreeItem
@ -335,7 +372,7 @@ class SourcesTree extends Component {
focusItem={this.onFocus}
selectSourceItem={this.selectSourceItem}
projectRoot={projectRoot}
setExpanded={setExpanded}
setExpanded={this.setExpanded}
getBlackBoxSourcesGroups={this.getBlackBoxSourcesGroups}
getParent={this.getParent}
/>
@ -345,8 +382,6 @@ class SourcesTree extends Component {
renderTree() {
const { expanded, focused } = this.props;
const { highlightItems } = this.state;
const treeProps = {
autoExpandAll: false,
autoExpandDepth: 1,
@ -354,20 +389,22 @@ class SourcesTree extends Component {
focused,
getChildren: this.getChildren,
getParent: this.getParent,
getPath: this.getPath,
getKey: this.getKey,
getRoots: this.getRoots,
highlightItems,
itemHeight: 21,
key: this.isEmpty() ? "empty" : "full",
onCollapse: this.onCollapse,
onExpand: this.onExpand,
onFocus: this.onFocus,
isExpanded: item => {
return this.props.expanded.has(this.getKey(item));
},
onActivate: this.onActivate,
renderItem: this.renderItem,
preventBlur: true,
};
return <ManagedTree {...treeProps} />;
return <Tree {...treeProps} />;
}
renderPane(child) {
@ -438,7 +475,7 @@ function getTreeLocation(state, location) {
if (source) {
return createLocation({
source,
// A source actor is required by getDirectoryForSource
// A source actor is required by getSourceItemForTreeLocation
// in order to know in which thread this source relates to.
sourceActor: location.sourceActor,
});

View File

@ -1,43 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
.managed-tree .tree {
user-select: none;
white-space: nowrap;
overflow: auto;
min-width: 100%;
display: grid;
grid-template-columns: 1fr;
align-content: start;
line-height: 1.4em;
}
.managed-tree .tree button {
display: block;
}
.managed-tree .tree .node {
padding: 2px 3px 2px 3px;
position: relative;
}
.managed-tree .tree .node.focused {
color: var(--theme-selection-color);
background-color: var(--theme-selection-background);
}
html:not([dir="rtl"]) .managed-tree .tree .node > div {
margin-left: 10px;
}
html[dir="rtl"] .managed-tree .tree .node > div {
margin-right: 10px;
}
.managed-tree .tree-node button {
position: fixed;
}

View File

@ -1,123 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import React, { Component } from "react";
import PropTypes from "prop-types";
import "./ManagedTree.css";
const Tree = require("devtools/client/shared/components/Tree");
class ManagedTree extends Component {
constructor(props) {
super(props);
this.state = {
expanded: props.expanded || new Set(),
};
}
static defaultProps = {
onFocus: () => {},
};
static get propTypes() {
return {
expanded: PropTypes.object,
focused: PropTypes.any,
getPath: PropTypes.func.isRequired,
highlightItems: PropTypes.array,
onCollapse: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
renderItem: PropTypes.func.isRequired,
};
}
// FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
UNSAFE_componentWillReceiveProps(nextProps) {
const { highlightItems } = this.props;
if (
nextProps.highlightItems &&
nextProps.highlightItems != highlightItems &&
nextProps.highlightItems.length
) {
this.highlightItem(nextProps.highlightItems);
}
}
setExpanded = (item, isExpanded, shouldIncludeChildren) => {
const { expanded } = this.state;
let changed = false;
const expandItem = i => {
const path = this.props.getPath(i);
if (isExpanded) {
changed |= !expanded.has(path);
expanded.add(path);
} else {
changed |= expanded.has(path);
expanded.delete(path);
}
};
expandItem(item);
if (shouldIncludeChildren) {
let parents = [item];
while (parents.length) {
const children = [];
for (const parent of parents) {
for (const child of this.props.getChildren(parent)) {
expandItem(child);
children.push(child);
}
}
parents = children;
}
}
if (changed) {
this.setState({ expanded });
if (isExpanded && this.props.onExpand) {
this.props.onExpand(item, expanded);
} else if (!isExpanded && this.props.onCollapse) {
this.props.onCollapse(item, expanded);
}
}
};
highlightItem(highlightItems) {
const { expanded } = this.state;
highlightItems.forEach(item => {
expanded.add(this.props.getPath(item));
});
this.props.onFocus(highlightItems[0]);
this.setState({ expanded });
}
render() {
const { expanded } = this.state;
return (
<div className="managed-tree">
<Tree
{...this.props}
isExpanded={item => expanded.has(this.props.getPath(item))}
focused={this.props.focused}
getKey={this.props.getPath}
onExpand={(item, shouldIncludeChildren) =>
this.setExpanded(item, true, shouldIncludeChildren)
}
onCollapse={(item, shouldIncludeChildren) =>
this.setExpanded(item, false, shouldIncludeChildren)
}
onFocus={this.props.onFocus}
renderItem={(...args) =>
this.props.renderItem(...args, {
setExpanded: this.setExpanded,
})
}
/>
</div>
);
}
}
export default ManagedTree;

View File

@ -13,7 +13,6 @@ CompiledModules(
"Badge.js",
"BracketArrow.js",
"Dropdown.js",
"ManagedTree.js",
"Modal.js",
"Popover.js",
"PreviewFunction.js",

View File

@ -1,101 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import React from "react";
import { mount, shallow } from "enzyme";
import ManagedTree from "../ManagedTree";
function getTestContent() {
const testTree = {
a: {
value: "FOO",
children: [
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 },
{ value: 5 },
],
},
b: {
value: "BAR",
children: [
{ value: "A" },
{ value: "B" },
{ value: "C" },
{ value: "D" },
{ value: "E" },
],
},
c: { value: "BAZ" },
};
const renderItem = item => <div>{item.value ? item.value : item}</div>;
const onFocus = jest.fn();
const onExpand = jest.fn();
const onCollapse = jest.fn();
const getPath = (item, i) => {
if (item.value) {
return item.value;
}
if (i) {
return `${i}`;
}
return `${item}-$`;
};
return {
testTree,
props: {
getRoots: () => Object.keys(testTree),
getParent: item => null,
getChildren: branch => branch.children || [],
itemHeight: 24,
autoExpandAll: true,
autoExpandDepth: 1,
getPath,
renderItem,
onFocus,
onExpand,
onCollapse,
},
};
}
describe("ManagedTree", () => {
it("render", () =>
expect(
shallow(<ManagedTree {...getTestContent().props} />)
).toMatchSnapshot());
it("highlights list items", () => {
const { props, testTree } = getTestContent();
const wrapper = shallow(<ManagedTree {...props} />);
wrapper.setProps({
highlightItems: testTree.a.children,
});
expect(wrapper).toMatchSnapshot();
});
it("sets expanded items", () => {
const { props, testTree } = getTestContent();
const wrapper = mount(<ManagedTree {...props} />);
expect(wrapper).toMatchSnapshot();
// We auto-expanded the first layer, so unexpand first node.
wrapper.find("TreeNode").first().simulate("click");
expect(wrapper).toMatchSnapshot();
expect(props.onExpand).toHaveBeenCalledWith(
"c",
new Set(
Object.keys(testTree)
.filter(i => i !== "a")
.map(k => `${k}-$`)
)
);
wrapper.find("TreeNode").first().simulate("click");
expect(props.onExpand).toHaveBeenCalledWith(
"c",
new Set(Object.keys(testTree).map(k => `${k}-$`))
);
});
});

View File

@ -1,486 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManagedTree highlights list items 1`] = `
<div
className="managed-tree"
>
<Tree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getKey={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
highlightItems={
Array [
Object {
"value": 1,
},
Object {
"value": 2,
},
Object {
"value": 3,
},
Object {
"value": 4,
},
Object {
"value": 5,
},
]
}
isExpanded={[Function]}
itemHeight={24}
onCollapse={[Function]}
onExpand={[Function]}
onFocus={
[MockFunction] {
"calls": Array [
Array [
Object {
"value": 1,
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
renderItem={[Function]}
/>
</div>
`;
exports[`ManagedTree render 1`] = `
<div
className="managed-tree"
>
<Tree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getKey={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
isExpanded={[Function]}
itemHeight={24}
onCollapse={[Function]}
onExpand={[Function]}
onFocus={[MockFunction]}
renderItem={[Function]}
/>
</div>
`;
exports[`ManagedTree sets expanded items 1`] = `
<ManagedTree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
itemHeight={24}
onCollapse={[MockFunction]}
onExpand={
[MockFunction] {
"calls": Array [
Array [
"a",
Set {
"a-$",
"b-$",
"c-$",
},
],
Array [
"b",
Set {
"a-$",
"b-$",
"c-$",
},
],
Array [
"c",
Set {
"a-$",
"b-$",
"c-$",
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
}
}
onFocus={[MockFunction]}
renderItem={[Function]}
>
<div
className="managed-tree"
>
<Tree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getKey={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
isExpanded={[Function]}
itemHeight={24}
onCollapse={[Function]}
onExpand={[Function]}
onFocus={[MockFunction]}
renderItem={[Function]}
>
<div
className="tree "
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyUp={[Function]}
role="tree"
style={Object {}}
tabIndex="0"
>
<TreeNode
active={false}
depth={0}
expanded={true}
focused={false}
id="a-$"
index={0}
isExpandable={false}
item="a"
key="a-$-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-expanded={true}
aria-level={1}
className="tree-node"
data-expandable={false}
id="a-$"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
a
</div>
</div>
</TreeNode>
<TreeNode
active={false}
depth={0}
expanded={true}
focused={false}
id="1"
index={1}
isExpandable={false}
item="b"
key="1-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-expanded={true}
aria-level={1}
className="tree-node"
data-expandable={false}
id="1"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
b
</div>
</div>
</TreeNode>
<TreeNode
active={false}
depth={0}
expanded={true}
focused={false}
id="2"
index={2}
isExpandable={false}
item="c"
key="2-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-expanded={true}
aria-level={1}
className="tree-node"
data-expandable={false}
id="2"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
c
</div>
</div>
</TreeNode>
</div>
</Tree>
</div>
</ManagedTree>
`;
exports[`ManagedTree sets expanded items 2`] = `
<ManagedTree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
itemHeight={24}
onCollapse={
[MockFunction] {
"calls": Array [
Array [
"a",
Set {
"b-$",
"c-$",
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
onExpand={
[MockFunction] {
"calls": Array [
Array [
"a",
Set {
"b-$",
"c-$",
},
],
Array [
"b",
Set {
"b-$",
"c-$",
},
],
Array [
"c",
Set {
"b-$",
"c-$",
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
}
}
onFocus={
[MockFunction] {
"calls": Array [
Array [
"a",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
renderItem={[Function]}
>
<div
className="managed-tree"
>
<Tree
autoExpandAll={true}
autoExpandDepth={1}
getChildren={[Function]}
getKey={[Function]}
getParent={[Function]}
getPath={[Function]}
getRoots={[Function]}
isExpanded={[Function]}
itemHeight={24}
onCollapse={[Function]}
onExpand={[Function]}
onFocus={
[MockFunction] {
"calls": Array [
Array [
"a",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
}
renderItem={[Function]}
>
<div
className="tree "
onBlur={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyPress={[Function]}
onKeyUp={[Function]}
role="tree"
style={Object {}}
tabIndex="0"
>
<TreeNode
active={false}
depth={0}
expanded={false}
focused={false}
id="a-$"
index={0}
isExpandable={false}
item="a"
key="a-$-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-level={1}
className="tree-node"
data-expandable={false}
id="a-$"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
a
</div>
</div>
</TreeNode>
<TreeNode
active={false}
depth={0}
expanded={true}
focused={false}
id="1"
index={1}
isExpandable={false}
item="b"
key="1-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-expanded={true}
aria-level={1}
className="tree-node"
data-expandable={false}
id="1"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
b
</div>
</div>
</TreeNode>
<TreeNode
active={false}
depth={0}
expanded={true}
focused={false}
id="2"
index={2}
isExpandable={false}
item="c"
key="2-inactive"
onClick={[Function]}
onCollapse={[Function]}
onExpand={[Function]}
renderItem={[Function]}
>
<div
aria-expanded={true}
aria-level={1}
className="tree-node"
data-expandable={false}
id="2"
onClick={[Function]}
onKeyDownCapture={null}
role="treeitem"
>
<div>
c
</div>
</div>
</TreeNode>
</div>
</Tree>
</div>
</ManagedTree>
`;

View File

@ -18,7 +18,6 @@
@import url("chrome://devtools/content/debugger/src/components/shared/Button/styles/CommandBarButton.css");
@import url("chrome://devtools/content/debugger/src/components/shared/Button/styles/PaneToggleButton.css");
@import url("chrome://devtools/content/debugger/src/components/shared/Dropdown.css");
@import url("chrome://devtools/content/debugger/src/components/shared/ManagedTree.css");
@import url("chrome://devtools/content/debugger/src/components/shared/menu.css");
@import url("chrome://devtools/content/debugger/src/components/shared/Modal.css");
@import url("chrome://devtools/content/debugger/src/components/shared/Popover.css");

View File

@ -1237,7 +1237,7 @@ async function expandSourceTree(dbg) {
// But when there is a project root, it can be directory or group items.
// Select only expandable in order to ignore source items.
for (const rootNode of dbg.win.document.querySelectorAll(
".sources-list > .managed-tree > .tree > .tree-node[data-expandable=true]"
".sources-list > .tree > .tree-node[data-expandable=true]"
)) {
await expandAllSourceNodes(dbg, rootNode);
}

View File

@ -299,7 +299,6 @@ devtools.jar:
content/debugger/src/components/shared/Button/styles/CommandBarButton.css (debugger/src/components/shared/Button/styles/CommandBarButton.css)
content/debugger/src/components/shared/Button/styles/PaneToggleButton.css (debugger/src/components/shared/Button/styles/PaneToggleButton.css)
content/debugger/src/components/shared/Dropdown.css (debugger/src/components/shared/Dropdown.css)
content/debugger/src/components/shared/ManagedTree.css (debugger/src/components/shared/ManagedTree.css)
content/debugger/src/components/shared/menu.css (debugger/src/components/shared/menu.css)
content/debugger/src/components/shared/Modal.css (debugger/src/components/shared/Modal.css)
content/debugger/src/components/shared/Popover.css (debugger/src/components/shared/Popover.css)