Support providing records directly to DataTable
Summary: This diff allows using a DataTable without setting up a full datasource, which is simpler in case the dataset is small and/or fixed. For an continuously growing dataset a DataSource should still be set up. Reviewed By: jknoxville Differential Revision: D28037897 fbshipit-source-id: 2e56b1970e19f967c3752a78737b8f7a3f934b87
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9a6e84fedd
commit
00e4d2440d
@@ -49,7 +49,7 @@ import {debounce} from 'lodash';
|
|||||||
|
|
||||||
interface DataTableProps<T = any> {
|
interface DataTableProps<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
dataSource: DataSource<T, any, any>;
|
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
extraActions?: React.ReactElement;
|
extraActions?: React.ReactElement;
|
||||||
onSelect?(record: T | undefined, records: T[]): void;
|
onSelect?(record: T | undefined, records: T[]): void;
|
||||||
@@ -61,6 +61,12 @@ interface DataTableProps<T = any> {
|
|||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataTableInput<T = any> =
|
||||||
|
| {dataSource: DataSource<T, any, any>}
|
||||||
|
| {
|
||||||
|
records: T[];
|
||||||
|
};
|
||||||
|
|
||||||
export type DataTableColumn<T = any> = {
|
export type DataTableColumn<T = any> = {
|
||||||
key: keyof T & string;
|
key: keyof T & string;
|
||||||
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
||||||
@@ -94,9 +100,10 @@ export interface RenderContext<T = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DataTable<T extends object>(
|
export function DataTable<T extends object>(
|
||||||
props: DataTableProps<T>,
|
props: DataTableInput<T> & DataTableProps<T>,
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const {dataSource, onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
|
const {onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
|
||||||
|
const dataSource = normalizeDataSourceInput(props);
|
||||||
useAssertStableRef(dataSource, 'dataSource');
|
useAssertStableRef(dataSource, 'dataSource');
|
||||||
useAssertStableRef(onRowStyle, 'onRowStyle');
|
useAssertStableRef(onRowStyle, 'onRowStyle');
|
||||||
useAssertStableRef(props.onSelect, 'onRowSelect');
|
useAssertStableRef(props.onSelect, 'onRowSelect');
|
||||||
@@ -445,6 +452,42 @@ export function DataTable<T extends object>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
||||||
|
if ('dataSource' in props) {
|
||||||
|
return props.dataSource;
|
||||||
|
}
|
||||||
|
if ('records' in props) {
|
||||||
|
const [dataSource] = useState(() => {
|
||||||
|
const ds = new DataSource<T>(undefined);
|
||||||
|
syncRecordsToDataSource(ds, props.records);
|
||||||
|
return ds;
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
syncRecordsToDataSource(dataSource, props.records);
|
||||||
|
}, [dataSource, props.records]);
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Either the 'dataSource' or 'records' prop should be provided to DataTable`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
function syncRecordsToDataSource<T>(ds: DataSource<T>, records: T[]) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
ds.clear();
|
||||||
|
// TODO: optimize in the case we're only dealing with appends or replacements
|
||||||
|
records.forEach((r) => ds.append(r));
|
||||||
|
const duration = Math.abs(Date.now() - startTime);
|
||||||
|
if (duration > 50 || records.length > 1000) {
|
||||||
|
console.warn(
|
||||||
|
"The 'records' props is only intended to be used on small datasets. Please use a 'dataSource' instead. See createDataSource for details: https://fbflipper.com/docs/extending/flipper-plugin#createdatasource",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function emptyRenderer(dataSource: DataSource<any>) {
|
function emptyRenderer(dataSource: DataSource<any>) {
|
||||||
return <EmptyTable dataSource={dataSource} />;
|
return <EmptyTable dataSource={dataSource} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import {DataTable, DataTableColumn} from '../DataTable';
|
||||||
|
import {render} from '@testing-library/react';
|
||||||
|
|
||||||
|
type Todo = {
|
||||||
|
title: string;
|
||||||
|
done: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createTestRecords(): Todo[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'test DataTable',
|
||||||
|
done: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: DataTableColumn[] = [
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
wrap: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'done',
|
||||||
|
wrap: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test('update and append', async () => {
|
||||||
|
let records = createTestRecords();
|
||||||
|
const rendering = render(
|
||||||
|
<DataTable records={records} columns={columns} _testHeight={400} />,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(elem[0].parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-744e08-TableBodyColumnContainer e1luu51r0"
|
||||||
|
>
|
||||||
|
test DataTable
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-744e08-TableBodyColumnContainer e1luu51r0"
|
||||||
|
>
|
||||||
|
true
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerender() {
|
||||||
|
rendering.rerender(
|
||||||
|
<DataTable records={records} columns={columns} _testHeight={400} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// append
|
||||||
|
{
|
||||||
|
records = [
|
||||||
|
...records,
|
||||||
|
{
|
||||||
|
title: 'Drink coffee',
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
rerender();
|
||||||
|
const elem = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
{
|
||||||
|
records = [
|
||||||
|
{
|
||||||
|
title: 'DataTable tested',
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
...records.slice(1),
|
||||||
|
];
|
||||||
|
rerender();
|
||||||
|
const elem = await rendering.findAllByText('DataTable tested');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(rendering.queryByText('test DataTable')).toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
{
|
||||||
|
records = [records[1]];
|
||||||
|
rerender();
|
||||||
|
const elem = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(rendering.queryByText('DataTable tested')).toBeNull();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user