Skip to content

Commit

Permalink
✨ Add form validation to Admin, Items and Login (fastapi#616)
Browse files Browse the repository at this point in the history
  • Loading branch information
alejsdev authored Feb 28, 2024
1 parent eb4769c commit 35c306b
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 94 deletions.
63 changes: 38 additions & 25 deletions src/new-frontend/src/components/Admin/AddUser.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

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

import { UserCreate } from '../../client';
Expand All @@ -14,29 +14,35 @@ interface AddUserProps {
}

interface UserCreateForm extends UserCreate {
confirmPassword: string;
confirm_password: string;

}

const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
const showToast = useCustomToast();
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserCreateForm>();
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UserCreateForm>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
email: '',
full_name: '',
password: '',
confirm_password: '',
is_superuser: false,
is_active: false
}
});
const { addUser } = useUsersStore();

const onSubmit: SubmitHandler<UserCreateForm> = async (data) => {
if (data.password === data.confirmPassword) {
try {
await addUser(data);
showToast('Success!', 'User created successfully.', 'success');
reset();
onClose();
} catch (err) {
const errDetail = (err as ApiError).body.detail;
showToast('Something went wrong.', `${errDetail}`, 'error');
}
} else {
// TODO: Complete when form validation is implemented
console.log("Passwords don't match")
try {
await addUser(data);
showToast('Success!', 'User created successfully.', 'success');
reset();
onClose();
} catch (err) {
const errDetail = (err as ApiError).body.detail;
showToast('Something went wrong.', `${errDetail}`, 'error');
}
}

Expand All @@ -53,21 +59,28 @@ const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
<ModalHeader>Add User</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6} >
<FormControl>
<FormControl isRequired isInvalid={!!errors.email}>
<FormLabel htmlFor='email'>Email</FormLabel>
<Input id='email' {...register('email')} placeholder='Email' type='email' />
<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' />
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormControl mt={4} isInvalid={!!errors.full_name}>
<FormLabel htmlFor='name'>Full name</FormLabel>
<Input id='name' {...register('full_name')} placeholder='Full name' type='text' />
{errors.full_name && <FormErrorMessage>{errors.full_name.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormControl mt={4} isRequired isInvalid={!!errors.password}>
<FormLabel htmlFor='password'>Set Password</FormLabel>
<Input id='password' {...register('password')} placeholder='Password' type='password' />
<Input id='password' {...register('password', { required: 'Password is required', minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='Password' type='password' />
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='confirmPassword'>Confirm Password</FormLabel>
<Input id='confirmPassword' {...register('confirmPassword')} placeholder='Password' type='password' />
<FormControl mt={4} isRequired isInvalid={!!errors.confirm_password}>
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel>
<Input id='confirm_password' {...register('confirm_password', {
required: 'Please confirm your password',
validate: value => value === getValues().password || 'The passwords do not match'
})} placeholder='Password' type='password' />
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>}
</FormControl>
<Flex mt={4}>
<FormControl>
Expand All @@ -79,7 +92,7 @@ const AddUser: React.FC<AddUserProps> = ({ isOpen, onClose }) => {
</Flex>
</ModalBody>
<ModalFooter gap={3}>
<Button bg='ui.main' color='white' type='submit' isLoading={isSubmitting}>
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}>
Save
</Button>
<Button onClick={onClose}>Cancel</Button>
Expand Down
66 changes: 40 additions & 26 deletions src/new-frontend/src/components/Admin/EditUser.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

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

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

const EditUser: React.FC<EditUserProps> = ({ user_id, isOpen, onClose }) => {
const showToast = useCustomToast();
const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<UserUpdateForm>();
const { editUser, users } = useUsersStore();

const currentUser = users.find((user) => user.id === user_id);
const { register, handleSubmit, reset, getValues, formState: { errors, isSubmitting } } = useForm<UserUpdateForm>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
email: currentUser?.email,
full_name: currentUser?.full_name,
password: '',
confirm_password: '',
is_superuser: currentUser?.is_superuser,
is_active: currentUser?.is_active
}
});


const onSubmit: SubmitHandler<UserUpdateForm> = async (data) => {
if (data.password === data.confirm_password) {
try {
await editUser(user_id, data);
showToast('Success!', 'User updated successfully.', 'success');
reset();
onClose();
} catch (err) {
const errDetail = (err as ApiError).body.detail;
showToast('Something went wrong.', `${errDetail}`, 'error');
try {
if (data.password === '') {
delete data.password;
}
} else {
// TODO: Complete when form validation is implemented
console.log("Passwords don't match")
await editUser(user_id, data);
showToast('Success!', 'User updated successfully.', 'success');
reset();
onClose();
} catch (err) {
const errDetail = (err as ApiError).body.detail;
showToast('Something went wrong.', `${errDetail}`, 'error');
}
}

Expand All @@ -59,28 +68,33 @@ const EditUser: React.FC<EditUserProps> = ({ user_id, isOpen, onClose }) => {
<ModalHeader>Edit User</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FormControl>
<FormControl isInvalid={!!errors.email}>
<FormLabel htmlFor='email'>Email</FormLabel>
<Input id="email" {...register('email')} defaultValue={currentUser?.email} type='email' />
<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' />
{errors.email && <FormErrorMessage>{errors.email.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='name'>Full name</FormLabel>
<Input id="name" {...register('full_name')} defaultValue={currentUser?.full_name} type='text' />
<Input id="name" {...register('full_name')} type='text' />
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='password'>Password</FormLabel>
<Input id="password" {...register('password')} placeholder='••••••••' type='password' />
<FormControl mt={4} isInvalid={!!errors.password}>
<FormLabel htmlFor='password'>Set Password</FormLabel>
<Input id='password' {...register('password', { minLength: { value: 8, message: 'Password must be at least 8 characters' } })} placeholder='••••••••' type='password' />
{errors.password && <FormErrorMessage>{errors.password.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='confirmPassword'>Confirmation Password</FormLabel>
<Input id='confirmPassword' {...register('confirm_password')} placeholder='••••••••' type='password' />
<FormControl mt={4} isInvalid={!!errors.confirm_password}>
<FormLabel htmlFor='confirm_password'>Confirm Password</FormLabel>
<Input id='confirm_password' {...register('confirm_password', {
validate: value => value === getValues().password || 'The passwords do not match'
})} placeholder='••••••••' type='password' />
{errors.confirm_password && <FormErrorMessage>{errors.confirm_password.message}</FormErrorMessage>}
</FormControl>
<Flex>
<FormControl mt={4}>
<Checkbox {...register('is_superuser')} defaultChecked={currentUser?.is_superuser} colorScheme='teal'>Is superuser?</Checkbox>
<Checkbox {...register('is_superuser')} colorScheme='teal'>Is superuser?</Checkbox>
</FormControl>
<FormControl mt={4}>
<Checkbox {...register('is_active')} defaultChecked={currentUser?.is_active} colorScheme='teal'>Is active?</Checkbox>
<Checkbox {...register('is_active')} colorScheme='teal'>Is active?</Checkbox>
</FormControl>
</Flex>
</ModalBody>
Expand Down
10 changes: 3 additions & 7 deletions src/new-frontend/src/components/Common/DeleteAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,17 @@ interface DeleteProps {
const Delete: React.FC<DeleteProps> = ({ type, id, isOpen, onClose }) => {
const showToast = useCustomToast();
const cancelRef = React.useRef<HTMLButtonElement | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { handleSubmit } = useForm();
const { handleSubmit, formState: {isSubmitting} } = useForm();
const { deleteItem } = useItemsStore();
const { deleteUser } = useUsersStore();

const onSubmit = async () => {
setIsLoading(true);
try {
type === 'Item' ? await deleteItem(id) : await deleteUser(id);
showToast('Success', `The ${type.toLowerCase()} was deleted successfully.`, 'success');
onClose();
} catch (err) {
showToast('An error occurred.', `An error occurred while deleting the ${type.toLowerCase()}.`, 'error');
} finally {
setIsLoading(false);
}
}

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

<AlertDialogFooter gap={3}>
<Button bg="ui.danger" color="white" _hover={{ opacity: 0.8 }} type="submit" isLoading={isLoading}>
<Button bg="ui.danger" color="white" _hover={{ opacity: 0.8 }} type="submit" isLoading={isSubmitting}>
Delete
</Button>
<Button ref={cancelRef} onClick={onClose} isDisabled={isLoading}>
<Button ref={cancelRef} onClick={onClose} isDisabled={isSubmitting}>
Cancel
</Button>
</AlertDialogFooter>
Expand Down
26 changes: 15 additions & 11 deletions src/new-frontend/src/components/Items/AddItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React from 'react';

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

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

const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
const showToast = useCustomToast();
const [isLoading, setIsLoading] = useState(false);
const { register, handleSubmit, reset } = useForm<ItemCreate>();
const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<ItemCreate>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
title: '',
description: '',
},
});
const { addItem } = useItemsStore();

const onSubmit: SubmitHandler<ItemCreate> = async (data) => {
setIsLoading(true);
try {
await addItem(data);
showToast('Success!', 'Item created successfully.', 'success');
Expand All @@ -28,8 +33,6 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
} catch (err) {
const errDetail = (err as ApiError).body.detail;
showToast('Something went wrong.', `${errDetail}`, 'error');
} finally {
setIsLoading(false);
}
};

Expand All @@ -46,14 +49,15 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
<ModalHeader>Add Item</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FormControl>
<FormControl isRequired isInvalid={!!errors.title}>
<FormLabel htmlFor='title'>Title</FormLabel>
<Input
id='title'
{...register('title')}
{...register('title', { required: 'Title is required.' })}
placeholder='Title'
type='text'
/>
{errors.title && <FormErrorMessage>{errors.title.message}</FormErrorMessage>}
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='description'>Description</FormLabel>
Expand All @@ -67,10 +71,10 @@ const AddItem: React.FC<AddItemProps> = ({ isOpen, onClose }) => {
</ModalBody>

<ModalFooter gap={3}>
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isLoading}>
<Button bg='ui.main' color='white' _hover={{ opacity: 0.8 }} type='submit' isLoading={isSubmitting}>
Save
</Button>
<Button onClick={onClose} isDisabled={isLoading}>
<Button onClick={onClose}>
Cancel
</Button>
</ModalFooter>
Expand Down
7 changes: 3 additions & 4 deletions src/new-frontend/src/components/Items/EditItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ interface EditItemProps {

const EditItem: React.FC<EditItemProps> = ({ id, isOpen, onClose }) => {
const showToast = useCustomToast();
const { register, handleSubmit, reset, formState: { isSubmitting }, } = useForm<ItemUpdate>();
const { editItem, items } = useItemsStore();

const currentItem = items.find((item) => item.id === id);
const { register, handleSubmit, reset, formState: { isSubmitting }, } = useForm<ItemUpdate>({ defaultValues: { title: currentItem?.title, description: currentItem?.description } });

const onSubmit: SubmitHandler<ItemUpdate> = async (data) => {
try {
Expand Down Expand Up @@ -52,11 +51,11 @@ const EditItem: React.FC<EditItemProps> = ({ id, isOpen, onClose }) => {
<ModalBody pb={6}>
<FormControl>
<FormLabel htmlFor='title'>Title</FormLabel>
<Input id='title' {...register('title')} defaultValue={currentItem?.title} type='text' />
<Input id='title' {...register('title')} type='text' />
</FormControl>
<FormControl mt={4}>
<FormLabel htmlFor='description'>Description</FormLabel>
<Input id='description' {...register('description')} defaultValue={currentItem?.description} placeholder='Description' type='text' />
<Input id='description' {...register('description')} placeholder='Description' type='text' />
</FormControl>
</ModalBody>
<ModalFooter gap={3}>
Expand Down
4 changes: 2 additions & 2 deletions src/new-frontend/src/components/UserSettings/Appearance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const Appearance: React.FC = () => {
<Stack>
{/* TODO: Add system default option */}
<Radio value='light' colorScheme='teal'>
Light Mode<Badge ml='1' colorScheme='teal'>Default</Badge>
Light mode<Badge ml='1' colorScheme='teal'>Default</Badge>
</Radio>
<Radio value='dark' colorScheme='teal'>
Dark Mode
Dark mode
</Radio>
</Stack>
</RadioGroup>
Expand Down
Loading

0 comments on commit 35c306b

Please sign in to comment.