From ea8a6546c9186f9b2515f01d194070cd198db1d7 Mon Sep 17 00:00:00 2001 From: Benjamin Elo Date: Fri, 2 Aug 2019 01:53:45 -0700 Subject: [PATCH] Added providers to the auto complete sheet Summary: Okay so the main changes here are integrating the providers into the auto complete sheet and getting the search bar to work with it also. For instance, in the search bar, I want to update the value string to whatever the user has highlighted in the auto complete sheet, without executing a new query. So thus, I had to create a new state variable in the search bar component for this. I've also moved the custom hook into its own file to keep the component short in size. It had to be mainly rewritten to support providers instead of only bookmarks. Same goes for the entire AutoCompleteSheet component. The bookmarksProvider is stored in the persisted state as to not regenerate every-time on render. It is only updated if the bookmarks are updated which are also now stored in the persistedState for the same reason. Lastly, a DefaultProvider object was also made for the initial persisted state object. Reviewed By: danielbuechele Differential Revision: D16581644 fbshipit-source-id: 88723a4081d96250f723a4cd7b1ade101bf3e8f3 --- .../__tests__/testNavigationPlugin.node.js | 7 +- .../components/AutoCompleteSheet.js | 69 ++++++------------- .../navigation/components/SearchBar.js | 55 +++++++++------ src/plugins/navigation/flow-types.js | 3 +- .../navigation/hooks/autoCompleteSheet.js | 51 ++++++++++++++ src/plugins/navigation/index.js | 37 +++++++--- .../navigation/util/autoCompleteProvider.js | 6 ++ 7 files changed, 146 insertions(+), 82 deletions(-) create mode 100644 src/plugins/navigation/hooks/autoCompleteSheet.js diff --git a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js index 18db6fb6f..f64fe3458 100644 --- a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js +++ b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js @@ -7,17 +7,22 @@ */ import NavigationPlugin from '../'; +import {DefaultProvider} from '../util/autoCompleteProvider'; -import type {PersistedState} from '../flow-types'; +import type {Bookmark, PersistedState, URI} from '../flow-types'; function constructPersistedStateMock(): PersistedState { return { + bookmarksProvider: new DefaultProvider(), + bookmarks: new Map(), navigationEvents: [], }; } function constructPersistedStateMockWithEvents(): PersistedState { return { + bookmarksProvider: new DefaultProvider(), + bookmarks: new Map(), navigationEvents: [ { uri: 'mock://this_is_a_mock_uri/mock/1', diff --git a/src/plugins/navigation/components/AutoCompleteSheet.js b/src/plugins/navigation/components/AutoCompleteSheet.js index 5625b6241..b2deba7ae 100644 --- a/src/plugins/navigation/components/AutoCompleteSheet.js +++ b/src/plugins/navigation/components/AutoCompleteSheet.js @@ -6,22 +6,23 @@ * @flow strict-local */ -import {styled} from 'flipper'; -import {useEffect, useState} from 'react'; +import {Glyph, styled} from 'flipper'; +import {useItemNavigation} from '../hooks/autoCompleteSheet'; +import {filterProvidersToLineItems} from '../util/autoCompleteProvider'; -import type {Bookmark} from '../flow-types'; +import type {AutoCompleteProvider} from '../flow-types'; type Props = {| - bookmarks: Map, + providers: Array, onHighlighted: string => void, onNavigate: string => void, + query: string, |}; const MAX_ITEMS = 5; const AutoCompleteSheetContainer = styled('div')({ width: '100%', - overflowY: 'scroll', position: 'absolute', top: '100%', backgroundColor: 'white', @@ -44,56 +45,26 @@ const SheetItem = styled('div')({ }, }); -// Menu Item Navigation Hook -const useItemNavigation = ( - bookmarks: Array, - onHighlighted: string => void, -) => { - const [selectedItem, setSelectedItem] = useState(-1); - - const handleKeyPress = ({key}) => { - switch (key) { - case 'ArrowDown': { - const newSelectedItem = - selectedItem < MAX_ITEMS - 1 ? selectedItem + 1 : selectedItem; - setSelectedItem(newSelectedItem); - onHighlighted(bookmarks[newSelectedItem].uri); - break; - } - case 'ArrowUp': { - const newSelectedItem = - selectedItem > 0 ? selectedItem - 1 : selectedItem; - setSelectedItem(newSelectedItem); - onHighlighted(bookmarks[newSelectedItem].uri); - break; - } - default: - break; - } - }; - - useEffect(() => { - window.addEventListener('keydown', handleKeyPress); - return () => { - window.removeEventListener('keydown', handleKeyPress); - }; - }); - - return selectedItem; -}; +const SheetItemIcon = styled('span')({ + padding: 8, +}); export default (props: Props) => { - const {bookmarks, onHighlighted, onNavigate} = props; - const filteredBookmarks = [...bookmarks.values()].slice(0, MAX_ITEMS); - const selectedItem = useItemNavigation(filteredBookmarks, onHighlighted); + const {providers, onHighlighted, onNavigate, query} = props; + const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS); + lineItems.unshift({uri: query, matchPattern: query, icon: 'send'}); + const selectedItem = useItemNavigation(lineItems, onHighlighted); return ( - {filteredBookmarks.map((bookmark, idx) => ( + {lineItems.map((lineItem, idx) => ( onNavigate(bookmark.uri)}> - {bookmark.uri} + key={idx} + onMouseDown={() => onNavigate(lineItem.uri)}> + + + + {lineItem.matchPattern} ))} diff --git a/src/plugins/navigation/components/SearchBar.js b/src/plugins/navigation/components/SearchBar.js index b36247e7a..9f3c89301 100644 --- a/src/plugins/navigation/components/SearchBar.js +++ b/src/plugins/navigation/components/SearchBar.js @@ -16,18 +16,20 @@ import { } from 'flipper'; import {AutoCompleteSheet, IconButton, FavoriteButton} from './'; -import type {Bookmark} from '../flow-types'; +import type {AutoCompleteProvider, Bookmark} from '../flow-types'; type Props = {| onFavorite: (query: string) => void, onNavigate: (query: string) => void, bookmarks: Map, + providers: Array, |}; type State = {| query: string, inputFocused: boolean, autoCompleteSheetOpen: boolean, + searchInputValue: string, |}; const IconContainer = styled('div')({ @@ -65,73 +67,84 @@ class SearchBar extends Component { inputFocused: false, autoCompleteSheetOpen: false, query: '', + searchInputValue: '', }; - favorite = (query: string) => { - this.props.onFavorite(query); + favorite = (searchInputValue: string) => { + this.props.onFavorite(searchInputValue); }; - navigateTo = (query: string) => { - this.setState({query}); - this.props.onNavigate(query); + navigateTo = (searchInputValue: string) => { + this.setState({query: searchInputValue, searchInputValue}); + this.props.onNavigate(searchInputValue); }; queryInputChanged = (event: SyntheticInputEvent<>) => { - this.setState({query: event.target.value}); + const value = event.target.value; + this.setState({query: value, searchInputValue: value}); }; render = () => { - const {bookmarks} = this.props; - const {autoCompleteSheetOpen, inputFocused, query} = this.state; + const {bookmarks, providers} = this.props; + const { + autoCompleteSheetOpen, + inputFocused, + searchInputValue, + query, + } = this.state; return ( this.setState({ autoCompleteSheetOpen: false, inputFocused: false, }) } - onFocus={() => + onFocus={event => { + event.target.select(); this.setState({ autoCompleteSheetOpen: true, inputFocused: true, - }) - } + }); + }} onChange={this.queryInputChanged} onKeyPress={e => { if (e.key === 'Enter') { - this.navigateTo(this.state.query); + this.navigateTo(this.state.searchInputValue); e.target.blur(); } }} placeholder="Navigate To..." /> - {autoCompleteSheetOpen ? ( + {autoCompleteSheetOpen && query.length > 0 ? ( this.setState({query: newQuery})} + onHighlighted={newInputValue => + this.setState({searchInputValue: newInputValue}) + } + query={query} /> ) : null} - {query.length > 0 ? ( + {searchInputValue.length > 0 ? ( this.navigateTo(this.state.query)} + onClick={() => this.navigateTo(searchInputValue)} /> this.favorite(this.state.query)} + highlighted={bookmarks.has(searchInputValue)} + onClick={() => this.favorite(searchInputValue)} /> ) : null} diff --git a/src/plugins/navigation/flow-types.js b/src/plugins/navigation/flow-types.js index f926eb65a..9ee06d539 100644 --- a/src/plugins/navigation/flow-types.js +++ b/src/plugins/navigation/flow-types.js @@ -9,13 +9,14 @@ export type URI = string; export type State = {| - bookmarks: Map, shouldShowSaveBookmarkDialog: boolean, saveBookmarkURI: ?URI, |}; export type PersistedState = {| + bookmarks: Map, navigationEvents: Array, + bookmarksProvider: AutoCompleteProvider, |}; export type NavigationEvent = {| diff --git a/src/plugins/navigation/hooks/autoCompleteSheet.js b/src/plugins/navigation/hooks/autoCompleteSheet.js new file mode 100644 index 000000000..8e6299893 --- /dev/null +++ b/src/plugins/navigation/hooks/autoCompleteSheet.js @@ -0,0 +1,51 @@ +/** + * Copyright 2018-present Facebook. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @format + * @flow strict-local + */ + +import {useEffect, useState} from 'react'; +import type {AutoCompleteLineItem} from '../flow-types'; + +export const useItemNavigation = ( + lineItems: Array, + onHighlighted: string => void, +) => { + const [selectedItem, setSelectedItem] = useState(0); + + const handleKeyPress = ({key}) => { + switch (key) { + case 'ArrowDown': { + const newSelectedItem = + selectedItem < lineItems.length - 1 + ? selectedItem + 1 + : lineItems.length - 1; + setSelectedItem(newSelectedItem); + onHighlighted(lineItems[newSelectedItem].uri); + break; + } + case 'ArrowUp': { + const newSelectedItem = + selectedItem > 0 ? selectedItem - 1 : selectedItem; + setSelectedItem(newSelectedItem); + onHighlighted(lineItems[newSelectedItem].uri); + break; + } + default: { + setSelectedItem(0); + break; + } + } + }; + + useEffect(() => { + window.addEventListener('keydown', handleKeyPress); + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }); + + return selectedItem; +}; diff --git a/src/plugins/navigation/index.js b/src/plugins/navigation/index.js index 723bf19b8..56ad37862 100644 --- a/src/plugins/navigation/index.js +++ b/src/plugins/navigation/index.js @@ -19,6 +19,10 @@ import { readBookmarksFromDB, writeBookmarkToDB, } from './util/indexedDB'; +import { + bookmarksToAutoCompleteProvider, + DefaultProvider, +} from './util/autoCompleteProvider'; import type { State, @@ -35,10 +39,11 @@ export default class extends FlipperPlugin { static defaultPersistedState: PersistedState = { navigationEvents: [], + bookmarks: new Map(), + bookmarksProvider: new DefaultProvider(), }; state = { - bookmarks: new Map(), shouldShowSaveBookmarkDialog: false, saveBookmarkURI: null, }; @@ -69,7 +74,10 @@ export default class extends FlipperPlugin { componentDidMount = () => { readBookmarksFromDB().then(bookmarks => { - this.setState({bookmarks}); + this.props.setPersistedState({ + bookmarks: bookmarks, + bookmarksProvider: bookmarksToAutoCompleteProvider(bookmarks), + }); }); }; @@ -95,29 +103,38 @@ export default class extends FlipperPlugin { commonName: bookmark.commonName.length > 0 ? bookmark.commonName : bookmark.uri, }; + writeBookmarkToDB(newBookmark); - const newMapRef = this.state.bookmarks; + const newMapRef = this.props.persistedState.bookmarks; newMapRef.set(newBookmark.uri, newBookmark); - this.setState({bookmarks: newMapRef}); + this.props.setPersistedState({ + bookmarks: newMapRef, + bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef), + }); }; removeBookmark = (uri: string) => { removeBookmark(uri); - const newMapRef = this.state.bookmarks; + const newMapRef = this.props.persistedState.bookmarks; newMapRef.delete(uri); - this.setState({bookmarks: newMapRef}); + this.props.setPersistedState({ + bookmarks: newMapRef, + bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef), + }); }; render() { + const {saveBookmarkURI, shouldShowSaveBookmarkDialog} = this.state; const { bookmarks, - saveBookmarkURI, - shouldShowSaveBookmarkDialog, - } = this.state; - const {navigationEvents} = this.props.persistedState; + bookmarksProvider, + navigationEvents, + } = this.props.persistedState; + const autoCompleteProviders = [bookmarksProvider]; return ( (); + return this; +} + export const bookmarksToAutoCompleteProvider: ( Map, ) => AutoCompleteProvider = bookmarks => {