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:
Michel Weststrate
2022-06-07 04:04:01 -07:00
committed by Facebook GitHub Bot
parent fd3f6a0435
commit 2037cf0595
3 changed files with 57 additions and 22 deletions

View File

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

View File

@@ -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!

View File

@@ -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;