preserve selection during filter changes
Summary: During filter changes, DataTable would loose any selections made, which was posted multiple times as papercut I didn't implement preserving multi line selections. That should be straightforward, but wasn't sure that'd be desirable or not. Changelog: DataTable: Data tables will now preserve the current selection and scroll it into view when changing the search filter. Reviewed By: aigoncharov Differential Revision: D36736496 fbshipit-source-id: 401ef351c847f58a5d411cf9f352390f6a110b24
This commit is contained in:
committed by
Facebook GitHub Bot
parent
fd3f6a0435
commit
2037cf0595
@@ -582,6 +582,21 @@ class DataSourceView<T, KeyType> {
|
|||||||
return this._output[this.normalizeIndex(viewIndex)]?.value;
|
return this._output[this.normalizeIndex(viewIndex)]?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getEntry(viewIndex: number): Entry<T> {
|
||||||
|
return this._output[this.normalizeIndex(viewIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewIndexOfEntry(entry: Entry<T>) {
|
||||||
|
// Note: this function leverages the fact that entry is an internal structure that is mutable,
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
public [Symbol.iterator](): IterableIterator<T> {
|
public [Symbol.iterator](): IterableIterator<T> {
|
||||||
const self = this;
|
const self = this;
|
||||||
let offset = this.windowStart;
|
let offset = this.windowStart;
|
||||||
@@ -797,6 +812,10 @@ class DataSourceView<T, KeyType> {
|
|||||||
output = lodashSort(output, sortHelper); // uses array.sort under the hood
|
output = lodashSort(output, sortHelper); // uses array.sort under the hood
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write approx indexes for faster lookup of entries in visible output
|
||||||
|
for (let i = 0; i < output.length; i++) {
|
||||||
|
output[i].approxIndex = i;
|
||||||
|
}
|
||||||
this._output = output;
|
this._output = output;
|
||||||
this.notifyReset(output.length);
|
this.notifyReset(output.length);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
computeDataTableFilter,
|
computeDataTableFilter,
|
||||||
createDataTableManager,
|
createDataTableManager,
|
||||||
createInitialState,
|
createInitialState,
|
||||||
|
DataManagerState,
|
||||||
DataTableManager,
|
DataTableManager,
|
||||||
dataTableManagerReducer,
|
dataTableManagerReducer,
|
||||||
DataTableReducer,
|
DataTableReducer,
|
||||||
@@ -323,37 +324,52 @@ export function DataTable<T extends object>(
|
|||||||
[dataSource, tableManager, props.scrollable],
|
[dataSource, tableManager, props.scrollable],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [setFilter] = useState(() => (tableState: DataManagerState<T>) => {
|
||||||
|
const selectedEntry =
|
||||||
|
tableState.selection.current >= 0
|
||||||
|
? dataSource.view.getEntry(tableState.selection.current)
|
||||||
|
: null;
|
||||||
|
dataSource.view.setFilter(
|
||||||
|
computeDataTableFilter(
|
||||||
|
tableState.searchValue,
|
||||||
|
tableState.useRegex,
|
||||||
|
tableState.columns,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// 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
|
||||||
|
// filter changes and try to find the same entry back again
|
||||||
|
if (selectedEntry) {
|
||||||
|
const selectionIndex = dataSource.view.getViewIndexOfEntry(selectedEntry);
|
||||||
|
tableManager.selectItem(selectionIndex, false, false);
|
||||||
|
// 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
|
||||||
|
dispatch({type: 'setAutoScroll', autoScroll: false});
|
||||||
|
virtualizerRef.current?.scrollToIndex(selectionIndex, {align: 'center'});
|
||||||
|
setTimeout(() => {
|
||||||
|
virtualizerRef.current?.scrollToIndex(selectionIndex, {
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
// TODO: could do the same for multiselections, doesn't seem to be requested so far
|
||||||
|
});
|
||||||
|
|
||||||
const [debouncedSetFilter] = useState(() => {
|
const [debouncedSetFilter] = useState(() => {
|
||||||
// we don't want to trigger filter changes too quickly, as they can be pretty expensive
|
// we don't want to trigger filter changes too quickly, as they can be pretty expensive
|
||||||
// and would block the user from entering text in the search bar for example
|
// and would block the user from entering text in the search bar for example
|
||||||
// (and in the future would really benefit from concurrent mode here :))
|
// (and in the future would really benefit from concurrent mode here :))
|
||||||
const setFilter = (
|
// leading is set to true so that an initial filter is immediately applied and a flash of wrong content is prevented
|
||||||
search: string,
|
// this also makes clear act faster
|
||||||
useRegex: boolean,
|
|
||||||
columns: DataTableColumn<T>[],
|
|
||||||
) => {
|
|
||||||
dataSource.view.setFilter(
|
|
||||||
computeDataTableFilter(search, useRegex, columns),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return isUnitTest ? setFilter : debounce(setFilter, 250);
|
return isUnitTest ? setFilter : debounce(setFilter, 250);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateFilter() {
|
function updateFilter() {
|
||||||
if (!dataSource.view.isFiltered) {
|
if (!dataSource.view.isFiltered) {
|
||||||
dataSource.view.setFilter(
|
setFilter(tableState);
|
||||||
computeDataTableFilter(
|
|
||||||
tableState.searchValue,
|
|
||||||
tableState.useRegex,
|
|
||||||
tableState.columns,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
debouncedSetFilter(
|
debouncedSetFilter(tableState);
|
||||||
tableState.searchValue,
|
|
||||||
tableState.useRegex,
|
|
||||||
tableState.columns,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Important dep optimization: we don't want to recalc filters if just the width or visibility changes!
|
// Important dep optimization: we don't want to recalc filters if just the width or visibility changes!
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ type DataManagerConfig<T> = {
|
|||||||
enablePersistSettings?: boolean;
|
enablePersistSettings?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DataManagerState<T> = {
|
export type DataManagerState<T> = {
|
||||||
config: DataManagerConfig<T>;
|
config: DataManagerConfig<T>;
|
||||||
usesWrapping: boolean;
|
usesWrapping: boolean;
|
||||||
storageKey: string;
|
storageKey: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user