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
This commit is contained in:
Benjamin Elo
2019-08-02 01:53:45 -07:00
committed by Facebook Github Bot
parent f591475f85
commit ea8a6546c9
7 changed files with 146 additions and 82 deletions

View File

@@ -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<URI, Bookmark>(),
navigationEvents: [],
};
}
function constructPersistedStateMockWithEvents(): PersistedState {
return {
bookmarksProvider: new DefaultProvider(),
bookmarks: new Map<URI, Bookmark>(),
navigationEvents: [
{
uri: 'mock://this_is_a_mock_uri/mock/1',

View File

@@ -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<string, Bookmark>,
providers: Array<AutoCompleteProvider>,
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<Bookmark>,
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 (
<AutoCompleteSheetContainer>
{filteredBookmarks.map((bookmark, idx) => (
{lineItems.map((lineItem, idx) => (
<SheetItem
className={idx === selectedItem ? 'selected' : null}
key={bookmark.uri}
onMouseDown={() => onNavigate(bookmark.uri)}>
{bookmark.uri}
key={idx}
onMouseDown={() => onNavigate(lineItem.uri)}>
<SheetItemIcon>
<Glyph name={lineItem.icon} size={16} variant="outline" />
</SheetItemIcon>
{lineItem.matchPattern}
</SheetItem>
))}
</AutoCompleteSheetContainer>

View File

@@ -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<string, Bookmark>,
providers: Array<AutoCompleteProvider>,
|};
type State = {|
query: string,
inputFocused: boolean,
autoCompleteSheetOpen: boolean,
searchInputValue: string,
|};
const IconContainer = styled('div')({
@@ -65,73 +67,84 @@ class SearchBar extends Component<Props, State> {
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 (
<ToolbarContainer>
<Toolbar>
<SearchBox className={inputFocused ? 'drop-shadow' : null}>
<SearchInputContainer>
<SearchInput
value={query}
value={searchInputValue}
onBlur={() =>
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 ? (
<AutoCompleteSheet
bookmarks={bookmarks}
providers={providers}
onNavigate={this.navigateTo}
onHighlighted={newQuery => this.setState({query: newQuery})}
onHighlighted={newInputValue =>
this.setState({searchInputValue: newInputValue})
}
query={query}
/>
) : null}
</SearchInputContainer>
</SearchBox>
{query.length > 0 ? (
{searchInputValue.length > 0 ? (
<IconContainer>
<IconButton
icon="send"
size={16}
outline={true}
onClick={() => this.navigateTo(this.state.query)}
onClick={() => this.navigateTo(searchInputValue)}
/>
<FavoriteButton
size={16}
highlighted={bookmarks.has(query)}
onClick={() => this.favorite(this.state.query)}
highlighted={bookmarks.has(searchInputValue)}
onClick={() => this.favorite(searchInputValue)}
/>
</IconContainer>
) : null}

View File

@@ -9,13 +9,14 @@
export type URI = string;
export type State = {|
bookmarks: Map<URI, Bookmark>,
shouldShowSaveBookmarkDialog: boolean,
saveBookmarkURI: ?URI,
|};
export type PersistedState = {|
bookmarks: Map<URI, Bookmark>,
navigationEvents: Array<NavigationEvent>,
bookmarksProvider: AutoCompleteProvider,
|};
export type NavigationEvent = {|

View File

@@ -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<AutoCompleteLineItem>,
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;
};

View File

@@ -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<State, {}, PersistedState> {
static defaultPersistedState: PersistedState = {
navigationEvents: [],
bookmarks: new Map<string, Bookmark>(),
bookmarksProvider: new DefaultProvider(),
};
state = {
bookmarks: new Map<string, Bookmark>(),
shouldShowSaveBookmarkDialog: false,
saveBookmarkURI: null,
};
@@ -69,7 +74,10 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
componentDidMount = () => {
readBookmarksFromDB().then(bookmarks => {
this.setState({bookmarks});
this.props.setPersistedState({
bookmarks: bookmarks,
bookmarksProvider: bookmarksToAutoCompleteProvider(bookmarks),
});
});
};
@@ -95,29 +103,38 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
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 (
<ScrollableFlexColumn>
<SearchBar
providers={autoCompleteProviders}
bookmarks={bookmarks}
onNavigate={this.navigateTo}
onFavorite={this.onFavorite}

View File

@@ -13,6 +13,12 @@ import type {
AutoCompleteLineItem,
} from '../flow-types';
export function DefaultProvider(): AutoCompleteProvider {
this.icon = 'caution';
this.matchPatterns = new Map<string, URI>();
return this;
}
export const bookmarksToAutoCompleteProvider: (
Map<URI, Bookmark>,
) => AutoCompleteProvider = bookmarks => {