Skip to content

Commit

Permalink
Use form schema types in addNoteSlice
Browse files Browse the repository at this point in the history
  • Loading branch information
pkirilin committed Oct 27, 2024
1 parent b4fa06b commit 457e86e
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export interface CategorySelectData {
isLoading: boolean;
}

const EMPTY_DATA: SelectOption[] = [];

export const useCategorySelectData = (): CategorySelectData => {
const { data, isLoading } = categoryApi.useGetCategorySelectOptionsQuery();

return useMemo(
() => ({
data: data ?? [],
data: data ?? EMPTY_DATA,
isLoading,
}),
[data, isLoading],
Expand Down
39 changes: 14 additions & 25 deletions src/frontend/src/features/addNote/model/addNoteSlice.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { type PayloadAction, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { noteApi, type noteModel } from '@/entities/note';
import { noteApi } from '@/entities/note';
import { productApi, type ProductSelectOption } from '@/entities/product';
import { type ProductFormValues } from './productForm';
import { type ProductDraft, type Image } from './types';
import { type NoteFormValues } from './noteSchema';
import { type ProductFormValues } from './productSchema';
import { type Image } from './types';

interface State {
note?: NoteDraft;
product?: ProductDraft;
note?: NoteFormValues;
product?: ProductFormValues;
image?: Image;
isValid: boolean;
isSubmitting: boolean;
}

interface NoteDraft {
date: string;
mealType: noteModel.MealType;
displayOrder: number;
product?: ProductSelectOption;
}

const initialState: State = {
isValid: false,
isSubmitting: false,
Expand All @@ -33,7 +27,7 @@ export const addNoteSlice = createSlice({
dialogTitle: state => (state.product ? 'New product' : 'New note'),
},
reducers: {
noteDraftCreated: (state, { payload }: PayloadAction<NoteDraft>) => {
noteDraftSaved: (state, { payload }: PayloadAction<NoteFormValues>) => {
state.note = payload;
},

Expand All @@ -53,23 +47,17 @@ export const addNoteSlice = createSlice({
}
},

productDraftSaved: (state, { payload }: PayloadAction<ProductFormValues>) => {
state.product = payload;
},

productDraftDiscarded: state => {
if (state.note?.product) {
delete state.note.product;
if (state.note) {
state.note.product = null;
delete state.product;
state.isValid = false;
}
},

productDraftSaved: (state, { payload }: PayloadAction<ProductFormValues>) => {
state.product = {
name: payload.name,
caloriesCost: payload.caloriesCost,
defaultQuantity: payload.defaultQuantity,
category: payload.category,
};
},

imageUploaded: (state, { payload }: PayloadAction<Image>) => {
state.image = payload;
},
Expand Down Expand Up @@ -116,6 +104,7 @@ export const addNoteSlice = createSlice({
name: state.product.name,
defaultQuantity: state.product.defaultQuantity,
};
delete state.product;
state.isSubmitting = false;
}
});
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/features/addNote/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ import { addNoteSlice } from './addNoteSlice';
export const { actions, selectors, reducer } = addNoteSlice;

export * from './types';
export * from './noteSchema';
export * from './productSchema';
20 changes: 20 additions & 0 deletions src/frontend/src/features/addNote/model/noteSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';
import { noteModel } from '@/entities/note';
import { quantitySchema } from './quantitySchema';

export const noteSchema = z.object({
date: z.string(),
mealType: z.nativeEnum(noteModel.MealType),
displayOrder: z.coerce.number().int().min(0),
product: z
.object({
id: z.number(),
name: z.string(),
defaultQuantity: quantitySchema,
})
.nullable()
.refine(product => product !== null, { message: 'Product is required' }),
quantity: quantitySchema,
});

export type NoteFormValues = z.infer<typeof noteSchema>;
16 changes: 0 additions & 16 deletions src/frontend/src/features/addNote/model/productForm.ts

This file was deleted.

17 changes: 17 additions & 0 deletions src/frontend/src/features/addNote/model/productSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';
import { quantitySchema } from './quantitySchema';

export const productSchema = z.object({
name: z.string().min(3).max(100),
caloriesCost: z.coerce.number().int().min(1).max(4999),
defaultQuantity: quantitySchema,
category: z
.object({
id: z.number(),
name: z.string().min(3).max(50),
})
.nullable()
.refine(category => category !== null, { message: 'Category is required' }),
});

export type ProductFormValues = z.infer<typeof productSchema>;
3 changes: 3 additions & 0 deletions src/frontend/src/features/addNote/model/quantitySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { z } from 'zod';

export const quantitySchema = z.coerce.number().int().min(1).max(999);
9 changes: 0 additions & 9 deletions src/frontend/src/features/addNote/model/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
import { type SelectOption } from '@/shared/types';

export interface ProductDraft {
name: string;
caloriesCost: number;
defaultQuantity: number;
category: SelectOption | null;
}

export interface Image {
name: string;
base64: string;
Expand Down
26 changes: 17 additions & 9 deletions src/frontend/src/features/addNote/ui/AddNoteButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AddIcon from '@mui/icons-material/Add';
import { type FC } from 'react';
import { type MouseEventHandler, type FC } from 'react';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { type noteModel } from '@/entities/note';
import { Button, Dialog } from '@/shared/ui';
Expand All @@ -20,28 +20,36 @@ export const AddNoteButton: FC<Props> = ({ date, mealType, displayOrder }) => {
const dialogVisible = useAppSelector(state => state.addNote.note?.mealType === mealType);
const dispatch = useAppDispatch();

const handleCloseDialog = (): void => {
const handleDialogOpen: MouseEventHandler = () => {
dispatch(
actions.noteDraftSaved({
date,
mealType,
displayOrder,
product: null,
quantity: 100,
}),
);
};

const handleDialogClose = (): void => {
dispatch(actions.noteDraftDiscarded());
};

return (
<>
<Button
fullWidth
startIcon={<AddIcon />}
onClick={() => dispatch(actions.noteDraftCreated({ date, mealType, displayOrder }))}
>
<Button fullWidth startIcon={<AddIcon />} onClick={handleDialogOpen}>
Add note (v2)
</Button>
<Dialog
pinToTop
renderMode="fullScreenOnMobile"
title={dialogTitle}
opened={dialogVisible}
onClose={handleCloseDialog}
onClose={handleDialogClose}
content={<NoteInputFlow />}
renderCancel={props => (
<Button {...props} type="button" onClick={handleCloseDialog}>
<Button {...props} type="button" onClick={handleDialogClose}>
Cancel
</Button>
)}
Expand Down
22 changes: 7 additions & 15 deletions src/frontend/src/features/addNote/ui/NoteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,19 @@ import CancelIcon from '@mui/icons-material/Cancel';
import { IconButton, InputAdornment, TextField, Tooltip } from '@mui/material';
import { useEffect, type FC } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { noteApi } from '@/entities/note';
import { actions, selectors } from '../model';

const schema = z.object({
quantity: z.coerce.number().int().min(1).max(999),
});

type FormValues = z.infer<typeof schema>;
import { actions, selectors, noteSchema, type NoteFormValues } from '../model';

interface Props {
quantity: number;
defaultValues: NoteFormValues;
}

export const NoteForm: FC<Props> = ({ quantity }) => {
const { control, formState, handleSubmit } = useForm<FormValues>({
export const NoteForm: FC<Props> = ({ defaultValues }) => {
const { control, formState, handleSubmit } = useForm<NoteFormValues>({
mode: 'onChange',
resolver: zodResolver(schema),
defaultValues: {
quantity,
},
resolver: zodResolver(noteSchema),
defaultValues,
});

const [createNote] = noteApi.useCreateNoteMutation();
Expand Down Expand Up @@ -53,6 +44,7 @@ export const NoteForm: FC<Props> = ({ quantity }) => {
});
})}
>
{/* TODO: show meal type */}
<TextField
label="Product"
value={noteDraft?.product?.name}
Expand Down
24 changes: 9 additions & 15 deletions src/frontend/src/features/addNote/ui/NoteInputFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,14 @@ import { useCallback, type FC } from 'react';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { categoryLib } from '@/entities/category';
import { type CreateProductRequest, productApi } from '@/entities/product';
import { type SelectOption } from '@/shared/types';
import { type ProductDraft, actions, selectors } from '../model';
import { type ProductFormValues } from '../model/productForm';
import { actions, selectors } from '../model';
import { type ProductFormValues } from '../model/productSchema';
import { ImagePreview } from './ImagePreview';
import { NoteForm } from './NoteForm';
import { ProductForm } from './ProductForm';
import { SearchProducts } from './SearchProducts';
import { SearchProductsOnImage } from './SearchProductsOnImage';

const toProductFormValues = (
{ name, caloriesCost, defaultQuantity, category }: ProductDraft,
categories: SelectOption[],
): ProductFormValues => ({
name,
caloriesCost,
defaultQuantity,
category: categories.at(0) ?? category,
});

const toCreateProductRequest = (
{ name, caloriesCost, defaultQuantity }: ProductFormValues,
categoryId: number,
Expand All @@ -33,6 +22,7 @@ const toCreateProductRequest = (

export const NoteInputFlow: FC = () => {
const product = useAppSelector(state => state.addNote.note?.product);
const noteDraft = useAppSelector(state => state.addNote.note);
const productDraft = useAppSelector(state => state.addNote.product);
const image = useAppSelector(state => state.addNote.image);
const activeFormId = useAppSelector(selectors.activeFormId);
Expand All @@ -59,7 +49,7 @@ export const NoteInputFlow: FC = () => {
return (
<ProductForm
formId={activeFormId}
defaultValues={toProductFormValues(productDraft, categorySelect.data)}
defaultValues={productDraft}
categories={categorySelect.data}
categoriesLoading={categorySelect.isLoading}
onSubmit={handleCreateProduct}
Expand All @@ -81,5 +71,9 @@ export const NoteInputFlow: FC = () => {
return <SearchProducts />;
}

return <NoteForm quantity={product.defaultQuantity} />;
if (!noteDraft) {
return null;
}

return <NoteForm defaultValues={{ ...noteDraft, quantity: product.defaultQuantity }} />;
};
12 changes: 9 additions & 3 deletions src/frontend/src/features/addNote/ui/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Autocomplete, CircularProgress, InputAdornment, TextField } from '@mui/
import { useEffect, type FC } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { type SelectOption } from '@/shared/types';
import { type ProductFormValues, productFormSchema } from '../model/productForm';
import { type ProductFormValues, productSchema } from '../model';

interface Props {
formId: string;
Expand All @@ -22,16 +22,22 @@ export const ProductForm: FC<Props> = ({
onSubmit,
onValidate,
}) => {
const { control, formState, handleSubmit } = useForm<ProductFormValues>({
const { control, formState, handleSubmit, setValue } = useForm<ProductFormValues>({
mode: 'onChange',
resolver: zodResolver(productFormSchema),
resolver: zodResolver(productSchema),
defaultValues,
});

useEffect(() => {
onValidate(formState.isValid);
}, [formState.isValid, onValidate]);

useEffect(() => {
if (categories.length > 0) {
setValue('category', categories[0], { shouldValidate: true });
}
}, [categories, setValue]);

return (
<form id={formId} onSubmit={handleSubmit(data => onSubmit(data))}>
<Controller
Expand Down

0 comments on commit 457e86e

Please sign in to comment.