Skip to content

Commit

Permalink
fix: refactor common selects (#603)
Browse files Browse the repository at this point in the history
* Extract common `Select` component

* Refactor all components to use common `Select`

* Delete duplicate component `SelectFormInput`

* Update question editor to use new select

* Update student name selection to use common component

* Refactor `FilterRow` to use new component

* PR feedback: remove unused props

* PR feedback: add more precise error messages

* PR feedback: improved validation

* PR feedback: forwardRef context

* PR feedback: make all selects searchable by default

* PR feedback: single-vs-multi-select errors

* Use correct defaults

* Remove duplicate form error messages
  • Loading branch information
jfdoming authored Nov 14, 2023
1 parent 5049a31 commit c39505c
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 332 deletions.
118 changes: 32 additions & 86 deletions frontend/src/components/admin/assessment-creation/BasicInformation.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import React from "react";
import React, { type ReactElement, useMemo } from "react";
import type {
Control,
FieldErrorsImpl,
UseFormClearErrors,
UseFormRegister,
UseFormSetValue,
UseFormWatch,
} from "react-hook-form";
import { Controller } from "react-hook-form";
import countryList from "react-select-country-list";
Expand All @@ -20,53 +17,28 @@ import {
Text,
VStack,
} from "@chakra-ui/react";
import type { SingleValue } from "chakra-react-select";
import { Select } from "chakra-react-select";

import type { TestRequest } from "../../../APIClients/types/TestClientTypes";
import { UseCase } from "../../../types/AssessmentTypes";
import type {
GradeOption,
StringOption,
} from "../../../types/SelectInputTypes";
import { gradeOptions } from "../../../utils/AssessmentUtils";
import ControlledSelect from "../../common/form/ControlledSelect";
import FormRadio from "../../common/form/FormRadio";
import ErrorToast from "../../common/info/toasts/ErrorToast";

interface BasicInformationProps {
register: UseFormRegister<TestRequest>;
setValue: UseFormSetValue<TestRequest>;
watch: UseFormWatch<TestRequest>;
control: Control<TestRequest, unknown>;
errors: Partial<FieldErrorsImpl<TestRequest>>;
errorMessage: string;
clearErrors: UseFormClearErrors<TestRequest>;
}

const BasicInformation = ({
register,
setValue,
watch,
control,
errors,
errorMessage,
clearErrors,
}: BasicInformationProps): React.ReactElement => {
const handleGradeChange = (option: SingleValue<GradeOption>) => {
if (option) {
setValue("grade", option.value);
clearErrors("grade");
}
};

const handleCountryChange = (option: SingleValue<StringOption>) => {
if (option) {
setValue("curriculumCountry", option.value);
clearErrors("curriculumCountry");
}
};

const countryOptions = React.useMemo(() => countryList().getData(), []);
}: BasicInformationProps): ReactElement => {
const countryOptions = useMemo(() => countryList().getData(), []);

return (
<Box width="100%">
Expand All @@ -85,30 +57,16 @@ const BasicInformation = ({
</FormControl>

<Box width="50%">
<Controller
control={control}
name="grade"
render={({ field: { name }, fieldState: { error } }) => (
<FormControl isInvalid={Boolean(error)} isRequired>
<FormLabel color="grey.400">Grade Level</FormLabel>
<Select
name={name}
onChange={handleGradeChange}
options={gradeOptions}
placeholder="Select a grade"
selectedOptionStyle="check"
useBasicStyles
value={
gradeOptions.find(
(option) => option.value === watch("grade"),
) || undefined
}
/>
<FormErrorMessage>{error?.message}</FormErrorMessage>
</FormControl>
)}
rules={{ required: "Please select a grade" }}
/>
<FormControl isInvalid={!!errors.grade} isRequired>
<FormLabel color="grey.400">Grade Level</FormLabel>
<ControlledSelect
isRequired="Please select a grade"
name="grade"
options={gradeOptions}
placeholder="Select a grade"
/>
<FormErrorMessage>{errors.grade?.message}</FormErrorMessage>
</FormControl>
</Box>

<Box width="50%">
Expand Down Expand Up @@ -152,37 +110,25 @@ const BasicInformation = ({
Curriculum
</Text>
<HStack alignItems="flex-start" width="100%">
<Controller
control={control}
name="curriculumCountry"
render={({ field: { name }, fieldState: { error } }) => (
<FormControl
isInvalid={Boolean(error)}
isRequired
mr={2}
variant="paragraph"
>
<FormLabel color="grey.400">Country</FormLabel>
<Select
name={name}
onChange={handleCountryChange}
options={countryOptions}
placeholder="Select a country"
useBasicStyles
value={
countryOptions.find(
(option) => option.value === watch("curriculumCountry"),
) || undefined
}
/>
<FormErrorMessage>{error?.message}</FormErrorMessage>
</FormControl>
)}
rules={{ required: "Please select a country" }}
/>

<FormControl
isInvalid={Boolean(errors.curriculumRegion)}
isInvalid={!!errors.curriculumCountry?.message}
isRequired
mr={2}
variant="paragraph"
>
<FormLabel color="grey.400">Country</FormLabel>
<ControlledSelect
isRequired="Please select a country"
name="curriculumCountry"
options={countryOptions}
placeholder="Select a country"
/>
<FormErrorMessage>
{errors.curriculumCountry?.message}
</FormErrorMessage>
</FormControl>
<FormControl
isInvalid={!!errors.curriculumRegion}
isRequired
variant="paragraph"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { FormControl, FormErrorMessage, FormLabel } from "@chakra-ui/react";
import { FormControl, FormLabel } from "@chakra-ui/react";

import type { FractionMetadata } from "../../../../../../types/QuestionMetadataTypes";
import type { FractionType } from "../../../../../../types/QuestionTypes";
Expand Down Expand Up @@ -34,6 +34,7 @@ const FractionModal = ({

useEffect(() => {
if (!isOpen) {
setError(false);
return;
}

Expand All @@ -42,11 +43,6 @@ const FractionModal = ({
setDenominator(String(data?.denominator ?? ""));
}, [data, isOpen]);

const handleClose = () => {
setError(false);
onClose();
};

const handleConfirm = () => {
const castedWholeNumber =
fractionType === "regular" ? null : stringToInt(wholeNumber);
Expand All @@ -58,7 +54,7 @@ const FractionModal = ({
typeof castedDenominator === "undefined"
) {
setError(true);
throw new FormValidationError("One or more fields are invalid");
throw new FormValidationError("Please provide all parts of the fraction");
}
onConfirm({
wholeNumber: castedWholeNumber,
Expand All @@ -73,7 +69,7 @@ const FractionModal = ({
header="Create fraction question"
isOpen={isOpen}
onBack={onBack}
onClose={handleClose}
onClose={onClose}
onSubmit={handleConfirm}
showDefaultToasts={false}
>
Expand All @@ -95,7 +91,6 @@ const FractionModal = ({
}
wholeNumber={fractionType === "mixed" ? wholeNumber : null}
/>
<FormErrorMessage>Enter a value before confirming.</FormErrorMessage>
</FormControl>
</Modal>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const MultiOptionModal = ({
resetErrors();
if (optionCount === 0) {
setOptionCountError(true);
throw new FormValidationError("Please add an option");
throw new FormValidationError("Please select a number of options");
} else if (!options.every((option) => option.value)) {
setEmptyOptionError(true);
throw new FormValidationError("Please ensure all fields are filled");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import React from "react";
import {
FormControl,
FormErrorMessage,
FormLabel,
Select,
} from "@chakra-ui/react";
import { FormControl, FormLabel } from "@chakra-ui/react";
import { v4 as uuidv4 } from "uuid";

import type { MultiOptionData } from "../../../../../../types/QuestionTypes";
import Select from "../../../../../common/form/Select";

interface SelectOptionCountProps {
optionCount: number;
Expand All @@ -34,15 +30,20 @@ const SelectOptionCount = ({
setOptions((prevOptions) => prevOptions.slice(0, n));
};

const handleSelectCount = (event: React.ChangeEvent<HTMLSelectElement>) => {
const count = parseInt(event.target.value, 10);
const countDiff = count - optionCount;
const handleSelectCount = (value: number | null) => {
if (value == null) {
setOptions([]);
setOptionCount(0);
return;
}

const countDiff = value - optionCount;
if (countDiff > 0) {
addOptions(countDiff);
} else {
removeOptions(countDiff);
}
setOptionCount(count);
setOptionCount(value);
};

return (
Expand All @@ -51,20 +52,20 @@ const SelectOptionCount = ({
How many options would you like?
</FormLabel>
<Select
onChange={(e) => handleSelectCount(e)}
chakraStyles={{
container: (provided) => ({
...provided,
width: "50%",
}),
}}
onChange={handleSelectCount}
options={[...Array(4)].map((_, i) => ({
label: String(i + 1),
value: i + 1,
}))}
placeholder="Select Input"
value={optionCount}
width="50%"
>
<option disabled value={0}>
Select Input
</option>
{[...Array(4)].map((i, count) => (
<option key={count + 1} value={count + 1}>
{count + 1}
</option>
))}
</Select>
<FormErrorMessage>Select a value before confirming.</FormErrorMessage>
/>
</FormControl>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import React, { useEffect, useState } from "react";
import {
FormControl,
FormErrorMessage,
FormLabel,
Input,
} from "@chakra-ui/react";
import { FormControl, FormLabel, Input } from "@chakra-ui/react";

import type { ShortAnswerMetadata } from "../../../../../../types/QuestionMetadataTypes";
import {
Expand Down Expand Up @@ -53,7 +48,7 @@ const ShortAnswerModal = ({
onConfirm({ answer: castedAnswer });
} else {
setError(true);
throw new FormValidationError("One or more fields are invalid");
throw new FormValidationError("Please enter a correct answer");
}
};

Expand All @@ -77,7 +72,6 @@ const ShortAnswerModal = ({
value={answer}
width="50%"
/>
<FormErrorMessage>Enter a value before confirming.</FormErrorMessage>
</FormControl>
</Modal>
);
Expand Down
11 changes: 4 additions & 7 deletions frontend/src/components/auth/student-login/NameSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import {
FormErrorMessage,
FormLabel,
} from "@chakra-ui/react";
import type { SingleValue } from "chakra-react-select";
import { Select } from "chakra-react-select";

import { GET_TESTABLE_STUDENTS_BY_TEST_SESSION } from "../../../APIClients/queries/ClassQueries";
import type { StudentResponse } from "../../../APIClients/types/ClassClientTypes";
import type { TestSessionSetupData } from "../../../APIClients/types/TestSessionClientTypes";
import { STUDENT_SIGNUP_IMAGE } from "../../../assets/images";
import { HOME_PAGE, STUDENT_LANDING_PAGE } from "../../../constants/Routes";
import AuthContext from "../../../contexts/AuthContext";
import type { StudentOption } from "../../../types/SelectInputTypes";
import Select from "../../common/form/Select";
import AuthWrapper from "../AuthWrapper";
import NavigationButtons from "../teacher-signup/NavigationButtons";

Expand All @@ -37,9 +35,9 @@ const NameSelection = ({
data?.testableStudentsByTestSessionId.students ?? [];
const className = data?.testableStudentsByTestSessionId.className ?? "";

const [selectedStudent, setSelectedStudent] = useState<StudentOption>();
const [selectedStudent, setSelectedStudent] = useState<StudentResponse>();
const [error, setError] = useState(false);
const handleStudentChange = (option: SingleValue<StudentOption>) => {
const handleStudentChange = (option: StudentResponse | null) => {
if (option) {
setSelectedStudent(option);
setError(false);
Expand All @@ -65,7 +63,6 @@ const NameSelection = ({
: `${student.firstName} ${student.lastName}`,
}))}
placeholder="Search for your name by typing it in the field"
useBasicStyles
value={selectedStudent}
/>
<FormErrorMessage>Please select your name</FormErrorMessage>
Expand All @@ -81,7 +78,7 @@ const NameSelection = ({
setError(true);
} else {
setAuthenticatedUser({
...selectedStudent.value,
...selectedStudent,
role: "Student",
});
history.push({
Expand Down
Loading

0 comments on commit c39505c

Please sign in to comment.