diff --git a/desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx b/desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx index e52db37e1..995b9314b 100644 --- a/desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx @@ -8,14 +8,18 @@ */ import React, {useCallback, useMemo} from 'react'; -import {AutoComplete, Input} from 'antd'; +import {AutoComplete, Input, Typography} from 'antd'; import {StarFilled, StarOutlined} from '@ant-design/icons'; import {useStore} from '../../utils/useStore'; -import {NUX, useValue} from 'flipper-plugin'; +import {Layout, NUX, useValue} from 'flipper-plugin'; import {navPluginStateSelector} from '../../chrome/LocationsButton'; // eslint-disable-next-line flipper/no-relative-imports-across-packages import type {NavigationPlugin} from '../../../../plugins/navigation/index'; +import {useMemoize} from '../../utils/useMemoize'; +import styled from '@emotion/styled'; + +const {Text} = Typography; export function BookmarkSection() { const navPlugin = useStore(navPluginStateSelector); @@ -32,15 +36,22 @@ export function BookmarkSection() { function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) { const currentURI = useValue(navPlugin.currentURI); const bookmarks = useValue(navPlugin.bookmarks); + const patterns = useValue(navPlugin.appMatchPatterns); const isBookmarked = useMemo(() => bookmarks.has(currentURI), [ bookmarks, currentURI, ]); + + const autoCompleteItems = useMemoize( + navPlugin.getAutoCompleteAppMatchPatterns, + [currentURI, bookmarks, patterns, 20], + ); + const handleBookmarkClick = useCallback(() => { if (isBookmarked) { navPlugin.removeBookmark(currentURI); - } else { + } else if (currentURI) { navPlugin.addBookmark({ uri: currentURI, commonName: null, @@ -55,15 +66,31 @@ function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) { ); return ( - ({ - value: bookmark.uri, - label: bookmark.commonName - ? `${bookmark.commonName} - ${bookmark.uri}` - : bookmark.uri, - }))}> + style={{flex: 1}} + options={[ + { + label: Bookmarks, + options: Array.from(bookmarks.values()).map((bookmark) => ({ + value: bookmark.uri, + label: ( + + ), + })), + }, + { + label: Entry points, + options: autoCompleteItems.map((value) => ({ + value: value.pattern, + label: ( + + ), + })), + }, + ]}> { navPlugin.currentURI.set(e.target.value); }} - onPressEnter={(e) => { + onPressEnter={() => { navPlugin.navigateTo(currentURI); }} /> - + ); } + +function NavigationEntry({label, uri}: {label: string | null; uri: string}) { + return ( + + {label ?? uri} + {uri} + + ); +} + +const StyledAutoComplete = styled(AutoComplete)({ + display: 'flex', + flex: 1, + '.ant-select-selector': { + flex: 1, + }, +}); diff --git a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx index dc3be0a9e..1f8def9bf 100644 --- a/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/LaunchEmulator.tsx @@ -10,11 +10,11 @@ import React, {useEffect, useState} from 'react'; import {Modal, Button, message, Alert} from 'antd'; import {AndroidOutlined, AppleOutlined} from '@ant-design/icons'; -import {renderReactRoot} from '../../utils/renderReactRoot'; import {Store} from '../../reducers'; import {useStore} from '../../utils/useStore'; import {launchEmulator} from '../../devices/AndroidDevice'; -import {Layout} from 'flipper-plugin'; +import {Layout, renderReactRoot} from 'flipper-plugin'; +import {Provider} from 'react-redux'; import { launchSimulator, getSimulators, @@ -22,12 +22,11 @@ import { } from '../../dispatcher/iOSDevice'; export function showEmulatorLauncher(store: Store) { - renderReactRoot( - (unmount) => ( + renderReactRoot((unmount) => ( + - ), - store, - ); + + )); } type GetSimulators = typeof getSimulators; diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 6103b5c2d..a49305bdd 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -36,6 +36,8 @@ export {theme} from './ui/theme'; export {Layout} from './ui/Layout'; export {NUX, NuxManagerContext, createNuxManager} from './ui/NUX'; +export {renderReactRoot} from './utils/renderReactRoot'; + // It's not ideal that this exists in flipper-plugin sources directly, // but is the least pain for plugin authors. // Probably we should make sure that testing-library doesn't end up in our final Flipper bundle (which packages flipper-plugin) diff --git a/desktop/app/src/utils/renderReactRoot.tsx b/desktop/flipper-plugin/src/utils/renderReactRoot.tsx similarity index 75% rename from desktop/app/src/utils/renderReactRoot.tsx rename to desktop/flipper-plugin/src/utils/renderReactRoot.tsx index 8a1c64763..24b813ce8 100644 --- a/desktop/app/src/utils/renderReactRoot.tsx +++ b/desktop/flipper-plugin/src/utils/renderReactRoot.tsx @@ -9,8 +9,6 @@ import React from 'react'; import {render, unmountComponentAtNode} from 'react-dom'; -import {Provider} from 'react-redux'; -import {Store} from '../reducers/'; /** * This utility creates a fresh react render hook, which is great to render elements imperatively, like opening dialogs. @@ -18,16 +16,13 @@ import {Store} from '../reducers/'; */ export function renderReactRoot( handler: (unmount: () => void) => React.ReactElement, - store: Store, ): void { const div = document.body.appendChild(document.createElement('div')); render( - - {handler(() => { - unmountComponentAtNode(div); - div.remove(); - })} - , + handler(() => { + unmountComponentAtNode(div); + div.remove(); + }), div, ); } diff --git a/desktop/plugins/navigation/components/RequiredParametersDialog.tsx b/desktop/plugins/navigation/components/RequiredParametersDialog.tsx index ccffe4cc5..f32c04842 100644 --- a/desktop/plugins/navigation/components/RequiredParametersDialog.tsx +++ b/desktop/plugins/navigation/components/RequiredParametersDialog.tsx @@ -7,7 +7,8 @@ * @format */ -import {Button, FlexColumn, Input, Sheet, styled, Glyph, colors} from 'flipper'; +import {Modal, Button, Alert, Input, Typography} from 'antd'; +import {Layout} from 'flipper-plugin'; import { replaceRequiredParametersWithValues, parameterIsNumberType, @@ -23,142 +24,76 @@ import {URI} from '../types'; type Props = { uri: string; requiredParameters: Array; - shouldShow: boolean; - onHide?: () => void; + onHide: () => void; onSubmit: (uri: URI) => void; }; -const Container = styled(FlexColumn)({ - padding: 10, - width: 600, -}); - -const Title = styled.span({ - display: 'flex', - marginTop: 8, - marginLeft: 2, - marginBottom: 8, -}); - -const Text = styled.span({ - lineHeight: 1.3, -}); - -const ErrorLabel = styled.span({ - color: colors.yellow, - lineHeight: 1.4, -}); - -const URIContainer = styled.div({ - lineHeight: 1.3, - marginLeft: 2, - marginBottom: 8, - marginTop: 10, - overflowWrap: 'break-word', -}); - -const ButtonContainer = styled.div({ - marginLeft: 'auto', -}); - -const RequiredParameterInput = styled(Input)({ - margin: 0, - marginTop: 8, - height: 30, - width: '100%', -}); - -const WarningIconContainer = styled.span({ - marginRight: 8, -}); - export default (props: Props) => { - const {shouldShow, onHide, onSubmit, uri, requiredParameters} = props; + const {onHide, onSubmit, uri, requiredParameters} = props; const {isValid, values, setValuesArray} = useRequiredParameterFormValidator( requiredParameters, ); - if (uri == null || !shouldShow) { - return null; - } else { - return ( - - {(hide: () => void) => { - return ( - - - <WarningIconContainer> - <Glyph - name="caution-triangle" - size={16} - variant="filled" - color={colors.yellow} - /> - </WarningIconContainer> - <Text> - This uri has required parameters denoted by {'{parameter}'}. - </Text> - - {requiredParameters.map((paramater, idx) => ( -
- ) => - setValuesArray([ - ...values.slice(0, idx), - event.target.value, - ...values.slice(idx + 1), - ]) - } - name={paramater} - placeholder={paramater} - /> - {values[idx] && - parameterIsNumberType(paramater) && - !validateParameter(values[idx], paramater) ? ( - Parameter must be a number - ) : null} - {values[idx] && - parameterIsBooleanType(paramater) && - !validateParameter(values[idx], paramater) ? ( - - Parameter must be either 'true' or 'false' - - ) : null} -
- ))} - {liveEdit(uri, values)} - - - - -
- ); - }} -
- ); - } + return ( + + + + + }> + + + + {requiredParameters.map((paramater, idx) => ( +
+ ) => + setValuesArray([ + ...values.slice(0, idx), + event.target.value, + ...values.slice(idx + 1), + ]) + } + name={paramater} + placeholder={paramater} + /> + {values[idx] && + parameterIsNumberType(paramater) && + !validateParameter(values[idx], paramater) ? ( + + ) : null} + {values[idx] && + parameterIsBooleanType(paramater) && + !validateParameter(values[idx], paramater) ? ( + + ) : null} +
+ ))} + {liveEdit(uri, values)} +
+
+ ); }; diff --git a/desktop/plugins/navigation/hooks/requiredParameters.tsx b/desktop/plugins/navigation/hooks/requiredParameters.tsx index 5183221bf..cef58e3c1 100644 --- a/desktop/plugins/navigation/hooks/requiredParameters.tsx +++ b/desktop/plugins/navigation/hooks/requiredParameters.tsx @@ -7,7 +7,7 @@ * @format */ -import {useEffect, useState} from 'react'; +import {useMemo, useState} from 'react'; import {validateParameter} from '../util/uri'; export const useRequiredParameterFormValidator = ( @@ -16,8 +16,7 @@ export const useRequiredParameterFormValidator = ( const [values, setValuesArray] = useState>( requiredParameters.map(() => ''), ); - const [isValid, setIsValid] = useState(false); - useEffect(() => { + const isValid = useMemo(() => { if (requiredParameters.length != values.length) { setValuesArray(requiredParameters.map(() => '')); } @@ -26,10 +25,10 @@ export const useRequiredParameterFormValidator = ( validateParameter(value, requiredParameters[idx]), ) ) { - setIsValid(true); + return true; } else { - setIsValid(false); + return false; } - }); + }, [requiredParameters, values]); return {isValid, values, setValuesArray}; }; diff --git a/desktop/plugins/navigation/index.tsx b/desktop/plugins/navigation/index.tsx index 1e21e8359..4f97fe404 100644 --- a/desktop/plugins/navigation/index.tsx +++ b/desktop/plugins/navigation/index.tsx @@ -41,6 +41,7 @@ import { useValue, usePlugin, Layout, + renderReactRoot, } from 'flipper-plugin'; export type State = { @@ -71,8 +72,6 @@ export function plugin(client: PluginClient) { persist: 'appMatchPatterns', }); const currentURI = createState(''); - const shouldShowURIErrorDialog = createState(false); - const requiredParameters = createState([]); const shouldShowSaveBookmarkDialog = createState(false); const saveBookmarkURI = createState(null); @@ -131,13 +130,18 @@ export function plugin(client: PluginClient) { ); } } else { - requiredParameters.set(params); - shouldShowURIErrorDialog.set(true); + renderReactRoot((unmount) => ( + + )); } } function onFavorite(uri: string) { - // TODO: why does this need a dialog? shouldShowSaveBookmarkDialog.set(true); saveBookmarkURI.set(uri); } @@ -169,11 +173,29 @@ export function plugin(client: PluginClient) { bookmarks, saveBookmarkURI, shouldShowSaveBookmarkDialog, - shouldShowURIErrorDialog, - requiredParameters, appMatchPatterns, navigationEvents, currentURI, + getAutoCompleteAppMatchPatterns( + query: string, + bookmarks: Map, + appMatchPatterns: AppMatchPattern[], + limit: number, + ): AppMatchPattern[] { + const q = query.toLowerCase(); + const results: AppMatchPattern[] = []; + for (const item of appMatchPatterns) { + if ( + !bookmarks.has(item.pattern) && + (item.className.toLowerCase().includes(q) || + item.pattern.toLowerCase().includes(q)) + ) { + results.push(item); + if (--limit < 1) break; + } + } + return results; + }, }; } @@ -185,8 +207,6 @@ export function Component() { const shouldShowSaveBookmarkDialog = useValue( instance.shouldShowSaveBookmarkDialog, ); - const shouldShowURIErrorDialog = useValue(instance.shouldShowURIErrorDialog); - const requiredParameters = useValue(instance.requiredParameters); const currentURI = useValue(instance.currentURI); const navigationEvents = useValue(instance.navigationEvents); @@ -227,15 +247,6 @@ export function Component() { onSubmit={instance.addBookmark} onRemove={instance.removeBookmark} /> - { - instance.shouldShowURIErrorDialog.set(false); - }} - uri={currentURI} - requiredParameters={requiredParameters} - onSubmit={instance.navigateTo} - /> ); } diff --git a/desktop/plugins/navigation/package.json b/desktop/plugins/navigation/package.json index fbbd7bd96..440dc018e 100644 --- a/desktop/plugins/navigation/package.json +++ b/desktop/plugins/navigation/package.json @@ -15,6 +15,7 @@ "email": "beneloca@fb.com" }, "peerDependencies": { - "flipper-plugin": "0.64.0" + "flipper-plugin": "0.64.0", + "antd": "*" } }