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
|
// rather than search and remove the affected individual items
|
||||||
const shiftRebuildTreshold = 0.05;
|
const shiftRebuildTreshold = 0.05;
|
||||||
|
|
||||||
|
const DEFAULT_VIEW_ID = '0';
|
||||||
|
|
||||||
type AppendEvent<T> = {
|
type AppendEvent<T> = {
|
||||||
type: 'append';
|
type: 'append';
|
||||||
entry: Entry<T>;
|
entry: Entry<T>;
|
||||||
@@ -28,7 +30,9 @@ type UpdateEvent<T> = {
|
|||||||
type: 'update';
|
type: 'update';
|
||||||
entry: Entry<T>;
|
entry: Entry<T>;
|
||||||
oldValue: T;
|
oldValue: T;
|
||||||
oldVisible: boolean;
|
oldVisible: {
|
||||||
|
[viewId: string]: boolean;
|
||||||
|
};
|
||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
type RemoveEvent<T> = {
|
type RemoveEvent<T> = {
|
||||||
@@ -51,8 +55,12 @@ type DataEvent<T> =
|
|||||||
type Entry<T> = {
|
type Entry<T> = {
|
||||||
value: T;
|
value: T;
|
||||||
id: number; // insertion based
|
id: number; // insertion based
|
||||||
visible: boolean; // matches current filter?
|
visible: {
|
||||||
approxIndex: number; // we could possible live at this index in the output. No guarantees.
|
[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;
|
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 view: DataSourceView<T, KeyType>;
|
||||||
|
|
||||||
|
public readonly additionalViews: {
|
||||||
|
[viewId: string]: DataSourceView<T, KeyType>;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(keyAttribute: keyof T | undefined) {
|
constructor(keyAttribute: keyof T | undefined) {
|
||||||
this.keyAttribute = keyAttribute;
|
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() {
|
public get size() {
|
||||||
@@ -228,12 +241,17 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
this._recordsById.set(key, value);
|
this._recordsById.set(key, value);
|
||||||
this.storeIndexOfKey(key, this._records.length);
|
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 = {
|
const entry = {
|
||||||
value,
|
value,
|
||||||
id: ++this.nextId,
|
id: ++this.nextId,
|
||||||
// once we have multiple views, the following fields should be stored per view
|
visible: visibleMap,
|
||||||
visible: true,
|
approxIndex: approxIndexMap,
|
||||||
approxIndex: -1,
|
|
||||||
};
|
};
|
||||||
this._records.push(entry);
|
this._records.push(entry);
|
||||||
this.emitDataEvent({
|
this.emitDataEvent({
|
||||||
@@ -268,7 +286,7 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
if (value === oldValue) {
|
if (value === oldValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldVisible = entry.visible;
|
const oldVisible = {...entry.visible};
|
||||||
entry.value = value;
|
entry.value = value;
|
||||||
if (this.keyAttribute) {
|
if (this.keyAttribute) {
|
||||||
const key = this.getKey(value);
|
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
|
// 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
|
// 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
|
// in background when new data arrives, and not explicitly on a user interaction
|
||||||
this.view.rebuild();
|
this.rebuild();
|
||||||
} else {
|
} else {
|
||||||
this.emitDataEvent({
|
this.emitDataEvent({
|
||||||
type: 'shift',
|
type: 'shift',
|
||||||
@@ -392,17 +410,51 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
this._recordsById = new Map();
|
this._recordsById = new Map();
|
||||||
this.shiftOffset = 0;
|
this.shiftOffset = 0;
|
||||||
this.idToIndex = new Map();
|
this.idToIndex = new Map();
|
||||||
|
this.rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rebuild function that would support rebuilding multiple views all at once
|
||||||
|
*/
|
||||||
|
public rebuild() {
|
||||||
this.view.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,
|
* 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
|
* but has it's own FSRW pipeline, to allow multiple views on the same data
|
||||||
*/
|
*/
|
||||||
public fork(): DataSourceView<T, KeyType> {
|
private fork(viewId: string): DataSourceView<T, KeyType> {
|
||||||
throw new Error(
|
this._records.forEach((entry) => {
|
||||||
'Not implemented. Please contact oncall if this feature is needed',
|
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() {
|
private assertKeySet() {
|
||||||
@@ -433,6 +485,9 @@ export class DataSource<T extends any, KeyType = never> {
|
|||||||
// using a queue,
|
// using a queue,
|
||||||
// or only if there is an active view (although that could leak memory)
|
// or only if there is an active view (although that could leak memory)
|
||||||
this.view.processEvent(event);
|
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;
|
return entry?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataSourceView<T, KeyType> {
|
export class DataSourceView<T, KeyType> {
|
||||||
public readonly datasource: DataSource<T, KeyType>;
|
public readonly datasource: DataSource<T, KeyType>;
|
||||||
private sortBy: undefined | ((a: T) => Primitive) = undefined;
|
private sortBy: undefined | ((a: T) => Primitive) = undefined;
|
||||||
private reverse: boolean = false;
|
private reverse: boolean = false;
|
||||||
@@ -471,6 +526,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
public windowEnd = 0;
|
public windowEnd = 0;
|
||||||
|
private viewId;
|
||||||
|
|
||||||
private outputChangeListeners = new Set<(change: OutputChange) => void>();
|
private outputChangeListeners = new Set<(change: OutputChange) => void>();
|
||||||
|
|
||||||
@@ -479,8 +535,9 @@ class DataSourceView<T, KeyType> {
|
|||||||
*/
|
*/
|
||||||
private _output: Entry<T>[] = [];
|
private _output: Entry<T>[] = [];
|
||||||
|
|
||||||
constructor(datasource: DataSource<T, KeyType>) {
|
constructor(datasource: DataSource<T, KeyType>, viewId: string) {
|
||||||
this.datasource = datasource;
|
this.datasource = datasource;
|
||||||
|
this.viewId = viewId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get size() {
|
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,
|
// 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
|
// and we just want to verify that this entry is indeed still the same element, visible, and still present in
|
||||||
// the output data set.
|
// the output data set.
|
||||||
if (entry.visible && entry.id === this._output[entry.approxIndex]?.id) {
|
if (
|
||||||
return this.normalizeIndex(entry.approxIndex);
|
entry.visible[this.viewId] &&
|
||||||
|
entry.id === this._output[entry.approxIndex[this.viewId]]?.id
|
||||||
|
) {
|
||||||
|
return this.normalizeIndex(entry.approxIndex[this.viewId]);
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -674,16 +734,16 @@ class DataSourceView<T, KeyType> {
|
|||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'append': {
|
case 'append': {
|
||||||
const {entry} = event;
|
const {entry} = event;
|
||||||
entry.visible = filter ? filter(entry.value) : true;
|
entry.visible[this.viewId] = filter ? filter(entry.value) : true;
|
||||||
if (!entry.visible) {
|
if (!entry.visible[this.viewId]) {
|
||||||
// not in filter? skip this entry
|
// not in filter? skip this entry
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!sortBy) {
|
if (!sortBy) {
|
||||||
// no sorting? insert at the end, or beginning
|
// no sorting? insert at the end, or beginning
|
||||||
entry.approxIndex = output.length;
|
entry.approxIndex[this.viewId] = output.length;
|
||||||
output.push(entry);
|
output.push(entry);
|
||||||
this.notifyItemShift(entry.approxIndex, 1);
|
this.notifyItemShift(entry.approxIndex[this.viewId], 1);
|
||||||
} else {
|
} else {
|
||||||
this.insertSorted(entry);
|
this.insertSorted(entry);
|
||||||
}
|
}
|
||||||
@@ -691,13 +751,13 @@ class DataSourceView<T, KeyType> {
|
|||||||
}
|
}
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const {entry} = event;
|
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
|
// short circuit; no view active so update straight away
|
||||||
if (!filter && !sortBy) {
|
if (!filter && !sortBy) {
|
||||||
output[event.index].approxIndex = event.index;
|
output[event.index].approxIndex[this.viewId] = event.index;
|
||||||
this.notifyItemUpdated(event.index);
|
this.notifyItemUpdated(event.index);
|
||||||
} else if (!event.oldVisible) {
|
} else if (!event.oldVisible[this.viewId]) {
|
||||||
if (!entry.visible) {
|
if (!entry.visible[this.viewId]) {
|
||||||
// Done!
|
// Done!
|
||||||
} else {
|
} else {
|
||||||
// insertion, not visible before
|
// insertion, not visible before
|
||||||
@@ -706,7 +766,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
} else {
|
} else {
|
||||||
// Entry was visible previously
|
// Entry was visible previously
|
||||||
const existingIndex = this.getSortedIndex(entry, event.oldValue);
|
const existingIndex = this.getSortedIndex(entry, event.oldValue);
|
||||||
if (!entry.visible) {
|
if (!entry.visible[this.viewId]) {
|
||||||
// Remove from output
|
// Remove from output
|
||||||
output.splice(existingIndex, 1);
|
output.splice(existingIndex, 1);
|
||||||
this.notifyItemShift(existingIndex, -1);
|
this.notifyItemShift(existingIndex, -1);
|
||||||
@@ -744,7 +804,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
} else {
|
} else {
|
||||||
// if there is a filter, count the visibles and shift those
|
// if there is a filter, count the visibles and shift those
|
||||||
for (let i = 0; i < event.entries.length; i++)
|
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);
|
output.splice(0, amount);
|
||||||
this.notifyItemShift(0, -amount);
|
this.notifyItemShift(0, -amount);
|
||||||
@@ -766,7 +826,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
const {_output: output, sortBy, filter} = this;
|
const {_output: output, sortBy, filter} = this;
|
||||||
|
|
||||||
// filter active, and not visible? short circuilt
|
// filter active, and not visible? short circuilt
|
||||||
if (!entry.visible) {
|
if (!entry.visible[this.viewId]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// no sorting, no filter?
|
// no sorting, no filter?
|
||||||
@@ -798,8 +858,8 @@ class DataSourceView<T, KeyType> {
|
|||||||
const records: Entry<T>[] = this.datasource._records;
|
const records: Entry<T>[] = this.datasource._records;
|
||||||
let output = filter
|
let output = filter
|
||||||
? records.filter((entry) => {
|
? records.filter((entry) => {
|
||||||
entry.visible = filter(entry.value);
|
entry.visible[this.viewId] = filter(entry.value);
|
||||||
return entry.visible;
|
return entry.visible[this.viewId];
|
||||||
})
|
})
|
||||||
: records.slice();
|
: records.slice();
|
||||||
if (sortBy) {
|
if (sortBy) {
|
||||||
@@ -818,7 +878,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
|
|
||||||
// write approx indexes for faster lookup of entries in visible output
|
// write approx indexes for faster lookup of entries in visible output
|
||||||
for (let i = 0; i < output.length; i++) {
|
for (let i = 0; i < output.length; i++) {
|
||||||
output[i].approxIndex = i;
|
output[i].approxIndex[this.viewId] = i;
|
||||||
}
|
}
|
||||||
this._output = output;
|
this._output = output;
|
||||||
this.notifyReset(output.length);
|
this.notifyReset(output.length);
|
||||||
@@ -829,17 +889,17 @@ class DataSourceView<T, KeyType> {
|
|||||||
|
|
||||||
private getSortedIndex(entry: Entry<T>, oldValue: T) {
|
private getSortedIndex(entry: Entry<T>, oldValue: T) {
|
||||||
const {_output: output} = this;
|
const {_output: output} = this;
|
||||||
if (output[entry.approxIndex] === entry) {
|
if (output[entry.approxIndex[this.viewId]] === entry) {
|
||||||
// yay!
|
// yay!
|
||||||
return entry.approxIndex;
|
return entry.approxIndex[this.viewId];
|
||||||
}
|
}
|
||||||
let index = sortedIndexBy(
|
let index = sortedIndexBy(
|
||||||
output,
|
output,
|
||||||
{
|
{
|
||||||
value: oldValue,
|
value: oldValue,
|
||||||
id: -1,
|
id: -1,
|
||||||
visible: true,
|
visible: entry.visible,
|
||||||
approxIndex: -1,
|
approxIndex: entry.approxIndex,
|
||||||
},
|
},
|
||||||
this.sortHelper,
|
this.sortHelper,
|
||||||
);
|
);
|
||||||
@@ -862,7 +922,7 @@ class DataSourceView<T, KeyType> {
|
|||||||
entry,
|
entry,
|
||||||
this.sortHelper,
|
this.sortHelper,
|
||||||
);
|
);
|
||||||
entry.approxIndex = insertionIndex;
|
entry.approxIndex[this.viewId] = insertionIndex;
|
||||||
this._output.splice(insertionIndex, 0, entry);
|
this._output.splice(insertionIndex, 0, entry);
|
||||||
this.notifyItemShift(insertionIndex, 1);
|
this.notifyItemShift(insertionIndex, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DataSource} from './DataSource';
|
import {DataSourceView} from './DataSource';
|
||||||
import React, {memo, useCallback, useEffect, useState} from 'react';
|
import React, {memo, useCallback, useEffect, useState} from 'react';
|
||||||
|
|
||||||
import {RedrawContext} from './DataSourceRendererVirtual';
|
import {RedrawContext} from './DataSourceRendererVirtual';
|
||||||
|
|
||||||
type DataSourceProps<T extends object, C> = {
|
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
|
* 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;
|
itemRenderer(item: T, index: number, context: C): React.ReactElement;
|
||||||
useFixedRowHeight: boolean;
|
useFixedRowHeight: boolean;
|
||||||
defaultRowHeight: number;
|
defaultRowHeight: number;
|
||||||
|
maxRecords: number;
|
||||||
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||||
emptyRenderer?:
|
emptyRenderer?:
|
||||||
| null
|
| 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>(
|
export const DataSourceRendererStatic: <T extends object, C>(
|
||||||
props: DataSourceProps<T, C>,
|
props: DataSourceProps<T, C>,
|
||||||
) => React.ReactElement = memo(function DataSourceRendererStatic({
|
) => React.ReactElement = memo(function DataSourceRendererStatic({
|
||||||
dataSource,
|
dataView,
|
||||||
|
maxRecords,
|
||||||
useFixedRowHeight,
|
useFixedRowHeight,
|
||||||
context,
|
context,
|
||||||
itemRenderer,
|
itemRenderer,
|
||||||
@@ -65,8 +67,8 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
|||||||
function subscribeToDataSource() {
|
function subscribeToDataSource() {
|
||||||
let unmounted = false;
|
let unmounted = false;
|
||||||
|
|
||||||
dataSource.view.setWindow(0, dataSource.limit);
|
dataView.setWindow(0, maxRecords);
|
||||||
const unsubscribe = dataSource.view.addListener((_event) => {
|
const unsubscribe = dataView.addListener((_event) => {
|
||||||
if (unmounted) {
|
if (unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -78,7 +80,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[dataSource, setForceUpdate, useFixedRowHeight],
|
[dataView, maxRecords, setForceUpdate, useFixedRowHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -89,7 +91,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
|
|||||||
/**
|
/**
|
||||||
* Rendering
|
* Rendering
|
||||||
*/
|
*/
|
||||||
const records = dataSource.view.output();
|
const records = dataView.output();
|
||||||
if (records.length > 500) {
|
if (records.length > 500) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"StaticDataSourceRenderer should only be used on small datasets. For large datasets the 'scrollable' flag should enabled on DataTable",
|
"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}>
|
<RedrawContext.Provider value={redraw}>
|
||||||
<div onKeyDown={onKeyDown} tabIndex={0}>
|
<div onKeyDown={onKeyDown} tabIndex={0}>
|
||||||
{records.length === 0
|
{records.length === 0
|
||||||
? emptyRenderer?.(dataSource)
|
? emptyRenderer?.(dataView)
|
||||||
: records.map((item, index) => (
|
: records.map((item, index) => (
|
||||||
<div key={index}>{itemRenderer(item, index, context)}</div>
|
<div key={index}>{itemRenderer(item, index, context)}</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import React, {
|
|||||||
useContext,
|
useContext,
|
||||||
createContext,
|
createContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {DataSource} from './DataSource';
|
import {DataSourceView} from './DataSource';
|
||||||
import {useVirtual} from 'react-virtual';
|
import {useVirtual} from 'react-virtual';
|
||||||
import observeRect from '@reach/observe-rect';
|
import observeRect from '@reach/observe-rect';
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
/**
|
/**
|
||||||
* The data source to render
|
* 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?
|
* Automatically scroll if the user is near the end?
|
||||||
*/
|
*/
|
||||||
@@ -68,7 +68,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||||
emptyRenderer?:
|
emptyRenderer?:
|
||||||
| null
|
| 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>(
|
export const DataSourceRendererVirtual: <T extends object, C>(
|
||||||
props: DataSourceProps<T, C>,
|
props: DataSourceProps<T, C>,
|
||||||
) => React.ReactElement = memo(function DataSourceRendererVirtual({
|
) => React.ReactElement = memo(function DataSourceRendererVirtual({
|
||||||
dataSource,
|
dataView,
|
||||||
defaultRowHeight,
|
defaultRowHeight,
|
||||||
useFixedRowHeight,
|
useFixedRowHeight,
|
||||||
context,
|
context,
|
||||||
@@ -102,7 +102,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
|||||||
const isUnitTest = useInUnitTest();
|
const isUnitTest = useInUnitTest();
|
||||||
|
|
||||||
const virtualizer = useVirtual({
|
const virtualizer = useVirtual({
|
||||||
size: dataSource.view.size,
|
size: dataView.size,
|
||||||
parentRef,
|
parentRef,
|
||||||
useObserver: isUnitTest ? () => ({height: 500, width: 1000}) : undefined,
|
useObserver: isUnitTest ? () => ({height: 500, width: 1000}) : undefined,
|
||||||
// eslint-disable-next-line
|
// 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) {
|
switch (event.type) {
|
||||||
case 'reset':
|
case 'reset':
|
||||||
rerender(UpdatePrio.HIGH, true);
|
rerender(UpdatePrio.HIGH, true);
|
||||||
break;
|
break;
|
||||||
case 'shift':
|
case 'shift':
|
||||||
if (dataSource.view.size < SMALL_DATASET) {
|
if (dataView.size < SMALL_DATASET) {
|
||||||
rerender(UpdatePrio.HIGH, false);
|
rerender(UpdatePrio.HIGH, false);
|
||||||
} else if (
|
} else if (
|
||||||
event.location === 'in' ||
|
event.location === 'in' ||
|
||||||
// to support smooth tailing we want to render on records directly at the end of the window immediately as well
|
// to support smooth tailing we want to render on records directly at the end of the window immediately as well
|
||||||
(event.location === 'after' &&
|
(event.location === 'after' &&
|
||||||
event.delta > 0 &&
|
event.delta > 0 &&
|
||||||
event.index === dataSource.view.windowEnd)
|
event.index === dataView.windowEnd)
|
||||||
) {
|
) {
|
||||||
rerender(UpdatePrio.HIGH, false);
|
rerender(UpdatePrio.HIGH, false);
|
||||||
} else {
|
} else {
|
||||||
@@ -204,7 +204,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[dataSource, setForceUpdate, useFixedRowHeight, isUnitTest],
|
[setForceUpdate, useFixedRowHeight, isUnitTest, dataView],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -215,15 +215,15 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
|||||||
useLayoutEffect(function updateWindow() {
|
useLayoutEffect(function updateWindow() {
|
||||||
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
||||||
const end = start + virtualizer.virtualItems.length;
|
const end = start + virtualizer.virtualItems.length;
|
||||||
if (start !== dataSource.view.windowStart && !autoScroll) {
|
if (start !== dataView.windowStart && !autoScroll) {
|
||||||
onRangeChange?.(
|
onRangeChange?.(
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
dataSource.view.size,
|
dataView.size,
|
||||||
parentRef.current?.scrollTop ?? 0,
|
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() {
|
useLayoutEffect(function scrollToEnd() {
|
||||||
if (autoScroll) {
|
if (autoScroll) {
|
||||||
virtualizer.scrollToIndex(
|
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*/
|
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
|
||||||
{
|
{
|
||||||
align: 'end',
|
align: 'end',
|
||||||
@@ -290,7 +290,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
|||||||
<RedrawContext.Provider value={redraw}>
|
<RedrawContext.Provider value={redraw}>
|
||||||
<div ref={parentRef} onScroll={onScroll} style={tableContainerStyle}>
|
<div ref={parentRef} onScroll={onScroll} style={tableContainerStyle}>
|
||||||
{virtualizer.virtualItems.length === 0
|
{virtualizer.virtualItems.length === 0
|
||||||
? emptyRenderer?.(dataSource)
|
? emptyRenderer?.(dataView)
|
||||||
: null}
|
: null}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -300,7 +300,7 @@ export const DataSourceRendererVirtual: <T extends object, C>(
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
tabIndex={0}>
|
tabIndex={0}>
|
||||||
{virtualizer.virtualItems.map((virtualRow) => {
|
{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.
|
// 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*/}
|
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
DataSource,
|
DataSource,
|
||||||
|
DataSourceView,
|
||||||
createDataSource,
|
createDataSource,
|
||||||
DataSourceOptions,
|
DataSourceOptions,
|
||||||
DataSourceOptionKey,
|
DataSourceOptionKey,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
DataSourceRendererVirtual,
|
DataSourceRendererVirtual,
|
||||||
DataSourceRendererStatic,
|
DataSourceRendererStatic,
|
||||||
DataSource,
|
DataSource,
|
||||||
|
DataSourceView,
|
||||||
DataSourceVirtualizer,
|
DataSourceVirtualizer,
|
||||||
} from '../../data-source/index';
|
} from '../../data-source/index';
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +46,7 @@ import {TableSearch} from './TableSearch';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
import {tableContextMenuFactory} from './TableContextMenu';
|
import {tableContextMenuFactory} from './TableContextMenu';
|
||||||
import {Typography} from 'antd';
|
import {Menu, Switch, Typography} from 'antd';
|
||||||
import {CoffeeOutlined, SearchOutlined, PushpinFilled} from '@ant-design/icons';
|
import {CoffeeOutlined, SearchOutlined, PushpinFilled} from '@ant-design/icons';
|
||||||
import {useAssertStableRef} from '../../utils/useAssertStableRef';
|
import {useAssertStableRef} from '../../utils/useAssertStableRef';
|
||||||
import {Formatter} from '../DataFormatter';
|
import {Formatter} from '../DataFormatter';
|
||||||
@@ -65,6 +66,7 @@ type DataTableBaseProps<T = any> = {
|
|||||||
enableMultiSelect?: boolean;
|
enableMultiSelect?: boolean;
|
||||||
enableContextMenu?: boolean;
|
enableContextMenu?: boolean;
|
||||||
enablePersistSettings?: boolean;
|
enablePersistSettings?: boolean;
|
||||||
|
enableMultiPanels?: boolean;
|
||||||
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
extraActions?: React.ReactElement;
|
extraActions?: React.ReactElement;
|
||||||
@@ -75,7 +77,7 @@ type DataTableBaseProps<T = any> = {
|
|||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
||||||
onRenderEmpty?:
|
onRenderEmpty?:
|
||||||
| null
|
| null
|
||||||
| ((dataSource?: DataSource<T, T[keyof T]>) => React.ReactElement);
|
| ((dataView?: DataSourceView<T, T[keyof T]>) => React.ReactElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ItemRenderer<T> = (
|
export type ItemRenderer<T> = (
|
||||||
@@ -87,12 +89,14 @@ export type ItemRenderer<T> = (
|
|||||||
type DataTableInput<T = any> =
|
type DataTableInput<T = any> =
|
||||||
| {
|
| {
|
||||||
dataSource: DataSource<T, T[keyof T]>;
|
dataSource: DataSource<T, T[keyof T]>;
|
||||||
|
viewId?: string;
|
||||||
records?: undefined;
|
records?: undefined;
|
||||||
recordsKey?: undefined;
|
recordsKey?: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
records: readonly T[];
|
records: readonly T[];
|
||||||
recordsKey?: keyof T;
|
recordsKey?: keyof T;
|
||||||
|
viewId?: string;
|
||||||
dataSource?: undefined;
|
dataSource?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,6 +143,9 @@ export function DataTable<T extends object>(
|
|||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
const {onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
|
const {onRowStyle, onSelect, onCopyRows, onContextMenu} = props;
|
||||||
const dataSource = normalizeDataSourceInput(props);
|
const dataSource = normalizeDataSourceInput(props);
|
||||||
|
const dataView = props?.viewId
|
||||||
|
? dataSource.getAdditionalView(props.viewId)
|
||||||
|
: dataSource.view;
|
||||||
useAssertStableRef(dataSource, 'dataSource');
|
useAssertStableRef(dataSource, 'dataSource');
|
||||||
useAssertStableRef(onRowStyle, 'onRowStyle');
|
useAssertStableRef(onRowStyle, 'onRowStyle');
|
||||||
useAssertStableRef(props.onSelect, 'onRowSelect');
|
useAssertStableRef(props.onSelect, 'onRowSelect');
|
||||||
@@ -157,6 +164,7 @@ export function DataTable<T extends object>(
|
|||||||
() =>
|
() =>
|
||||||
createInitialState({
|
createInitialState({
|
||||||
dataSource,
|
dataSource,
|
||||||
|
dataView,
|
||||||
defaultColumns: props.columns,
|
defaultColumns: props.columns,
|
||||||
onSelect,
|
onSelect,
|
||||||
scope,
|
scope,
|
||||||
@@ -172,9 +180,10 @@ export function DataTable<T extends object>(
|
|||||||
const dragging = useRef(false);
|
const dragging = useRef(false);
|
||||||
|
|
||||||
const [tableManager] = useState(() =>
|
const [tableManager] = useState(() =>
|
||||||
createDataTableManager(dataSource, dispatch, stateRef),
|
createDataTableManager(dataView, dispatch, stateRef),
|
||||||
);
|
);
|
||||||
if (props.tableManagerRef) {
|
// Make sure this is the main table
|
||||||
|
if (props.tableManagerRef && !props.viewId) {
|
||||||
(props.tableManagerRef as MutableRefObject<any>).current = tableManager;
|
(props.tableManagerRef as MutableRefObject<any>).current = tableManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,22 +192,22 @@ export function DataTable<T extends object>(
|
|||||||
const latestSelectionRef = useLatestRef(selection);
|
const latestSelectionRef = useLatestRef(selection);
|
||||||
const latestOnSelectRef = useLatestRef(onSelect);
|
const latestOnSelectRef = useLatestRef(onSelect);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataSource) {
|
if (dataView) {
|
||||||
const unsubscribe = dataSource.view.addListener((change) => {
|
const unsubscribe = dataView.addListener((change) => {
|
||||||
if (
|
if (
|
||||||
change.type === 'update' &&
|
change.type === 'update' &&
|
||||||
latestSelectionRef.current.items.has(change.index)
|
latestSelectionRef.current.items.has(change.index)
|
||||||
) {
|
) {
|
||||||
latestOnSelectRef.current?.(
|
latestOnSelectRef.current?.(
|
||||||
getSelectedItem(dataSource, latestSelectionRef.current),
|
getSelectedItem(dataView, latestSelectionRef.current),
|
||||||
getSelectedItems(dataSource, latestSelectionRef.current),
|
getSelectedItems(dataView, latestSelectionRef.current),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
}, [dataSource, latestSelectionRef, latestOnSelectRef]);
|
}, [dataView, latestSelectionRef, latestOnSelectRef]);
|
||||||
|
|
||||||
const visibleColumns = useMemo(
|
const visibleColumns = useMemo(
|
||||||
() => columns.filter((column) => column.visible),
|
() => columns.filter((column) => column.visible),
|
||||||
@@ -290,10 +299,10 @@ export function DataTable<T extends object>(
|
|||||||
(e: React.KeyboardEvent<any>) => {
|
(e: React.KeyboardEvent<any>) => {
|
||||||
let handled = true;
|
let handled = true;
|
||||||
const shiftPressed = e.shiftKey;
|
const shiftPressed = e.shiftKey;
|
||||||
const outputSize = dataSource.view.size;
|
const outputSize = dataView.size;
|
||||||
const windowSize = props.scrollable
|
const windowSize = props.scrollable
|
||||||
? virtualizerRef.current?.virtualItems.length ?? 0
|
? virtualizerRef.current?.virtualItems.length ?? 0
|
||||||
: dataSource.view.size;
|
: dataView.size;
|
||||||
if (!windowSize) {
|
if (!windowSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,15 +355,15 @@ export function DataTable<T extends object>(
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dataSource, tableManager, props.scrollable],
|
[dataView, props.scrollable, tableManager],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [setFilter] = useState(() => (tableState: DataManagerState<T>) => {
|
const [setFilter] = useState(() => (tableState: DataManagerState<T>) => {
|
||||||
const selectedEntry =
|
const selectedEntry =
|
||||||
tableState.selection.current >= 0
|
tableState.selection.current >= 0
|
||||||
? dataSource.view.getEntry(tableState.selection.current)
|
? dataView.getEntry(tableState.selection.current)
|
||||||
: null;
|
: null;
|
||||||
dataSource.view.setFilter(
|
dataView.setFilter(
|
||||||
computeDataTableFilter(
|
computeDataTableFilter(
|
||||||
tableState.searchValue,
|
tableState.searchValue,
|
||||||
tableState.useRegex,
|
tableState.useRegex,
|
||||||
@@ -362,10 +371,10 @@ export function DataTable<T extends object>(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
// TODO: in the future setFilter effects could be async, at the moment it isn't,
|
// TODO: in the future setFilter effects could be async, at the moment it isn't,
|
||||||
// so we can safely assume the internal state of the dataSource.view is updated with the
|
// so we can safely assume the internal state of the dataView is updated with the
|
||||||
// filter changes and try to find the same entry back again
|
// filter changes and try to find the same entry back again
|
||||||
if (selectedEntry) {
|
if (selectedEntry) {
|
||||||
const selectionIndex = dataSource.view.getViewIndexOfEntry(selectedEntry);
|
const selectionIndex = dataView.getViewIndexOfEntry(selectedEntry);
|
||||||
tableManager.selectItem(selectionIndex, false, false);
|
tableManager.selectItem(selectionIndex, false, false);
|
||||||
// we disable autoScroll as is it can accidentally be annoying if it was never turned off and
|
// we disable autoScroll as is it can accidentally be annoying if it was never turned off and
|
||||||
// filter causes items to not fill the available space
|
// filter causes items to not fill the available space
|
||||||
@@ -391,7 +400,7 @@ export function DataTable<T extends object>(
|
|||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateFilter() {
|
function updateFilter() {
|
||||||
if (!dataSource.view.isFiltered) {
|
if (!dataView.isFiltered) {
|
||||||
setFilter(tableState);
|
setFilter(tableState);
|
||||||
} else {
|
} else {
|
||||||
debouncedSetFilter(tableState);
|
debouncedSetFilter(tableState);
|
||||||
@@ -413,14 +422,14 @@ export function DataTable<T extends object>(
|
|||||||
useEffect(
|
useEffect(
|
||||||
function updateSorting() {
|
function updateSorting() {
|
||||||
if (tableState.sorting === undefined) {
|
if (tableState.sorting === undefined) {
|
||||||
dataSource.view.setSortBy(undefined);
|
dataView.setSortBy(undefined);
|
||||||
dataSource.view.setReversed(false);
|
dataView.setReversed(false);
|
||||||
} else {
|
} else {
|
||||||
dataSource.view.setSortBy(tableState.sorting.key);
|
dataView.setSortBy(tableState.sorting.key);
|
||||||
dataSource.view.setReversed(tableState.sorting.direction === 'desc');
|
dataView.setReversed(tableState.sorting.direction === 'desc');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dataSource, tableState.sorting],
|
[dataView, tableState.sorting],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
@@ -428,13 +437,13 @@ export function DataTable<T extends object>(
|
|||||||
function triggerSelection() {
|
function triggerSelection() {
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
onSelect?.(
|
onSelect?.(
|
||||||
getSelectedItem(dataSource, tableState.selection),
|
getSelectedItem(dataView, tableState.selection),
|
||||||
getSelectedItems(dataSource, tableState.selection),
|
getSelectedItems(dataView, tableState.selection),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
isMounted.current = true;
|
isMounted.current = true;
|
||||||
},
|
},
|
||||||
[onSelect, dataSource, tableState.selection],
|
[onSelect, dataView, tableState.selection],
|
||||||
);
|
);
|
||||||
|
|
||||||
// The initialScrollPosition is used to both capture the initial px we want to scroll to,
|
// The initialScrollPosition is used to both capture the initial px we want to scroll to,
|
||||||
@@ -484,6 +493,30 @@ export function DataTable<T extends object>(
|
|||||||
[props.enableAutoScroll],
|
[props.enableAutoScroll],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sidePanelToggle = useMemo(
|
||||||
|
() => (
|
||||||
|
<Menu.Item key="toggle side by side">
|
||||||
|
<Layout.Horizontal
|
||||||
|
gap
|
||||||
|
center
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}}>
|
||||||
|
Side By Side View
|
||||||
|
<Switch
|
||||||
|
checked={tableState.sideBySide}
|
||||||
|
size="small"
|
||||||
|
onChange={() => {
|
||||||
|
tableManager.toggleSideBySide();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Layout.Horizontal>
|
||||||
|
</Menu.Item>
|
||||||
|
),
|
||||||
|
[tableManager, tableState.sideBySide],
|
||||||
|
);
|
||||||
|
|
||||||
/** Context menu */
|
/** Context menu */
|
||||||
const contexMenu = isUnitTest
|
const contexMenu = isUnitTest
|
||||||
? undefined
|
? undefined
|
||||||
@@ -491,7 +524,7 @@ export function DataTable<T extends object>(
|
|||||||
useCallback(
|
useCallback(
|
||||||
() =>
|
() =>
|
||||||
tableContextMenuFactory(
|
tableContextMenuFactory(
|
||||||
dataSource,
|
dataView,
|
||||||
dispatch,
|
dispatch,
|
||||||
selection,
|
selection,
|
||||||
tableState.highlightSearchSetting,
|
tableState.highlightSearchSetting,
|
||||||
@@ -500,17 +533,19 @@ export function DataTable<T extends object>(
|
|||||||
visibleColumns,
|
visibleColumns,
|
||||||
onCopyRows,
|
onCopyRows,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
props.enableMultiPanels ? sidePanelToggle : undefined,
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
dataSource,
|
dataView,
|
||||||
dispatch,
|
|
||||||
selection,
|
selection,
|
||||||
tableState.columns,
|
|
||||||
tableState.highlightSearchSetting,
|
tableState.highlightSearchSetting,
|
||||||
tableState.filterSearchHistory,
|
tableState.filterSearchHistory,
|
||||||
|
tableState.columns,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
onCopyRows,
|
onCopyRows,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
props.enableMultiPanels,
|
||||||
|
sidePanelToggle,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -523,9 +558,13 @@ export function DataTable<T extends object>(
|
|||||||
savePreferences(stateRef.current, lastOffset.current);
|
savePreferences(stateRef.current, lastOffset.current);
|
||||||
// if the component unmounts, we reset the SFRW pipeline to
|
// if the component unmounts, we reset the SFRW pipeline to
|
||||||
// avoid wasting resources in the background
|
// avoid wasting resources in the background
|
||||||
dataSource.view.reset();
|
dataView.reset();
|
||||||
// clean ref
|
if (props.viewId) {
|
||||||
if (props.tableManagerRef) {
|
// this is a side panel
|
||||||
|
dataSource.deleteView(props.viewId);
|
||||||
|
}
|
||||||
|
// clean ref && Make sure this is the main table
|
||||||
|
if (props.tableManagerRef && !props.viewId) {
|
||||||
(props.tableManagerRef as MutableRefObject<any>).current = undefined;
|
(props.tableManagerRef as MutableRefObject<any>).current = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -544,7 +583,7 @@ export function DataTable<T extends object>(
|
|||||||
dispatch={dispatch as any}
|
dispatch={dispatch as any}
|
||||||
searchHistory={tableState.searchHistory}
|
searchHistory={tableState.searchHistory}
|
||||||
contextMenu={props.enableContextMenu ? contexMenu : undefined}
|
contextMenu={props.enableContextMenu ? contexMenu : undefined}
|
||||||
extraActions={props.extraActions}
|
extraActions={!props.viewId ? props.extraActions : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
@@ -575,7 +614,7 @@ export function DataTable<T extends object>(
|
|||||||
if (props.scrollable) {
|
if (props.scrollable) {
|
||||||
const dataSourceRenderer = (
|
const dataSourceRenderer = (
|
||||||
<DataSourceRendererVirtual<T, TableRowRenderContext<T>>
|
<DataSourceRendererVirtual<T, TableRowRenderContext<T>>
|
||||||
dataSource={dataSource}
|
dataView={dataView}
|
||||||
autoScroll={tableState.autoScroll && !dragging.current}
|
autoScroll={tableState.autoScroll && !dragging.current}
|
||||||
useFixedRowHeight={!tableState.usesWrapping}
|
useFixedRowHeight={!tableState.usesWrapping}
|
||||||
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
||||||
@@ -614,10 +653,11 @@ export function DataTable<T extends object>(
|
|||||||
{header}
|
{header}
|
||||||
{columnHeaders}
|
{columnHeaders}
|
||||||
<DataSourceRendererStatic<T, TableRowRenderContext<T>>
|
<DataSourceRendererStatic<T, TableRowRenderContext<T>>
|
||||||
dataSource={dataSource}
|
dataView={dataView}
|
||||||
useFixedRowHeight={!tableState.usesWrapping}
|
useFixedRowHeight={!tableState.usesWrapping}
|
||||||
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
||||||
context={renderingConfig}
|
context={renderingConfig}
|
||||||
|
maxRecords={dataSource.limit}
|
||||||
itemRenderer={itemRenderer}
|
itemRenderer={itemRenderer}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
emptyRenderer={emptyRenderer}
|
emptyRenderer={emptyRenderer}
|
||||||
@@ -625,9 +665,8 @@ export function DataTable<T extends object>(
|
|||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const mainPanel = (
|
||||||
return (
|
<Layout.Container grow={props.scrollable} style={{position: 'relative'}}>
|
||||||
<Layout.Container grow={props.scrollable}>
|
|
||||||
<HighlightProvider
|
<HighlightProvider
|
||||||
text={
|
text={
|
||||||
tableState.highlightSearchSetting.highlightEnabled
|
tableState.highlightSearchSetting.highlightEnabled
|
||||||
@@ -655,6 +694,15 @@ export function DataTable<T extends object>(
|
|||||||
{range && !isUnitTest && <RangeFinder>{range}</RangeFinder>}
|
{range && !isUnitTest && <RangeFinder>{range}</RangeFinder>}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
|
return props.enableMultiPanels && tableState.sideBySide ? (
|
||||||
|
//TODO: Make the panels resizable by having a dynamic maxWidth for Layout.Right/Left possibly?
|
||||||
|
<Layout.Horizontal style={{height: '100%'}}>
|
||||||
|
{mainPanel}
|
||||||
|
{<DataTable<T> viewId={'1'} {...props} enableMultiPanels={false} />}
|
||||||
|
</Layout.Horizontal>
|
||||||
|
) : (
|
||||||
|
mainPanel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable.defaultProps = {
|
DataTable.defaultProps = {
|
||||||
@@ -709,16 +757,16 @@ function syncRecordsToDataSource<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultEmptyRenderer<T>(dataTableManager?: DataTableManager<T>) {
|
function createDefaultEmptyRenderer<T>(dataTableManager?: DataTableManager<T>) {
|
||||||
return (dataSource?: DataSource<T, T[keyof T]>) => (
|
return (dataView?: DataSourceView<T, T[keyof T]>) => (
|
||||||
<EmptyTable dataSource={dataSource} dataManager={dataTableManager} />
|
<EmptyTable dataView={dataView} dataManager={dataTableManager} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmptyTable<T>({
|
function EmptyTable<T>({
|
||||||
dataSource,
|
dataView,
|
||||||
dataManager,
|
dataManager,
|
||||||
}: {
|
}: {
|
||||||
dataSource?: DataSource<T, T[keyof T]>;
|
dataView?: DataSourceView<T, T[keyof T]>;
|
||||||
dataManager?: DataTableManager<T>;
|
dataManager?: DataTableManager<T>;
|
||||||
}) {
|
}) {
|
||||||
const resetFilters = useCallback(() => {
|
const resetFilters = useCallback(() => {
|
||||||
@@ -728,7 +776,7 @@ function EmptyTable<T>({
|
|||||||
<Layout.Container
|
<Layout.Container
|
||||||
center
|
center
|
||||||
style={{width: '100%', padding: 40, color: theme.textColorSecondary}}>
|
style={{width: '100%', padding: 40, color: theme.textColorSecondary}}>
|
||||||
{dataSource?.size === 0 ? (
|
{dataView?.size === 0 ? (
|
||||||
<>
|
<>
|
||||||
<CoffeeOutlined style={{fontSize: '2em', margin: 8}} />
|
<CoffeeOutlined style={{fontSize: '2em', margin: 8}} />
|
||||||
<Typography.Text type="secondary">No records yet</Typography.Text>
|
<Typography.Text type="secondary">No records yet</Typography.Text>
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
import type {DataTableColumn} from './DataTable';
|
import type {DataTableColumn} from './DataTable';
|
||||||
import {Percentage} from '../../utils/widthUtils';
|
import {Percentage} from '../../utils/widthUtils';
|
||||||
import {MutableRefObject, Reducer} from 'react';
|
import {MutableRefObject, Reducer} from 'react';
|
||||||
import {DataSource, DataSourceVirtualizer} from '../../data-source/index';
|
import {
|
||||||
|
DataSource,
|
||||||
|
DataSourceView,
|
||||||
|
DataSourceVirtualizer,
|
||||||
|
} from '../../data-source/index';
|
||||||
import produce, {castDraft, immerable, original} from 'immer';
|
import produce, {castDraft, immerable, original} from 'immer';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
|
|
||||||
@@ -110,10 +114,12 @@ type DataManagerActions<T> =
|
|||||||
| Action<'clearSearchHistory'>
|
| Action<'clearSearchHistory'>
|
||||||
| Action<'toggleHighlightSearch'>
|
| Action<'toggleHighlightSearch'>
|
||||||
| Action<'setSearchHighlightColor', {color: string}>
|
| Action<'setSearchHighlightColor', {color: string}>
|
||||||
| Action<'toggleFilterSearchHistory'>;
|
| Action<'toggleFilterSearchHistory'>
|
||||||
|
| Action<'toggleSideBySide'>;
|
||||||
|
|
||||||
type DataManagerConfig<T> = {
|
type DataManagerConfig<T> = {
|
||||||
dataSource: DataSource<T, T[keyof T]>;
|
dataSource: DataSource<T, T[keyof T]>;
|
||||||
|
dataView: DataSourceView<T, T[keyof T]>;
|
||||||
defaultColumns: DataTableColumn<T>[];
|
defaultColumns: DataTableColumn<T>[];
|
||||||
scope: string;
|
scope: string;
|
||||||
onSelect: undefined | ((item: T | undefined, items: T[]) => void);
|
onSelect: undefined | ((item: T | undefined, items: T[]) => void);
|
||||||
@@ -138,6 +144,7 @@ export type DataManagerState<T> = {
|
|||||||
previousSearchValue: string;
|
previousSearchValue: string;
|
||||||
searchHistory: string[];
|
searchHistory: string[];
|
||||||
highlightSearchSetting: SearchHighlightSetting;
|
highlightSearchSetting: SearchHighlightSetting;
|
||||||
|
sideBySide: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableReducer<T> = Reducer<
|
export type DataTableReducer<T> = Reducer<
|
||||||
@@ -288,7 +295,7 @@ export const dataTableManagerReducer = produce<
|
|||||||
}
|
}
|
||||||
case 'setColumnFilterFromSelection': {
|
case 'setColumnFilterFromSelection': {
|
||||||
const items = getSelectedItems(
|
const items = getSelectedItems(
|
||||||
config.dataSource as DataSource<any>,
|
config.dataView as DataSourceView<any, any>,
|
||||||
draft.selection,
|
draft.selection,
|
||||||
);
|
);
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
@@ -324,6 +331,10 @@ export const dataTableManagerReducer = produce<
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'toggleSideBySide': {
|
||||||
|
draft.sideBySide = !draft.sideBySide;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Unknown action ' + (action as any).type);
|
throw new Error('Unknown action ' + (action as any).type);
|
||||||
}
|
}
|
||||||
@@ -353,14 +364,15 @@ export type DataTableManager<T> = {
|
|||||||
toggleColumnVisibility(column: keyof T): void;
|
toggleColumnVisibility(column: keyof T): void;
|
||||||
sortColumn(column: keyof T, direction?: SortDirection): void;
|
sortColumn(column: keyof T, direction?: SortDirection): void;
|
||||||
setSearchValue(value: string, addToHistory?: boolean): void;
|
setSearchValue(value: string, addToHistory?: boolean): void;
|
||||||
dataSource: DataSource<T, T[keyof T]>;
|
dataView: DataSourceView<T, T[keyof T]>;
|
||||||
toggleSearchValue(): void;
|
toggleSearchValue(): void;
|
||||||
toggleHighlightSearch(): void;
|
toggleHighlightSearch(): void;
|
||||||
setSearchHighlightColor(color: string): void;
|
setSearchHighlightColor(color: string): void;
|
||||||
|
toggleSideBySide(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createDataTableManager<T>(
|
export function createDataTableManager<T>(
|
||||||
dataSource: DataSource<T, T[keyof T]>,
|
dataView: DataSourceView<T, T[keyof T]>,
|
||||||
dispatch: DataTableDispatch<T>,
|
dispatch: DataTableDispatch<T>,
|
||||||
stateRef: MutableRefObject<DataManagerState<T>>,
|
stateRef: MutableRefObject<DataManagerState<T>>,
|
||||||
): DataTableManager<T> {
|
): DataTableManager<T> {
|
||||||
@@ -389,10 +401,10 @@ export function createDataTableManager<T>(
|
|||||||
dispatch({type: 'clearSelection'});
|
dispatch({type: 'clearSelection'});
|
||||||
},
|
},
|
||||||
getSelectedItem() {
|
getSelectedItem() {
|
||||||
return getSelectedItem(dataSource, stateRef.current.selection);
|
return getSelectedItem(dataView, stateRef.current.selection);
|
||||||
},
|
},
|
||||||
getSelectedItems() {
|
getSelectedItems() {
|
||||||
return getSelectedItems(dataSource, stateRef.current.selection);
|
return getSelectedItems(dataView, stateRef.current.selection);
|
||||||
},
|
},
|
||||||
toggleColumnVisibility(column) {
|
toggleColumnVisibility(column) {
|
||||||
dispatch({type: 'toggleColumnVisibility', column});
|
dispatch({type: 'toggleColumnVisibility', column});
|
||||||
@@ -412,7 +424,10 @@ export function createDataTableManager<T>(
|
|||||||
setSearchHighlightColor(color) {
|
setSearchHighlightColor(color) {
|
||||||
dispatch({type: 'setSearchHighlightColor', color});
|
dispatch({type: 'setSearchHighlightColor', color});
|
||||||
},
|
},
|
||||||
dataSource,
|
toggleSideBySide() {
|
||||||
|
dispatch({type: 'toggleSideBySide'});
|
||||||
|
},
|
||||||
|
dataView,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +477,7 @@ export function createInitialState<T>(
|
|||||||
highlightEnabled: false,
|
highlightEnabled: false,
|
||||||
color: theme.searchHighlightBackground.yellow,
|
color: theme.searchHighlightBackground.yellow,
|
||||||
},
|
},
|
||||||
|
sideBySide: false,
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res.config[immerable] = false; // optimization: never proxy anything in config
|
res.config[immerable] = false; // optimization: never proxy anything in config
|
||||||
@@ -497,21 +513,19 @@ function addColumnFilter<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedItem<T>(
|
export function getSelectedItem<T>(
|
||||||
dataSource: DataSource<T, T[keyof T]>,
|
dataView: DataSourceView<T, T[keyof T]>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
): T | undefined {
|
): T | undefined {
|
||||||
return selection.current < 0
|
return selection.current < 0 ? undefined : dataView.get(selection.current);
|
||||||
? undefined
|
|
||||||
: dataSource.view.get(selection.current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedItems<T>(
|
export function getSelectedItems<T>(
|
||||||
dataSource: DataSource<T, T[keyof T]>,
|
dataView: DataSourceView<T, T[keyof T]>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
): T[] {
|
): T[] {
|
||||||
return [...selection.items]
|
return [...selection.items]
|
||||||
.sort((a, b) => a - b) // https://stackoverflow.com/a/15765283/1983583
|
.sort((a, b) => a - b) // https://stackoverflow.com/a/15765283/1983583
|
||||||
.map((i) => dataSource.view.get(i))
|
.map((i) => dataView.get(i))
|
||||||
.filter(Boolean) as any[];
|
.filter(Boolean) as any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,12 +659,10 @@ export function computeDataTableFilter(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//free search all top level keys as well as any (nested) columns in the table
|
//free search all top level keys as well as any (nested) columns in the table
|
||||||
const nestedColumns = columns
|
const nestedColumns = columns
|
||||||
.map((col) => col.key)
|
.map((col) => col.key)
|
||||||
.filter((path) => path.includes('.'));
|
.filter((path) => path.includes('.'));
|
||||||
|
|
||||||
return [...Object.keys(item), ...nestedColumns]
|
return [...Object.keys(item), ...nestedColumns]
|
||||||
.map((key) => getValueAtPath(item, key))
|
.map((key) => getValueAtPath(item, key))
|
||||||
.filter((val) => typeof val !== 'object')
|
.filter((val) => typeof val !== 'object')
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import React from 'react';
|
|||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
import {DataTableColumn} from './DataTable';
|
import {DataTableColumn} from './DataTable';
|
||||||
import {toFirstUpper} from '../../utils/toFirstUpper';
|
import {toFirstUpper} from '../../utils/toFirstUpper';
|
||||||
import {DataSource} from '../../data-source/index';
|
import {DataSourceView} from '../../data-source/index';
|
||||||
import {renderColumnValue} from './TableRow';
|
import {renderColumnValue} from './TableRow';
|
||||||
import {textContent} from '../../utils/textContent';
|
import {textContent} from '../../utils/textContent';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
@@ -31,7 +31,7 @@ const {Item, SubMenu} = Menu;
|
|||||||
const {Option} = Select;
|
const {Option} = Select;
|
||||||
|
|
||||||
export function tableContextMenuFactory<T>(
|
export function tableContextMenuFactory<T>(
|
||||||
datasource: DataSource<T, T[keyof T]>,
|
dataView: DataSourceView<T, T[keyof T]>,
|
||||||
dispatch: DataTableDispatch<T>,
|
dispatch: DataTableDispatch<T>,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
highlightSearchSetting: SearchHighlightSetting,
|
highlightSearchSetting: SearchHighlightSetting,
|
||||||
@@ -43,6 +43,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
visibleColumns: DataTableColumn<T>[],
|
visibleColumns: DataTableColumn<T>[],
|
||||||
) => string = defaultOnCopyRows,
|
) => string = defaultOnCopyRows,
|
||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement,
|
onContextMenu?: (selection: undefined | T) => React.ReactElement,
|
||||||
|
sideBySideOption?: React.ReactElement,
|
||||||
) {
|
) {
|
||||||
const lib = tryGetFlipperLibImplementation();
|
const lib = tryGetFlipperLibImplementation();
|
||||||
if (!lib) {
|
if (!lib) {
|
||||||
@@ -56,7 +57,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
{onContextMenu
|
{onContextMenu
|
||||||
? onContextMenu(getSelectedItem(datasource, selection))
|
? onContextMenu(getSelectedItem(dataView, selection))
|
||||||
: null}
|
: null}
|
||||||
<SubMenu
|
<SubMenu
|
||||||
key="filter same"
|
key="filter same"
|
||||||
@@ -85,7 +86,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
key="copyToClipboard"
|
key="copyToClipboard"
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(dataView, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.writeTextToClipboard(onCopyRows(items, visibleColumns));
|
lib.writeTextToClipboard(onCopyRows(items, visibleColumns));
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
key="createPaste"
|
key="createPaste"
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(dataView, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.createPaste(onCopyRows(items, visibleColumns));
|
lib.createPaste(onCopyRows(items, visibleColumns));
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
key="copyToClipboardJSON"
|
key="copyToClipboardJSON"
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(dataView, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.writeTextToClipboard(rowsToJson(items));
|
lib.writeTextToClipboard(rowsToJson(items));
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
key="createPasteJSON"
|
key="createPasteJSON"
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(dataView, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.createPaste(rowsToJson(items));
|
lib.createPaste(rowsToJson(items));
|
||||||
}
|
}
|
||||||
@@ -140,7 +141,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
<Item
|
<Item
|
||||||
key={'copy cell' + (column.key ?? idx)}
|
key={'copy cell' + (column.key ?? idx)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(dataView, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.writeTextToClipboard(
|
lib.writeTextToClipboard(
|
||||||
items
|
items
|
||||||
@@ -269,6 +270,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
{sideBySideOption}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -764,3 +764,273 @@ test('selection always has the latest state', () => {
|
|||||||
|
|
||||||
rendering.unmount();
|
rendering.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('open second panel and append', async () => {
|
||||||
|
const ds = createTestDataSource();
|
||||||
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
|
const rendering = render(
|
||||||
|
<DataTable
|
||||||
|
enableMultiPanels
|
||||||
|
dataSource={ds}
|
||||||
|
columns={columns}
|
||||||
|
tableManagerRef={ref}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(elem[0].parentElement?.parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
test DataTable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
true
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
// hide done
|
||||||
|
act(() => {
|
||||||
|
ref.current?.toggleSideBySide();
|
||||||
|
});
|
||||||
|
expect(Object.keys(ds.additionalViews).length).toBeGreaterThan(0);
|
||||||
|
act(() => {
|
||||||
|
ds.append({
|
||||||
|
title: 'Drink coffee',
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elem.length).toBe(2);
|
||||||
|
}
|
||||||
|
act(() => {
|
||||||
|
ds.append({
|
||||||
|
title: 'Drink tea',
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('Drink tea');
|
||||||
|
expect(elem.length).toBe(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('open second panel and update', async () => {
|
||||||
|
const ds = createTestDataSource();
|
||||||
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
|
const rendering = render(
|
||||||
|
<DataTable
|
||||||
|
enableMultiPanels
|
||||||
|
dataSource={ds}
|
||||||
|
columns={columns}
|
||||||
|
tableManagerRef={ref}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(elem[0].parentElement?.parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
test DataTable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
true
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
// hide done
|
||||||
|
act(() => {
|
||||||
|
ds.append({
|
||||||
|
title: 'Drink coffee',
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const elems = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elems.length).toBe(1);
|
||||||
|
}
|
||||||
|
act(() => {
|
||||||
|
ref.current?.toggleSideBySide();
|
||||||
|
});
|
||||||
|
expect(Object.keys(ds.additionalViews).length).toBeGreaterThan(0);
|
||||||
|
{
|
||||||
|
const elems = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elems.length).toBe(2);
|
||||||
|
}
|
||||||
|
act(() => {
|
||||||
|
ds.update(0, {
|
||||||
|
title: 'DataTable tested',
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const elems = await rendering.findAllByText('Drink coffee');
|
||||||
|
expect(elems.length).toBe(2);
|
||||||
|
expect(rendering.queryByText('test DataTable')).toBeNull();
|
||||||
|
const newElems = await rendering.findAllByText('DataTable tested');
|
||||||
|
expect(newElems.length).toBe(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('open second panel and column visibility', async () => {
|
||||||
|
const ds = createTestDataSource();
|
||||||
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
|
const rendering = render(
|
||||||
|
<DataTable
|
||||||
|
enableMultiPanels
|
||||||
|
dataSource={ds}
|
||||||
|
columns={columns}
|
||||||
|
tableManagerRef={ref}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
|
expect(elem.length).toBe(1);
|
||||||
|
expect(elem[0].parentElement?.parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
test DataTable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
true
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle column visibility of first table(main panel)
|
||||||
|
act(() => {
|
||||||
|
ref.current?.toggleSideBySide();
|
||||||
|
ref.current?.toggleColumnVisibility('done');
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const elem = await rendering.findAllByText('test DataTable');
|
||||||
|
expect(elem.length).toBe(2);
|
||||||
|
expect(elem[0].parentElement?.parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
test DataTable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
ds.update(0, {
|
||||||
|
title: 'DataTable tested',
|
||||||
|
done: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
{
|
||||||
|
expect(rendering.queryByText('test DataTable')).toBeNull();
|
||||||
|
const elem = await rendering.findAllByText('DataTable tested');
|
||||||
|
expect(elem.length).toBe(2);
|
||||||
|
expect(elem[0].parentElement?.parentElement).toMatchInlineSnapshot(`
|
||||||
|
<div
|
||||||
|
class="ant-dropdown-trigger css-1k3kr6b-TableBodyRowContainer e1luu51r1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="css-1baxqcf-TableBodyColumnContainer e1luu51r0"
|
||||||
|
width="50%"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style="background-color: rgb(255, 245, 102);"
|
||||||
|
/>
|
||||||
|
DataTable tested
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('open second panel and closing deletes dataView', async () => {
|
||||||
|
const ds = createTestDataSource();
|
||||||
|
const ref = createRef<DataTableManager<Todo>>();
|
||||||
|
render(
|
||||||
|
<DataTable
|
||||||
|
enableMultiPanels
|
||||||
|
dataSource={ds}
|
||||||
|
columns={columns}
|
||||||
|
tableManagerRef={ref}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(Object.keys(ds.additionalViews).length).toBe(0);
|
||||||
|
act(() => {
|
||||||
|
ref.current?.toggleSideBySide();
|
||||||
|
});
|
||||||
|
expect(Object.keys(ds.additionalViews).length).toBe(1);
|
||||||
|
act(() => {
|
||||||
|
ref.current?.toggleSideBySide();
|
||||||
|
});
|
||||||
|
expect(Object.keys(ds.additionalViews).length).toBe(0);
|
||||||
|
});
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ test('it supports deeplink and select nodes + navigating to bottom', async () =>
|
|||||||
sendLogEntry(entry3);
|
sendLogEntry(entry3);
|
||||||
|
|
||||||
expect(instance.tableManagerRef).not.toBeUndefined();
|
expect(instance.tableManagerRef).not.toBeUndefined();
|
||||||
|
expect(instance.tableManagerRef.current).not.toBeNull();
|
||||||
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([]);
|
expect(instance.tableManagerRef.current?.getSelectedItems()).toEqual([]);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import {
|
|||||||
DeviceLogEntry,
|
DeviceLogEntry,
|
||||||
usePlugin,
|
usePlugin,
|
||||||
createDataSource,
|
createDataSource,
|
||||||
DataTable,
|
|
||||||
DataTableColumn,
|
DataTableColumn,
|
||||||
theme,
|
theme,
|
||||||
DataTableManager,
|
DataTableManager,
|
||||||
createState,
|
createState,
|
||||||
useValue,
|
useValue,
|
||||||
DataFormatter,
|
DataFormatter,
|
||||||
|
DataTable,
|
||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {
|
import {
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
@@ -227,6 +227,7 @@ export function Component() {
|
|||||||
dataSource={plugin.rows}
|
dataSource={plugin.rows}
|
||||||
columns={plugin.columns}
|
columns={plugin.columns}
|
||||||
enableAutoScroll
|
enableAutoScroll
|
||||||
|
enableMultiPanels
|
||||||
onRowStyle={getRowStyle}
|
onRowStyle={getRowStyle}
|
||||||
enableHorizontalScroll={false}
|
enableHorizontalScroll={false}
|
||||||
extraActions={
|
extraActions={
|
||||||
|
|||||||
Reference in New Issue
Block a user