Implement context menu
Summary: Re-introduced context menu to DataTable, due to popular demand. Originally it wasn't there to better align with ant design principles, but in an app like Flipper it makes just too much sense to have it See e.g. https://fb.workplace.com/groups/flippersupport/permalink/1138285579985432/ changelog: Restored context menu in data tables Reviewed By: passy Differential Revision: D28996137 fbshipit-source-id: 16ef4c90997c9313efa62da7576fd453a7853761
This commit is contained in:
committed by
Facebook GitHub Bot
parent
abc9785e0e
commit
7e4df00138
@@ -169,6 +169,8 @@ DataList.defaultProps = {
|
||||
enableSearchbar: false,
|
||||
enableColumnHeaders: false,
|
||||
enableArrow: true,
|
||||
enableContextMenu: false,
|
||||
enableMultiSelect: false,
|
||||
};
|
||||
|
||||
const DataListItem = memo(
|
||||
|
||||
@@ -32,7 +32,7 @@ export {DataValueExtractor} from './DataPreview';
|
||||
|
||||
export const RootDataContext = createContext<() => any>(() => ({}));
|
||||
|
||||
const contextMenuTrigger = ['contextMenu' as const];
|
||||
export const contextMenuTrigger = ['contextMenu' as const];
|
||||
|
||||
const BaseContainer = styled.div<{depth?: number; disabled?: boolean}>(
|
||||
(props) => ({
|
||||
|
||||
@@ -59,6 +59,7 @@ interface DataTableBaseProps<T = any> {
|
||||
enableAutoScroll?: boolean;
|
||||
enableColumnHeaders?: boolean;
|
||||
enableMultiSelect?: boolean;
|
||||
enableContextMenu?: boolean;
|
||||
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
||||
scrollable?: boolean;
|
||||
extraActions?: React.ReactElement;
|
||||
@@ -119,6 +120,7 @@ export interface TableRowRenderContext<T = any> {
|
||||
itemId: number,
|
||||
): void;
|
||||
onRowStyle?(item: T): React.CSSProperties | undefined;
|
||||
onContextMenu?(): React.ReactElement;
|
||||
}
|
||||
|
||||
export type DataTableProps<T> = DataTableInput<T> & DataTableBaseProps<T>;
|
||||
@@ -185,7 +187,10 @@ export function DataTable<T extends object>(
|
||||
},
|
||||
onMouseDown(e, _item, index) {
|
||||
if (!dragging.current) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.buttons > 1) {
|
||||
// for right click we only want to add if needed, not deselect
|
||||
tableManager.addRangeToSelection(index, index, false);
|
||||
} else if (e.ctrlKey || e.metaKey) {
|
||||
tableManager.addRangeToSelection(index, index, true);
|
||||
} else if (e.shiftKey) {
|
||||
tableManager.selectItem(index, true, true);
|
||||
@@ -205,8 +210,15 @@ export function DataTable<T extends object>(
|
||||
}
|
||||
},
|
||||
onRowStyle,
|
||||
onContextMenu: props.enableContextMenu
|
||||
? () => {
|
||||
// using a ref keeps the config stable, so that a new context menu doesn't need
|
||||
// all rows to be rerendered, but rather shows it conditionally
|
||||
return contextMenuRef.current?.()!;
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}, [visibleColumns, tableManager, onRowStyle]);
|
||||
}, [visibleColumns, tableManager, onRowStyle, props.enableContextMenu]);
|
||||
|
||||
const itemRenderer = useCallback(
|
||||
function itemRenderer(
|
||||
@@ -415,6 +427,9 @@ export function DataTable<T extends object>(
|
||||
],
|
||||
);
|
||||
|
||||
const contextMenuRef = useRef(contexMenu);
|
||||
contextMenuRef.current = contexMenu;
|
||||
|
||||
useEffect(function initialSetup() {
|
||||
return function cleanup() {
|
||||
// write current prefs to local storage
|
||||
@@ -519,7 +534,8 @@ DataTable.defaultProps = {
|
||||
enableSearchbar: true,
|
||||
enableAutoScroll: false,
|
||||
enableColumnHeaders: true,
|
||||
eanbleMultiSelect: true,
|
||||
enableMultiSelect: true,
|
||||
enableContextMenu: true,
|
||||
onRenderEmpty: emptyRenderer,
|
||||
} as Partial<DataTableProps<any>>;
|
||||
|
||||
|
||||
@@ -45,19 +45,19 @@ export function tableContextMenuFactory<T>(
|
||||
);
|
||||
}
|
||||
const hasSelection = selection.items.size > 0 ?? false;
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{onContextMenu
|
||||
? onContextMenu(getSelectedItem(datasource, selection))
|
||||
: null}
|
||||
<SubMenu
|
||||
key="filter same"
|
||||
title="Filter on same"
|
||||
icon={<FilterOutlined />}
|
||||
disabled={!hasSelection}>
|
||||
{visibleColumns.map((column) => (
|
||||
{visibleColumns.map((column, idx) => (
|
||||
<Item
|
||||
key={column.key}
|
||||
key={column.key ?? idx}
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'setColumnFilterFromSelection',
|
||||
@@ -69,12 +69,13 @@ export function tableContextMenuFactory<T>(
|
||||
))}
|
||||
</SubMenu>
|
||||
<SubMenu
|
||||
key="copy cells"
|
||||
title="Copy cell(s)"
|
||||
icon={<CopyOutlined />}
|
||||
disabled={!hasSelection}>
|
||||
{visibleColumns.map((column) => (
|
||||
{visibleColumns.map((column, idx) => (
|
||||
<Item
|
||||
key={column.key}
|
||||
key={column.key ?? idx}
|
||||
onClick={() => {
|
||||
const items = getSelectedItems(datasource, selection);
|
||||
if (items.length) {
|
||||
@@ -88,6 +89,7 @@ export function tableContextMenuFactory<T>(
|
||||
))}
|
||||
</SubMenu>
|
||||
<Item
|
||||
key="copyToClipboard"
|
||||
disabled={!hasSelection}
|
||||
onClick={() => {
|
||||
const items = getSelectedItems(datasource, selection);
|
||||
@@ -99,6 +101,7 @@ export function tableContextMenuFactory<T>(
|
||||
</Item>
|
||||
{lib.isFB && (
|
||||
<Item
|
||||
key="createPaste"
|
||||
disabled={!hasSelection}
|
||||
onClick={() => {
|
||||
const items = getSelectedItems(datasource, selection);
|
||||
@@ -110,9 +113,9 @@ export function tableContextMenuFactory<T>(
|
||||
</Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<SubMenu title="Visible columns">
|
||||
{columns.map((column) => (
|
||||
<Menu.Item key={column.key}>
|
||||
<SubMenu title="Visible columns" key="visible columns">
|
||||
{columns.map((column, idx) => (
|
||||
<Menu.Item key={column.key ?? idx}>
|
||||
<Checkbox
|
||||
checked={column.visible}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -13,6 +13,8 @@ import {theme} from '../theme';
|
||||
import type {TableRowRenderContext} from './DataTable';
|
||||
import {Width} from '../../utils/widthUtils';
|
||||
import {DataFormatter} from '../DataFormatter';
|
||||
import {Dropdown} from 'antd';
|
||||
import {contextMenuTrigger} from '../data-inspector/DataInspectorNode';
|
||||
|
||||
// heuristic for row estimation, should match any future styling updates
|
||||
export const DEFAULT_ROW_HEIGHT = 24;
|
||||
@@ -106,7 +108,7 @@ export const TableRow = memo(function TableRow<T>({
|
||||
highlighted,
|
||||
config,
|
||||
}: TableRowProps<T>) {
|
||||
return (
|
||||
const row = (
|
||||
<TableBodyRowContainer
|
||||
highlighted={highlighted}
|
||||
onMouseDown={(e) => {
|
||||
@@ -135,4 +137,13 @@ export const TableRow = memo(function TableRow<T>({
|
||||
})}
|
||||
</TableBodyRowContainer>
|
||||
);
|
||||
if (config.onContextMenu) {
|
||||
return (
|
||||
<Dropdown overlay={config.onContextMenu} trigger={contextMenuTrigger}>
|
||||
{row}
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ test('update and append', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<div
|
||||
class="css-9bipfg-TableBodyColumnContainer e1luu51r0"
|
||||
@@ -104,7 +104,7 @@ test('column visibility', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<div
|
||||
class="css-9bipfg-TableBodyColumnContainer e1luu51r0"
|
||||
@@ -131,7 +131,7 @@ test('column visibility', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<div
|
||||
class="css-9bipfg-TableBodyColumnContainer e1luu51r0"
|
||||
|
||||
@@ -44,7 +44,7 @@ test('update and append', async () => {
|
||||
expect(elem.length).toBe(1);
|
||||
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<div
|
||||
class="css-9bipfg-TableBodyColumnContainer e1luu51r0"
|
||||
|
||||
@@ -116,7 +116,7 @@ test('It can render rows', async () => {
|
||||
expect((await renderer.findByText('unique-string')).parentElement)
|
||||
.toMatchInlineSnapshot(`
|
||||
<div
|
||||
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||
>
|
||||
<div
|
||||
class="css-1vr131n-TableBodyColumnContainer e1luu51r0"
|
||||
|
||||
Reference in New Issue
Block a user