diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index f6c17f2dc..e6cf3644f 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -512,6 +512,7 @@ export function DataTable( searchValue={searchValue} useRegex={tableState.useRegex} dispatch={dispatch as any} + searchHistory={tableState.searchHistory} contextMenu={props.enableContextMenu ? contexMenu : undefined} extraActions={props.extraActions} /> diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx index 8e991fd0d..e4b1d45ec 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTableManager.tsx @@ -28,6 +28,8 @@ const emptySelection: Selection = { current: -1, }; +const MAX_HISTORY = 1000; + type PersistedState = { /** Active search value */ search: string; @@ -43,6 +45,7 @@ type PersistedState = { >[]; scrollOffset: number; autoScroll: boolean; + searchHistory: string[]; }; type Action = {type: Name} & Args; @@ -58,7 +61,7 @@ type DataManagerActions = | Action<'sortColumn', {column: keyof T; direction: SortDirection}> /** Show / hide the given column */ | Action<'toggleColumnVisibility', {column: keyof T}> - | Action<'setSearchValue', {value: string}> + | Action<'setSearchValue', {value: string; addToHistory: boolean}> | Action< 'selectItem', { @@ -96,7 +99,8 @@ type DataManagerActions = | Action<'toggleUseRegex'> | Action<'toggleAutoScroll'> | Action<'setAutoScroll', {autoScroll: boolean}> - | Action<'toggleSearchValue'>; + | Action<'toggleSearchValue'> + | Action<'clearSearchHistory'>; type DataManagerConfig = { dataSource: DataSource; @@ -116,11 +120,12 @@ export type DataManagerState = { columns: DataTableColumn[]; sorting: Sorting | undefined; selection: Selection; - searchValue: string; useRegex: boolean; autoScroll: boolean; + searchValue: string; /** Used to remember the record entry to lookup when user presses ctrl */ previousSearchValue: string; + searchHistory: string[]; }; export type DataTableReducer = Reducer< @@ -173,6 +178,17 @@ export const dataTableManagerReducer = produce< case 'setSearchValue': { draft.searchValue = action.value; draft.previousSearchValue = ''; + if ( + action.addToHistory && + action.value && + !draft.searchHistory.includes(action.value) + ) { + draft.searchHistory.unshift(action.value); + // FIFO if history too large + if (draft.searchHistory.length > MAX_HISTORY) { + draft.searchHistory.length = MAX_HISTORY; + } + } break; } case 'toggleSearchValue': { @@ -185,6 +201,10 @@ export const dataTableManagerReducer = produce< } break; } + case 'clearSearchHistory': { + draft.searchHistory = []; + break; + } case 'toggleUseRegex': { draft.useRegex = !draft.useRegex; break; @@ -305,7 +325,7 @@ export type DataTableManager = { getSelectedItems(): readonly T[]; toggleColumnVisibility(column: keyof T): void; sortColumn(column: keyof T, direction?: SortDirection): void; - setSearchValue(value: string): void; + setSearchValue(value: string, addToHistory?: boolean): void; dataSource: DataSource; toggleSearchValue(): void; }; @@ -351,8 +371,8 @@ export function createDataTableManager( sortColumn(column, direction) { dispatch({type: 'sortColumn', column, direction}); }, - setSearchValue(value) { - dispatch({type: 'setSearchValue', value}); + setSearchValue(value, addToHistory = false) { + dispatch({type: 'setSearchValue', value, addToHistory}); }, toggleSearchValue() { dispatch({type: 'toggleSearchValue'}); @@ -399,6 +419,7 @@ export function createInitialState( : emptySelection, searchValue: prefs?.search ?? '', previousSearchValue: '', + searchHistory: prefs?.searchHistory ?? [], useRegex: prefs?.useRegex ?? false, autoScroll: prefs?.autoScroll ?? config.autoScroll ?? false, }; @@ -478,6 +499,7 @@ export function savePreferences( })), scrollOffset, autoScroll: state.autoScroll, + searchHistory: state.searchHistory, }; localStorage.setItem(state.storageKey, JSON.stringify(prefs)); } diff --git a/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx b/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx index af1caccbe..fe8187106 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableContextMenu.tsx @@ -177,6 +177,13 @@ export function tableContextMenuFactory( }}> Reset view + { + dispatch({type: 'clearSearchHistory'}); + }}> + Clear search history + ); } diff --git a/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx b/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx index 0d5af4ef3..5b7fca49e 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableSearch.tsx @@ -7,9 +7,9 @@ * @format */ -import {MenuOutlined} from '@ant-design/icons'; -import {Button, Dropdown, Input} from 'antd'; -import React, {memo, useCallback, useMemo} from 'react'; +import {HistoryOutlined, MenuOutlined} from '@ant-design/icons'; +import {Button, Dropdown, Input, AutoComplete} from 'antd'; +import React, {memo, useCallback, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Layout} from '../Layout'; @@ -20,18 +20,21 @@ export const TableSearch = memo(function TableSearch({ searchValue, useRegex, dispatch, + searchHistory, extraActions, contextMenu, }: { searchValue: string; useRegex: boolean; dispatch: DataTableDispatch; + searchHistory: string[]; extraActions?: React.ReactElement; contextMenu: undefined | (() => JSX.Element); }) { + const [showHistory, setShowHistory] = useState(false); const onSearch = useCallback( - (value: string) => { - dispatch({type: 'setSearchValue', value}); + (value: string, addToHistory: boolean) => { + dispatch({type: 'setSearchValue', value, addToHistory}); }, [dispatch], ); @@ -54,38 +57,72 @@ export const TableSearch = memo(function TableSearch({ } }, [useRegex, searchValue]); + const options = useMemo( + () => searchHistory.map((value) => ({label: value, value})), + [searchHistory], + ); + return ( - - .* - + + option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 } - onChange={(e) => { - onSearch(e.target.value); + onSelect={(value: string) => { + setShowHistory(false); + onSearch(value, false); }} - /> + onDropdownVisibleChange={(open) => { + if (!open) { + setShowHistory(false); + } + }}> + + {options.length ? ( + { + setShowHistory((v) => !v); + }}> + + + ) : null} + + .* + + + } + onChange={(e) => { + onSearch(e.target.value, false); + }} + onSearch={(value) => { + onSearch(value, true); + }} + /> + {extraActions} {contextMenu && ( @@ -108,17 +145,21 @@ const Searchbar = styled(Layout.Horizontal)({ padding: `${theme.space.tiny}px ${theme.space.small}px`, background: 'transparent', }, + '> .ant-select': { + flex: 1, + }, }); const RegexButton = styled(Button)({ padding: '0px !important', borderRadius: 4, - marginRight: -6, - marginLeft: 4, + // marginRight: -6, + // marginLeft: 4, lineHeight: '20px', - width: 20, + width: 16, height: 20, border: 'none', + color: theme.disabledColor, '& :hover': { color: theme.primaryColor, },