Files
flipper/desktop/plugins/public/ui-debugger/components/tree/ContextMenu.tsx
Lorenzo Blasa c1b0d9d753 Export node as JSON
Summary:
There has been multiple requests to incorporate an export to plain text functionality for a while.

This diff adds it.

It will export a node and optionally its chidren as JSON.

Reviewed By: antonk52

Differential Revision: D49596476

fbshipit-source-id: 3681bc0c2d02e1ea64ff589e0e272f6d54ad0524
2023-09-25 08:44:37 -07:00

224 lines
6.3 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,
MetadataId,
Metadata,
} from '../../ClientTypes';
import {OnSelectNode, ViewMode} from '../../DesktopTypes';
import React, {useState} from 'react';
import {DataSource, getFlipperLib} from 'flipper-plugin';
import {Dropdown, MenuProps, message} from 'antd';
import {tracker} from '../../utils/tracker';
import {
bigGrepContextMenuItems,
ideContextMenuItems,
} from '../fb-stubs/IDEContextMenu';
import {
CopyOutlined,
ExportOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
NodeExpandOutlined,
SnippetsOutlined,
TableOutlined,
} from '@ant-design/icons';
import {filterOutFalsy} from '../../utils/array';
import {exportNode} from '../../utils/dataTransform';
type MenuItems = MenuProps['items'];
export const ContextMenu: React.FC<{
frameworkEvents: DataSource<FrameworkEvent>;
nodes: Map<Id, ClientNode>;
metadata: Map<MetadataId, Metadata>;
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,
metadata,
}) => {
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);
},
}),
),
{type: 'divider'},
{
key: 'export-node',
label: 'Export',
icon: <ExportOutlined />,
onClick: () => {
getFlipperLib().writeTextToClipboard(
exportNode(hoveredNode, metadata, nodes),
);
message.success('Exported');
},
},
{
key: 'export-node-recursive',
label: 'Export with children',
icon: <ExportOutlined />,
onClick: () => {
getFlipperLib().writeTextToClipboard(
exportNode(hoveredNode, metadata, nodes, true),
);
message.success('Exported');
},
},
{type: 'divider'},
...(bigGrepContextMenuItems(hoveredNode) || []),
...(ideContextMenuItems(hoveredNode, () =>
setIdeItemsRerender((value) => value + 1),
) || []),
]);
return (
<Dropdown
onOpenChange={(visible) => {
onContextMenuOpen(visible);
}}
menu={{
items,
onClick: () => {
onContextMenuOpen(false);
},
}}
trigger={['contextMenu']}>
{children}
</Dropdown>
);
};