Add ability to focus on a node to tree

Summary: Added context menu to tree nodes that 'focuses' a node. This will make the node the root of the tree. Focus state can be removed again via context menu but we could add a permanent button in the future

Reviewed By: lblasa

Differential Revision: D41493002

fbshipit-source-id: 43ec7a25aeea0b169cbcbb1ac20ac22ea893fee2
This commit is contained in:
Luke De Feo
2022-11-24 09:23:16 -08:00
committed by Facebook GitHub Bot
parent 01f7fa34e5
commit 4b566dbe03
2 changed files with 99 additions and 33 deletions

View File

@@ -25,12 +25,12 @@ import {
useValue, useValue,
HighlightManager, HighlightManager,
HighlightProvider, HighlightProvider,
HighlightContext,
useHighlighter, useHighlighter,
theme, theme,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {head} from 'lodash'; import {head} from 'lodash';
import {Dropdown, Menu} from 'antd';
export function Tree(props: { export function Tree(props: {
rootId: Id; rootId: Id;
@@ -40,7 +40,12 @@ export function Tree(props: {
}) { }) {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const expandedItems = useValue(instance.treeState).expandedNodes; const expandedItems = useValue(instance.treeState).expandedNodes;
const items = useMemo(() => toComplexTree(props.nodes), [props.nodes]); const focused = useValue(instance.focusedNode);
const items = useMemo(
() => toComplexTree(focused || props.rootId, props.nodes),
[focused, props.nodes, props.rootId],
);
const hoveredNodes = useValue(instance.hoveredNodes); const hoveredNodes = useValue(instance.hoveredNodes);
const treeEnvRef = useRef<TreeEnvironmentRef>(); const treeEnvRef = useRef<TreeEnvironmentRef>();
@@ -109,7 +114,7 @@ export function Tree(props: {
}}> }}>
<ComplexTree <ComplexTree
treeId="tree" treeId="tree"
rootItem={props.rootId as any} //the typing in in the library is wrong here rootItem={FakeNode.id as any} //the typing in in the library is wrong here
treeLabel="UI" treeLabel="UI"
/> />
</ControlledTreeEnvironment> </ControlledTreeEnvironment>
@@ -149,58 +154,116 @@ function renderItem<C extends string = never>({
context.isDraggingOver && 'rct-tree-item-li-dragging-over', context.isDraggingOver && 'rct-tree-item-li-dragging-over',
context.isSearchMatching && 'rct-tree-item-li-search-match', context.isSearchMatching && 'rct-tree-item-li-search-match',
)}> )}>
<div <ContextMenu node={item.data} id={item.index} title={item.data.name}>
{...(context.itemContainerWithoutChildrenProps as any)}
style={{
paddingLeft: `${(depth + 1) * renderDepthOffset}px`,
}}
className={cx(
'rct-tree-item-title-container',
item.hasChildren && 'rct-tree-item-title-container-hasChildren',
context.isSelected && 'rct-tree-item-title-container-selected',
context.isExpanded && 'rct-tree-item-title-container-expanded',
context.isFocused && 'rct-tree-item-title-container-focused',
context.isDraggingOver &&
'rct-tree-item-title-container-dragging-over',
context.isSearchMatching &&
'rct-tree-item-title-container-search-match',
)}>
{arrow}
<div <div
{...(context.interactiveElementProps as any)} {...(context.itemContainerWithoutChildrenProps as any)}
style={{
paddingLeft: `${(depth + 1) * renderDepthOffset}px`,
}}
className={cx( className={cx(
'rct-tree-item-button', 'rct-tree-item-title-container',
item.hasChildren && 'rct-tree-item-button-hasChildren', item.hasChildren && 'rct-tree-item-title-container-hasChildren',
context.isSelected && 'rct-tree-item-button-selected', context.isSelected && 'rct-tree-item-title-container-selected',
context.isExpanded && 'rct-tree-item-button-expanded', context.isExpanded && 'rct-tree-item-title-container-expanded',
context.isFocused && 'rct-tree-item-button-focused', context.isFocused && 'rct-tree-item-title-container-focused',
context.isDraggingOver && 'rct-tree-item-button-dragging-over', context.isDraggingOver &&
context.isSearchMatching && 'rct-tree-item-button-search-match', 'rct-tree-item-title-container-dragging-over',
context.isSearchMatching &&
'rct-tree-item-title-container-search-match',
)}> )}>
<HighlightedText text={item.data.name} /> {arrow}
<div
{...(context.interactiveElementProps as any)}
className={cx(
'rct-tree-item-button',
item.hasChildren && 'rct-tree-item-button-hasChildren',
context.isSelected && 'rct-tree-item-button-selected',
context.isExpanded && 'rct-tree-item-button-expanded',
context.isFocused && 'rct-tree-item-button-focused',
context.isDraggingOver && 'rct-tree-item-button-dragging-over',
context.isSearchMatching && 'rct-tree-item-button-search-match',
)}>
<HighlightedText text={item.data.name} />
</div>
</div> </div>
</div> </ContextMenu>
{children} {children}
</li> </li>
); );
} }
type ContextMenuProps = {node: UINode; id: Id; title: string};
const ContextMenu: React.FC<ContextMenuProps> = ({id, title, children}) => {
const instance = usePlugin(plugin);
const focusedNode = instance.focusedNode.get();
return (
<Dropdown
overlay={() => (
<Menu>
{focusedNode !== head(instance.hoveredNodes.get()) && (
<Menu.Item
onClick={() => {
instance.focusedNode.set(id);
}}>
Focus {title}
</Menu.Item>
)}
{focusedNode && (
<Menu.Item
onClick={() => {
instance.focusedNode.set(undefined);
}}>
Remove focus
</Menu.Item>
)}
</Menu>
)}
trigger={['contextMenu']}>
<div>{children}</div>
</Dropdown>
);
};
function HighlightedText(props: {text: string}) { function HighlightedText(props: {text: string}) {
const highlightManager: HighlightManager = useHighlighter(); const highlightManager: HighlightManager = useHighlighter();
return <span>{highlightManager.render(props.text)}</span>; return <span>{highlightManager.render(props.text)}</span>;
} }
function toComplexTree(nodes: Map<Id, UINode>): Record<Id, TreeItem<UINode>> { const FakeNode: UINode = {
id: 'Fakeroot',
qualifiedName: 'Fakeroot',
name: 'Fakeroot',
children: [],
attributes: {},
bounds: {x: 0, y: 0, height: 0, width: 0},
tags: [],
};
function toComplexTree(
root: Id,
nodes: Map<Id, UINode>,
): Record<Id, TreeItem<UINode>> {
const res: Record<Id, TreeItem<UINode>> = {}; const res: Record<Id, TreeItem<UINode>> = {};
for (const node of nodes.values()) { for (const node of nodes.values()) {
res[node.id] = { res[node.id] = {
index: node.id, index: node.id,
canMove: false,
canRename: false,
children: node.children, children: node.children,
data: node, data: node,
hasChildren: node.children.length > 0, hasChildren: node.children.length > 0,
}; };
} }
//the library doesnt render the root node so we insert a fake one which will never be rendered
//https://github.com/lukasbach/react-complex-tree/issues/42
res[FakeNode.id] = {
index: FakeNode.id,
children: [root],
hasChildren: true,
data: FakeNode,
};
return res; return res;
} }

View File

@@ -48,6 +48,8 @@ export function plugin(client: PluginClient<Events>) {
perfEvents.append(event); perfEvents.append(event);
}); });
const focusedNode = createState<Id | undefined>(undefined);
const nodes = createState<Map<Id, UINode>>(new Map()); const nodes = createState<Map<Id, UINode>>(new Map());
const snapshot = createState<{nodeId: Id; base64Image: Snapshot} | null>( const snapshot = createState<{nodeId: Id; base64Image: Snapshot} | null>(
null, null,
@@ -108,6 +110,7 @@ export function plugin(client: PluginClient<Events>) {
rootId, rootId,
nodes, nodes,
metadata, metadata,
focusedNode,
snapshot, snapshot,
hoveredNodes, hoveredNodes,
perfEvents, perfEvents,