Refactored DataView to be the primary data driver for DataTable instead
Summary: In order to accomplish multi-panel mode, we need to use multiple data views on the same data source so that the filters can be applied differently, etc. This diff serves to refactor DataTable and some of its associated classes to use DataView as the primary driver for data management. Additionally, the diff refactored the state to allow multi-paneling to be on the DataPanel layer instead of the DataTable layer for ease of usage This is the last diff of the larger stack which introduces the multi-panel mode feature. A possible next step could be allowing infinite(up to a certain limit) panels to be populated. Changelog: Introduced side by side view feature for `DataTable`. There is now a new boolean for `DataTable` props called `enableMultiPanels`. If this is passed in, then the table will have an option to open a different "side panel" using a completely different dataview which allows different filters, searches, etc. Reviewed By: mweststrate Differential Revision: D37685390 fbshipit-source-id: 51e35f59da1ceba07ba8d379066970b57ab1734e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
96a23495c9
commit
3fbf1215ec
@@ -20,6 +20,8 @@ const defaultLimit = 100 * 1000;
|
||||
// rather than search and remove the affected individual items
|
||||
const shiftRebuildTreshold = 0.05;
|
||||
|
||||
const DEFAULT_VIEW_ID = '0';
|
||||
|
||||
type AppendEvent<T> = {
|
||||
type: 'append';
|
||||
entry: Entry<T>;
|
||||
@@ -28,7 +30,9 @@ type UpdateEvent<T> = {
|
||||
type: 'update';
|
||||
entry: Entry<T>;
|
||||
oldValue: T;
|
||||
oldVisible: boolean;
|
||||
oldVisible: {
|
||||
[viewId: string]: boolean;
|
||||
};
|
||||
index: number;
|
||||
};
|
||||
type RemoveEvent<T> = {
|
||||
@@ -51,8 +55,12 @@ type DataEvent<T> =
|
||||
type Entry<T> = {
|
||||
value: T;
|
||||
id: number; // insertion based
|
||||
visible: boolean; // matches current filter?
|
||||
approxIndex: number; // we could possible live at this index in the output. No guarantees.
|
||||
visible: {
|
||||
[viewId: string]: boolean;
|
||||
}; // matches current filter?
|
||||
approxIndex: {
|
||||
[viewId: string]: number;
|
||||
}; // we could possible live at this index in the output. No guarantees.
|
||||
};
|
||||
|
||||
type Primitive = number | string | boolean | null | undefined;
|
||||
@@ -138,9 +146,14 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
*/
|
||||
public readonly view: DataSourceView<T, KeyType>;
|
||||
|
||||
public readonly additionalViews: {
|
||||
[viewId: string]: DataSourceView<T, KeyType>;
|
||||
};
|
||||
|
||||
constructor(keyAttribute: keyof T | undefined) {
|
||||
this.keyAttribute = keyAttribute;
|
||||
this.view = new DataSourceView<T, KeyType>(this);
|
||||
this.view = new DataSourceView<T, KeyType>(this, DEFAULT_VIEW_ID);
|
||||
this.additionalViews = {};
|
||||
}
|
||||
|
||||
public get size() {
|
||||
@@ -228,12 +241,17 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
this._recordsById.set(key, value);
|
||||
this.storeIndexOfKey(key, this._records.length);
|
||||
}
|
||||
const visibleMap: {[viewId: string]: boolean} = {[DEFAULT_VIEW_ID]: false};
|
||||
const approxIndexMap: {[viewId: string]: number} = {[DEFAULT_VIEW_ID]: -1};
|
||||
Object.keys(this.additionalViews).forEach((viewId) => {
|
||||
visibleMap[viewId] = false;
|
||||
approxIndexMap[viewId] = -1;
|
||||
});
|
||||
const entry = {
|
||||
value,
|
||||
id: ++this.nextId,
|
||||
// once we have multiple views, the following fields should be stored per view
|
||||
visible: true,
|
||||
approxIndex: -1,
|
||||
visible: visibleMap,
|
||||
approxIndex: approxIndexMap,
|
||||
};
|
||||
this._records.push(entry);
|
||||
this.emitDataEvent({
|
||||
@@ -268,7 +286,7 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
if (value === oldValue) {
|
||||
return;
|
||||
}
|
||||
const oldVisible = entry.visible;
|
||||
const oldVisible = {...entry.visible};
|
||||
entry.value = value;
|
||||
if (this.keyAttribute) {
|
||||
const key = this.getKey(value);
|
||||
@@ -374,7 +392,7 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
// let's fallback to the async processing of all data instead
|
||||
// MWE: there is a risk here that rebuilding is too blocking, as this might happen
|
||||
// in background when new data arrives, and not explicitly on a user interaction
|
||||
this.view.rebuild();
|
||||
this.rebuild();
|
||||
} else {
|
||||
this.emitDataEvent({
|
||||
type: 'shift',
|
||||
@@ -392,17 +410,51 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
this._recordsById = new Map();
|
||||
this.shiftOffset = 0;
|
||||
this.idToIndex = new Map();
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* The rebuild function that would support rebuilding multiple views all at once
|
||||
*/
|
||||
public rebuild() {
|
||||
this.view.rebuild();
|
||||
Object.entries(this.additionalViews).forEach(([, dataView]) => {
|
||||
dataView.rebuild();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fork of this dataSource, that shares the source data with this dataSource,
|
||||
* but has it's own FSRW pipeline, to allow multiple views on the same data
|
||||
*/
|
||||
public fork(): DataSourceView<T, KeyType> {
|
||||
throw new Error(
|
||||
'Not implemented. Please contact oncall if this feature is needed',
|
||||
);
|
||||
private fork(viewId: string): DataSourceView<T, KeyType> {
|
||||
this._records.forEach((entry) => {
|
||||
entry.visible[viewId] = entry.visible[DEFAULT_VIEW_ID];
|
||||
entry.approxIndex[viewId] = entry.approxIndex[DEFAULT_VIEW_ID];
|
||||
});
|
||||
const newView = new DataSourceView<T, KeyType>(this, viewId);
|
||||
// Refresh the new view so that it has all the existing records.
|
||||
newView.rebuild();
|
||||
return newView;
|
||||
}
|
||||
|
||||
public getAdditionalView(viewId: string): DataSourceView<T, KeyType> {
|
||||
if (viewId in this.additionalViews) {
|
||||
return this.additionalViews[viewId];
|
||||
}
|
||||
this.additionalViews[viewId] = this.fork(viewId);
|
||||
return this.additionalViews[viewId];
|
||||
}
|
||||
|
||||
public deleteView(viewId: string): void {
|
||||
if (viewId in this.additionalViews) {
|
||||
delete this.additionalViews[viewId];
|
||||
// TODO: Ideally remove the viewId in the visible and approxIndex of DataView outputs
|
||||
this._records.forEach((entry) => {
|
||||
delete entry.visible[viewId];
|
||||
delete entry.approxIndex[viewId];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private assertKeySet() {
|
||||
@@ -433,6 +485,9 @@ export class DataSource<T extends any, KeyType = never> {
|
||||
// using a queue,
|
||||
// or only if there is an active view (although that could leak memory)
|
||||
this.view.processEvent(event);
|
||||
Object.entries(this.additionalViews).forEach(([, dataView]) => {
|
||||
dataView.processEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,7 +512,7 @@ function unwrap<T>(entry: Entry<T>): T {
|
||||
return entry?.value;
|
||||
}
|
||||
|
||||
class DataSourceView<T, KeyType> {
|
||||
export class DataSourceView<T, KeyType> {
|
||||
public readonly datasource: DataSource<T, KeyType>;
|
||||
private sortBy: undefined | ((a: T) => Primitive) = undefined;
|
||||
private reverse: boolean = false;
|
||||
@@ -471,6 +526,7 @@ class DataSourceView<T, KeyType> {
|
||||
* @readonly
|
||||
*/
|
||||
public windowEnd = 0;
|
||||
private viewId;
|
||||
|
||||
private outputChangeListeners = new Set<(change: OutputChange) => void>();
|
||||
|
||||
@@ -479,8 +535,9 @@ class DataSourceView<T, KeyType> {
|
||||
*/
|
||||
private _output: Entry<T>[] = [];
|
||||
|
||||
constructor(datasource: DataSource<T, KeyType>) {
|
||||
constructor(datasource: DataSource<T, KeyType>, viewId: string) {
|
||||
this.datasource = datasource;
|
||||
this.viewId = viewId;
|
||||
}
|
||||
|
||||
public get size() {
|
||||
@@ -591,8 +648,11 @@ class DataSourceView<T, KeyType> {
|
||||
// so any changes in the entry being moved around etc will be reflected in the original `entry` object,
|
||||
// and we just want to verify that this entry is indeed still the same element, visible, and still present in
|
||||
// the output data set.
|
||||
if (entry.visible && entry.id === this._output[entry.approxIndex]?.id) {
|
||||
return this.normalizeIndex(entry.approxIndex);
|
||||
if (
|
||||
entry.visible[this.viewId] &&
|
||||
entry.id === this._output[entry.approxIndex[this.viewId]]?.id
|
||||
) {
|
||||
return this.normalizeIndex(entry.approxIndex[this.viewId]);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -674,16 +734,16 @@ class DataSourceView<T, KeyType> {
|
||||
switch (event.type) {
|
||||
case 'append': {
|
||||
const {entry} = event;
|
||||
entry.visible = filter ? filter(entry.value) : true;
|
||||
if (!entry.visible) {
|
||||
entry.visible[this.viewId] = filter ? filter(entry.value) : true;
|
||||
if (!entry.visible[this.viewId]) {
|
||||
// not in filter? skip this entry
|
||||
return;
|
||||
}
|
||||
if (!sortBy) {
|
||||
// no sorting? insert at the end, or beginning
|
||||
entry.approxIndex = output.length;
|
||||
entry.approxIndex[this.viewId] = output.length;
|
||||
output.push(entry);
|
||||
this.notifyItemShift(entry.approxIndex, 1);
|
||||
this.notifyItemShift(entry.approxIndex[this.viewId], 1);
|
||||
} else {
|
||||
this.insertSorted(entry);
|
||||
}
|
||||
@@ -691,13 +751,13 @@ class DataSourceView<T, KeyType> {
|
||||
}
|
||||
case 'update': {
|
||||
const {entry} = event;
|
||||
entry.visible = filter ? filter(entry.value) : true;
|
||||
entry.visible[this.viewId] = filter ? filter(entry.value) : true;
|
||||
// short circuit; no view active so update straight away
|
||||
if (!filter && !sortBy) {
|
||||
output[event.index].approxIndex = event.index;
|
||||
output[event.index].approxIndex[this.viewId] = event.index;
|
||||
this.notifyItemUpdated(event.index);
|
||||
} else if (!event.oldVisible) {
|
||||
if (!entry.visible) {
|
||||
} else if (!event.oldVisible[this.viewId]) {
|
||||
if (!entry.visible[this.viewId]) {
|
||||
// Done!
|
||||
} else {
|
||||
// insertion, not visible before
|
||||
@@ -706,7 +766,7 @@ class DataSourceView<T, KeyType> {
|
||||
} else {
|
||||
// Entry was visible previously
|
||||
const existingIndex = this.getSortedIndex(entry, event.oldValue);
|
||||
if (!entry.visible) {
|
||||
if (!entry.visible[this.viewId]) {
|
||||
// Remove from output
|
||||
output.splice(existingIndex, 1);
|
||||
this.notifyItemShift(existingIndex, -1);
|
||||
@@ -744,7 +804,7 @@ class DataSourceView<T, KeyType> {
|
||||
} else {
|
||||
// if there is a filter, count the visibles and shift those
|
||||
for (let i = 0; i < event.entries.length; i++)
|
||||
if (event.entries[i].visible) amount++;
|
||||
if (event.entries[i].visible[this.viewId]) amount++;
|
||||
}
|
||||
output.splice(0, amount);
|
||||
this.notifyItemShift(0, -amount);
|
||||
@@ -766,7 +826,7 @@ class DataSourceView<T, KeyType> {
|
||||
const {_output: output, sortBy, filter} = this;
|
||||
|
||||
// filter active, and not visible? short circuilt
|
||||
if (!entry.visible) {
|
||||
if (!entry.visible[this.viewId]) {
|
||||
return;
|
||||
}
|
||||
// no sorting, no filter?
|
||||
@@ -798,8 +858,8 @@ class DataSourceView<T, KeyType> {
|
||||
const records: Entry<T>[] = this.datasource._records;
|
||||
let output = filter
|
||||
? records.filter((entry) => {
|
||||
entry.visible = filter(entry.value);
|
||||
return entry.visible;
|
||||
entry.visible[this.viewId] = filter(entry.value);
|
||||
return entry.visible[this.viewId];
|
||||
})
|
||||
: records.slice();
|
||||
if (sortBy) {
|
||||
@@ -818,7 +878,7 @@ class DataSourceView<T, KeyType> {
|
||||
|
||||
// write approx indexes for faster lookup of entries in visible output
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
output[i].approxIndex = i;
|
||||
output[i].approxIndex[this.viewId] = i;
|
||||
}
|
||||
this._output = output;
|
||||
this.notifyReset(output.length);
|
||||
@@ -829,17 +889,17 @@ class DataSourceView<T, KeyType> {
|
||||
|
||||
private getSortedIndex(entry: Entry<T>, oldValue: T) {
|
||||
const {_output: output} = this;
|
||||
if (output[entry.approxIndex] === entry) {
|
||||
if (output[entry.approxIndex[this.viewId]] === entry) {
|
||||
// yay!
|
||||
return entry.approxIndex;
|
||||
return entry.approxIndex[this.viewId];
|
||||
}
|
||||
let index = sortedIndexBy(
|
||||
output,
|
||||
{
|
||||
value: oldValue,
|
||||
id: -1,
|
||||
visible: true,
|
||||
approxIndex: -1,
|
||||
visible: entry.visible,
|
||||
approxIndex: entry.approxIndex,
|
||||
},
|
||||
this.sortHelper,
|
||||
);
|
||||
@@ -862,7 +922,7 @@ class DataSourceView<T, KeyType> {
|
||||
entry,
|
||||
this.sortHelper,
|
||||
);
|
||||
entry.approxIndex = insertionIndex;
|
||||
entry.approxIndex[this.viewId] = insertionIndex;
|
||||
this._output.splice(insertionIndex, 0, entry);
|
||||
this.notifyItemShift(insertionIndex, 1);
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {DataSource} from './DataSource';
|
||||
import {DataSourceView} from './DataSource';
|
||||
import React, {memo, useCallback, useEffect, useState} from 'react';
|
||||
|
||||
import {RedrawContext} from './DataSourceRendererVirtual';
|
||||
|
||||
type DataSourceProps<T extends object, C> = {
|
||||
/**
|
||||
* The data source to render
|
||||
* The data view to render
|
||||
*/
|
||||
dataSource: DataSource<T, T[keyof T]>;
|
||||
dataView: DataSourceView<T, T[keyof T]>;
|
||||
/**
|
||||
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
|
||||
*/
|
||||
@@ -30,11 +30,12 @@ type DataSourceProps<T extends object, C> = {
|
||||
itemRenderer(item: T, index: number, context: C): React.ReactElement;
|
||||
useFixedRowHeight: boolean;
|
||||
defaultRowHeight: number;
|
||||
maxRecords: number;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||
emptyRenderer?:
|
||||
| null
|
||||
| ((dataSource: DataSource<T, T[keyof T]>) => React.ReactElement);
|
||||
| ((dataView: DataSourceView<T, T[keyof T]>) => React.ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -44,7 +45,8 @@ type DataSourceProps<T extends object, C> = {
|
||||
export const DataSourceRendererStatic: <T extends object, C>(
|
||||
props: DataSourceProps<T, C>,
|
||||
) => React.ReactElement = memo(function DataSourceRendererStatic({
|
||||
dataSource,
|
||||
dataView,
|
||||
maxRecords,
|
||||
useFixedRowHeight,
|
||||
context,
|
||||
itemRenderer,
|
||||
@@ -65,8 +67,8 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
||||
function subscribeToDataSource() {
|
||||
let unmounted = false;
|
||||
|
||||
dataSource.view.setWindow(0, dataSource.limit);
|
||||
const unsubscribe = dataSource.view.addListener((_event) => {
|
||||
dataView.setWindow(0, maxRecords);
|
||||
const unsubscribe = dataView.addListener((_event) => {
|
||||
if (unmounted) {
|
||||
return;
|
||||
}
|
||||
@@ -78,7 +80,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
[dataSource, setForceUpdate, useFixedRowHeight],
|
||||
[dataView, maxRecords, setForceUpdate, useFixedRowHeight],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -89,7 +91,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
||||
/**
|
||||
* Rendering
|
||||
*/
|
||||
const records = dataSource.view.output();
|
||||
const records = dataView.output();
|
||||
if (records.length > 500) {
|
||||
console.warn(
|
||||
"StaticDataSourceRenderer should only be used on small datasets. For large datasets the 'scrollable' flag should enabled on DataTable",
|
||||
@@ -100,7 +102,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
||||
<RedrawContext.Provider value={redraw}>
|
||||
<div onKeyDown={onKeyDown} tabIndex={0}>
|
||||
{records.length === 0
|
||||
? emptyRenderer?.(dataSource)
|
||||
? emptyRenderer?.(dataView)
|
||||
: records.map((item, index) => (
|
||||
<div key={index}>{itemRenderer(item, index, context)}</div>
|
||||
))}
|
||||
|
||||
@@ -18,7 +18,7 @@ import React, {
|
||||
useContext,
|
||||
createContext,
|
||||
} from 'react';
|
||||
import {DataSource} from './DataSource';
|
||||
import {DataSourceView} from './DataSource';
|
||||
import {useVirtual} from 'react-virtual';
|
||||
import observeRect from '@reach/observe-rect';
|
||||
|
||||
@@ -39,7 +39,7 @@ type DataSourceProps<T extends object, C> = {
|
||||
/**
|
||||
* The data source to render
|
||||
*/
|
||||
dataSource: DataSource<T, T[keyof T]>;
|
||||
dataView: DataSourceView<T, T[keyof T]>;
|
||||
/**
|
||||
* Automatically scroll if the user is near the end?
|
||||
*/
|
||||
@@ -68,7 +68,7 @@ type DataSourceProps<T extends object, C> = {
|
||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||
emptyRenderer?:
|
||||
| null
|
||||
| ((dataSource: DataSource<T, T[keyof T]>) => React.ReactElement);
|
||||
| ((dataView: DataSourceView<T, T[keyof T]>) => React.ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,7 +78,7 @@ type DataSourceProps<T extends object, C> = {
|
||||
export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
props: DataSourceProps<T, C>,
|
||||
) => React.ReactElement = memo(function DataSourceRendererVirtual({
|
||||
dataSource,
|
||||
dataView,
|
||||
defaultRowHeight,
|
||||
useFixedRowHeight,
|
||||
context,
|
||||
@@ -102,7 +102,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
const isUnitTest = useInUnitTest();
|
||||
|
||||
const virtualizer = useVirtual({
|
||||
size: dataSource.view.size,
|
||||
size: dataView.size,
|
||||
parentRef,
|
||||
useObserver: isUnitTest ? () => ({height: 500, width: 1000}) : undefined,
|
||||
// eslint-disable-next-line
|
||||
@@ -170,20 +170,20 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = dataSource.view.addListener((event) => {
|
||||
const unsubscribe = dataView.addListener((event) => {
|
||||
switch (event.type) {
|
||||
case 'reset':
|
||||
rerender(UpdatePrio.HIGH, true);
|
||||
break;
|
||||
case 'shift':
|
||||
if (dataSource.view.size < SMALL_DATASET) {
|
||||
if (dataView.size < SMALL_DATASET) {
|
||||
rerender(UpdatePrio.HIGH, false);
|
||||
} else if (
|
||||
event.location === 'in' ||
|
||||
// to support smooth tailing we want to render on records directly at the end of the window immediately as well
|
||||
(event.location === 'after' &&
|
||||
event.delta > 0 &&
|
||||
event.index === dataSource.view.windowEnd)
|
||||
event.index === dataView.windowEnd)
|
||||
) {
|
||||
rerender(UpdatePrio.HIGH, false);
|
||||
} else {
|
||||
@@ -204,7 +204,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
unsubscribe();
|
||||
};
|
||||
},
|
||||
[dataSource, setForceUpdate, useFixedRowHeight, isUnitTest],
|
||||
[setForceUpdate, useFixedRowHeight, isUnitTest, dataView],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -215,15 +215,15 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
useLayoutEffect(function updateWindow() {
|
||||
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
||||
const end = start + virtualizer.virtualItems.length;
|
||||
if (start !== dataSource.view.windowStart && !autoScroll) {
|
||||
if (start !== dataView.windowStart && !autoScroll) {
|
||||
onRangeChange?.(
|
||||
start,
|
||||
end,
|
||||
dataSource.view.size,
|
||||
dataView.size,
|
||||
parentRef.current?.scrollTop ?? 0,
|
||||
);
|
||||
}
|
||||
dataSource.view.setWindow(start, end);
|
||||
dataView.setWindow(start, end);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -245,7 +245,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
useLayoutEffect(function scrollToEnd() {
|
||||
if (autoScroll) {
|
||||
virtualizer.scrollToIndex(
|
||||
dataSource.view.size - 1,
|
||||
dataView.size - 1,
|
||||
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
|
||||
{
|
||||
align: 'end',
|
||||
@@ -290,7 +290,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
<RedrawContext.Provider value={redraw}>
|
||||
<div ref={parentRef} onScroll={onScroll} style={tableContainerStyle}>
|
||||
{virtualizer.virtualItems.length === 0
|
||||
? emptyRenderer?.(dataSource)
|
||||
? emptyRenderer?.(dataView)
|
||||
: null}
|
||||
<div
|
||||
style={{
|
||||
@@ -300,7 +300,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={0}>
|
||||
{virtualizer.virtualItems.map((virtualRow) => {
|
||||
const value = dataSource.view.get(virtualRow.index);
|
||||
const value = dataView.get(virtualRow.index);
|
||||
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
||||
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export {
|
||||
DataSource,
|
||||
DataSourceView,
|
||||
createDataSource,
|
||||
DataSourceOptions,
|
||||
DataSourceOptionKey,
|
||||
|
||||
Reference in New Issue
Block a user