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 (
-
- );
- }
-
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}
);