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:
committed by
Facebook GitHub Bot
parent
44bb5b1beb
commit
fb7c09c972
@@ -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,6 +61,11 @@ export function DataTable<T extends object>(props: DataTableProps<T>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Top>
|
<Layout.Top>
|
||||||
|
<Layout.Container>
|
||||||
|
<TableSearch
|
||||||
|
onSearch={tableManager.setSearchValue}
|
||||||
|
extraActions={props.extraActions}
|
||||||
|
/>
|
||||||
<TableHead
|
<TableHead
|
||||||
columns={tableManager.columns}
|
columns={tableManager.columns}
|
||||||
visibleColumns={tableManager.visibleColumns}
|
visibleColumns={tableManager.visibleColumns}
|
||||||
@@ -67,6 +75,7 @@ export function DataTable<T extends object>(props: DataTableProps<T>) {
|
|||||||
sorting={tableManager.sorting}
|
sorting={tableManager.sorting}
|
||||||
onColumnSort={tableManager.sortColumn}
|
onColumnSort={tableManager.sortColumn}
|
||||||
/>
|
/>
|
||||||
|
</Layout.Container>
|
||||||
<DataSourceRenderer<any, RenderContext>
|
<DataSourceRenderer<any, RenderContext>
|
||||||
dataSource={props.dataSource}
|
dataSource={props.dataSource}
|
||||||
autoScroll={props.autoScroll}
|
autoScroll={props.autoScroll}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
33
desktop/flipper-plugin/src/ui/datatable/TableSearch.tsx
Normal file
33
desktop/flipper-plugin/src/ui/datatable/TableSearch.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user