Add support for search and custom actions

Summary: Introduced search bar and support for custom buttons therein.

Reviewed By: nikoant

Differential Revision: D26338666

fbshipit-source-id: e53cd3c4381e11f5f90c05c92e39a6c8ac2eca65
This commit is contained in:
Michel Weststrate
2021-03-16 14:54:53 -07:00
committed by Facebook GitHub Bot
parent 44bb5b1beb
commit fb7c09c972
6 changed files with 147 additions and 13 deletions

View File

@@ -15,11 +15,14 @@ import {TableHead} from './TableHead';
import {Percentage} from '../utils/widthUtils'; import {Percentage} from '../utils/widthUtils';
import {DataSourceRenderer} from './DataSourceRenderer'; import {DataSourceRenderer} from './DataSourceRenderer';
import {useDataTableManager, TableManager} from './useDataTableManager'; import {useDataTableManager, TableManager} from './useDataTableManager';
import {TableSearch} from './TableSearch';
interface DataTableProps<T = any> { interface DataTableProps<T = any> {
columns: DataTableColumn<T>[]; columns: DataTableColumn<T>[];
dataSource: DataSource<T, any, any>; dataSource: DataSource<T, any, any>;
autoScroll?: boolean; autoScroll?: boolean;
extraActions?: React.ReactElement;
// custom onSearch(text, row) option?
tableManagerRef?: RefObject<TableManager>; tableManagerRef?: RefObject<TableManager>;
_testHeight?: number; // exposed for unit testing only _testHeight?: number; // exposed for unit testing only
} }
@@ -58,15 +61,21 @@ export function DataTable<T extends object>(props: DataTableProps<T>) {
return ( return (
<Layout.Top> <Layout.Top>
<TableHead <Layout.Container>
columns={tableManager.columns} <TableSearch
visibleColumns={tableManager.visibleColumns} onSearch={tableManager.setSearchValue}
onColumnResize={tableManager.resizeColumn} extraActions={props.extraActions}
onReset={tableManager.reset} />
onColumnToggleVisibility={tableManager.toggleColumnVisibility} <TableHead
sorting={tableManager.sorting} columns={tableManager.columns}
onColumnSort={tableManager.sortColumn} visibleColumns={tableManager.visibleColumns}
/> onColumnResize={tableManager.resizeColumn}
onReset={tableManager.reset}
onColumnToggleVisibility={tableManager.toggleColumnVisibility}
sorting={tableManager.sorting}
onColumnSort={tableManager.sortColumn}
/>
</Layout.Container>
<DataSourceRenderer<any, RenderContext> <DataSourceRenderer<any, RenderContext>
dataSource={props.dataSource} dataSource={props.dataSource}
autoScroll={props.autoScroll} autoScroll={props.autoScroll}

View File

@@ -13,7 +13,7 @@ import {
Percentage, Percentage,
Width, Width,
} from '../utils/widthUtils'; } from '../utils/widthUtils';
import {useRef} from 'react'; import {memo, useRef} from 'react';
import {Interactive, InteractiveProps} from '../Interactive'; import {Interactive, InteractiveProps} from '../Interactive';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react'; import React from 'react';
@@ -181,7 +181,7 @@ function TableHeadColumn({
); );
} }
export function TableHead({ export const TableHead = memo(function TableHead({
columns, columns,
visibleColumns, visibleColumns,
...props ...props
@@ -243,7 +243,7 @@ export function TableHead({
</Dropdown> </Dropdown>
</TableHeadContainer> </TableHeadContainer>
); );
} });
const SettingsButton = styled(Button)({ const SettingsButton = styled(Button)({
padding: 4, padding: 4,
@@ -251,4 +251,5 @@ const SettingsButton = styled(Button)({
right: 0, right: 0,
top: 0, top: 0,
backgroundColor: theme.backgroundWash, backgroundColor: theme.backgroundWash,
borderRadius: 0,
}); });

View File

@@ -12,6 +12,7 @@ import styled from '@emotion/styled';
import {theme} from 'flipper-plugin'; import {theme} from 'flipper-plugin';
import type {RenderContext} from './DataTable'; import type {RenderContext} from './DataTable';
import {Width} from '../utils/widthUtils'; import {Width} from '../utils/widthUtils';
import {pad} from 'lodash';
// heuristic for row estimation, should match any future styling updates // heuristic for row estimation, should match any future styling updates
export const DEFAULT_ROW_HEIGHT = 24; export const DEFAULT_ROW_HEIGHT = 24;
@@ -91,6 +92,13 @@ export const TableRow = memo(function TableRow(props: Props) {
value = value ? 'true' : 'false'; value = value ? 'true' : 'false';
} }
if (value instanceof Date) {
value =
value.toTimeString().split(' ')[0] +
'.' +
pad('' + value.getMilliseconds(), 3);
}
return ( return (
<TableBodyColumnContainer <TableBodyColumnContainer
className="ant-table-cell" className="ant-table-cell"

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {Input} from 'antd';
import React, {memo} from 'react';
import {Layout} from '../Layout';
import {theme} from '../theme';
export const TableSearch = memo(function TableSearch({
onSearch,
extraActions,
}: {
onSearch(value: string): void;
extraActions?: React.ReactElement;
}) {
return (
<Layout.Horizontal
gap
style={{
backgroundColor: theme.backgroundWash,
padding: theme.space.small,
}}>
<Input.Search allowClear placeholder="Search..." onSearch={onSearch} />
{extraActions}
</Layout.Horizontal>
);
});

View File

@@ -12,6 +12,7 @@ import {DataTable, DataTableColumn} from '../DataTable';
import {render, act} from '@testing-library/react'; import {render, act} from '@testing-library/react';
import {createDataSource} from '../../../state/datasource/DataSource'; import {createDataSource} from '../../../state/datasource/DataSource';
import {TableManager} from '../useDataTableManager'; import {TableManager} from '../useDataTableManager';
import {Button} from 'antd';
type Todo = { type Todo = {
title: string; title: string;
@@ -222,3 +223,57 @@ test('sorting', async () => {
]); ]);
} }
}); });
test('search', async () => {
const ds = createTestDataSource();
ds.clear();
ds.append({
title: 'item abc',
done: false,
});
ds.append({
title: 'item x',
done: false,
});
ds.append({
title: 'item b',
done: false,
});
const ref = createRef<TableManager>();
const rendering = render(
<DataTable
dataSource={ds}
columns={columns}
tableManagerRef={ref}
extraActions={<Button>Test Button</Button>}
_testHeight={400}
/>,
);
{
// button is visible
rendering.getByText('Test Button');
const elem = await rendering.findAllByText(/item/);
expect(elem.length).toBe(3);
expect(elem.map((e) => e.textContent)).toEqual([
'item abc',
'item x',
'item b',
]);
}
{
// filter
act(() => {
ref.current?.setSearchValue('b');
});
const elem = await rendering.findAllByText(/item/);
expect(elem.map((e) => e.textContent)).toEqual(['item abc', 'item b']);
}
{
// reset
act(() => {
ref.current?.reset();
});
const elem = await rendering.findAllByText(/item/);
expect(elem.length).toBe(3);
}
});

View File

@@ -10,7 +10,7 @@
import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable'; import {DataTableColumn} from 'flipper-plugin/src/ui/datatable/DataTable';
import {Percentage} from '../utils/widthUtils'; import {Percentage} from '../utils/widthUtils';
import produce from 'immer'; import produce from 'immer';
import {useCallback, useMemo, useState} from 'react'; import {useCallback, useEffect, useMemo, useState} from 'react';
import {DataSource} from '../../state/datasource/DataSource'; import {DataSource} from '../../state/datasource/DataSource';
export type OnColumnResize = (id: string, size: number | Percentage) => void; export type OnColumnResize = (id: string, size: number | Percentage) => void;
@@ -33,11 +33,30 @@ export function useDataTableManager<T extends object>(
computeInitialColumns(defaultColumns), computeInitialColumns(defaultColumns),
); );
const [sorting, setSorting] = useState<Sorting | undefined>(undefined); const [sorting, setSorting] = useState<Sorting | undefined>(undefined);
const [searchValue, setSearchValue] = useState('');
const visibleColumns = useMemo( const visibleColumns = useMemo(
() => columns.filter((column) => column.visible), () => columns.filter((column) => column.visible),
[columns], [columns],
); );
// filter is computed by useMemo to support adding column filters etc here in the future
const currentFilter = useMemo(
function computeFilter() {
if (searchValue === '') {
// unset
return undefined;
}
const searchString = searchValue.toLowerCase();
return function dataTableFilter(item: object) {
return Object.values(item).some((v) =>
String(v).toLowerCase().includes(searchString),
);
};
},
[searchValue],
);
const reset = useCallback(() => { const reset = useCallback(() => {
setEffectiveColumns(computeInitialColumns(defaultColumns)); setEffectiveColumns(computeInitialColumns(defaultColumns));
setSorting(undefined); setSorting(undefined);
@@ -88,6 +107,13 @@ export function useDataTableManager<T extends object>(
); );
}, []); }, []);
useEffect(
function applyFilter() {
dataSource.setFilter(currentFilter);
},
[currentFilter, dataSource],
);
return { return {
/** The default columns, but normalized */ /** The default columns, but normalized */
columns, columns,
@@ -103,6 +129,8 @@ export function useDataTableManager<T extends object>(
sortColumn, sortColumn,
/** Show / hide the given column */ /** Show / hide the given column */
toggleColumnVisibility, toggleColumnVisibility,
/** Active search value */
setSearchValue,
}; };
} }