From f2bf48d4e4b53bc7cf956b16b354b01187eaba23 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 7 Jun 2022 04:04:01 -0700 Subject: [PATCH] DataTable delighter: Introduce search history Summary: Getting the behavior exactly right was tricky, now settled on the following: * Don't automatically show the search history (the default) but introduce an explicit button to toggle it, as opening it by default was pretty obtrusive in testing * Items are added to the history when using return / explicitly clicking search, to only get "clean" entries into the history, and not half complete searches. Needing to press enter might be to subtle since datatable will also search without that, but not searching on keypress felt as a regression as well. * Introduced a menu item for clearing the search history * Search history is persisted like search filters. Yay to Antd's AutoComplete, which is really straightforward and cleanly composes with Input.Search. Changelog: DataTable will now keep a history of search items when hitting to search. Use the history button to bring up the history. Reviewed By: aigoncharov Differential Revision: D36736821 fbshipit-source-id: 8d18b85308a39bd1644057371040855d199545c7 --- .../src/ui/data-table/DataTable.tsx | 1 + .../src/ui/data-table/DataTableManager.tsx | 34 +++++- .../src/ui/data-table/TableContextMenu.tsx | 7 ++ .../src/ui/data-table/TableSearch.tsx | 113 ++++++++++++------ 4 files changed, 113 insertions(+), 42 deletions(-) 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, },