diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index 89cb8fa12..4b8afed83 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -308,6 +308,9 @@ export function DataTable( case 'Escape': tableManager.clearSelection(); break; + case 'Control': + tableManager.setSelectedSearchRecord(); + break; default: handled = false; } @@ -348,7 +351,9 @@ export function DataTable( [ tableState.searchValue, tableState.useRegex, + // eslint-disable-next-line react-hooks/exhaustive-deps ...tableState.columns.map((c) => c.filters), + // eslint-disable-next-line react-hooks/exhaustive-deps ...tableState.columns.map((c) => c.inversed), ], ); @@ -473,6 +478,30 @@ export function DataTable( // eslint-disable-next-line }, []); + useEffect( + function findMappedIndex() { + // Hardcoded delay to give dataSource.view time to update, otherwise + // the entries we loop over here won't be the list of unfiltered records + // the user sees, so there won't be a match found + const delay = 300; + + if (tableState.selectedSearchRecord) { + const timer = setTimeout(() => { + for (let i = 0; i < dataSource.view.size; i++) { + if (dataSource.view.get(i) === tableState.selectedSearchRecord) { + tableManager.clearSelectedSearchRecord(); + tableManager.selectItem(i, false, true); + break; + } + } + }, delay); + + return () => clearTimeout(timer); + } + }, + [dataSource, selection, tableManager, tableState.selectedSearchRecord], + ); + const header = ( {props.enableSearchbar && ( diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx index 6d09ad49a..dee476c9e 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx @@ -40,6 +40,7 @@ type PersistedState = { columns: Pick[]; scrollOffset: number; autoScroll: boolean; + selectedSearchRecord: any; }; type Action = {type: Name} & Args; @@ -90,7 +91,9 @@ type DataManagerActions = | Action<'appliedInitialScroll'> | Action<'toggleUseRegex'> | Action<'toggleAutoScroll'> - | Action<'setAutoScroll', {autoScroll: boolean}>; + | Action<'setAutoScroll', {autoScroll: boolean}> + | Action<'clearSelectedSearchRecord'> + | Action<'setSelectedSearchRecord'>; type DataManagerConfig = { dataSource: DataSource; @@ -113,6 +116,8 @@ type DataManagerState = { searchValue: string; useRegex: boolean; autoScroll: boolean; + /** Used to remember the record entry to lookup when user presses ctrl */ + selectedSearchRecord: any; }; export type DataTableReducer = Reducer< @@ -251,6 +256,24 @@ export const dataTableManagerReducer = produce< draft.autoScroll = action.autoScroll; break; } + case 'setSelectedSearchRecord': { + if (draft.searchValue !== '') { + // Clear search to get unfiltered list of records + draft.searchValue = ''; + + // Save the record to map back in DataTable + draft.selectedSearchRecord = config.dataSource.view.get( + draft.selection.current, + ); + + draft.selection = castDraft(emptySelection); + } + break; + } + case 'clearSelectedSearchRecord': { + draft.selectedSearchRecord = null; + break; + } default: { throw new Error('Unknown action ' + (action as any).type); } @@ -280,6 +303,8 @@ export type DataTableManager = { sortColumn(column: keyof T, direction?: SortDirection): void; setSearchValue(value: string): void; dataSource: DataSource; + setSelectedSearchRecord(): void; + clearSelectedSearchRecord(): void; }; export function createDataTableManager( @@ -323,6 +348,12 @@ export function createDataTableManager( setSearchValue(value) { dispatch({type: 'setSearchValue', value}); }, + setSelectedSearchRecord() { + dispatch({type: 'setSelectedSearchRecord'}); + }, + clearSelectedSearchRecord() { + dispatch({type: 'clearSelectedSearchRecord'}); + }, dataSource, }; } @@ -366,6 +397,7 @@ export function createInitialState( searchValue: prefs?.search ?? '', useRegex: prefs?.useRegex ?? false, autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false, + selectedSearchRecord: null, }; // @ts-ignore res.config[immerable] = false; // optimization: never proxy anything in config @@ -442,6 +474,7 @@ export function savePreferences( })), scrollOffset, autoScroll: state.autoScroll, + selectedSearchRecord: state.selectedSearchRecord, }; localStorage.setItem(state.storageKey, JSON.stringify(prefs)); }