Skip to content

Commit

Permalink
Rework submit buttons and adapt UI for desktop
Browse files Browse the repository at this point in the history
  • Loading branch information
pkirilin committed Oct 25, 2024
1 parent 596d03e commit fdc2ee1
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 38 deletions.
14 changes: 14 additions & 0 deletions src/frontend/src/features/addNote/model/addNoteSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface NoteDraft {
mealType: noteModel.MealType;
displayOrder: number;
product?: productModel.AutocompleteOption;
isValid?: boolean;
}

export interface State {
Expand All @@ -20,6 +21,12 @@ const initialState: State = {};
export const addNoteSlice = createSlice({
name: 'addNote',
initialState,
selectors: {
activeFormId: state =>
state.draft?.product?.freeSolo && state.draft?.product?.editing
? 'product-form'
: 'note-form',
},
reducers: {
draftCreated: (state, { payload }: PayloadAction<NoteDraft>) => {
state.draft = payload;
Expand All @@ -29,6 +36,12 @@ export const addNoteSlice = createSlice({
state.draft = initialState.draft;
},

draftValidated: (state, { payload }: PayloadAction<boolean>) => {
if (state.draft) {
state.draft.isValid = payload;
}
},

productSelected: (state, { payload }: PayloadAction<ProductSelectOption>) => {
if (state.draft) {
state.draft.product = {
Expand All @@ -43,6 +56,7 @@ export const addNoteSlice = createSlice({
productDiscarded: state => {
if (state.draft) {
delete state.draft.product;
state.draft.isValid = false;
}
},

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/features/addNote/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { addNoteSlice } from './addNoteSlice';

export const { actions, reducer } = addNoteSlice;
export const { actions, selectors, reducer } = addNoteSlice;
20 changes: 14 additions & 6 deletions src/frontend/src/features/addNote/ui/AddNoteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type FC } from 'react';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { type noteModel } from '@/entities/note';
import { Button, Dialog } from '@/shared/ui';
import { actions } from '../model';
import { actions, selectors } from '../model';
import { NoteInputFlow } from './NoteInputFlow';

interface Props {
Expand All @@ -13,9 +13,15 @@ interface Props {
}

export const AddNoteButton: FC<Props> = ({ date, mealType, displayOrder }) => {
const activeFormId = useAppSelector(selectors.activeFormId);
const canAddNote = useAppSelector(state => state.addNote.draft?.isValid);
const dialogVisible = useAppSelector(state => state.addNote.draft?.mealType === mealType);
const dispatch = useAppDispatch();

const closeDialog = (): void => {
dispatch(actions.draftDiscarded());
};

return (
<>
<Button
Expand All @@ -25,21 +31,23 @@ export const AddNoteButton: FC<Props> = ({ date, mealType, displayOrder }) => {
>
Add note (v2)
</Button>
{/* TODO: use custom dialog with navigation, drop desktop support? */}
<Dialog
pinToTop
renderMode="fullScreenOnMobile"
title="New note"
opened={dialogVisible}
onClose={() => dispatch(actions.draftDiscarded())}
onClose={closeDialog}
content={<NoteInputFlow />}
renderCancel={props => (
<Button {...props} type="button">
<Button {...props} type="button" onClick={closeDialog}>
Cancel
</Button>
)}
// TODO: make prop optional?
renderSubmit={() => <div></div>}
renderSubmit={props => (
<Button {...props} type="submit" form={activeFormId} disabled={!canAddNote}>
Save
</Button>
)}
/>
</>
);
Expand Down
34 changes: 18 additions & 16 deletions src/frontend/src/features/addNote/ui/NoteForm.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { zodResolver } from '@hookform/resolvers/zod';
import CancelIcon from '@mui/icons-material/Cancel';
import { IconButton, InputAdornment, TextField } from '@mui/material';
import { type FC } from 'react';
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 { Button } from '@/shared/ui';
import { actions } from '../model';
import { actions, selectors } from '../model';

const schema = z.object({
quantity: z.coerce.number().int().min(1).max(999),
Expand All @@ -26,12 +25,19 @@ export const NoteForm: FC = () => {

const [createNote] = noteApi.useCreateNoteMutation();
const noteDraft = useAppSelector(state => state.addNote.draft);
const activeFormId = useAppSelector(selectors.activeFormId);
const dispatch = useAppDispatch();

useEffect(() => {
dispatch(actions.draftValidated(formState.isValid));
}, [dispatch, formState.isValid]);

return (
<form
id={activeFormId}
onSubmit={handleSubmit(({ quantity }) => {
if (!noteDraft?.product || noteDraft.product.freeSolo) {
// TODO: create free solo products
return;
}

Expand All @@ -44,22 +50,27 @@ export const NoteForm: FC = () => {
});
})}
>
{/* TODO: show if product is new */}
<TextField
label="Product"
value={noteDraft?.product?.name}
fullWidth
margin="normal"
helperText=" "
slotProps={{
input: {
readOnly: true,
endAdornment: (
<IconButton edge="end" onClick={() => dispatch(actions.productDiscarded())}>
<CancelIcon />
</IconButton>
<Tooltip title="Discard and choose another product" placement="left">
<IconButton edge="end" onClick={() => dispatch(actions.productDiscarded())}>
<CancelIcon />
</IconButton>
</Tooltip>
),
},
}}
/>
{/* TODO: set quantity as product's default quantity on init */}
<Controller
name="quantity"
control={control}
Expand All @@ -81,15 +92,6 @@ export const NoteForm: FC = () => {
/>
)}
/>
<Button
type="submit"
variant="contained"
fullWidth
disabled={!formState.isValid}
loading={formState.isSubmitting}
>
Add
</Button>
</form>
);
};
12 changes: 10 additions & 2 deletions src/frontend/src/features/addNote/ui/NoteInputFlow.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type FC } from 'react';
import { useCallback, type FC } from 'react';
import { useAppDispatch, useAppSelector } from '@/app/store';
import { categoryLib } from '@/entities/category';
import { type productModel } from '@/entities/product';
import { type SelectOption } from '@/shared/types';
import { actions } from '../model';
import { actions, selectors } from '../model';
import { type ProductFormValues } from '../model/productForm';
import { NoteForm } from './NoteForm';
import { ProductForm } from './ProductForm';
Expand All @@ -21,20 +21,28 @@ const toProductFormValues = (

export const NoteInputFlow: FC = () => {
const product = useAppSelector(state => state.addNote.draft?.product);
const activeFormId = useAppSelector(selectors.activeFormId);
const categorySelect = categoryLib.useCategorySelectData();
const dispatch = useAppDispatch();

const validateProduct = useCallback(
(isValid: boolean) => dispatch(actions.draftValidated(isValid)),
[dispatch],
);

if (!product) {
return <SearchProducts />;
}

if (product.freeSolo && product.editing) {
return (
<ProductForm
formId={activeFormId}
defaultValues={toProductFormValues(product, categorySelect.data)}
categories={categorySelect.data}
categoriesLoading={categorySelect.isLoading}
onSubmit={data => dispatch(actions.productSaved(data))}
onValidate={validateProduct}
/>
);
}
Expand Down
23 changes: 11 additions & 12 deletions src/frontend/src/features/addNote/ui/ProductForm.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Autocomplete, CircularProgress, InputAdornment, TextField } from '@mui/material';
import { type FC } from 'react';
import { useEffect, type FC } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { type SelectOption } from '@/shared/types';
import { Button } from '@/shared/ui';
import { type ProductFormValues, productFormSchema } from '../model/productForm';

interface Props {
formId: string;
defaultValues: ProductFormValues;
categories: SelectOption[];
categoriesLoading: boolean;
onSubmit: (data: ProductFormValues) => void;
onValidate: (isValid: boolean) => void;
}

export const ProductForm: FC<Props> = ({
formId,
defaultValues,
categories,
categoriesLoading,
onSubmit,
onValidate,
}) => {
const { control, formState, handleSubmit } = useForm<ProductFormValues>({
mode: 'onChange',
resolver: zodResolver(productFormSchema),
defaultValues,
});

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

return (
<form onSubmit={handleSubmit(data => onSubmit(data))}>
<form id={formId} onSubmit={handleSubmit(data => onSubmit(data))}>
<Controller
name="name"
control={control}
Expand All @@ -42,6 +49,7 @@ export const ProductForm: FC<Props> = ({
/>
)}
/>
{/* TODO: maybe show calories cost and quantity in the single row? */}
<Controller
name="caloriesCost"
control={control}
Expand Down Expand Up @@ -118,15 +126,6 @@ export const ProductForm: FC<Props> = ({
/>
)}
/>
<Button
fullWidth
variant="outlined"
type="submit"
disabled={!formState.isValid}
loading={formState.isSubmitting}
>
Save
</Button>
</form>
);
};
1 change: 0 additions & 1 deletion src/frontend/src/features/addNote/ui/SearchProducts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const DEBOUNCE_QUERY_LENGTH_THRESHOLD = 3;
const EMPTY_PRODUCTS: ProductSelectOption[] = [];

// TODO: add tests
// TODO: desktop adaptation
export const SearchProducts: FC = () => {
const { register, watch } = useForm<FormValues>({
mode: 'onChange',
Expand Down

0 comments on commit fdc2ee1

Please sign in to comment.