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
191 lines
5.4 KiB
TypeScript
191 lines
5.4 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 {FrameworkEvent, Id, ClientNode} from '../../ClientTypes';
|
|
import {OnSelectNode, ViewMode} from '../../DesktopTypes';
|
|
import React, {useEffect, useState} from 'react';
|
|
import {DataSource, getFlipperLib} from 'flipper-plugin';
|
|
import {Dropdown, MenuProps} from 'antd';
|
|
import {tracker} from '../../utils/tracker';
|
|
import {
|
|
bigGrepContextMenuItems,
|
|
ideContextMenuItems,
|
|
} from '../fb-stubs/IDEContextMenu';
|
|
import {
|
|
CopyFilled,
|
|
CopyOutlined,
|
|
FullscreenExitOutlined,
|
|
FullscreenOutlined,
|
|
MenuFoldOutlined,
|
|
MenuUnfoldOutlined,
|
|
NodeExpandOutlined,
|
|
SnippetsOutlined,
|
|
TableOutlined,
|
|
} from '@ant-design/icons';
|
|
import {filterOutFalsy} from '../../utils/array';
|
|
|
|
type MenuItems = MenuProps['items'];
|
|
|
|
export const ContextMenu: React.FC<{
|
|
frameworkEvents: DataSource<FrameworkEvent>;
|
|
nodes: Map<Id, ClientNode>;
|
|
hoveredNodeId?: Id;
|
|
focusedNodeId?: Id;
|
|
onFocusNode: (id?: Id) => void;
|
|
onContextMenuOpen: (open: boolean) => void;
|
|
onSetViewMode: (viewMode: ViewMode) => void;
|
|
onExpandRecursively: (id: Id) => void;
|
|
onCollapseRecursively: (id: Id) => void;
|
|
onCollapseNonAncestors: (id: Id) => void;
|
|
onSelectNode: OnSelectNode;
|
|
}> = ({
|
|
nodes,
|
|
frameworkEvents,
|
|
hoveredNodeId,
|
|
children,
|
|
focusedNodeId,
|
|
onFocusNode,
|
|
onContextMenuOpen,
|
|
onSetViewMode,
|
|
onExpandRecursively,
|
|
onCollapseRecursively,
|
|
onCollapseNonAncestors,
|
|
onSelectNode,
|
|
}) => {
|
|
const [_, setIdeItemsRerender] = useState(0);
|
|
const hoveredNode = nodes.get(hoveredNodeId ?? Number.MAX_SAFE_INTEGER);
|
|
|
|
const focus = hoveredNode != null &&
|
|
focusedNodeId !== hoveredNodeId &&
|
|
hoveredNode.bounds.height !== 0 &&
|
|
hoveredNode.bounds.width !== 0 && {
|
|
key: 'focus',
|
|
label: `Focus element`,
|
|
icon: <FullscreenExitOutlined />,
|
|
onClick: () => {
|
|
onFocusNode(hoveredNodeId);
|
|
},
|
|
};
|
|
|
|
const removeFocus = focusedNodeId && {
|
|
key: 'remove-focus',
|
|
label: 'Remove focus',
|
|
icon: <FullscreenOutlined />,
|
|
onClick: () => {
|
|
onFocusNode(undefined);
|
|
},
|
|
};
|
|
|
|
const matchingFrameworkEvents =
|
|
(hoveredNode &&
|
|
frameworkEvents.getAllRecordsByIndex({nodeId: hoveredNode.id})) ??
|
|
[];
|
|
|
|
const frameworkEventsTable = matchingFrameworkEvents.length > 0 &&
|
|
hoveredNode && {
|
|
key: 'events-table',
|
|
label: 'Explore events',
|
|
icon: <TableOutlined />,
|
|
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: <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
|
|
onOpenChange={(visible) => {
|
|
onContextMenuOpen(visible);
|
|
}}
|
|
menu={{
|
|
items,
|
|
onClick: () => {
|
|
onContextMenuOpen(false);
|
|
},
|
|
}}
|
|
trigger={['contextMenu']}>
|
|
{children}
|
|
</Dropdown>
|
|
);
|
|
};
|