diff --git a/desktop/flipper-plugin/src/data-source/DataSource.tsx b/desktop/flipper-plugin/src/data-source/DataSource.tsx index d97b0f427..a53792265 100644 --- a/desktop/flipper-plugin/src/data-source/DataSource.tsx +++ b/desktop/flipper-plugin/src/data-source/DataSource.tsx @@ -582,6 +582,21 @@ class DataSourceView { return this._output[this.normalizeIndex(viewIndex)]?.value; } + public getEntry(viewIndex: number): Entry { + return this._output[this.normalizeIndex(viewIndex)]; + } + + public getViewIndexOfEntry(entry: Entry) { + // 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 { const self = this; let offset = this.windowStart; @@ -797,6 +812,10 @@ class DataSourceView { 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.notifyReset(output.length); } diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index 8c56ca7dc..2ad8f1fb0 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -33,6 +33,7 @@ import { computeDataTableFilter, createDataTableManager, createInitialState, + DataManagerState, DataTableManager, dataTableManagerReducer, DataTableReducer, @@ -323,37 +324,52 @@ export function DataTable( [dataSource, tableManager, props.scrollable], ); + const [setFilter] = useState(() => (tableState: DataManagerState) => { + 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(() => { // 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 in the future would really benefit from concurrent mode here :)) - const setFilter = ( - search: string, - useRegex: boolean, - columns: DataTableColumn[], - ) => { - dataSource.view.setFilter( - computeDataTableFilter(search, useRegex, columns), - ); - }; + // leading is set to true so that an initial filter is immediately applied and a flash of wrong content is prevented + // this also makes clear act faster return isUnitTest ? setFilter : debounce(setFilter, 250); }); + useEffect( function updateFilter() { if (!dataSource.view.isFiltered) { - dataSource.view.setFilter( - computeDataTableFilter( - tableState.searchValue, - tableState.useRegex, - tableState.columns, - ), - ); + setFilter(tableState); } else { - debouncedSetFilter( - tableState.searchValue, - tableState.useRegex, - tableState.columns, - ); + debouncedSetFilter(tableState); } }, // Important dep optimization: we don't want to recalc filters if just the width or visibility changes! diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx index 77a47b078..f53db92b7 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx @@ -110,7 +110,7 @@ type DataManagerConfig = { enablePersistSettings?: boolean; }; -type DataManagerState = { +export type DataManagerState = { config: DataManagerConfig; usesWrapping: boolean; storageKey: string;