diff --git a/desktop/plugins/public/ui-debugger/components/fb-stubs/IDEContextMenu.tsx b/desktop/plugins/public/ui-debugger/components/fb-stubs/IDEContextMenu.tsx index 062d09029..a80b8c651 100644 --- a/desktop/plugins/public/ui-debugger/components/fb-stubs/IDEContextMenu.tsx +++ b/desktop/plugins/public/ui-debugger/components/fb-stubs/IDEContextMenu.tsx @@ -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 []; } diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 3f281ead7..d88c4f487 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -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 ( - - - {props.children} - - - ); -} - type BottomPanelProps = { title: string; dismiss: () => void; diff --git a/desktop/plugins/public/ui-debugger/components/shared/Centered.tsx b/desktop/plugins/public/ui-debugger/components/shared/Centered.tsx new file mode 100644 index 000000000..657a0b7bb --- /dev/null +++ b/desktop/plugins/public/ui-debugger/components/shared/Centered.tsx @@ -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 ( + + + {props.children} + + + ); +} diff --git a/desktop/plugins/public/ui-debugger/components/tree/ContextMenu.tsx b/desktop/plugins/public/ui-debugger/components/tree/ContextMenu.tsx index 56044a33b..657dce2cc 100644 --- a/desktop/plugins/public/ui-debugger/components/tree/ContextMenu.tsx +++ b/desktop/plugins/public/ui-debugger/components/tree/ContextMenu.tsx @@ -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; @@ -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 = [ - } - onClick={() => { - onExpandRecursively(hoveredNode.id); - onSelectNode(hoveredNode.id, 'context-menu'); - tracker.track('context-menu-expand-recursive', {}); - }} - />, - - } - onClick={() => { - onCollapseRecursively(hoveredNode.id); - onSelectNode(hoveredNode.id, 'context-menu'); - tracker.track('context-menu-collapse-recursive', {}); - }} - />, - } - onClick={() => { - onCollapseNonAncestors(hoveredNode.id); - onSelectNode(hoveredNode.id, 'context-menu'); - tracker.track('context-menu-collapse-non-ancestors', {}); - }} - />, - , - ]; - - copyItems.push( - } - onClick={() => { - tracker.track('context-menu-name-copied', {name: hoveredNode.name}); - getFlipperLib().writeTextToClipboard(hoveredNode.name); - }} - />, - ); - - copyItems.push( - Object.entries(hoveredNode.inlineAttributes).map(([key, value]) => ( - } - onClick={() => { - tracker.track('context-menu-copied', { - name: hoveredNode.name, - key, - value, - }); - getFlipperLib().writeTextToClipboard(value); - }} - /> - )), - ); - - copyItems.push( - , - ); - } const focus = hoveredNode != null && focusedNodeId !== hoveredNodeId && hoveredNode.bounds.height !== 0 && - hoveredNode.bounds.width !== 0 && ( - } - onClick={() => { - onFocusNode(hoveredNodeId); - }} - /> - ); + hoveredNode.bounds.width !== 0 && { + key: 'focus', + label: `Focus element`, + icon: , + onClick: () => { + onFocusNode(hoveredNodeId); + }, + }; - const removeFocus = focusedNodeId && ( - } - onClick={() => { - onFocusNode(undefined); - }} - /> - ); + const removeFocus = focusedNodeId && { + key: 'remove-focus', + label: 'Remove focus', + icon: , + onClick: () => { + onFocusNode(undefined); + }, + }; const matchingFrameworkEvents = (hoveredNode && @@ -160,42 +88,100 @@ export const ContextMenu: React.FC<{ []; const frameworkEventsTable = matchingFrameworkEvents.length > 0 && - hoveredNode && ( - { - onSetViewMode({ - mode: 'frameworkEventsTable', - nodeId: hoveredNode.id, - isTree: hoveredNode.tags.includes('TreeRoot'), - }); - }} - icon={} - /> - ); + hoveredNode && { + key: 'events-table', + label: 'Explore events', + icon: , + onClick: () => { + onSetViewMode({ + mode: 'frameworkEventsTable', + nodeId: hoveredNode.id, + isTree: hoveredNode.tags.includes('TreeRoot'), + }); + }, + }; + + const focusItems = [focus, removeFocus, frameworkEventsTable]; + + const items: MenuItems = + hoveredNode == null + ? [] + : filterOutFalsy([ + { + key: 'expand-recursive', + label: 'Expand recursively', + icon: , + onClick: () => { + onExpandRecursively(hoveredNode.id); + onSelectNode(hoveredNode.id, 'context-menu'); + tracker.track('context-menu-expand-recursive', {}); + }, + }, + { + key: 'collapse-recursive', + label: 'Collapse recurisvely', + icon: , + onClick: () => { + onCollapseRecursively(hoveredNode.id); + onSelectNode(hoveredNode.id, 'context-menu'); + tracker.track('context-menu-collapse-recursive', {}); + }, + }, + { + key: 'collapse-non-ancestors', + label: 'Collapse non ancestors', + icon: , + 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: , + 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: , + onClick: () => { + tracker.track('context-menu-copied', { + name: hoveredNode.name, + key, + value, + }); + getFlipperLib().writeTextToClipboard(value); + }, + }), + ), + ...(bigGrepContextMenuItems(hoveredNode) || []), + ...(ideContextMenuItems(hoveredNode, () => + setIdeItemsRerender((value) => value + 1), + ) || []), + ]); return ( { + onOpenChange={(visible) => { onContextMenuOpen(visible); }} - overlay={() => { - return ( - - {treeCollapseItems} - {focus} - {removeFocus} - {frameworkEventsTable} - {(focus || removeFocus || frameworkEventsTable) && ( - - )} - {copyItems} - - {hoveredNode && ( - - )} - - ); + menu={{ + items, + onClick: () => { + onContextMenuOpen(false); + }, }} trigger={['contextMenu']}> {children} diff --git a/desktop/plugins/public/ui-debugger/components/util/UIDebuggerMenuItem.tsx b/desktop/plugins/public/ui-debugger/components/util/UIDebuggerMenuItem.tsx deleted file mode 100644 index 765ddb9c0..000000000 --- a/desktop/plugins/public/ui-debugger/components/util/UIDebuggerMenuItem.tsx +++ /dev/null @@ -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 ( - { - onClick?.(); - instance.uiActions.onContextMenuOpen(false); - }}> - - {icon} - {text} - - - ); -}; diff --git a/desktop/plugins/public/ui-debugger/utils/array.tsx b/desktop/plugins/public/ui-debugger/utils/array.tsx new file mode 100644 index 000000000..fa8c990d5 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/utils/array.tsx @@ -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( + arr: T[], +): Exclude[] { + return arr.filter(Boolean) as Exclude[]; +}