feat: adding form validation to LtiConfigForm and LegacyConfigForm (#65)

* feat: adding form validation to LtiConfigForm and LegacyConfigForm

The three fields in LtiConfigForm each get their validation/feedback hooked up properly, and the blackout dates field in LegacyConfigForm now uses a regex-based validator for blackout date strings.

* doc: Improving comments for the blackout dates regex.

* fix: allow no blackout dates
This commit is contained in:
David Joy
2021-04-05 15:45:05 -04:00
committed by GitHub
parent 044131f991
commit 796d69e3fb
4 changed files with 113 additions and 21 deletions

View File

@@ -2,21 +2,32 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Form } from '@edx/paragon'; import { Form } from '@edx/paragon';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import DivisionByGroupFields from '../shared/DivisionByGroupFields'; import DivisionByGroupFields from '../shared/DivisionByGroupFields';
import AnonymousPostingFields from '../shared/AnonymousPostingFields'; import AnonymousPostingFields from '../shared/AnonymousPostingFields';
import BlackoutDatesField from '../shared/BlackoutDatesField'; import BlackoutDatesField, { blackoutDatesRegex } from '../shared/BlackoutDatesField';
export default function LegacyConfigForm({ import messages from '../shared/messages';
appConfig, onSubmit, formRef,
function LegacyConfigForm({
appConfig, onSubmit, formRef, intl,
}) { }) {
const { const {
handleSubmit, handleSubmit,
handleChange, handleChange,
handleBlur, handleBlur,
values, values,
errors,
} = useFormik({ } = useFormik({
initialValues: appConfig, initialValues: appConfig,
validationSchema: Yup.object().shape({
blackoutDates: Yup.string().matches(
blackoutDatesRegex,
intl.formatMessage(messages.blackoutDatesFormattingError),
),
}),
onSubmit, onSubmit,
}); });
@@ -37,6 +48,7 @@ export default function LegacyConfigForm({
values={values} values={values}
/> />
<BlackoutDatesField <BlackoutDatesField
errors={errors}
onBlur={handleBlur} onBlur={handleBlur}
onChange={handleChange} onChange={handleChange}
values={values} values={values}
@@ -59,4 +71,7 @@ LegacyConfigForm.propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
// eslint-disable-next-line react/forbid-prop-types // eslint-disable-next-line react/forbid-prop-types
formRef: PropTypes.object.isRequired, formRef: PropTypes.object.isRequired,
intl: intlShape.isRequired,
}; };
export default injectIntl(LegacyConfigForm);

View File

@@ -47,44 +47,44 @@ function LtiConfigForm({
}} }}
/> />
</p> </p>
<Form.Group controlId="consumerKey"> <Form.Group controlId="consumerKey" isInvalid={errors.consumerKey}>
<Form.Label>{intl.formatMessage(messages.consumerKey)}</Form.Label> <Form.Label>{intl.formatMessage(messages.consumerKey)}</Form.Label>
<Form.Control <Form.Control
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values.consumerKey} value={values.consumerKey}
className={{ 'is-invalid': !!errors.consumerKey }}
aria-describedby="consumerKeyFeedback"
/> />
<Form.Control.Feedback id="consumerKeyFeedback" type="invalid"> {errors.consumerKey && (
{intl.formatMessage(messages.consumerKeyRequired)} <Form.Control.Feedback type="invalid">
</Form.Control.Feedback> {errors.consumerKey}
</Form.Control.Feedback>
)}
</Form.Group> </Form.Group>
<Form.Group controlId="consumerSecret"> <Form.Group controlId="consumerSecret" isInvalid={errors.consumerSecret}>
<Form.Label>{intl.formatMessage(messages.consumerSecret)}</Form.Label> <Form.Label>{intl.formatMessage(messages.consumerSecret)}</Form.Label>
<Form.Control <Form.Control
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values.consumerSecret} value={values.consumerSecret}
className={{ 'is-invalid': !!errors.consumerSecret }}
aria-describedby="consumerSecretFeedback"
/> />
<Form.Control.Feedback id="consumerSecretFeedback" type="invalid"> {errors.consumerSecret && (
{intl.formatMessage(messages.consumerSecretRequired)} <Form.Control.Feedback type="invalid">
</Form.Control.Feedback> {errors.consumerSecret}
</Form.Control.Feedback>
)}
</Form.Group> </Form.Group>
<Form.Group controlId="launchUrl"> <Form.Group controlId="launchUrl" isInvalid={errors.launchUrl}>
<Form.Label>{intl.formatMessage(messages.launchUrl)}</Form.Label> <Form.Label>{intl.formatMessage(messages.launchUrl)}</Form.Label>
<Form.Control <Form.Control
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values.launchUrl} value={values.launchUrl}
className={{ 'is-invalid': !!errors.launchUrl }}
aria-describedby="launchUrlFeedback"
/> />
<Form.Control.Feedback id="launchUrlFeedback" type="invalid"> {errors.launchUrl && (
{intl.formatMessage(messages.launchUrlRequired)} <Form.Control.Feedback type="invalid">
</Form.Control.Feedback> {errors.launchUrl}
</Form.Control.Feedback>
)}
</Form.Group> </Form.Group>
</Form> </Form>
); );

View File

@@ -4,11 +4,76 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon'; import { Form } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
/**
* Lets break this regex down.
*
* The goal is to accept arrays of dates like the following:
*
* [["2015-09-15", "2015-09-21"], ["2015-10-01T11:45", "2015-10-08"]]
*
* Any date can be YYYY-MM-DDTHH:MM or just YYYY-MM-DD like the above.
* The hours and minutes are optional, as illustrated.
*
* The regex errs on the side of being too loose, so you'll see things that are not perfect. It's
* better to be too liberal than to accidentally reject something that should be fine.
*
* So let multi-line this regex and explain the parts:
*
* Beginning of the string:
* ^
* The outer square brackets:
* \[
* Start of a group for a pair of dates with their square brackets:
* (\[
* A group for the first date (YYYY-MM-DDTHH:MM) with its opening double quote:
* ("
* Any four digits for the YYYY year, and a dash:
* [0-9]{4}-
* MM Months 00 - 12 with either a 0 followed by a digit, 1-9, OR a 1 followed by a digit 0-2
* Then a dash:
* (0[1-9]|1[0-2])-
* Finally, for days, accepts any digit 0-3 followed by any digit 0-9. Not a very exact regex.
* [0-3][0-9]
* A sub-group for the hours and minutes. T is just part of it:
* (T
* The hours HH are a 0 or 1 followed by 0-9, OR a 2 followed by 0-3. Captures digits 00-23:
* ([0-1][0-9]|2[0-3])
* Then a colon!
* :
* The minutes MM are any digit 0-5 followed by any digit 0-9, capturing 00-59:
* ([0-5][0-9])
* The THH:MM is optional, so end the group by allowing it to repeat 0 or 1 times
* ){0,1}
* Now end the first date group with its closing double quote, the group parenthesis, and a comma.
* The comma is a necessary separator between the first and second dates:
* "),
* Now start the second date of the pair:
* ("
* The second date is identical to the first, so here it is in all its glory:
* [0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9] // YYYY-MM-DD, identical to above
* (T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1} // THH:MM, identical to above
* Close out the second date with its closing double quotes:
* ")
* Close out the pair of dates with its closing square bracket:
* \]
* An optional comma after the pair of dates, in case there's another pair. If there isn't another
* date, there shouldn't be another comma, but this regex errors on the side of looseness.
* (,){0,1}
* This entire group, ["YYYY-MM-DDTHH:MM", "YYYY-MM-DDTHH:MM"], can be repeated zero or more times.
* )*
* Close out the last square bracket around all the groups:
* \]
* End of string:
* $
*/
export const blackoutDatesRegex = /^\[(\[("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}"),("[0-9]{4}-(0[1-9]|1[0-2])-[0-3][0-9](T([0-1][0-9]|2[0-3]):([0-5][0-9])){0,1}")\](,){0,1})*\]$/;
function BlackoutDatesField({ function BlackoutDatesField({
onBlur, onBlur,
onChange, onChange,
intl, intl,
values, values,
errors,
}) { }) {
return ( return (
<> <>
@@ -22,6 +87,11 @@ function BlackoutDatesField({
onBlur={onBlur} onBlur={onBlur}
floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)} floatingLabel={intl.formatMessage(messages.blackoutDatesLabel)}
/> />
{errors.blackoutDates && (
<Form.Control.Feedback type="invalid">
{errors.blackoutDates}
</Form.Control.Feedback>
)}
<Form.Text muted> <Form.Text muted>
{intl.formatMessage(messages.blackoutDatesHelp)} {intl.formatMessage(messages.blackoutDatesHelp)}
</Form.Text> </Form.Text>
@@ -37,6 +107,9 @@ BlackoutDatesField.propTypes = {
values: PropTypes.shape({ values: PropTypes.shape({
blackoutDates: PropTypes.string, blackoutDates: PropTypes.string,
}).isRequired, }).isRequired,
errors: PropTypes.shape({
blackoutDates: PropTypes.string,
}).isRequired,
}; };
export default injectIntl(BlackoutDatesField); export default injectIntl(BlackoutDatesField);

View File

@@ -111,6 +111,10 @@ const messages = defineMessages({
the date and time. For example, an entry defining two blackout periods looks like this, including the date and time. For example, an entry defining two blackout periods looks like this, including
the outer pair of square brackets: [["2015-09-15", "2015-09-21"], ["2015-10-01", "2015-10-08"]]`, the outer pair of square brackets: [["2015-09-15", "2015-09-21"], ["2015-10-01", "2015-10-08"]]`,
}, },
blackoutDatesFormattingError: {
id: 'authoring.discussions.builtIn.blackoutDates.formattingError',
defaultMessage: "There's a formatting error in your blackout dates.",
},
}); });
export default messages; export default messages;