Update context menu for new ant design api

Summary:
Mostly mechanical change from jsx to object based api. However some changes:

1. Managed to get rid of UIDebugger context menu item. its now possible to listen to when any context menu is clicked

2. The construction code is cleaner. no more mutable arrary and pushing, its just a big spliced literal

3. Had to change how the ide function worked. It is  dynamic and used react query hook to update the number of items. Added a callback to recreate this behaviour.

Reviewed By: aigoncharov

Differential Revision: D48910165

fbshipit-source-id: 9a71f5ecd302e6ff72194f83a13839f78e9b0796
This commit is contained in:
Luke De Feo
2023-09-04 02:19:53 -07:00
committed by Facebook GitHub Bot
parent 32ef1bd565
commit b336ed38fa
6 changed files with 160 additions and 197 deletions

View File

@@ -7,16 +7,21 @@
* @format
*/
import React from 'react';
import {MenuProps} from 'antd';
import {ClientNode} from '../../ClientTypes';
export async function prefetchSourceFileLocation(_: ClientNode) {}
export function IDEContextMenuItems(_: {node: ClientNode}) {
return <></>;
type MenuItems = MenuProps['items'];
export function ideContextMenuItems(
_node: ClientNode,
_onResultsUpdated: () => void,
): MenuItems {
return [];
}
export function BigGrepContextMenuItems(_: {node: ClientNode}) {
return <></>;
export function bigGrepContextMenuItems(_: ClientNode): MenuItems {
return [];
}

View File

@@ -29,6 +29,7 @@ import {Tree2} from './tree/Tree';
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
import {queryClient} from '../utils/reactQuery';
import {FrameworkEventsTable} from './FrameworkEventsTable';
import {Centered} from './shared/Centered';
export function Component() {
const instance = usePlugin(plugin);
@@ -175,16 +176,6 @@ export function Component() {
);
}
export function Centered(props: {children: React.ReactNode}) {
return (
<Layout.Horizontal center grow>
<Layout.Container center grow>
{props.children}
</Layout.Container>
</Layout.Horizontal>
);
}
type BottomPanelProps = {
title: string;
dismiss: () => void;

View File

@@ -0,0 +1,21 @@
/**
* 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 {Layout} from 'flipper-plugin';
import React from 'react';
export function Centered(props: {children: React.ReactNode}) {
return (
<Layout.Horizontal center grow>
<Layout.Container center grow>
{props.children}
</Layout.Container>
</Layout.Horizontal>
);
}

View File

@@ -9,16 +9,16 @@
import {FrameworkEvent, Id, ClientNode} from '../../ClientTypes';
import {OnSelectNode, ViewMode} from '../../DesktopTypes';
import React, {ReactNode} from 'react';
import React, {useEffect, useState} from 'react';
import {DataSource, getFlipperLib} from 'flipper-plugin';
import {Dropdown, Menu} from 'antd';
import {UIDebuggerMenuItem} from '../util/UIDebuggerMenuItem';
import {Dropdown, MenuProps} from 'antd';
import {tracker} from '../../utils/tracker';
import {
BigGrepContextMenuItems,
IDEContextMenuItems,
bigGrepContextMenuItems,
ideContextMenuItems,
} from '../fb-stubs/IDEContextMenu';
import {
CopyFilled,
CopyOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
@@ -28,6 +28,9 @@ import {
SnippetsOutlined,
TableOutlined,
} from '@ant-design/icons';
import {filterOutFalsy} from '../../utils/array';
type MenuItems = MenuProps['items'];
export const ContextMenu: React.FC<{
frameworkEvents: DataSource<FrameworkEvent>;
@@ -55,104 +58,29 @@ export const ContextMenu: React.FC<{
onCollapseNonAncestors,
onSelectNode,
}) => {
const copyItems: ReactNode[] = [];
const [_, setIdeItemsRerender] = useState(0);
const hoveredNode = nodes.get(hoveredNodeId ?? Number.MAX_SAFE_INTEGER);
let treeCollapseItems: ReactNode[] = [];
if (hoveredNode) {
treeCollapseItems = [
<UIDebuggerMenuItem
key="expand-recursive"
text="Expand recursively"
icon={<MenuUnfoldOutlined />}
onClick={() => {
onExpandRecursively(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-expand-recursive', {});
}}
/>,
<UIDebuggerMenuItem
key="collapse-recursive"
text="Collapse recurisvely"
icon={<MenuFoldOutlined />}
onClick={() => {
onCollapseRecursively(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-collapse-recursive', {});
}}
/>,
<UIDebuggerMenuItem
key="collapse-non-ancestors"
text="Collapse non ancestors"
icon={<NodeExpandOutlined />}
onClick={() => {
onCollapseNonAncestors(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-collapse-non-ancestors', {});
}}
/>,
<Menu.Divider key="expand-divider" />,
];
copyItems.push(
<UIDebuggerMenuItem
key="Copy Element name"
text="Copy Element name"
icon={<CopyOutlined />}
onClick={() => {
tracker.track('context-menu-name-copied', {name: hoveredNode.name});
getFlipperLib().writeTextToClipboard(hoveredNode.name);
}}
/>,
);
copyItems.push(
Object.entries(hoveredNode.inlineAttributes).map(([key, value]) => (
<UIDebuggerMenuItem
key={key}
text={`Copy ${key}`}
icon={<SnippetsOutlined />}
onClick={() => {
tracker.track('context-menu-copied', {
name: hoveredNode.name,
key,
value,
});
getFlipperLib().writeTextToClipboard(value);
}}
/>
)),
);
copyItems.push(
<BigGrepContextMenuItems key="big-grep" node={hoveredNode} />,
);
}
const focus = hoveredNode != null &&
focusedNodeId !== hoveredNodeId &&
hoveredNode.bounds.height !== 0 &&
hoveredNode.bounds.width !== 0 && (
<UIDebuggerMenuItem
key="focus"
text={`Focus element`}
icon={<FullscreenExitOutlined />}
onClick={() => {
hoveredNode.bounds.width !== 0 && {
key: 'focus',
label: `Focus element`,
icon: <FullscreenExitOutlined />,
onClick: () => {
onFocusNode(hoveredNodeId);
}}
/>
);
},
};
const removeFocus = focusedNodeId && (
<UIDebuggerMenuItem
key="remove-focus"
text="Remove focus"
icon={<FullscreenOutlined />}
onClick={() => {
const removeFocus = focusedNodeId && {
key: 'remove-focus',
label: 'Remove focus',
icon: <FullscreenOutlined />,
onClick: () => {
onFocusNode(undefined);
}}
/>
);
},
};
const matchingFrameworkEvents =
(hoveredNode &&
@@ -160,42 +88,100 @@ export const ContextMenu: React.FC<{
[];
const frameworkEventsTable = matchingFrameworkEvents.length > 0 &&
hoveredNode && (
<UIDebuggerMenuItem
text="Explore events"
onClick={() => {
hoveredNode && {
key: 'events-table',
label: 'Explore events',
icon: <TableOutlined />,
onClick: () => {
onSetViewMode({
mode: 'frameworkEventsTable',
nodeId: hoveredNode.id,
isTree: hoveredNode.tags.includes('TreeRoot'),
});
}}
icon={<TableOutlined />}
/>
);
},
};
const focusItems = [focus, removeFocus, frameworkEventsTable];
const items: MenuItems =
hoveredNode == null
? []
: filterOutFalsy([
{
key: 'expand-recursive',
label: 'Expand recursively',
icon: <MenuUnfoldOutlined />,
onClick: () => {
onExpandRecursively(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-expand-recursive', {});
},
},
{
key: 'collapse-recursive',
label: 'Collapse recurisvely',
icon: <MenuFoldOutlined />,
onClick: () => {
onCollapseRecursively(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-collapse-recursive', {});
},
},
{
key: 'collapse-non-ancestors',
label: 'Collapse non ancestors',
icon: <NodeExpandOutlined />,
onClick: () => {
onCollapseNonAncestors(hoveredNode.id);
onSelectNode(hoveredNode.id, 'context-menu');
tracker.track('context-menu-collapse-non-ancestors', {});
},
},
{type: 'divider'},
...focusItems,
focusItems.length > 0 && {type: 'divider'},
{
key: 'Copy Element name',
label: 'Copy Element name',
icon: <CopyOutlined />,
onClick: () => {
tracker.track('context-menu-name-copied', {
name: hoveredNode.name,
});
getFlipperLib().writeTextToClipboard(hoveredNode.name);
},
},
...Object.entries(hoveredNode.inlineAttributes).map(
([key, value]) => ({
key: key,
label: `Copy ${key}`,
icon: <SnippetsOutlined />,
onClick: () => {
tracker.track('context-menu-copied', {
name: hoveredNode.name,
key,
value,
});
getFlipperLib().writeTextToClipboard(value);
},
}),
),
...(bigGrepContextMenuItems(hoveredNode) || []),
...(ideContextMenuItems(hoveredNode, () =>
setIdeItemsRerender((value) => value + 1),
) || []),
]);
return (
<Dropdown
onVisibleChange={(visible) => {
onOpenChange={(visible) => {
onContextMenuOpen(visible);
}}
overlay={() => {
return (
<Menu>
{treeCollapseItems}
{focus}
{removeFocus}
{frameworkEventsTable}
{(focus || removeFocus || frameworkEventsTable) && (
<Menu.Divider key="divider-focus" />
)}
{copyItems}
{hoveredNode && (
<IDEContextMenuItems key="ide" node={hoveredNode} />
)}
</Menu>
);
menu={{
items,
onClick: () => {
onContextMenuOpen(false);
},
}}
trigger={['contextMenu']}>
{children}

View File

@@ -1,54 +0,0 @@
/**
* 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 {Menu} from 'antd';
import {usePlugin, useValue, Layout} from 'flipper-plugin';
import {plugin} from '../../index';
import React from 'react';
/**
* The Menu item visibility event does not fire when a menu item is clicked.
* This is apparently by design https://github.com/ant-design/ant-design/issues/4994#issuecomment-281585872
* This component simply wraps a menu item but will ensure that the atom is set to false when an item is clicked.
* Additionally, it ensures menu items do not render when the atom is false
*/
export const UIDebuggerMenuItem: React.FC<{
text: string;
icon?: React.ReactNode;
onClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
}> = ({text, onClick, icon, onMouseEnter, onMouseLeave}) => {
const instance = usePlugin(plugin);
const isMenuOpen = useValue(instance.uiState.isContextMenuOpen);
/**
* The menu is not a controlled component and seems to be a bit slow to close when user clicks on it.
* React may rerender the menu before it has time to close resulting in seeing an incorrect context menu for a frame.
* This is here to just hide all the menu items when the menu closes. A little strange but works well in practice.
*/
if (!isMenuOpen) {
return null;
}
return (
<Menu.Item
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
disabled={onClick == null}
onClick={() => {
onClick?.();
instance.uiActions.onContextMenuOpen(false);
}}>
<Layout.Horizontal center gap="small">
{icon}
{text}
</Layout.Horizontal>
</Menu.Item>
);
};

View File

@@ -0,0 +1,14 @@
/**
* 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
*/
export function filterOutFalsy<T>(
arr: T[],
): Exclude<T, false | 0 | '' | null | undefined>[] {
return arr.filter(Boolean) as Exclude<T, false | 0 | '' | null | undefined>[];
}