From 1ae3b9001911a751da24c638fec1b675a0b34b96 Mon Sep 17 00:00:00 2001 From: Benjamin Elo Date: Wed, 14 Aug 2019 05:24:39 -0700 Subject: [PATCH] Added LocationsButton when Navigation Plugin is active Summary: Here I've added the LocationsButton to the TitleBar in Flipper. This allows the user to navigate to saved bookmarks, or display the current page URI without ever opening the Navigation Plugin (Except to add bookmarks). The challenge of this diff was having a TitleBar child be controlled by a plugin. The LocationsButton pulls bookmarks straight from the database whenever a mouseDown event is called on the button. (The Electron popup menu does not respond to props changes, so the menu is opened on mouse up and getting the bookmarks from the database occurs on mouse down... This seems to work fine). The nav plugin on the Android side will now also send a welcome message alerting the app to created a persisted state for the navigation plugin, which shows the button in the TitleBar. Let me know if I can answer any questions. Reviewed By: danielbuechele Differential Revision: D16786330 fbshipit-source-id: afc95348d9b7ec4ee041f42bb4d022f58c6bb969 --- src/chrome/LocationsButton.tsx | 101 ++++++++++++++---- src/chrome/TitleBar.tsx | 19 +++- .../__tests__/testNavigationPlugin.node.js | 2 + src/plugins/navigation/flow-types.js | 2 +- src/plugins/navigation/index.js | 8 +- src/ui/components/Button.js | 11 +- 6 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/chrome/LocationsButton.tsx b/src/chrome/LocationsButton.tsx index f9df175fc..56b9ca5de 100644 --- a/src/chrome/LocationsButton.tsx +++ b/src/chrome/LocationsButton.tsx @@ -8,27 +8,55 @@ import {Button, styled} from 'flipper'; import {connect} from 'react-redux'; import React, {Component} from 'react'; -import {State} from '../reducers'; - +import {State as Store} from '../reducers'; +import {readBookmarksFromDB} from '../plugins/navigation/util/indexedDB.js'; +import {State as NavPluginState} from '../plugins/navigation/flow-types'; import BaseDevice from '../devices/BaseDevice'; +import {State as PluginState} from 'src/reducers/pluginStates'; -type OwnProps = { - locations: Array; - selectedLocation?: string; +type State = { + bookmarks: Array; }; +type OwnProps = {}; + type StateFromProps = { + currentURI: string; selectedDevice: BaseDevice | null | undefined; }; type DispatchFromProps = {}; +type Bookmark = { + uri: string; + commonName: string; +}; + const DropdownButton = styled(Button)({ fontSize: 11, }); +const shortenText = (text: String, MAX_CHARACTERS = 30): String => { + if (text.length <= MAX_CHARACTERS) { + return text; + } else { + return ( + text + .split('') + .slice(0, MAX_CHARACTERS) + .join('') + '...' + ); + } +}; + type Props = OwnProps & StateFromProps & DispatchFromProps; -class LocationsButton extends Component { +class LocationsButton extends Component { + state = { + bookmarks: [], + hasRetrievedBookmarks: false, + retreivingBookmarks: false, + }; + goToLocation = (location: string) => { const {selectedDevice} = this.props; if (selectedDevice != null) { @@ -36,27 +64,64 @@ class LocationsButton extends Component { } }; + updateBookmarks = () => { + readBookmarksFromDB().then(bookmarksMap => { + const bookmarks: Array = []; + bookmarksMap.forEach((bookmark: Bookmark) => { + bookmarks.push(bookmark); + }); + this.setState({bookmarks}); + }); + }; + + componentDidMount = () => { + this.updateBookmarks(); + }; + render() { - const {locations, selectedLocation} = this.props; + const {currentURI} = this.props; + const {bookmarks} = this.state; return ( { - return { - click: () => { - this.goToLocation(location); - }, - label: location, - }; - })}> - {selectedLocation || '(none)'} + dropdown={[ + { + label: 'Bookmarks', + enabled: false, + }, + ...bookmarks.map(bookmark => { + return { + click: () => { + this.goToLocation(bookmark.uri); + }, + label: shortenText( + bookmark.commonName + ' - ' + bookmark.uri, + 100, + ), + }; + }), + ]}> + {(currentURI && shortenText(currentURI)) || '(none)'} ); } } -export default connect( - ({connections: {selectedDevice}}) => ({ +const mapStateFromPluginStatesToProps = (pluginStates: PluginState) => { + const navPluginState: NavPluginState = + pluginStates[ + Object.keys(pluginStates).find(key => /#Navigation$/.test(key)) + ]; + const currentURI = navPluginState && navPluginState.currentURI; + return { + currentURI, + }; +}; + +export default connect( + ({connections: {selectedDevice}, pluginStates}) => ({ selectedDevice, + ...mapStateFromPluginStatesToProps(pluginStates), }), )(LocationsButton); diff --git a/src/chrome/TitleBar.tsx b/src/chrome/TitleBar.tsx index 527db3b0f..e9681db20 100644 --- a/src/chrome/TitleBar.tsx +++ b/src/chrome/TitleBar.tsx @@ -19,6 +19,7 @@ import { colors, Button, ButtonGroup, + ButtonGroupChain, FlexRow, Spacer, styled, @@ -28,6 +29,7 @@ import { import {connect} from 'react-redux'; import RatingButton from './RatingButton'; import DevicesButton from './DevicesButton'; +import LocationsButton from './LocationsButton'; import ScreenCaptureButtons from './ScreenCaptureButtons'; import AutoUpdateVersion from './AutoUpdateVersion'; import UpdateIndicator from './UpdateIndicator'; @@ -78,6 +80,7 @@ type StateFromProps = { launcherMsg: LauncherMsg; flipperRating: number | null; share: ShareType | null | undefined; + navPluginIsActive: boolean; }; const VersionText = styled(Text)({ @@ -139,10 +142,18 @@ function statusMessageComponent( type Props = OwnProps & DispatchFromProps & StateFromProps; class TitleBar extends React.Component { render() { - const {share} = this.props; + const {navPluginIsActive, share} = this.props; return ( - + {navPluginIsActive ? ( + + + + + ) : ( + + )} + {statusMessageComponent( this.props.downloadingImportData, @@ -206,6 +217,7 @@ export default connect( flipperRating, share, }, + pluginStates, }) => ({ windowIsFocused, leftSidebarVisible, @@ -215,6 +227,9 @@ export default connect( launcherMsg, flipperRating, share, + navPluginIsActive: Object.keys(pluginStates).some(key => + /#Navigation$/.test(key), + ), }), { setActiveSheet, diff --git a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js index 4dea66416..3588c665c 100644 --- a/src/plugins/navigation/__tests__/testNavigationPlugin.node.js +++ b/src/plugins/navigation/__tests__/testNavigationPlugin.node.js @@ -13,6 +13,7 @@ import type {Bookmark, PersistedState, URI} from '../flow-types'; function constructPersistedStateMock(): PersistedState { return { + currentURI: '', appMatchPatterns: [], appMatchPatternsProvider: new DefaultProvider(), bookmarksProvider: new DefaultProvider(), @@ -23,6 +24,7 @@ function constructPersistedStateMock(): PersistedState { function constructPersistedStateMockWithEvents(): PersistedState { return { + currentURI: '', appMatchPatterns: [], appMatchPatternsProvider: new DefaultProvider(), bookmarksProvider: new DefaultProvider(), diff --git a/src/plugins/navigation/flow-types.js b/src/plugins/navigation/flow-types.js index 37b07ebde..9c99bd96e 100644 --- a/src/plugins/navigation/flow-types.js +++ b/src/plugins/navigation/flow-types.js @@ -9,7 +9,6 @@ export type URI = string; export type State = {| - currentURI: string, shouldShowSaveBookmarkDialog: boolean, shouldShowURIErrorDialog: boolean, saveBookmarkURI: ?URI, @@ -22,6 +21,7 @@ export type PersistedState = {| bookmarksProvider: AutoCompleteProvider, appMatchPatterns: Array, appMatchPatternsProvider: AutoCompleteProvider, + currentURI: string, |}; export type NavigationEvent = {| diff --git a/src/plugins/navigation/index.js b/src/plugins/navigation/index.js index 4e11c3c6c..4882e0903 100644 --- a/src/plugins/navigation/index.js +++ b/src/plugins/navigation/index.js @@ -44,6 +44,7 @@ export default class extends FlipperPlugin { static defaultPersistedState: PersistedState = { navigationEvents: [], bookmarks: new Map(), + currentURI: '', bookmarksProvider: new DefaultProvider(), appMatchPatterns: [], appMatchPatternsProvider: new DefaultProvider(), @@ -53,7 +54,6 @@ export default class extends FlipperPlugin { shouldShowSaveBookmarkDialog: false, saveBookmarkURI: null, shouldShowURIErrorDialog: false, - currentURI: '', requiredParameters: [], }; @@ -66,6 +66,8 @@ export default class extends FlipperPlugin { case 'nav_event': return { ...persistedState, + currentURI: + payload.uri == null ? persistedState.currentURI : payload.uri, navigationEvents: [ { uri: payload.uri === undefined ? null : payload.uri, @@ -110,7 +112,7 @@ export default class extends FlipperPlugin { }; navigateTo = (query: string) => { - this.setState({currentURI: query}); + this.props.setPersistedState({currentURI: query}); const requiredParameters = getRequiredParameters(query); if (requiredParameters.length === 0) { this.getDevice().then(device => { @@ -156,7 +158,6 @@ export default class extends FlipperPlugin { render() { const { - currentURI, saveBookmarkURI, shouldShowSaveBookmarkDialog, shouldShowURIErrorDialog, @@ -165,6 +166,7 @@ export default class extends FlipperPlugin { const { bookmarks, bookmarksProvider, + currentURI, appMatchPatternsProvider, navigationEvents, } = this.props.persistedState; diff --git a/src/ui/components/Button.js b/src/ui/components/Button.js index 68fae2234..a8e39031d 100644 --- a/src/ui/components/Button.js +++ b/src/ui/components/Button.js @@ -163,6 +163,10 @@ const Icon = styled(Glyph)(({hasText}) => ({ })); type Props = { + /** + * onMouseUp handler. + */ + onMouseDown?: (event: SyntheticMouseEvent<>) => any, /** * onClick handler. */ @@ -248,7 +252,12 @@ class Button extends React.Component< _ref = React.createRef(); - onMouseDown = () => this.setState({active: true, wasClosed: false}); + onMouseDown = (e: SyntheticMouseEvent<>) => { + this.setState({active: true, wasClosed: false}); + if (this.props.onMouseDown != null) { + this.props.onMouseDown(e); + } + }; onMouseUp = () => { if (this.props.disabled === true) { return;