Summary: This is temporary solution to get to parity with the old plugin. In future would like to make this more flexible on the desktop side Additionally getData was renamed to getAttributes for consistency Reviewed By: lblasa Differential Revision: D41845248 fbshipit-source-id: 50e94a7712f5d42938229134e212cef5d379475d
307 lines
9.1 KiB
TypeScript
307 lines
9.1 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import {Id, UINode} from '../types';
|
|
import React, {useEffect, useMemo, useRef} from 'react';
|
|
import {
|
|
Tree as ComplexTree,
|
|
ControlledTreeEnvironment,
|
|
TreeItem,
|
|
TreeInformation,
|
|
TreeItemRenderContext,
|
|
InteractionMode,
|
|
TreeEnvironmentRef,
|
|
} from 'react-complex-tree';
|
|
|
|
import {plugin} from '../index';
|
|
import {
|
|
usePlugin,
|
|
useValue,
|
|
HighlightManager,
|
|
HighlightProvider,
|
|
useHighlighter,
|
|
theme,
|
|
styled,
|
|
} from 'flipper-plugin';
|
|
|
|
import {head} from 'lodash';
|
|
import {Dropdown, Menu} from 'antd';
|
|
import {UIDebuggerMenuItem} from './util/UIDebuggerMenuItem';
|
|
|
|
export function Tree(props: {
|
|
rootId: Id;
|
|
nodes: Map<Id, UINode>;
|
|
selectedNode?: Id;
|
|
onSelectNode: (id: Id) => void;
|
|
}) {
|
|
const instance = usePlugin(plugin);
|
|
const expandedItems = useValue(instance.uiState.expandedNodes);
|
|
const focused = useValue(instance.uiState.focusedNode);
|
|
|
|
const items = useMemo(
|
|
() => toComplexTree(focused || props.rootId, props.nodes),
|
|
[focused, props.nodes, props.rootId],
|
|
);
|
|
const hoveredNodes = useValue(instance.uiState.hoveredNodes);
|
|
const treeEnvRef = useRef<TreeEnvironmentRef>();
|
|
|
|
const searchTerm = useValue(instance.uiState.searchTerm);
|
|
|
|
useEffect(() => {
|
|
//this makes the keyboard arrow controls work always, even when using the visualiser
|
|
treeEnvRef.current?.focusTree('tree', true);
|
|
}, [props.selectedNode]);
|
|
|
|
return (
|
|
<div
|
|
onMouseLeave={() => {
|
|
instance.uiState.hoveredNodes.set([]);
|
|
}}
|
|
style={
|
|
{
|
|
'--rct-color-tree-bg': theme.white,
|
|
'--rct-color-tree-focus-outline': theme.dividerColor,
|
|
'--rct-color-focustree-item-focused-border':
|
|
theme.selectionBackgroundColor,
|
|
'--rct-color-focustree-item-selected-bg':
|
|
theme.selectionBackgroundColor,
|
|
'--rct-color-nonfocustree-item-selected-bg':
|
|
theme.selectionBackgroundColor,
|
|
} as React.CSSProperties
|
|
}>
|
|
<HighlightProvider
|
|
text={searchTerm}
|
|
highlightColor={theme.searchHighlightBackground.yellow}>
|
|
<ControlledTreeEnvironment
|
|
ref={treeEnvRef as any}
|
|
items={items}
|
|
getItemTitle={(item) => item.data.name}
|
|
canRename={false}
|
|
canDragAndDrop={false}
|
|
viewState={{
|
|
tree: {
|
|
focusedItem: head(hoveredNodes),
|
|
expandedItems: [...expandedItems],
|
|
selectedItems: props.selectedNode ? [props.selectedNode] : [],
|
|
},
|
|
}}
|
|
onFocusItem={(item) => {
|
|
instance.uiState.hoveredNodes.set([item.index]);
|
|
}}
|
|
onExpandItem={(item) => {
|
|
instance.uiState.expandedNodes.update((draft) => {
|
|
draft.add(item.index);
|
|
});
|
|
}}
|
|
onCollapseItem={(item) =>
|
|
instance.uiState.expandedNodes.update((draft) => {
|
|
draft.delete(item.index);
|
|
})
|
|
}
|
|
renderItem={renderItem}
|
|
onSelectItems={(items) => props.onSelectNode(items[0])}
|
|
defaultInteractionMode={{
|
|
mode: 'custom',
|
|
extends: InteractionMode.DoubleClickItemToExpand,
|
|
createInteractiveElementProps: (
|
|
item,
|
|
treeId,
|
|
actions,
|
|
renderFlags,
|
|
) => ({
|
|
onClick: () => {
|
|
if (renderFlags.isSelected) {
|
|
actions.unselectItem();
|
|
} else {
|
|
actions.selectItem();
|
|
}
|
|
},
|
|
|
|
onMouseOver: () => {
|
|
if (!instance.uiState.isContextMenuOpen.get()) {
|
|
instance.uiState.hoveredNodes.set([item.index]);
|
|
}
|
|
},
|
|
}),
|
|
}}>
|
|
<ComplexTree
|
|
treeId="tree"
|
|
rootItem={FakeNode.id as any} //the typing in in the library is wrong here
|
|
treeLabel="UI"
|
|
/>
|
|
</ControlledTreeEnvironment>
|
|
</HighlightProvider>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
//copied from https://github.com/lukasbach/react-complex-tree/blob/e3dcc435933284376a0fc6e3cc651e67ead678b5/packages/core/src/renderers/createDefaultRenderers.tsx
|
|
const cx = (...classNames: Array<string | undefined | false>) =>
|
|
classNames.filter((cn) => !!cn).join(' ');
|
|
const renderDepthOffset = 5;
|
|
|
|
const DecorationImage = styled.img({
|
|
height: 12,
|
|
marginRight: 5,
|
|
width: 12,
|
|
});
|
|
function defaultIcon(node: UINode) {
|
|
if (node.tags.includes('Litho')) {
|
|
return <DecorationImage src="icons/litho-logo.png" />;
|
|
}
|
|
}
|
|
|
|
function renderItem<C extends string = never>({
|
|
item,
|
|
depth,
|
|
children,
|
|
arrow,
|
|
context,
|
|
}: {
|
|
item: TreeItem<UINode>;
|
|
depth: number;
|
|
children: React.ReactNode | null;
|
|
title: React.ReactNode;
|
|
arrow: React.ReactNode;
|
|
context: TreeItemRenderContext<C>;
|
|
info: TreeInformation;
|
|
}) {
|
|
return (
|
|
<li
|
|
{...(context.itemContainerWithChildrenProps as any)}
|
|
className={cx(
|
|
'rct-tree-item-li',
|
|
item.hasChildren && 'rct-tree-item-li-hasChildren',
|
|
context.isSelected && 'rct-tree-item-li-selected',
|
|
context.isExpanded && 'rct-tree-item-li-expanded',
|
|
context.isFocused && 'rct-tree-item-li-focused',
|
|
context.isDraggingOver && 'rct-tree-item-li-dragging-over',
|
|
context.isSearchMatching && 'rct-tree-item-li-search-match',
|
|
)}>
|
|
<ContextMenu node={item.data} id={item.index} title={item.data.name}>
|
|
<div
|
|
{...(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
|
|
{...(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',
|
|
)}>
|
|
{defaultIcon(item.data)}
|
|
<HighlightedText text={item.data.name} />
|
|
</div>
|
|
</div>
|
|
</ContextMenu>
|
|
|
|
{children}
|
|
</li>
|
|
);
|
|
}
|
|
|
|
type ContextMenuProps = {node: UINode; id: Id; title: string};
|
|
|
|
const ContextMenu: React.FC<ContextMenuProps> = ({id, title, children}) => {
|
|
const instance = usePlugin(plugin);
|
|
const focusedNode = instance.uiState.focusedNode.get();
|
|
|
|
return (
|
|
<Dropdown
|
|
onVisibleChange={(visible) => {
|
|
instance.uiState.isContextMenuOpen.set(visible);
|
|
}}
|
|
overlay={() => (
|
|
<Menu>
|
|
{focusedNode !== head(instance.uiState.hoveredNodes.get()) && (
|
|
<UIDebuggerMenuItem
|
|
key="focus"
|
|
text={`Focus ${title}`}
|
|
onClick={() => {
|
|
instance.uiState.focusedNode.set(id);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{focusedNode && (
|
|
<UIDebuggerMenuItem
|
|
key="remove-focus"
|
|
text="Remove focus"
|
|
onClick={() => {
|
|
instance.uiState.focusedNode.set(undefined);
|
|
}}
|
|
/>
|
|
)}
|
|
</Menu>
|
|
)}
|
|
trigger={['contextMenu']}>
|
|
<div>{children}</div>
|
|
</Dropdown>
|
|
);
|
|
};
|
|
|
|
function HighlightedText(props: {text: string}) {
|
|
const highlightManager: HighlightManager = useHighlighter();
|
|
return <span>{highlightManager.render(props.text)}</span>;
|
|
}
|
|
|
|
const FakeNode: UINode = {
|
|
id: 'Fakeroot',
|
|
qualifiedName: 'Fakeroot',
|
|
name: 'Fakeroot',
|
|
inlineAttributes: {},
|
|
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>> = {};
|
|
for (const node of nodes.values()) {
|
|
res[node.id] = {
|
|
index: node.id,
|
|
children: node.children,
|
|
data: node,
|
|
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;
|
|
}
|