Added URI validation functions and UI for correcting errors
Summary: Some uris parsed from the device contain required parameters. Here we parse the uri and check if there is a required parameter on navigation. If there is we alert the user to correct the error. In the next diff, I will strip away non-required parameters if they are present but not filled in. Reviewed By: danielbuechele Differential Revision: D16710944 fbshipit-source-id: ea32cfe60e2bb5e4f395caebf585ba1b220dcefe
This commit is contained in:
committed by
Facebook Github Bot
parent
ce34c20506
commit
0d5850d723
54
src/plugins/navigation/__tests__/testURI.node.js
Normal file
54
src/plugins/navigation/__tests__/testURI.node.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 {
|
||||
getRequiredParameters,
|
||||
parameterIsNumberType,
|
||||
replaceRequiredParametersWithValues,
|
||||
} from '../util/uri';
|
||||
|
||||
test('parse required parameters from uri', () => {
|
||||
const testURI =
|
||||
'fb://test_uri/?parameter1={parameter1}¶meter2={parameter2}';
|
||||
const expectedResult = ['{parameter1}', '{parameter2}'];
|
||||
expect(getRequiredParameters(testURI)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('parse required numeric parameters from uri', () => {
|
||||
const testURI =
|
||||
'fb://test_uri/?parameter1={#parameter1}¶meter2={#parameter2}';
|
||||
const expectedResult = ['{#parameter1}', '{#parameter2}'];
|
||||
expect(getRequiredParameters(testURI)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('replace required parameters with values', () => {
|
||||
const testURI =
|
||||
'fb://test_uri/?parameter1={parameter1}¶meter2={parameter2}';
|
||||
const expectedResult = 'fb://test_uri/?parameter1=okay¶meter2=sure';
|
||||
expect(
|
||||
replaceRequiredParametersWithValues(testURI, ['okay', 'sure']),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('skip non-required parameters in replacement', () => {
|
||||
const testURI =
|
||||
'fb://test_uri/?parameter1={parameter1}¶meter2={?parameter2}¶meter3={parameter3}';
|
||||
const expectedResult =
|
||||
'fb://test_uri/?parameter1=okay¶meter2={?parameter2}¶meter3=sure';
|
||||
expect(
|
||||
replaceRequiredParametersWithValues(testURI, ['okay', 'sure']),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('detect if required parameter is numeric type', () => {
|
||||
expect(parameterIsNumberType('{#numerictype}')).toBe(true);
|
||||
});
|
||||
|
||||
test('detect if required parameter is not numeric type', () => {
|
||||
expect(parameterIsNumberType('{numerictype}')).toBe(false);
|
||||
});
|
||||
137
src/plugins/navigation/components/RequiredParametersDialog.js
Normal file
137
src/plugins/navigation/components/RequiredParametersDialog.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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 {Button, FlexColumn, Input, Sheet, styled, Glyph, colors} from 'flipper';
|
||||
import {replaceRequiredParametersWithValues} from '../util/uri';
|
||||
import {useRequiredParameterFormValidator} from '../hooks/requiredParameters';
|
||||
|
||||
import type {URI} from '../flow-types';
|
||||
|
||||
type Props = {|
|
||||
uri: string,
|
||||
requiredParameters: Array<string>,
|
||||
shouldShow: boolean,
|
||||
onHide: ?() => void,
|
||||
onSubmit: 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 URIContainer = styled('div')({
|
||||
lineHeight: 1.3,
|
||||
marginLeft: 2,
|
||||
marginBottom: 8,
|
||||
overflowWrap: 'break-word',
|
||||
});
|
||||
|
||||
const ButtonContainer = styled('div')({
|
||||
marginLeft: 'auto',
|
||||
});
|
||||
|
||||
const RequiredParameterInput = styled(Input)({
|
||||
margin: 0,
|
||||
marginBottom: 10,
|
||||
height: 30,
|
||||
});
|
||||
|
||||
const WarningIconContainer = styled('span')({
|
||||
marginRight: 8,
|
||||
});
|
||||
|
||||
export default (props: Props) => {
|
||||
const {shouldShow, onHide, onSubmit, uri, requiredParameters} = props;
|
||||
const [isValid, values, setValuesArray] = useRequiredParameterFormValidator(
|
||||
requiredParameters,
|
||||
);
|
||||
if (uri == null || !shouldShow) {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<Sheet onHideSheet={onHide}>
|
||||
{hide => {
|
||||
return (
|
||||
<Container>
|
||||
<Title>
|
||||
<WarningIconContainer>
|
||||
<Glyph
|
||||
name="caution-triangle"
|
||||
size={16}
|
||||
variant="filled"
|
||||
color={colors.yellow}
|
||||
/>
|
||||
</WarningIconContainer>
|
||||
<Text>
|
||||
This uri has required parameters denoted by {'{parameter}'}.
|
||||
Numeric fields are spcified with a '#' symbol. Please fix the
|
||||
errors to navigate to the specified uri.
|
||||
</Text>
|
||||
</Title>
|
||||
{requiredParameters.map((paramater, idx) => (
|
||||
<RequiredParameterInput
|
||||
key={idx}
|
||||
onChange={event =>
|
||||
setValuesArray([
|
||||
...values.slice(0, idx),
|
||||
event.target.value,
|
||||
...values.slice(idx + 1),
|
||||
])
|
||||
}
|
||||
placeholder={paramater}
|
||||
/>
|
||||
))}
|
||||
<URIContainer>{uri}</URIContainer>
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onHide != null) {
|
||||
onHide();
|
||||
}
|
||||
setValuesArray([]);
|
||||
hide();
|
||||
}}
|
||||
compact
|
||||
padded>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type={isValid ? 'primary' : null}
|
||||
onClick={() => {
|
||||
onSubmit(replaceRequiredParametersWithValues(uri, values));
|
||||
if (onHide != null) {
|
||||
onHide();
|
||||
}
|
||||
setValuesArray([]);
|
||||
hide();
|
||||
}}
|
||||
disabled={!isValid}
|
||||
compact
|
||||
padded>
|
||||
Submit
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</Container>
|
||||
);
|
||||
}}
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -6,14 +6,7 @@
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
import {
|
||||
Component,
|
||||
styled,
|
||||
SearchBox,
|
||||
SearchInput,
|
||||
Toolbar,
|
||||
Glyph,
|
||||
} from 'flipper';
|
||||
import {Component, styled, SearchBox, SearchInput, Toolbar} from 'flipper';
|
||||
import {AutoCompleteSheet, IconButton, FavoriteButton} from './';
|
||||
|
||||
import type {AutoCompleteProvider, Bookmark} from '../flow-types';
|
||||
@@ -23,6 +16,7 @@ type Props = {|
|
||||
onNavigate: (query: string) => void,
|
||||
bookmarks: Map<string, Bookmark>,
|
||||
providers: Array<AutoCompleteProvider>,
|
||||
uriFromAbove: string,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
@@ -30,6 +24,7 @@ type State = {|
|
||||
inputFocused: boolean,
|
||||
autoCompleteSheetOpen: boolean,
|
||||
searchInputValue: string,
|
||||
prevURIFromAbove: string,
|
||||
|};
|
||||
|
||||
const IconContainer = styled('div')({
|
||||
@@ -68,6 +63,7 @@ class SearchBar extends Component<Props, State> {
|
||||
autoCompleteSheetOpen: false,
|
||||
query: '',
|
||||
searchInputValue: '',
|
||||
prevURIFromAbove: '',
|
||||
};
|
||||
|
||||
favorite = (searchInputValue: string) => {
|
||||
@@ -84,6 +80,19 @@ class SearchBar extends Component<Props, State> {
|
||||
this.setState({query: value, searchInputValue: value});
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps = (newProps: Props, state: State) => {
|
||||
const {uriFromAbove: newURIFromAbove} = newProps;
|
||||
const {prevURIFromAbove} = state;
|
||||
if (newURIFromAbove !== prevURIFromAbove) {
|
||||
return {
|
||||
searchInputValue: newURIFromAbove,
|
||||
query: newURIFromAbove,
|
||||
prevURIFromAbove: newURIFromAbove,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const {bookmarks, providers} = this.props;
|
||||
const {
|
||||
|
||||
@@ -11,6 +11,7 @@ export {default as BookmarksSidebar} from './BookmarksSidebar';
|
||||
export {default as FavoriteButton} from './FavoriteButton';
|
||||
export {default as IconButton} from './IconButton';
|
||||
export {default as NavigationInfoBox} from './NavigationInfoBox';
|
||||
export {default as RequiredParametersDialog} from './RequiredParametersDialog';
|
||||
export {default as SaveBookmarkDialog} from './SaveBookmarkDialog';
|
||||
export {default as ScrollableFlexColumn} from './ScrollableFlexColumn';
|
||||
export {default as SearchBar} from './SearchBar';
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
export type URI = string;
|
||||
|
||||
export type State = {|
|
||||
currentURI: string,
|
||||
shouldShowSaveBookmarkDialog: boolean,
|
||||
shouldShowURIErrorDialog: boolean,
|
||||
saveBookmarkURI: ?URI,
|
||||
requiredParameters: Array<string>,
|
||||
|};
|
||||
|
||||
export type PersistedState = {|
|
||||
|
||||
41
src/plugins/navigation/hooks/requiredParameters.js
Normal file
41
src/plugins/navigation/hooks/requiredParameters.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 {parameterIsNumberType} from '../util/uri';
|
||||
|
||||
const validateParameter = (value: string, parameter: string) => {
|
||||
return (
|
||||
value.length > 0 &&
|
||||
(parameterIsNumberType(parameter) ? !isNaN(parseInt(value, 10)) : true)
|
||||
);
|
||||
};
|
||||
|
||||
export const useRequiredParameterFormValidator = (
|
||||
requiredParameters: Array<string>,
|
||||
) => {
|
||||
const [values, setValuesArray] = useState<Array<string>>(
|
||||
requiredParameters.map(() => ''),
|
||||
);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
useEffect(() => {
|
||||
if (requiredParameters.length != values.length) {
|
||||
setValuesArray(requiredParameters.map(() => ''));
|
||||
}
|
||||
if (
|
||||
values.every((value, idx) =>
|
||||
validateParameter(value, requiredParameters[idx]),
|
||||
)
|
||||
) {
|
||||
setIsValid(true);
|
||||
} else {
|
||||
setIsValid(false);
|
||||
}
|
||||
});
|
||||
return [isValid, values, setValuesArray];
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
SearchBar,
|
||||
Timeline,
|
||||
ScrollableFlexColumn,
|
||||
RequiredParametersDialog,
|
||||
} from './components';
|
||||
import {
|
||||
removeBookmark,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
DefaultProvider,
|
||||
} from './util/autoCompleteProvider';
|
||||
import {getAppMatchPatterns} from './util/appMatchPatterns';
|
||||
import {getRequiredParameters} from './util/uri';
|
||||
|
||||
import type {
|
||||
State,
|
||||
@@ -50,6 +52,9 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
|
||||
state = {
|
||||
shouldShowSaveBookmarkDialog: false,
|
||||
saveBookmarkURI: null,
|
||||
shouldShowURIErrorDialog: false,
|
||||
currentURI: '',
|
||||
requiredParameters: [],
|
||||
};
|
||||
|
||||
static persistedStateReducer = (
|
||||
@@ -105,9 +110,18 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
|
||||
};
|
||||
|
||||
navigateTo = (query: string) => {
|
||||
this.getDevice().then(device => {
|
||||
device.navigateToLocation(query);
|
||||
});
|
||||
this.setState({currentURI: query});
|
||||
const requiredParameters = getRequiredParameters(query);
|
||||
if (requiredParameters.length === 0) {
|
||||
this.getDevice().then(device => {
|
||||
device.navigateToLocation(query);
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
requiredParameters,
|
||||
shouldShowURIErrorDialog: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onFavorite = (uri: string) => {
|
||||
@@ -141,7 +155,13 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {saveBookmarkURI, shouldShowSaveBookmarkDialog} = this.state;
|
||||
const {
|
||||
currentURI,
|
||||
saveBookmarkURI,
|
||||
shouldShowSaveBookmarkDialog,
|
||||
shouldShowURIErrorDialog,
|
||||
requiredParameters,
|
||||
} = this.state;
|
||||
const {
|
||||
bookmarks,
|
||||
bookmarksProvider,
|
||||
@@ -156,6 +176,7 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
|
||||
bookmarks={bookmarks}
|
||||
onNavigate={this.navigateTo}
|
||||
onFavorite={this.onFavorite}
|
||||
uriFromAbove={currentURI}
|
||||
/>
|
||||
<Timeline
|
||||
bookmarks={bookmarks}
|
||||
@@ -174,6 +195,13 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
|
||||
onSubmit={this.addBookmark}
|
||||
onRemove={this.removeBookmark}
|
||||
/>
|
||||
<RequiredParametersDialog
|
||||
shouldShow={shouldShowURIErrorDialog}
|
||||
onHide={() => this.setState({shouldShowURIErrorDialog: false})}
|
||||
uri={currentURI}
|
||||
requiredParameters={requiredParameters}
|
||||
onSubmit={this.navigateTo}
|
||||
/>
|
||||
</ScrollableFlexColumn>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,3 +23,38 @@ export const parseURIParameters: string => Map<string, string> = (
|
||||
}
|
||||
return parametersMap;
|
||||
};
|
||||
|
||||
export const parameterIsNumberType = (parameter: string) => {
|
||||
const regExp = /^{(#|\?#)/g;
|
||||
return regExp.test(parameter);
|
||||
};
|
||||
|
||||
export const replaceRequiredParametersWithValues = (
|
||||
uri: string,
|
||||
values: Array<string>,
|
||||
) => {
|
||||
const parameterRegExp = /{[^?]*?}/g;
|
||||
const replaceRegExp = /{[^?]*?}/;
|
||||
let newURI = uri;
|
||||
let index = 0;
|
||||
let match = parameterRegExp.exec(uri);
|
||||
while (match != null) {
|
||||
newURI = newURI.replace(replaceRegExp, values[index]);
|
||||
match = parameterRegExp.exec(uri);
|
||||
index++;
|
||||
}
|
||||
return newURI;
|
||||
};
|
||||
|
||||
export const getRequiredParameters = (uri: string) => {
|
||||
const parameterRegExp = /{[^?]*?}/g;
|
||||
const matches: Array<string> = [];
|
||||
let match = parameterRegExp.exec(uri);
|
||||
while (match != null) {
|
||||
if (match[0]) {
|
||||
matches.push(match[0]);
|
||||
}
|
||||
match = parameterRegExp.exec(uri);
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user