From 499275af8aee608bdf1bba059e4d24c9220f216b Mon Sep 17 00:00:00 2001 From: Feiyu Wong Date: Wed, 27 Jul 2022 11:37:59 -0700 Subject: [PATCH] Add keyboard shortcut to support quick selecting recent searches Summary: Currently there's no way of quickly selecting recent searches other than manually opening the recent search history and then clicking one of the search terms. This diff seeks to add a new feature that would allow not only a keyboard short cut to open the recent search history drop down but also number the most recent 5 search terms so that the user could quickly select them with a number on their keyboard Additionally, fixed bug found in terms of the search bar not showing the current search value correctly `Changelog`: Introduced keyboard shortcut(ctrl + f) option to toggle the search history dropdown along with numbers attached to the options in order to quickly navigate to recent search terms. Have to first enable the option(search shortcut) in menu in order to use the feature. Also added a new button in the options menu that would trigger the search result toggle as triggered by the keyboard shortcut `ctrl` before(`ctrl` + `t` now) WARNING: The current behavior of "ctrl" toggling back and forth to focus the selected item has been migrated to "ctrl + t" key combo Reviewed By: mweststrate Differential Revision: D37685738 fbshipit-source-id: a7ac4dd3dceb846a98258de2d884ebc279ee5995 --- .../src/ui/data-table/DataTable.tsx | 20 ++- .../src/ui/data-table/DataTableManager.tsx | 24 +++- .../src/ui/data-table/TableSearch.tsx | 116 +++++++++++++++--- 3 files changed, 140 insertions(+), 20 deletions(-) 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); }}>