Better form validation for required parameters plus live editing

Summary: Taking on the feedback from the demo yesterday, I've improved the required parameter's dialog by showing where specific errors occur in the form and adding live editing to the URI displayed.

Reviewed By: danielbuechele

Differential Revision: D16802921

fbshipit-source-id: 2e729549306a8efb79ca76d3da6f70632ccd9212
This commit is contained in:
Benjamin Elo
2019-08-14 05:24:39 -07:00
committed by Facebook Github Bot
parent 1ae3b90019
commit c40a88b117
4 changed files with 57 additions and 27 deletions

View File

@@ -7,7 +7,12 @@
*/ */
import {Button, FlexColumn, Input, Sheet, styled, Glyph, colors} from 'flipper'; import {Button, FlexColumn, Input, Sheet, styled, Glyph, colors} from 'flipper';
import {replaceRequiredParametersWithValues} from '../util/uri'; import {
replaceRequiredParametersWithValues,
parameterIsNumberType,
validateParameter,
liveEdit,
} from '../util/uri';
import {useRequiredParameterFormValidator} from '../hooks/requiredParameters'; import {useRequiredParameterFormValidator} from '../hooks/requiredParameters';
import type {URI} from '../flow-types'; import type {URI} from '../flow-types';
@@ -36,10 +41,16 @@ const Text = styled('span')({
lineHeight: 1.3, lineHeight: 1.3,
}); });
const ErrorLabel = styled('span')({
color: colors.yellow,
lineHeight: 1.4,
});
const URIContainer = styled('div')({ const URIContainer = styled('div')({
lineHeight: 1.3, lineHeight: 1.3,
marginLeft: 2, marginLeft: 2,
marginBottom: 8, marginBottom: 8,
marginTop: 10,
overflowWrap: 'break-word', overflowWrap: 'break-word',
}); });
@@ -49,8 +60,9 @@ const ButtonContainer = styled('div')({
const RequiredParameterInput = styled(Input)({ const RequiredParameterInput = styled(Input)({
margin: 0, margin: 0,
marginBottom: 10, marginTop: 8,
height: 30, height: 30,
width: '100%',
}); });
const WarningIconContainer = styled('span')({ const WarningIconContainer = styled('span')({
@@ -81,24 +93,29 @@ export default (props: Props) => {
</WarningIconContainer> </WarningIconContainer>
<Text> <Text>
This uri has required parameters denoted by {'{parameter}'}. 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> </Text>
</Title> </Title>
{requiredParameters.map((paramater, idx) => ( {requiredParameters.map((paramater, idx) => (
<RequiredParameterInput <div key={idx}>
key={idx} <RequiredParameterInput
onChange={event => onChange={event =>
setValuesArray([ setValuesArray([
...values.slice(0, idx), ...values.slice(0, idx),
event.target.value, event.target.value,
...values.slice(idx + 1), ...values.slice(idx + 1),
]) ])
} }
placeholder={paramater} name={paramater}
/> placeholder={paramater}
/>
{values[idx] &&
parameterIsNumberType(paramater) &&
!validateParameter(values[idx], paramater) ? (
<ErrorLabel>Parameter must be a number</ErrorLabel>
) : null}
</div>
))} ))}
<URIContainer>{uri}</URIContainer> <URIContainer>{liveEdit(uri, values)}</URIContainer>
<ButtonContainer> <ButtonContainer>
<Button <Button
onClick={() => { onClick={() => {

View File

@@ -7,14 +7,7 @@
*/ */
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {parameterIsNumberType} from '../util/uri'; import {validateParameter} from '../util/uri';
const validateParameter = (value: string, parameter: string) => {
return (
value.length > 0 &&
(parameterIsNumberType(parameter) ? !isNaN(parseInt(value, 10)) : true)
);
};
export const useRequiredParameterFormValidator = ( export const useRequiredParameterFormValidator = (
requiredParameters: Array<string>, requiredParameters: Array<string>,

View File

@@ -112,11 +112,12 @@ export default class extends FlipperPlugin<State, {}, PersistedState> {
}; };
navigateTo = (query: string) => { navigateTo = (query: string) => {
this.props.setPersistedState({currentURI: query}); const filteredQuery = filterOptionalParameters(query);
const requiredParameters = getRequiredParameters(query); this.props.setPersistedState({currentURI: filteredQuery});
const requiredParameters = getRequiredParameters(filteredQuery);
if (requiredParameters.length === 0) { if (requiredParameters.length === 0) {
this.getDevice().then(device => { this.getDevice().then(device => {
device.navigateToLocation(filterOptionalParameters(query)); device.navigateToLocation(filterOptionalParameters(filteredQuery));
}); });
} else { } else {
this.setState({ this.setState({

View File

@@ -8,6 +8,13 @@
import querystring from 'querystring'; import querystring from 'querystring';
export const validateParameter = (value: string, parameter: string) => {
return (
value &&
(parameterIsNumberType(parameter) ? !isNaN(parseInt(value, 10)) : true)
);
};
export const filterOptionalParameters: string => string = (uri: string) => { export const filterOptionalParameters: string => string = (uri: string) => {
return uri.replace(/[/&]?([^&?={}\/]*=)?{\?.*?}/g, ''); return uri.replace(/[/&]?([^&?={}\/]*=)?{\?.*?}/g, '');
}; };
@@ -62,3 +69,15 @@ export const getRequiredParameters = (uri: string) => {
} }
return matches; return matches;
}; };
export const liveEdit = (uri: string, formValues: Array<string>): string => {
const parameterRegExp = /({[^?]*?})/g;
const uriArray = uri.split(parameterRegExp);
return uriArray.reduce((acc, uriComponent, idx) => {
if (idx % 2 === 0 || !formValues[(idx - 1) / 2]) {
return acc + uriComponent;
} else {
return acc + formValues[(idx - 1) / 2];
}
});
};