From 9fd6cbf61b3d0d7e436bf33e50cea21d956291bd Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Thu, 21 Oct 2021 12:50:36 +0530 Subject: [PATCH] fix: UX feedback suggestions from TNL-8730 [BD-38] [BB-4981] (#201) * fix: UX feedback suggestions from TNL-8730 --- src/generic/CollapsableEditor.jsx | 2 +- src/generic/FormikControl.jsx | 51 ++++++++++ .../app-settings-modal/AppSettingsModal.jsx | 25 +++-- src/pages-and-resources/teams/GroupEditor.jsx | 93 +++++++------------ src/pages-and-resources/teams/Settings.jsx | 36 ++++--- src/pages-and-resources/teams/messages.js | 15 +-- 6 files changed, 130 insertions(+), 92 deletions(-) create mode 100644 src/generic/FormikControl.jsx diff --git a/src/generic/CollapsableEditor.jsx b/src/generic/CollapsableEditor.jsx index 3703963d8..a01f358c2 100644 --- a/src/generic/CollapsableEditor.jsx +++ b/src/generic/CollapsableEditor.jsx @@ -28,7 +28,7 @@ const CollapsableEditor = ({ className="collapsible-trigger d-flex border-0 align-items-center" style={{ justifyContent: 'unset' }} > -
+
{title}
diff --git a/src/generic/FormikControl.jsx b/src/generic/FormikControl.jsx new file mode 100644 index 000000000..606f1989b --- /dev/null +++ b/src/generic/FormikControl.jsx @@ -0,0 +1,51 @@ +import { Form } from '@edx/paragon'; +import { getIn, useFormikContext } from 'formik'; +import PropTypes from 'prop-types'; +import React from 'react'; +import FormikErrorFeedback from './FormikErrorFeedback'; + +function FormikControl({ + name, + label, + help, + className, + ...params +}) { + const { + touched, errors, handleChange, handleBlur, setFieldError, + } = useFormikContext(); + const fieldTouched = getIn(touched, name); + const fieldError = getIn(errors, name); + const handleFocus = (e) => setFieldError(e.target.name, undefined); + + return ( + + {label} + + + {help} + + + ); +} + +FormikControl.propTypes = { + name: PropTypes.element.isRequired, + label: PropTypes.element.isRequired, + help: PropTypes.element.isRequired, + className: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, +}; + +export default FormikControl; diff --git a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx index c6b9fe716..e3e58554c 100644 --- a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx +++ b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx @@ -1,4 +1,6 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { + useContext, useEffect, useRef, useState, +} from 'react'; import PropTypes from 'prop-types'; import { Formik } from 'formik'; @@ -124,6 +126,7 @@ function AppSettingsModal({ const { courseId } = useContext(PagesAndResourcesContext); const loadingStatus = useSelector(getLoadingStatus); const updateSettingsRequestStatus = useSelector(getSavingStatus); + const alertRef = useRef(null); const [saveError, setSaveError] = useState(false); const appInfo = useModel('courseApps', appId); const dispatch = useDispatch(); @@ -147,7 +150,17 @@ function AppSettingsModal({ if (onSettingsSave) { success = success && await onSettingsSave(values); } - setSaveError(!success); + await setSaveError(!success); + !success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions + }; + + const handleFormikSubmit = ({ handleSubmit, errors }) => async (event) => { + // If submitting the form with errors, show the alert and scroll to it. + await handleSubmit(event); + if (Object.keys(errors).length > 0) { + await setSaveError(true); + alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions + } }; const learnMoreLink = appInfo.documentationLinks?.learnMoreConfiguration && ( @@ -178,9 +191,7 @@ function AppSettingsModal({ onSubmit={handleFormSubmit} > {(formikProps) => ( -
+ )} > {saveError && ( - + {intl.formatMessage(messages.errorSavingTitle)} diff --git a/src/pages-and-resources/teams/GroupEditor.jsx b/src/pages-and-resources/teams/GroupEditor.jsx index d95f83ea0..b0d62edd6 100644 --- a/src/pages-and-resources/teams/GroupEditor.jsx +++ b/src/pages-and-resources/teams/GroupEditor.jsx @@ -1,13 +1,11 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { - Button, Form, TransitionReplace, -} from '@edx/paragon'; +import { Button, Form, TransitionReplace } from '@edx/paragon'; import PropTypes from 'prop-types'; import React, { useState } from 'react'; import { GroupTypes, TeamSizes } from '../../data/constants'; import CollapsableEditor from '../../generic/CollapsableEditor'; -import FormikErrorFeedback from '../../generic/FormikErrorFeedback'; +import FormikControl from '../../generic/FormikControl'; import messages from './messages'; // Maps a team type to its corresponding intl message @@ -27,7 +25,7 @@ const TeamTypeNameMessage = { }; function GroupEditor({ - intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors, setFieldError, + intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors, }) { const [isDeleting, setDeleting] = useState(false); const [isOpen, setOpen] = useState(group.id === null); @@ -35,7 +33,6 @@ function GroupEditor({ const cancelDeletion = () => setDeleting(false); const handleToggle = (open) => setOpen(Boolean(errors.name || errors.maxTeamSize || errors.description) || open); - const handleFocus = (e) => setFieldError(e.target.name, undefined); const formGroupClasses = 'mb-4 mx-2'; @@ -45,7 +42,7 @@ function GroupEditor({ ? (

{intl.formatMessage(messages.groupDeleteHeading)}

-

{intl.formatMessage(messages.groupDeleteBody)}

+ {intl.formatMessage(messages.groupDeleteBody).split('\n').map(text =>

{text}

)}
) : ( -
- {intl.formatMessage(TeamTypeNameMessage[group.type].label)} - {group.name || ''} - {group.description || ''} +
+
{intl.formatMessage(TeamTypeNameMessage[group.type].label)}
+
{group.name}
+
{group.description}
) } > - - - - {intl.formatMessage(messages.groupFormNameHelp)} - - - - - - {intl.formatMessage(messages.groupFormDescriptionHelp)} - - + + {intl.formatMessage(messages.groupFormTypeLabel)} @@ -135,22 +119,16 @@ function GroupEditor({ ))} - - {intl.formatMessage(messages.teamSize)} - - - {intl.formatMessage(messages.groupFormMaxSizeHelp)} - - + {intl.formatMessage(messages.teamSize)}} + className="mx-2" + placeholder={TeamSizes.DEFAULT} + /> )} @@ -177,7 +155,6 @@ GroupEditor.propTypes = { onDelete: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onBlur: PropTypes.func.isRequired, - setFieldError: PropTypes.func.isRequired, }; GroupEditor.defaultProps = { diff --git a/src/pages-and-resources/teams/Settings.jsx b/src/pages-and-resources/teams/Settings.jsx index 22186647d..32313c311 100644 --- a/src/pages-and-resources/teams/Settings.jsx +++ b/src/pages-and-resources/teams/Settings.jsx @@ -8,8 +8,7 @@ import React from 'react'; import { v4 as uuid } from 'uuid'; import * as Yup from 'yup'; import { GroupTypes, TeamSizes } from '../../data/constants'; - -import FormikErrorFeedback from '../../generic/FormikErrorFeedback'; +import FormikControl from '../../generic/FormikControl'; import { setupYupExtensions, useAppSetting } from '../../utils'; import AppSettingsModal from '../app-settings-modal/AppSettingsModal'; import GroupEditor from './GroupEditor'; @@ -26,7 +25,7 @@ function TeamSettings({ name: '', description: '', type: GroupTypes.OPEN, - maxTeamSize: TeamSizes.DEFAULT, + maxTeamSize: null, id: null, key: uuid(), }; @@ -90,6 +89,11 @@ function TeamSettings({ .default(null), }), ) + .when('enabled', { + is: true, + then: Yup.array().min(1), + otherwise: Yup.array().length(0), + }) .default([]) .uniqueProperty('name', intl.formatMessage(messages.groupFormNameExists)), }} @@ -98,25 +102,18 @@ function TeamSettings({ > { ({ - handleChange, handleBlur, values, errors, setFieldError, + handleChange, handleBlur, values, errors, }) => ( <>

{intl.formatMessage(messages.teamSize)}

- - setFieldError(event.target.name, undefined)} - /> - - {intl.formatMessage(messages.maxTeamSizeHelp)} - - +

{intl.formatMessage(messages.groups)}

{intl.formatMessage(messages.groupsHelp)} @@ -132,7 +129,6 @@ function TeamSettings({ onDelete={() => remove(index)} onChange={handleChange} onBlur={handleBlur} - setFieldError={setFieldError} /> ))}