Added indent guides to parent and children for selected node

Reviewed By: lblasa

Differential Revision: D41995460

fbshipit-source-id: cd4574caa6aa164d2b3a026f656609585cae65c0
This commit is contained in:
Luke De Feo
2022-12-13 08:21:22 -08:00
committed by Facebook GitHub Bot
parent 5043e5292f
commit b686567e2b

View File

@@ -21,13 +21,27 @@ import {
} from 'flipper-plugin'; } from 'flipper-plugin';
import {plugin} from '../index'; import {plugin} from '../index';
import {Glyph} from 'flipper'; import {Glyph} from 'flipper';
import {head} from 'lodash'; import {head, last} from 'lodash';
import {reverse} from 'lodash/fp'; import {reverse} from 'lodash/fp';
import {Dropdown, Menu, Typography} from 'antd'; import {Dropdown, Menu, Typography} from 'antd';
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem'; import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
const {Text} = Typography; const {Text} = Typography;
type LineStyle = 'ToParent' | 'ToChildren';
type NodeIndentGuide = {
depth: number;
style: LineStyle;
addHorizontalMarker: boolean;
trimBottom: boolean;
};
export type TreeNode = UINode & {
depth: number;
isExpanded: boolean;
indentGuide: NodeIndentGuide | null;
};
export function Tree2({ export function Tree2({
nodes, nodes,
rootId, rootId,
@@ -47,14 +61,19 @@ export function Tree2({
const hoveredNode = head(useValue(instance.uiState.hoveredNodes)); const hoveredNode = head(useValue(instance.uiState.hoveredNodes));
const {treeNodes, refs} = useMemo(() => { const {treeNodes, refs} = useMemo(() => {
const treeNodes = toTreeList(nodes, focusedNode || rootId, expandedNodes); const treeNodes = toTreeNodes(
nodes,
focusedNode || rootId,
expandedNodes,
selectedNode,
);
const refs: React.RefObject<HTMLLIElement>[] = treeNodes.map(() => const refs: React.RefObject<HTMLLIElement>[] = treeNodes.map(() =>
React.createRef<HTMLLIElement>(), React.createRef<HTMLLIElement>(),
); );
return {treeNodes, refs}; return {treeNodes, refs};
}, [expandedNodes, focusedNode, nodes, rootId]); }, [expandedNodes, focusedNode, nodes, rootId, selectedNode]);
const isUsingKBToScroll = useRef(false); const isUsingKBToScroll = useRef(false);
@@ -116,11 +135,6 @@ export function Tree2({
); );
} }
export type TreeNode = UINode & {
depth: number;
isExpanded: boolean;
};
const MemoTreeItemContainer = React.memo( const MemoTreeItemContainer = React.memo(
TreeItemContainer, TreeItemContainer,
(prevProps, nextProps) => { (prevProps, nextProps) => {
@@ -137,6 +151,41 @@ const MemoTreeItemContainer = React.memo(
}, },
); );
function IndentGuide({indentGuide}: {indentGuide: NodeIndentGuide}) {
const verticalLinePadding = `${renderDepthOffset * indentGuide.depth + 8}px`;
const verticalLineStyle = `${
indentGuide.style === 'ToParent' ? 'dashed' : 'solid'
}`;
const horizontalLineStyle = `${
indentGuide.style === 'ToParent' ? 'dotted' : 'solid'
}`;
const color = indentGuide.style === 'ToParent' ? '#B0B0B0' : '#C0C0C0';
return (
<div>
<div
style={{
position: 'absolute',
width: verticalLinePadding,
height: indentGuide.trimBottom ? HalfTreeItemHeight : TreeItemHeight,
borderRight: `1px ${verticalLineStyle} ${color}`,
}}></div>
{indentGuide.addHorizontalMarker && (
<div
style={{
position: 'absolute',
width: 8,
height: HalfTreeItemHeight,
borderBottom: `2px ${horizontalLineStyle} ${color}`,
marginLeft: verticalLinePadding,
}}></div>
)}
</div>
);
}
function TreeItemContainer({ function TreeItemContainer({
innerRef, innerRef,
treeNode, treeNode,
@@ -161,34 +210,42 @@ function TreeItemContainer({
onHoverNode: (node: Id) => void; onHoverNode: (node: Id) => void;
}) { }) {
return ( return (
<TreeItem <div>
ref={innerRef} {treeNode.indentGuide != null && (
isSelected={treeNode.id === selectedNode} <IndentGuide indentGuide={treeNode.indentGuide} />
isHovered={hoveredNode === treeNode.id} )}
onMouseEnter={() => { <TreeItemContent
if (isUsingKBToScroll.current === false && isContextMenuOpen == false) { ref={innerRef}
onHoverNode(treeNode.id); isSelected={treeNode.id === selectedNode}
} isHovered={hoveredNode === treeNode.id}
}} onMouseEnter={() => {
onClick={() => { if (
onSelectNode(treeNode.id); isUsingKBToScroll.current === false &&
}} isContextMenuOpen == false
item={treeNode}> ) {
<ExpandedIconOrSpace onHoverNode(treeNode.id);
expanded={treeNode.isExpanded}
showIcon={treeNode.children.length > 0}
onClick={() => {
if (treeNode.isExpanded) {
onCollapseNode(treeNode.id);
} else {
onExpandNode(treeNode.id);
} }
}} }}
/> onClick={() => {
{nodeIcon(treeNode)} onSelectNode(treeNode.id);
<HighlightedText text={treeNode.name} /> }}
<InlineAttributes attributes={treeNode.inlineAttributes} /> item={treeNode}>
</TreeItem> <ExpandedIconOrSpace
expanded={treeNode.isExpanded}
showIcon={treeNode.children.length > 0}
onClick={() => {
if (treeNode.isExpanded) {
onCollapseNode(treeNode.id);
} else {
onExpandNode(treeNode.id);
}
}}
/>
{nodeIcon(treeNode)}
<HighlightedText text={treeNode.name} />
<InlineAttributes attributes={treeNode.inlineAttributes} />
</TreeItemContent>
</div>
); );
} }
@@ -216,15 +273,18 @@ function InlineAttributes({attributes}: {attributes: Record<string, string>}) {
); );
} }
const TreeItem = styled.li<{ const TreeItemHeight = '26px';
const HalfTreeItemHeight = `calc(${TreeItemHeight} / 2)`;
const TreeItemContent = styled.li<{
item: TreeNode; item: TreeNode;
isHovered: boolean; isHovered: boolean;
isSelected: boolean; isSelected: boolean;
}>(({item, isHovered, isSelected}) => ({ }>(({item, isHovered, isSelected}) => ({
display: 'flex', display: 'flex',
alignItems: 'baseline', alignItems: 'baseline',
height: '26px', height: TreeItemHeight,
paddingLeft: `${(item.depth + 1) * renderDepthOffset}px`, paddingLeft: `${item.depth * renderDepthOffset}px`,
borderWidth: '1px', borderWidth: '1px',
borderRadius: '3px', borderRadius: '3px',
borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent', borderColor: isHovered ? theme.selectionBackgroundColor : 'transparent',
@@ -243,7 +303,10 @@ function ExpandedIconOrSpace(props: {
role="button" role="button"
tabIndex={0} tabIndex={0}
style={{display: 'flex'}} style={{display: 'flex'}}
onClick={props.onClick}> onClick={(e) => {
e.stopPropagation();
props.onClick();
}}>
<Glyph <Glyph
style={{ style={{
transform: props.expanded ? 'rotate(90deg)' : '', transform: props.expanded ? 'rotate(90deg)' : '',
@@ -278,7 +341,7 @@ const DecorationImage = styled.img({
width: 12, width: 12,
}); });
const renderDepthOffset = 8; const renderDepthOffset = 16;
const ContextMenu: React.FC<{ const ContextMenu: React.FC<{
nodes: Map<Id, UINode>; nodes: Map<Id, UINode>;
@@ -328,41 +391,97 @@ const ContextMenu: React.FC<{
); );
}; };
function toTreeList( type TreeListStackItem = {
node: UINode;
depth: number;
isChildOfSelectedNode: boolean;
selectedNodeDepth: number;
};
function toTreeNodes(
nodes: Map<Id, UINode>, nodes: Map<Id, UINode>,
rootId: Id, rootId: Id,
expandedNodes: Set<Id>, expandedNodes: Set<Id>,
selectedNode: Id | undefined,
): TreeNode[] { ): TreeNode[] {
const root = nodes.get(rootId); const root = nodes.get(rootId);
if (root == null) { if (root == null) {
return []; return [];
} }
const stack = [[root, 0]] as [UINode, number][]; const stack = [
{node: root, depth: 0, isChildOfSelectedNode: false, selectedNodeDepth: 0},
] as TreeListStackItem[];
const res = [] as TreeNode[]; const treeNodes = [] as TreeNode[];
while (stack.length > 0) { while (stack.length > 0) {
const [cur, depth] = stack.pop()!!; const stackItem = stack.pop()!!;
const isExpanded = expandedNodes.has(cur.id); const {node, depth} = stackItem;
res.push({
...cur, //if the previous item has an indent guide but we don't then it was the last segment
//so we trim the bottom
const prevItemLine = last(treeNodes)?.indentGuide;
if (prevItemLine != null && stackItem.isChildOfSelectedNode === false) {
prevItemLine.trimBottom = true;
}
const isExpanded = expandedNodes.has(node.id);
const isSelected = node.id === selectedNode;
treeNodes.push({
...node,
depth, depth,
isExpanded, isExpanded,
indentGuide: stackItem.isChildOfSelectedNode
? {
depth: stackItem.selectedNodeDepth,
style: 'ToChildren',
//if first child of selected node add horizontal marker
addHorizontalMarker: depth === stackItem.selectedNodeDepth + 1,
trimBottom: false,
}
: null,
}); });
let isChildOfSelectedNode = stackItem.isChildOfSelectedNode;
let selectedNodeDepth = stackItem.selectedNodeDepth;
if (isSelected) {
isChildOfSelectedNode = true;
selectedNodeDepth = depth;
// walk back through tree nodes, while depth is greater or equal than current it is your
// parents child / your previous cousin so set dashed line
for (let i = treeNodes.length - 1; i >= 0; i--) {
const prevNode = treeNodes[i];
if (prevNode.depth < depth) {
break;
}
prevNode.indentGuide = {
depth: selectedNodeDepth - 1,
style: 'ToParent',
addHorizontalMarker: prevNode.depth == depth,
trimBottom: prevNode.id === selectedNode,
};
}
}
if (isExpanded) { if (isExpanded) {
//since we do dfs and use a stack we have to reverse children to get the order correct //since we do dfs and use a stack we have to reverse children to get the order correct
for (const childId of reverse(cur.children)) { for (const childId of reverse(node.children)) {
const child = nodes.get(childId); const child = nodes.get(childId);
if (child != null) { if (child != null) {
stack.push([child, depth + 1]); stack.push({
node: child,
depth: depth + 1,
isChildOfSelectedNode: isChildOfSelectedNode,
selectedNodeDepth: selectedNodeDepth,
});
} }
} }
} }
} }
return res; return treeNodes;
} }
function useKeyboardShortcuts( function useKeyboardShortcuts(