diff --git a/desktop/flipper-plugin/src/ui/data-inspector/DataInspector.tsx b/desktop/flipper-plugin/src/ui/data-inspector/DataInspector.tsx index e3713016ff3..6e3dff96081 100644 --- a/desktop/flipper-plugin/src/ui/data-inspector/DataInspector.tsx +++ b/desktop/flipper-plugin/src/ui/data-inspector/DataInspector.tsx @@ -14,6 +14,10 @@ import React from 'react'; import {DataValueExtractor} from './DataPreview'; import {HighlightProvider, HighlightManager} from '../Highlight'; import {Layout} from '../Layout'; +import {Dropdown, MenuProps} from 'antd'; +import {_tryGetFlipperLibImplementation} from 'flipper-plugin'; +import {safeStringify} from 'flipper-plugin'; +import {getValueAtPath} from '../data-table/DataTableManager'; export type DataInspectorProps = { /** @@ -109,7 +113,9 @@ export class DataInspector extends PureComponent< filterExpanded: {}, filter: '', hoveredNodePath: undefined, - }; + } as DataInspectorState; + + isContextMenuOpen = false; static getDerivedStateFromProps( nextProps: DataInspectorProps, @@ -193,9 +199,11 @@ export class DataInspector extends PureComponent< }; setHoveredNodePath = (path?: string) => { - this.setState({ - hoveredNodePath: path, - }); + if (!this.isContextMenuOpen) { + this.setState({ + hoveredNodePath: path, + }); + } }; removeHover = () => { @@ -209,34 +217,108 @@ export class DataInspector extends PureComponent< render() { return ( - - - - - - - + { + this.isContextMenuOpen = open; + }} + trigger={['contextMenu']}> + + + + + + + + ); } + + getContextMenu = () => { + const lib = _tryGetFlipperLibImplementation(); + + let extraItems = [] as MenuProps['items']; + + const hoveredNodePath = this.state.hoveredNodePath; + const value = + hoveredNodePath != null && hoveredNodePath.length > 0 + ? getValueAtPath(this.props.data, hoveredNodePath) + : this.props.data; + if ( + this.props.additionalContextMenuItems != null && + hoveredNodePath != null && + hoveredNodePath.length > 0 + ) { + const fullPath = hoveredNodePath.split('.'); + const parentPath = fullPath.slice(0, fullPath.length - 1); + const name = fullPath[fullPath.length - 1]; + + const additionalItems = this.props.additionalContextMenuItems( + parentPath, + value, + name, + ); + + extraItems = [ + ...additionalItems.map((component) => ({ + key: `additionalItem-${parentPath}.${name}`, + label: component, + })), + {type: 'divider'}, + ]; + } + + const items = [ + ...(extraItems as []), + {key: 'copy-value', label: 'Copy'}, + ...(this.props.onDelete != null + ? [{key: 'delete-value', label: 'Delete'}] + : []), + {type: 'divider'}, + {key: 'copy-tree', label: 'Copy full tree'}, + ...(lib?.isFB ? [{key: 'create-paste', label: 'Create paste'}] : []), + ] as MenuProps['items']; + + return { + items, + onClick: (info) => { + this.isContextMenuOpen = false; + if (info.key === 'copy-value') { + if (this.state.hoveredNodePath != null) { + const value = getValueAtPath( + this.props.data, + this.state.hoveredNodePath, + ); + lib?.writeTextToClipboard(safeStringify(value)); + } + } else if (info.key === 'delete-value') { + const pathStr = this.state.hoveredNodePath as string | undefined; + this.props.onDelete?.(pathStr?.split('.') ?? []); + } else if (info.key === 'copy-tree') { + lib?.writeTextToClipboard(safeStringify(this.props.data)); + } else if (info.key === 'create-paste') { + lib?.createPaste(safeStringify(this.props.data)); + } + }, + } as MenuProps; + }; } diff --git a/desktop/flipper-plugin/src/ui/data-inspector/DataInspectorNode.tsx b/desktop/flipper-plugin/src/ui/data-inspector/DataInspectorNode.tsx index 3a099aad9a1..f24ee77649f 100644 --- a/desktop/flipper-plugin/src/ui/data-inspector/DataInspectorNode.tsx +++ b/desktop/flipper-plugin/src/ui/data-inspector/DataInspectorNode.tsx @@ -16,27 +16,20 @@ import { useEffect, useCallback, createContext, - useContext, - ReactElement, - SyntheticEvent, } from 'react'; import styled from '@emotion/styled'; import DataPreview, {DataValueExtractor, InspectorName} from './DataPreview'; import {getSortedKeys} from './utils'; import React from 'react'; import {useHighlighter, HighlightManager} from '../Highlight'; -import {Dropdown, Menu, Tooltip} from 'antd'; +import {Tooltip} from 'antd'; import {useInUnitTest} from '../../utils/useInUnitTest'; import {theme} from '../theme'; -import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib'; -import {safeStringify} from '../../utils/safeStringify'; export {DataValueExtractor} from './DataPreview'; export const RootDataContext = createContext<() => any>(() => ({})); -export const contextMenuTrigger = ['contextMenu' as const]; - const BaseContainer = styled.div<{ depth?: number; disabled?: boolean; @@ -144,9 +137,6 @@ type DataInspectorProps = { */ onExpanded?: ((path: string, expanded: boolean) => void) | undefined | null; /** - * Callback whenever delete action is invoked on current path. - */ - onDelete?: DataInspectorDeleteValue | undefined | null; /** * Render callback that can be used to customize the rendering of object keys. */ @@ -175,11 +165,6 @@ type DataInspectorProps = { * Object of properties that will have tooltips */ tooltips?: any; - additionalContextMenuItems?: ( - parentPath: string[], - value: any, - name?: string, - ) => ReactElement[]; hoveredNodePath?: string; @@ -308,7 +293,6 @@ export const DataInspectorNode: React.FC = memo( expandRoot, parentPath, onExpanded, - onDelete, onRenderName, onRenderDescription, extractValue: extractValueProp, @@ -318,12 +302,10 @@ export const DataInspectorNode: React.FC = memo( collapsed, tooltips, setValue: setValueProp, - additionalContextMenuItems, hoveredNodePath, setHoveredNodePath, }) { const highlighter = useHighlighter(); - const getRoot = useContext(RootDataContext); const isUnitTest = useInUnitTest(); const shouldExpand = useRef(false); @@ -417,22 +399,19 @@ export const DataInspectorNode: React.FC = memo( [onExpanded, expandedPaths], ); - const handleClick = useCallback(() => { - if (!isUnitTest) { - cancelIdleCallback(expandHandle.current); - } - const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed); - setExpanded(path, !isExpanded); - }, [expandedPaths, path, collapsed, isUnitTest]); - - const handleDelete = useCallback( - (path: Array) => { - if (!onDelete) { + const handleClick = useCallback( + (event) => { + if (!isUnitTest) { + cancelIdleCallback(expandHandle.current); + } + if (event.buttons !== 0) { + //only process left click return; } - onDelete(path); + const isExpanded = shouldBeExpanded(expandedPaths, path, collapsed); + setExpanded(path, !isExpanded); }, - [onDelete], + [expandedPaths, path, collapsed, isUnitTest], ); /** @@ -488,7 +467,6 @@ export const DataInspectorNode: React.FC = memo( expanded={expandedPaths} collapsed={collapsed} onExpanded={onExpanded} - onDelete={onDelete} onRenderName={onRenderName} onRenderDescription={onRenderDescription} parentPath={path} @@ -498,7 +476,6 @@ export const DataInspectorNode: React.FC = memo( data={metadata.data} diff={metadata.diff} tooltips={tooltips} - additionalContextMenuItems={additionalContextMenuItems} /> ); @@ -592,78 +569,27 @@ export const DataInspectorNode: React.FC = memo( } } - function getContextMenu() { - const lib = tryGetFlipperLibImplementation(); - const extraItems = additionalContextMenuItems - ? [ - additionalContextMenuItems(parentPath, value, name), - , - ] - : []; - return ( - - {extraItems} - { - lib?.writeTextToClipboard(safeStringify(getRoot())); - }}> - Copy tree - - {lib?.isFB && ( - { - lib?.createPaste(safeStringify(getRoot())); - }}> - Create paste from tree - - )} - - { - lib?.writeTextToClipboard(safeStringify(data)); - }}> - Copy value - - {!isExpandable && onDelete ? ( - { - handleDelete(path); - }}> - Delete - - ) : null} - - ); - } - const nodePath = path.join('.'); return ( - - { - setHoveredNodePath(nodePath); - }} - onMouseLeave={() => { - setHoveredNodePath(parentPath.join('.')); - }} - depth={depth} - disabled={!!setValueProp && !!setValue === false}> - - {expandedPaths && {expandGlyph}} - {descriptionOrPreview} - {wrapperStart} - - {propertyNodesContainer} - {wrapperEnd} - - + { + setHoveredNodePath(nodePath); + }} + onMouseLeave={() => { + setHoveredNodePath(parentPath.join('.')); + }} + depth={depth} + disabled={!!setValueProp && !!setValue === false}> + + {expandedPaths && {expandGlyph}} + {descriptionOrPreview} + {wrapperStart} + + {propertyNodesContainer} + {wrapperEnd} + ); }, dataInspectorPropsAreEqual, @@ -748,7 +674,6 @@ function dataInspectorPropsAreEqual( nextProps.depth === props.depth && nextProps.parentPath === props.parentPath && nextProps.onExpanded === props.onExpanded && - nextProps.onDelete === props.onDelete && nextProps.setValue === props.setValue && nextProps.collapsed === props.collapsed && nextProps.expandRoot === props.expandRoot && @@ -761,8 +686,3 @@ function isValueExpandable(data: any) { typeof data === 'object' && data !== null && Object.keys(data).length > 0 ); } - -function stopPropagation(e: SyntheticEvent) { - //without this the parent element will receive the context menu event and multiple context menus overlap - e.stopPropagation(); -} diff --git a/desktop/flipper-plugin/src/ui/data-inspector/__tests__/DataInspector.node.tsx b/desktop/flipper-plugin/src/ui/data-inspector/__tests__/DataInspector.node.tsx index 657187239cd..1b3ba807b5a 100644 --- a/desktop/flipper-plugin/src/ui/data-inspector/__tests__/DataInspector.node.tsx +++ b/desktop/flipper-plugin/src/ui/data-inspector/__tests__/DataInspector.node.tsx @@ -45,21 +45,27 @@ test('additional context menu items are rendered', async () => { data={json} expandRoot additionalContextMenuItems={(parentPath, value, name) => { - return [ - - path={[...parentPath, name].join('->')} - , - ]; + const path = [...parentPath, name].join('->'); + return [path={path}]; }} />, ); expect(await res.queryByText('path=data')).toBeFalsy; - fireEvent.contextMenu(await res.findByText(/data/), {}); - expect(await res.findByText('path=data')).toBeTruthy; + const dataContainer = await res.findByText(/data/); + fireEvent.mouseEnter(dataContainer, {}); + fireEvent.contextMenu(dataContainer, {}); + + const contextItem = await res.findByText('path=data'); + expect(contextItem).toBeTruthy; + fireEvent.click(contextItem); + expect(await res.queryByText('path=data')).toBeFalsy; + fireEvent.mouseLeave(dataContainer, {}); //try on a nested element - fireEvent.contextMenu(await res.findByText(/awesomely/), {}); + const awesomely = await res.findByText(/awesomely/); + fireEvent.mouseEnter(awesomely, {}); + fireEvent.contextMenu(awesomely, {}); expect(await res.findByText('path=data->is->awesomely')).toBeTruthy; }); diff --git a/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx b/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx index 81211b10532..95d276ba1f8 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableRow.tsx @@ -14,7 +14,7 @@ import {DataTableColumn, TableRowRenderContext} from './DataTable'; import {Width} from '../../utils/widthUtils'; import {DataFormatter} from '../DataFormatter'; import {Dropdown} from 'antd'; -import {contextMenuTrigger} from '../data-inspector/DataInspectorNode'; + import {getValueAtPath} from './DataTableManager'; import {HighlightManager, useHighlighter} from '../Highlight'; @@ -146,7 +146,7 @@ export const TableRow = memo(function TableRow({ ); if (config.onContextMenu) { return ( - + {row} );