diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index d5331bdfa..a9d674f5b 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -46,7 +46,7 @@ import {TableSearch} from './TableSearch'; import styled from '@emotion/styled'; import {theme} from '../theme'; import {tableContextMenuFactory} from './TableContextMenu'; -import {Menu, Switch, Typography} from 'antd'; +import {Menu, Switch, InputRef, Typography} from 'antd'; import {CoffeeOutlined, SearchOutlined, PushpinFilled} from '@ant-design/icons'; import {useAssertStableRef} from '../../utils/useAssertStableRef'; import {Formatter} from '../DataFormatter'; @@ -176,6 +176,7 @@ export function DataTable( const stateRef = useRef(tableState); stateRef.current = tableState; + const searchInputRef = useRef(null) as MutableRefObject; const lastOffset = useRef(0); const dragging = useRef(false); @@ -300,6 +301,7 @@ export function DataTable( let handled = true; const shiftPressed = e.shiftKey; const outputSize = dataView.size; + const controlPressed = e.ctrlKey; const windowSize = props.scrollable ? virtualizerRef.current?.virtualItems.length ?? 0 : dataView.size; @@ -341,12 +343,21 @@ export function DataTable( case 'Escape': tableManager.clearSelection(); break; - case 'Control': - tableManager.toggleSearchValue(); + case 't': + if (controlPressed) { + tableManager.toggleSearchValue(); + } break; case 'H': tableManager.toggleHighlightSearch(); break; + case 'f': + if (controlPressed && searchInputRef?.current) { + searchInputRef?.current.focus(); + tableManager.showSearchDropdown(true); + tableManager.setShowNumberedHistory(true); + } + break; default: handled = false; } @@ -580,10 +591,13 @@ export function DataTable( searchValue={searchValue} useRegex={tableState.useRegex} filterSearchHistory={tableState.filterSearchHistory} + showHistory={tableState.showSearchHistory} + showNumbered={tableState.showNumberedHistory} dispatch={dispatch as any} searchHistory={tableState.searchHistory} contextMenu={props.enableContextMenu ? contexMenu : undefined} extraActions={!props.viewId ? props.extraActions : undefined} + searchInputRef={searchInputRef} /> )} diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx index fc1ff362a..ab5719149 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx @@ -115,7 +115,9 @@ type DataManagerActions = | Action<'toggleHighlightSearch'> | Action<'setSearchHighlightColor', {color: string}> | Action<'toggleFilterSearchHistory'> - | Action<'toggleSideBySide'>; + | Action<'toggleSideBySide'> + | Action<'showSearchDropdown', {show: boolean}> + | Action<'setShowNumberedHistory', {showNumberedHistory: boolean}>; type DataManagerConfig = { dataSource: DataSource; @@ -138,6 +140,8 @@ export type DataManagerState = { selection: Selection; useRegex: boolean; filterSearchHistory: boolean; + showSearchHistory: boolean; + showNumberedHistory: boolean; autoScroll: boolean; searchValue: string; /** Used to remember the record entry to lookup when user presses ctrl */ @@ -335,6 +339,14 @@ export const dataTableManagerReducer = produce< draft.sideBySide = !draft.sideBySide; break; } + case 'showSearchDropdown': { + draft.showSearchHistory = action.show; + break; + } + case 'setShowNumberedHistory': { + draft.showNumberedHistory = action.showNumberedHistory; + break; + } default: { throw new Error('Unknown action ' + (action as any).type); } @@ -369,6 +381,8 @@ export type DataTableManager = { toggleHighlightSearch(): void; setSearchHighlightColor(color: string): void; toggleSideBySide(): void; + showSearchDropdown(show: boolean): void; + setShowNumberedHistory(showNumberedHistory: boolean): void; }; export function createDataTableManager( @@ -427,6 +441,12 @@ export function createDataTableManager( toggleSideBySide() { dispatch({type: 'toggleSideBySide'}); }, + showSearchDropdown(show) { + dispatch({type: 'showSearchDropdown', show}); + }, + setShowNumberedHistory(showNumberedHistory) { + dispatch({type: 'setShowNumberedHistory', showNumberedHistory}); + }, dataView, }; } @@ -478,6 +498,8 @@ export function createInitialState( color: theme.searchHighlightBackground.yellow, }, sideBySide: false, + showSearchHistory: false, + showNumberedHistory: false, }; // @ts-ignore res.config[immerable] = false; // optimization: never proxy anything in config diff --git a/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx b/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx index 1003a6f74..256b88455 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx @@ -8,32 +8,58 @@ */ import {HistoryOutlined, MenuOutlined} from '@ant-design/icons'; -import {Button, Dropdown, Input, AutoComplete} from 'antd'; -import React, {memo, useCallback, useMemo, useState} from 'react'; +import {Button, Dropdown, Input, AutoComplete, InputRef} from 'antd'; +import React, {memo, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {Layout} from '../Layout'; import {theme} from '../theme'; import type {DataTableDispatch} from './DataTableManager'; +const MAX_RECENT = 5; + export const TableSearch = memo(function TableSearch({ searchValue, useRegex, filterSearchHistory, + showHistory, + showNumbered, dispatch, searchHistory, extraActions, contextMenu, + searchInputRef, }: { searchValue: string; useRegex: boolean; filterSearchHistory: boolean; + showHistory: boolean; + showNumbered: boolean; dispatch: DataTableDispatch; searchHistory: string[]; extraActions?: React.ReactElement; contextMenu: undefined | (() => JSX.Element); + searchInputRef?: React.MutableRefObject; }) { - const [showHistory, setShowHistory] = useState(false); + const filteredSearchHistory = useMemo( + () => + filterSearchHistory + ? searchHistory.filter( + (value) => + value.toUpperCase().indexOf(searchValue.toUpperCase()) !== -1, + ) + : searchHistory, + [filterSearchHistory, searchHistory, searchValue], + ); + + const options = useMemo(() => { + return filteredSearchHistory.map((value, index) => ({ + label: + showNumbered && index < MAX_RECENT ? `${index + 1}: ${value}` : value, + value, + })); + }, [filteredSearchHistory, showNumbered]); + const onSearch = useCallback( (value: string, addToHistory: boolean) => { dispatch({type: 'setSearchValue', value, addToHistory}); @@ -48,6 +74,71 @@ export const TableSearch = memo(function TableSearch({ }, [dispatch], ); + const toggleSearchDropdown = useCallback( + (show: boolean) => { + dispatch({type: 'showSearchDropdown', show: show}); + }, + [dispatch], + ); + const toggleShowNumberedHistory = useCallback( + (showNumberedHistory: boolean) => { + dispatch({type: 'setShowNumberedHistory', showNumberedHistory}); + }, + [dispatch], + ); + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case 'f': + if (e.ctrlKey && searchHistory.length > 0) { + if (!showHistory) { + toggleShowNumberedHistory(true); + } + toggleSearchDropdown(!showHistory); + } + break; + case 'Control': + if (showHistory) { + toggleShowNumberedHistory(true); + } + break; + default: + const possNumber = Number(e.key); + if ( + e.ctrlKey && + possNumber && + showNumbered && + possNumber <= Math.min(MAX_RECENT, filteredSearchHistory.length) + ) { + toggleSearchDropdown(false); + onSearch(filteredSearchHistory[possNumber - 1], false); + e.preventDefault(); + } + } + e.stopPropagation(); + }, + [ + searchHistory.length, + showHistory, + showNumbered, + filteredSearchHistory, + toggleSearchDropdown, + toggleShowNumberedHistory, + onSearch, + ], + ); + const onKeyUp = useCallback( + (e: React.KeyboardEvent) => { + switch (e.key) { + case 'Control': + toggleShowNumberedHistory(false); + break; + } + e.stopPropagation(); + e.preventDefault(); + }, + [toggleShowNumberedHistory], + ); const regexError = useMemo(() => { if (!useRegex || !searchValue) { return; @@ -59,40 +150,33 @@ export const TableSearch = memo(function TableSearch({ } }, [useRegex, searchValue]); - const options = useMemo( - () => searchHistory.map((value) => ({label: value, value})), - [searchHistory], - ); - return ( - + - !filterSearchHistory || - option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 - } onSelect={(value: string) => { - setShowHistory(false); + toggleSearchDropdown(false); onSearch(value, false); }} onDropdownVisibleChange={(open) => { if (!open) { - setShowHistory(false); + toggleSearchDropdown(false); } }} value={searchValue}> {options.length ? ( { - setShowHistory((v) => !v); + toggleSearchDropdown(!showHistory); }}>