Skip to content

Commit 9fd6cbf

Browse files
authored
fix: UX feedback suggestions from TNL-8730 [BD-38] [BB-4981] (#201)
* fix: UX feedback suggestions from TNL-8730
1 parent d6fda14 commit 9fd6cbf

File tree

6 files changed

+130
-92
lines changed

6 files changed

+130
-92
lines changed

src/generic/CollapsableEditor.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const CollapsableEditor = ({
2828
className="collapsible-trigger d-flex border-0 align-items-center"
2929
style={{ justifyContent: 'unset' }}
3030
>
31-
<div className="d-flex flex-grow-1">
31+
<div className="d-flex flex-grow-1 w-75">
3232
{title}
3333
</div>
3434
<Collapsible.Visible whenClosed>

src/generic/FormikControl.jsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Form } from '@edx/paragon';
2+
import { getIn, useFormikContext } from 'formik';
3+
import PropTypes from 'prop-types';
4+
import React from 'react';
5+
import FormikErrorFeedback from './FormikErrorFeedback';
6+
7+
function FormikControl({
8+
name,
9+
label,
10+
help,
11+
className,
12+
...params
13+
}) {
14+
const {
15+
touched, errors, handleChange, handleBlur, setFieldError,
16+
} = useFormikContext();
17+
const fieldTouched = getIn(touched, name);
18+
const fieldError = getIn(errors, name);
19+
const handleFocus = (e) => setFieldError(e.target.name, undefined);
20+
21+
return (
22+
<Form.Group className={className}>
23+
{label}
24+
<Form.Control
25+
{...params}
26+
name={name}
27+
className="pb-2"
28+
onChange={handleChange}
29+
onBlur={handleBlur}
30+
onFocus={handleFocus}
31+
isInvalid={fieldTouched && fieldError}
32+
/>
33+
<FormikErrorFeedback name={name}>
34+
<Form.Text>{help}</Form.Text>
35+
</FormikErrorFeedback>
36+
</Form.Group>
37+
);
38+
}
39+
40+
FormikControl.propTypes = {
41+
name: PropTypes.element.isRequired,
42+
label: PropTypes.element.isRequired,
43+
help: PropTypes.element.isRequired,
44+
className: PropTypes.string.isRequired,
45+
value: PropTypes.oneOfType([
46+
PropTypes.string,
47+
PropTypes.number,
48+
]).isRequired,
49+
};
50+
51+
export default FormikControl;

src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import React, { useContext, useEffect, useState } from 'react';
1+
import React, {
2+
useContext, useEffect, useRef, useState,
3+
} from 'react';
24
import PropTypes from 'prop-types';
35

46
import { Formik } from 'formik';
@@ -124,6 +126,7 @@ function AppSettingsModal({
124126
const { courseId } = useContext(PagesAndResourcesContext);
125127
const loadingStatus = useSelector(getLoadingStatus);
126128
const updateSettingsRequestStatus = useSelector(getSavingStatus);
129+
const alertRef = useRef(null);
127130
const [saveError, setSaveError] = useState(false);
128131
const appInfo = useModel('courseApps', appId);
129132
const dispatch = useDispatch();
@@ -147,7 +150,17 @@ function AppSettingsModal({
147150
if (onSettingsSave) {
148151
success = success && await onSettingsSave(values);
149152
}
150-
setSaveError(!success);
153+
await setSaveError(!success);
154+
!success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions
155+
};
156+
157+
const handleFormikSubmit = ({ handleSubmit, errors }) => async (event) => {
158+
// If submitting the form with errors, show the alert and scroll to it.
159+
await handleSubmit(event);
160+
if (Object.keys(errors).length > 0) {
161+
await setSaveError(true);
162+
alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions
163+
}
151164
};
152165

153166
const learnMoreLink = appInfo.documentationLinks?.learnMoreConfiguration && (
@@ -178,9 +191,7 @@ function AppSettingsModal({
178191
onSubmit={handleFormSubmit}
179192
>
180193
{(formikProps) => (
181-
<Form
182-
onSubmit={formikProps.handleSubmit}
183-
>
194+
<Form onSubmit={handleFormikSubmit(formikProps)}>
184195
<AppSettingsModalBase
185196
title={title}
186197
isOpen
@@ -197,12 +208,12 @@ function AppSettingsModal({
197208
complete: intl.formatMessage(messages.saved),
198209
}}
199210
state={submitButtonState}
200-
onClick={formikProps.handleSubmit}
211+
onClick={handleFormikSubmit(formikProps)}
201212
/>
202213
)}
203214
>
204215
{saveError && (
205-
<Alert variant="danger" icon={Info}>
216+
<Alert variant="danger" icon={Info} ref={alertRef}>
206217
<Alert.Heading>
207218
{intl.formatMessage(messages.errorSavingTitle)}
208219
</Alert.Heading>

src/pages-and-resources/teams/GroupEditor.jsx

Lines changed: 35 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
2-
import {
3-
Button, Form, TransitionReplace,
4-
} from '@edx/paragon';
2+
import { Button, Form, TransitionReplace } from '@edx/paragon';
53
import PropTypes from 'prop-types';
64
import React, { useState } from 'react';
75
import { GroupTypes, TeamSizes } from '../../data/constants';
86

97
import CollapsableEditor from '../../generic/CollapsableEditor';
10-
import FormikErrorFeedback from '../../generic/FormikErrorFeedback';
8+
import FormikControl from '../../generic/FormikControl';
119
import messages from './messages';
1210

1311
// Maps a team type to its corresponding intl message
@@ -27,15 +25,14 @@ const TeamTypeNameMessage = {
2725
};
2826

2927
function GroupEditor({
30-
intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors, setFieldError,
28+
intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
3129
}) {
3230
const [isDeleting, setDeleting] = useState(false);
3331
const [isOpen, setOpen] = useState(group.id === null);
3432
const initiateDeletion = () => setDeleting(true);
3533
const cancelDeletion = () => setDeleting(false);
3634

3735
const handleToggle = (open) => setOpen(Boolean(errors.name || errors.maxTeamSize || errors.description) || open);
38-
const handleFocus = (e) => setFieldError(e.target.name, undefined);
3936

4037
const formGroupClasses = 'mb-4 mx-2';
4138

@@ -45,7 +42,7 @@ function GroupEditor({
4542
? (
4643
<div className="d-flex flex-column card rounded mb-3 px-3 py-2 p-4" key="isDeleting">
4744
<h3>{intl.formatMessage(messages.groupDeleteHeading)}</h3>
48-
<p>{intl.formatMessage(messages.groupDeleteBody)}</p>
45+
{intl.formatMessage(messages.groupDeleteBody).split('\n').map(text => <p>{text}</p>)}
4946
<div className="d-flex flex-row justify-content-end">
5047
<Button variant="muted" size="sm" onClick={cancelDeletion}>
5148
{intl.formatMessage(messages.cancel)}
@@ -73,44 +70,31 @@ function GroupEditor({
7370
{intl.formatMessage(messages.configureGroup)}
7471
</div>
7572
) : (
76-
<div className="d-flex flex-column flex-shrink-1 small">
77-
<span className="small text-gray-500">{intl.formatMessage(TeamTypeNameMessage[group.type].label)}</span>
78-
<span className="text-truncate text-black">{group.name || '<new>'}</span>
79-
<span className="small text-muted text-gray-500">{group.description || '<new>'}</span>
73+
<div className="d-flex flex-column flex-shrink-1 small mw-100">
74+
<div className="small text-gray-500">{intl.formatMessage(TeamTypeNameMessage[group.type].label)}</div>
75+
<div className="h4 text-truncate my-1">{group.name}</div>
76+
<div className="small text-truncate text-muted text-gray-500">{group.description}</div>
8077
</div>
8178
)
8279
}
8380
>
84-
<Form.Group className={`${formGroupClasses} mt-2.5`}>
85-
<Form.Control
86-
className="pb-2"
87-
name={`${fieldNameCommonBase}.name`}
88-
floatingLabel={intl.formatMessage(messages.groupFormNameLabel)}
89-
defaultValue={group.name}
90-
onChange={onChange}
91-
onBlur={onBlur}
92-
onFocus={handleFocus}
93-
/>
94-
<FormikErrorFeedback name={`${fieldNameCommonBase}.name`}>
95-
<Form.Text>{intl.formatMessage(messages.groupFormNameHelp)}</Form.Text>
96-
</FormikErrorFeedback>
97-
</Form.Group>
98-
<Form.Group className={formGroupClasses}>
99-
<Form.Control
100-
className="pb-2"
101-
as="textarea"
102-
rows={4}
103-
name={`${fieldNameCommonBase}.description`}
104-
floatingLabel={intl.formatMessage(messages.groupFormDescriptionLabel)}
105-
defaultValue={group.description}
106-
onChange={onChange}
107-
onBlur={onBlur}
108-
onFocus={handleFocus}
109-
/>
110-
<FormikErrorFeedback name={`${fieldNameCommonBase}.description`}>
111-
<Form.Text>{intl.formatMessage(messages.groupFormDescriptionHelp)}</Form.Text>
112-
</FormikErrorFeedback>
113-
</Form.Group>
81+
<FormikControl
82+
name={`${fieldNameCommonBase}.name`}
83+
value={group.name}
84+
floatingLabel={intl.formatMessage(messages.groupFormNameLabel)}
85+
help={intl.formatMessage(messages.groupFormNameHelp)}
86+
className={`${formGroupClasses} mt-2.5`}
87+
/>
88+
<FormikControl
89+
name={`${fieldNameCommonBase}.description`}
90+
value={group.description}
91+
floatingLabel={intl.formatMessage(messages.groupFormDescriptionLabel)}
92+
help={intl.formatMessage(messages.groupFormDescriptionHelp)}
93+
as="textarea"
94+
rows={4}
95+
style={{ minHeight: '2.5rem' }}
96+
className={formGroupClasses}
97+
/>
11498
<Form.Group className={formGroupClasses}>
11599
<Form.Label className="h4 my-3">
116100
{intl.formatMessage(messages.groupFormTypeLabel)}
@@ -135,22 +119,16 @@ function GroupEditor({
135119
))}
136120
</Form.RadioSet>
137121
</Form.Group>
138-
<Form.Group className="mx-2">
139-
<Form.Label className="h4 pb-4">{intl.formatMessage(messages.teamSize)}</Form.Label>
140-
<Form.Control
141-
type="number"
142-
name={`${fieldNameCommonBase}.maxTeamSize`}
143-
floatingLabel={intl.formatMessage(messages.groupFormMaxSizeLabel)}
144-
value={group.maxTeamSize}
145-
placeholder={TeamSizes.DEFAULT}
146-
onChange={onChange}
147-
onBlur={onBlur}
148-
onFocus={handleFocus}
149-
/>
150-
<FormikErrorFeedback name={`${fieldNameCommonBase}.maxTeamSize`}>
151-
<Form.Text>{intl.formatMessage(messages.groupFormMaxSizeHelp)}</Form.Text>
152-
</FormikErrorFeedback>
153-
</Form.Group>
122+
<FormikControl
123+
type="number"
124+
name={`${fieldNameCommonBase}.maxTeamSize`}
125+
floatingLabel={intl.formatMessage(messages.groupFormMaxSizeLabel)}
126+
value={group.maxTeamSize}
127+
help={intl.formatMessage(messages.groupFormMaxSizeHelp)}
128+
label={<Form.Label className="h4 pb-4">{intl.formatMessage(messages.teamSize)}</Form.Label>}
129+
className="mx-2"
130+
placeholder={TeamSizes.DEFAULT}
131+
/>
154132
</CollapsableEditor>
155133
)}
156134
</TransitionReplace>
@@ -177,7 +155,6 @@ GroupEditor.propTypes = {
177155
onDelete: PropTypes.func.isRequired,
178156
onChange: PropTypes.func.isRequired,
179157
onBlur: PropTypes.func.isRequired,
180-
setFieldError: PropTypes.func.isRequired,
181158
};
182159

183160
GroupEditor.defaultProps = {

src/pages-and-resources/teams/Settings.jsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import React from 'react';
88
import { v4 as uuid } from 'uuid';
99
import * as Yup from 'yup';
1010
import { GroupTypes, TeamSizes } from '../../data/constants';
11-
12-
import FormikErrorFeedback from '../../generic/FormikErrorFeedback';
11+
import FormikControl from '../../generic/FormikControl';
1312
import { setupYupExtensions, useAppSetting } from '../../utils';
1413
import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
1514
import GroupEditor from './GroupEditor';
@@ -26,7 +25,7 @@ function TeamSettings({
2625
name: '',
2726
description: '',
2827
type: GroupTypes.OPEN,
29-
maxTeamSize: TeamSizes.DEFAULT,
28+
maxTeamSize: null,
3029
id: null,
3130
key: uuid(),
3231
};
@@ -90,6 +89,11 @@ function TeamSettings({
9089
.default(null),
9190
}),
9291
)
92+
.when('enabled', {
93+
is: true,
94+
then: Yup.array().min(1),
95+
otherwise: Yup.array().length(0),
96+
})
9397
.default([])
9498
.uniqueProperty('name', intl.formatMessage(messages.groupFormNameExists)),
9599
}}
@@ -98,25 +102,18 @@ function TeamSettings({
98102
>
99103
{
100104
({
101-
handleChange, handleBlur, values, errors, setFieldError,
105+
handleChange, handleBlur, values, errors,
102106
}) => (
103107
<>
104108
<h4 className="my-3 pb-2">{intl.formatMessage(messages.teamSize)}</h4>
105-
<Form.Group className="pb-1">
106-
<Form.Control
107-
className="pb-2"
108-
type="number"
109-
name="maxTeamSize"
110-
floatingLabel={intl.formatMessage(messages.maxTeamSize)}
111-
onChange={handleChange}
112-
onBlur={handleBlur}
113-
value={values.maxTeamSize}
114-
onFocus={(event) => setFieldError(event.target.name, undefined)}
115-
/>
116-
<FormikErrorFeedback name="maxTeamSize">
117-
<Form.Text>{intl.formatMessage(messages.maxTeamSizeHelp)}</Form.Text>
118-
</FormikErrorFeedback>
119-
</Form.Group>
109+
<FormikControl
110+
name="maxTeamSize"
111+
value={values.maxTeamSize}
112+
floatingLabel={intl.formatMessage(messages.maxTeamSize)}
113+
help={intl.formatMessage(messages.maxTeamSizeHelp)}
114+
className="pb-1"
115+
type="number"
116+
/>
120117
<div className="bg-light-200 d-flex flex-column mx-n4 px-4 py-4 border border-top mb-n3.5">
121118
<h4>{intl.formatMessage(messages.groups)}</h4>
122119
<Form.Text className="mb-3">{intl.formatMessage(messages.groupsHelp)}</Form.Text>
@@ -132,7 +129,6 @@ function TeamSettings({
132129
onDelete={() => remove(index)}
133130
onChange={handleChange}
134131
onBlur={handleBlur}
135-
setFieldError={setFieldError}
136132
/>
137133
))}
138134
<Button

0 commit comments

Comments
 (0)