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:
Michel Weststrate
2021-04-27 14:52:34 -07:00
committed by Facebook GitHub Bot
parent 9a6e84fedd
commit 00e4d2440d
2 changed files with 154 additions and 3 deletions

View File

@@ -49,7 +49,7 @@ import {debounce} from 'lodash';
interface DataTableProps<T = any> {
columns: DataTableColumn<T>[];
dataSource: DataSource<T, any, any>;
autoScroll?: boolean;
extraActions?: React.ReactElement;
onSelect?(record: T | undefined, records: T[]): void;
@@ -61,6 +61,12 @@ interface DataTableProps<T = any> {
onContextMenu?: (selection: undefined | T) => React.ReactElement;
}
type DataTableInput<T = any> =
| {dataSource: DataSource<T, any, any>}
| {
records: T[];
};
export type DataTableColumn<T = any> = {
key: keyof T & string;
// 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>(
props: DataTableProps<T>,
props: DataTableInput<T> & DataTableProps<T>,
): React.ReactElement {
const {dataSource, onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
const {onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
const dataSource = normalizeDataSourceInput(props);
useAssertStableRef(dataSource, 'dataSource');
useAssertStableRef(onRowStyle, 'onRowStyle');
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>) {
return <EmptyTable dataSource={dataSource} />;
}

View File

@@ -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();
}
});