Skip to content

Commit e4a4bed

Browse files
authored
✨ Add form validation to Admin, Items and Login (#616)
1 parent 0839575 commit e4a4bed

File tree

9 files changed

+136
-94
lines changed

9 files changed

+136
-94
lines changed

src/new-frontend/src/components/Admin/AddUser.tsx

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
3+
import { Button, Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
44
import { SubmitHandler, useForm } from 'react-hook-form';
55

66
import { UserCreate } from '../../client';
@@ -14,29 +14,35 @@ interface AddUserProps {
1414
}
1515

1616
interface UserCreateForm extends UserCreate {
17-
confirmPassword: string;
17+
confirm_password: string;
1818

1919
}
2020

2121
const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
2222
const showToast = useCustomToast();
23-
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserCreateForm>();
23+
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UserCreateForm>({
24+
mode: 'onBlur',
25+
criteriaMode: 'all',
26+
defaultValues: {
27+
email: '',
28+
full_name: '',
29+
password: '',
30+
confirm_password: '',
31+
is_superuser: false,
32+
is_active: false
33+
}
34+
});
2435
const { addUser } = useUsersStore();
2536

2637
const onSubmit: SubmitHandler<UserCreateForm> = async (data) => {
27-
if (data.password === data.confirmPassword) {
28-
try {
29-
await addUser(data);
30-
showToast('Success!', 'User created successfully.', 'success');
31-
reset();
32-
onClose();
33-
} catch (err) {
34-
const errDetail = (err as ApiError).body.detail;
35-
showToast('Something went wrong.', `${errDetail}`, 'error');
36-
}
37-
} else {
38-
// TODO: Complete when form validation is implemented
39-
console.log("Passwords don't match")
38+
try {
39+
await addUser(data);
40+
showToast('Success!', 'User created successfully.', 'success');
41+
reset();
42+
onClose();
43+
} catch (err) {
44+
const errDetail = (err as ApiError).body.detail;
45+
showToast('Something went wrong.', `${errDetail}`, 'error');
4046
}
4147
}
4248

@@ -53,21 +59,28 @@ const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
5359
<ModalHeader>Add User</ModalHeader>
5460
<ModalCloseButton />
5561
<ModalBody pb={6} >
56-
<FormControl>
62+
<FormControl isRequired isInvalid={!!errors.email}>
5763
<FormLabel htmlFor='email'>Email</FormLabel>
58-
<Input id='email' {...register('email')} placeholder='Email' type='email' />
64+
<Input id='email' {...register('email', { required: 'Email is required', pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address' } })} placeholder='Email' type='email' />
65+
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>}
5966
</FormControl>
60-
<FormControl mt={4}>
67+
<FormControl mt={4} isInvalid={!!errors.full_name}>
6168
<FormLabel htmlFor='name'>Full name</FormLabel>
6269
<Input id='name' {...register('full_name')} placeholder='Full name' type='text' />
70+
{errors.full_name && <FormErrorMessage>{errors.full_name.message}</FormErrorMessage>}
6371
</FormControl>
64-
<FormControl mt={4}>
72+
<FormControl mt={4} isRequired isInvalid={!!errors.password}>
6573
<FormLabel htmlFor='password'>Set Password</FormLabel>
66-
<Input id='password' {...register('password')} placeholder='Password' type='password' />
74+
<Input id='password' {...register('password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' />
75+
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>}
6776
</FormControl>
68-
<FormControl mt={4}>
69-
<FormLabel htmlFor='confirmPassword'>Confirm Password</FormLabel>
70-
<Input id='confirmPassword' {...register('confirmPassword')} placeholder='Password' type='password' />
77+
<FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}>
78+
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel>
79+
<Input id='confirm_password' {...register('confirm_password', {
80+
required: 'Please confirm your password',
81+
validate: value => value === getValues().password || 'The passwords do not match'
82+
})} placeholder='Password' type='password' />
83+
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>}
7184
</FormControl>
7285
<Flex mt={4}>
7386
<FormControl>
@@ -79,7 +92,7 @@ const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
7992
</Flex>
8093
</ModalBody>
8194
<ModalFooter gap={3}>
82-
<Button bg='ui.main' color='white' type='submit' isLoading={isSubmitting}>
95+
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}>
8396
Save
8497
</Button>
8598
<Button onClick={onClose}>Cancel</Button>

src/new-frontend/src/components/Admin/EditUser.tsx

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import { Button, Checkbox, Flex, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
3+
import { Button, Checkbox, Flex, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
44
import { SubmitHandler, useForm } from 'react-hook-form';
55

66
import { ApiError, UserUpdate } from '../../client';
@@ -19,25 +19,34 @@ interface UserUpdateForm extends UserUpdate {
1919

2020
const EditUser: React.FC<EditUserProps> = ({ user_id, isOpen, onClose }) => {
2121
const showToast = useCustomToast();
22-
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserUpdateForm>();
2322
const { editUser, users } = useUsersStore();
24-
2523
const currentUser = users.find((user) => user.id === user_id);
24+
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UserUpdateForm>({
25+
mode: 'onBlur',
26+
criteriaMode: 'all',
27+
defaultValues: {
28+
email: currentUser?.email,
29+
full_name: currentUser?.full_name,
30+
password: '',
31+
confirm_password: '',
32+
is_superuser: currentUser?.is_superuser,
33+
is_active: currentUser?.is_active
34+
}
35+
});
36+
2637

2738
const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => {
28-
if (data.password === data.confirm_password) {
29-
try {
30-
await editUser(user_id, data);
31-
showToast('Success!', 'User updated successfully.', 'success');
32-
reset();
33-
onClose();
34-
} catch (err) {
35-
const errDetail = (err as ApiError).body.detail;
36-
showToast('Something went wrong.', `${errDetail}`, 'error');
39+
try {
40+
if (data.password === '') {
41+
delete data.password;
3742
}
38-
} else {
39-
// TODO: Complete when form validation is implemented
40-
console.log("Passwords don't match")
43+
await editUser(user_id, data);
44+
showToast('Success!', 'User updated successfully.', 'success');
45+
reset();
46+
onClose();
47+
} catch (err) {
48+
const errDetail = (err as ApiError).body.detail;
49+
showToast('Something went wrong.', `${errDetail}`, 'error');
4150
}
4251
}
4352

@@ -59,28 +68,33 @@ const EditUser: React.FC<EditUserProps> = ({ user_id, isOpen, onClose }) => {
5968
<ModalHeader>Edit User</ModalHeader>
6069
<ModalCloseButton />
6170
<ModalBody pb={6}>
62-
<FormControl>
71+
<FormControl isInvalid={!!errors.email}>
6372
<FormLabel htmlFor='email'>Email</FormLabel>
64-
<Input id="email" {...register('email')} defaultValue={currentUser?.email} type='email' />
73+
<Input id='email' {...register('email', { pattern: { value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i, message: 'Invalid email address' } })} placeholder='Email' type='email' />
74+
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>}
6575
</FormControl>
6676
<FormControl mt={4}>
6777
<FormLabel htmlFor='name'>Full name</FormLabel>
68-
<Input id="name" {...register('full_name')} defaultValue={currentUser?.full_name} type='text' />
78+
<Input id="name" {...register('full_name')} type='text' />
6979
</FormControl>
70-
<FormControl mt={4}>
71-
<FormLabel htmlFor='password'>Password</FormLabel>
72-
<Input id="password" {...register('password')} placeholder='••••••••' type='password' />
80+
<FormControl mt={4} isInvalid={!!errors.password}>
81+
<FormLabel htmlFor='password'>Set Password</FormLabel>
82+
<Input id='password' {...register('password', { minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='••••••••' type='password' />
83+
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>}
7384
</FormControl>
74-
<FormControl mt={4}>
75-
<FormLabel htmlFor='confirmPassword'>Confirmation Password</FormLabel>
76-
<Input id='confirmPassword' {...register('confirm_password')} placeholder='••••••••' type='password' />
85+
<FormControl mt={4} isInvalid={!!errors.confirm_password}>
86+
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel>
87+
<Input id='confirm_password' {...register('confirm_password', {
88+
validate: value => value === getValues().password || 'The passwords do not match'
89+
})} placeholder='••••••••' type='password' />
90+
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>}
7791
</FormControl>
7892
<Flex>
7993
<FormControl mt={4}>
80-
<Checkbox {...register('is_superuser')} defaultChecked={currentUser?.is_superuser} colorScheme='teal'>Is superuser?</Checkbox>
94+
<Checkbox {...register('is_superuser')} colorScheme='teal'>Is superuser?</Checkbox>
8195
</FormControl>
8296
<FormControl mt={4}>
83-
<Checkbox {...register('is_active')} defaultChecked={currentUser?.is_active} colorScheme='teal'>Is active?</Checkbox>
97+
<Checkbox {...register('is_active')} colorScheme='teal'>Is active?</Checkbox>
8498
</FormControl>
8599
</Flex>
86100
</ModalBody>

src/new-frontend/src/components/Common/DeleteAlert.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,17 @@ interface DeleteProps {
1717
const Delete: React.FC<DeleteProps> = ({ type, id, isOpen, onClose }) => {
1818
const showToast = useCustomToast();
1919
const cancelRef = React.useRef<HTMLButtonElement | null>(null);
20-
const [isLoading, setIsLoading] = useState(false);
21-
const { handleSubmit } = useForm();
20+
const { handleSubmit, formState: {isSubmitting} } = useForm();
2221
const { deleteItem } = useItemsStore();
2322
const { deleteUser } = useUsersStore();
2423

2524
const onSubmit = async () => {
26-
setIsLoading(true);
2725
try {
2826
type === 'Item' ? await deleteItem(id) : await deleteUser(id);
2927
showToast('Success', `The ${type.toLowerCase()} was deleted successfully.`, 'success');
3028
onClose();
3129
} catch (err) {
3230
showToast('An error occurred.', `An error occurred while deleting the ${type.toLowerCase()}.`, 'error');
33-
} finally {
34-
setIsLoading(false);
3531
}
3632
}
3733

@@ -56,10 +52,10 @@ const Delete: React.FC<DeleteProps> = ({ type, id, isOpen, onClose }) => {
5652
</AlertDialogBody>
5753

5854
<AlertDialogFooter gap={3}>
59-
<Button bg="ui.danger" color="white" _hover={{ opacity: 0.8 }} type="submit" isLoading={isLoading}>
55+
<Button bg="ui.danger" color="white" _hover={{ opacity: 0.8 }} type="submit" isLoading={isSubmitting}>
6056
Delete
6157
</Button>
62-
<Button ref={cancelRef} onClick={onClose} isDisabled={isLoading}>
58+
<Button ref={cancelRef} onClick={onClose} isDisabled={isSubmitting}>
6359
Cancel
6460
</Button>
6561
</AlertDialogFooter>

src/new-frontend/src/components/Items/AddItem.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22

3-
import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
3+
import { Button, FormControl, FormErrorMessage, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react';
44
import { SubmitHandler, useForm } from 'react-hook-form';
55

66
import { ApiError, ItemCreate } from '../../client';
@@ -14,12 +14,17 @@ interface AddItemProps {
1414

1515
const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
1616
const showToast = useCustomToast();
17-
const [isLoading, setIsLoading] = useState(false);
18-
const { register, handleSubmit, reset } = useForm<ItemCreate>();
17+
const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<ItemCreate>({
18+
mode: 'onBlur',
19+
criteriaMode: 'all',
20+
defaultValues: {
21+
title: '',
22+
description: '',
23+
},
24+
});
1925
const { addItem } = useItemsStore();
2026

2127
const onSubmit: SubmitHandler<ItemCreate> = async (data) => {
22-
setIsLoading(true);
2328
try {
2429
await addItem(data);
2530
showToast('Success!', 'Item created successfully.', 'success');
@@ -28,8 +33,6 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
2833
} catch (err) {
2934
const errDetail = (err as ApiError).body.detail;
3035
showToast('Something went wrong.', `${errDetail}`, 'error');
31-
} finally {
32-
setIsLoading(false);
3336
}
3437
};
3538

@@ -46,14 +49,15 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
4649
<ModalHeader>Add Item</ModalHeader>
4750
<ModalCloseButton />
4851
<ModalBody pb={6}>
49-
<FormControl>
52+
<FormControl isRequired isInvalid={!!errors.title}>
5053
<FormLabel htmlFor='title'>Title</FormLabel>
5154
<Input
5255
id='title'
53-
{...register('title')}
56+
{...register('title', { required: 'Title is required.' })}
5457
placeholder='Title'
5558
type='text'
5659
/>
60+
{errors.title && <FormErrorMessage>{errors.title.message}</FormErrorMessage>}
5761
</FormControl>
5862
<FormControl mt={4}>
5963
<FormLabel htmlFor='description'>Description</FormLabel>
@@ -67,10 +71,10 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
6771
</ModalBody>
6872

6973
<ModalFooter gap={3}>
70-
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isLoading}>
74+
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}>
7175
Save
7276
</Button>
73-
<Button onClick={onClose} isDisabled={isLoading}>
77+
<Button onClick={onClose}>
7478
Cancel
7579
</Button>
7680
</ModalFooter>

src/new-frontend/src/components/Items/EditItem.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ interface EditItemProps {
1515

1616
const EditItem: React.FC<EditItemProps> = ({ id, isOpen, onClose }) => {
1717
const showToast = useCustomToast();
18-
const { register, handleSubmit, reset, formState: { isSubmitting }, } = useForm<ItemUpdate>();
1918
const { editItem, items } = useItemsStore();
20-
2119
const currentItem = items.find((item) => item.id === id);
20+
const { register, handleSubmit, reset, formState: { isSubmitting }, } = useForm<ItemUpdate>({ defaultValues: { title: currentItem?.title, description: currentItem?.description } });
2221

2322
const onSubmit: SubmitHandler<ItemUpdate> = async (data) => {
2423
try {
@@ -52,11 +51,11 @@ const EditItem: React.FC<EditItemProps> = ({ id, isOpen, onClose }) => {
5251
<ModalBody pb={6}>
5352
<FormControl>
5453
<FormLabel htmlFor='title'>Title</FormLabel>
55-
<Input id='title' {...register('title')} defaultValue={currentItem?.title} type='text' />
54+
<Input id='title' {...register('title')} type='text' />
5655
</FormControl>
5756
<FormControl mt={4}>
5857
<FormLabel htmlFor='description'>Description</FormLabel>
59-
<Input id='description' {...register('description')} defaultValue={currentItem?.description} placeholder='Description' type='text' />
58+
<Input id='description' {...register('description')} placeholder='Description' type='text' />
6059
</FormControl>
6160
</ModalBody>
6261
<ModalFooter gap={3}>

src/new-frontend/src/components/UserSettings/Appearance.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ const Appearance: React.FC = () => {
1515
<Stack>
1616
{/* TODO: Add system default option */}
1717
<Radio value='light' colorScheme='teal'>
18-
Light Mode<Badge ml='1' colorScheme='teal'>Default</Badge>
18+
Light mode<Badge ml='1' colorScheme='teal'>Default</Badge>
1919
</Radio>
2020
<Radio value='dark' colorScheme='teal'>
21-
Dark Mode
21+
Dark mode
2222
</Radio>
2323
</Stack>
2424
</RadioGroup>

0 commit comments

Comments
 (0)