From 61d8c0d481acb147c0026d6b6f8448949601b59f Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Sat, 11 May 2024 21:54:27 +0300 Subject: [PATCH 1/2] Added product addEdit feature folder --- src/frontend/src/app/store.ts | 4 +- .../src/entities/product/api/contracts.ts | 14 ++ .../src/entities/product/api/productApi.ts | 4 +- .../src/entities/product/lib/index.ts | 3 + .../src/entities/product/lib/mapping.ts | 30 ++++ .../product/lib/useCheckedProductIds.ts | 9 ++ .../src/entities/product/lib/useProducts.ts | 33 ++++ .../src/entities/product/model/index.ts | 1 + .../model/productSlice.actions.test.ts} | 3 +- .../product/model/productSlice.ts} | 16 +- .../src/features/product/addEdit/index.ts | 1 + .../src/features/product/addEdit/lib/index.ts | 1 + .../features/product/addEdit/lib/mapping.ts | 27 ++++ .../addEdit/ui/AddProduct.tsx} | 17 +- .../product/addEdit/ui/EditProduct.tsx | 56 +++++++ .../ProductInputDialog.fixture.tsx | 0 .../ProductInputDialog.test.tsx | 0 .../ProductInputDialog/ProductInputDialog.tsx | 0 .../addEdit/ui}/ProductInputDialog/index.ts | 0 .../src/features/product/addEdit/ui/index.ts | 2 + .../components/DeleteProductsDialog.tsx | 9 +- .../ProductsTable/ProductsTable.tsx | 2 +- .../components/ProductsTablePagination.tsx | 16 +- .../products/components/ProductsTableRow.tsx | 146 +++++++----------- .../components/ProductsTableToolbar.tsx | 9 +- .../products/components/SearchByCategory.tsx | 4 +- .../products/components/SearchByName.tsx | 6 +- src/frontend/src/features/products/index.ts | 3 - src/frontend/src/features/products/mapping.ts | 58 ------- .../src/features/products/model/index.ts | 1 - .../features/products/model/useProducts.ts | 22 --- .../src/features/products/routes/Products.tsx | 15 +- .../src/features/products/selectors/index.ts | 15 -- .../src/features/products/store/index.ts | 17 -- .../src/features/products/store/types.ts | 8 - .../src/features/products/types/index.ts | 13 -- src/frontend/src/pages/ui/ProductsPage.tsx | 6 +- .../mockApi/products/products.handlers.ts | 4 +- src/frontend/tests/render/TestEnvironment.tsx | 4 +- 39 files changed, 282 insertions(+), 297 deletions(-) create mode 100644 src/frontend/src/entities/product/lib/mapping.ts create mode 100644 src/frontend/src/entities/product/lib/useCheckedProductIds.ts create mode 100644 src/frontend/src/entities/product/lib/useProducts.ts rename src/frontend/src/{features/products/store/actions.test.ts => entities/product/model/productSlice.actions.test.ts} (97%) rename src/frontend/src/{features/products/store/slice.ts => entities/product/model/productSlice.ts} (87%) create mode 100644 src/frontend/src/features/product/addEdit/index.ts create mode 100644 src/frontend/src/features/product/addEdit/lib/index.ts create mode 100644 src/frontend/src/features/product/addEdit/lib/mapping.ts rename src/frontend/src/features/{products/components/CreateProduct.tsx => product/addEdit/ui/AddProduct.tsx} (74%) create mode 100644 src/frontend/src/features/product/addEdit/ui/EditProduct.tsx rename src/frontend/src/features/{products/components => product/addEdit/ui}/ProductInputDialog/ProductInputDialog.fixture.tsx (100%) rename src/frontend/src/features/{products/components => product/addEdit/ui}/ProductInputDialog/ProductInputDialog.test.tsx (100%) rename src/frontend/src/features/{products/components => product/addEdit/ui}/ProductInputDialog/ProductInputDialog.tsx (100%) rename src/frontend/src/features/{products/components => product/addEdit/ui}/ProductInputDialog/index.ts (100%) create mode 100644 src/frontend/src/features/product/addEdit/ui/index.ts delete mode 100644 src/frontend/src/features/products/mapping.ts delete mode 100644 src/frontend/src/features/products/model/index.ts delete mode 100644 src/frontend/src/features/products/model/useProducts.ts delete mode 100644 src/frontend/src/features/products/selectors/index.ts delete mode 100644 src/frontend/src/features/products/store/index.ts delete mode 100644 src/frontend/src/features/products/store/types.ts delete mode 100644 src/frontend/src/features/products/types/index.ts diff --git a/src/frontend/src/app/store.ts b/src/frontend/src/app/store.ts index 6f0f64cfe..a578d9f4a 100644 --- a/src/frontend/src/app/store.ts +++ b/src/frontend/src/app/store.ts @@ -1,7 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; import { type TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { productModel } from '@/entities/product'; import pagesReducer from '../features/pages/slice'; -import productsReducer from '../features/products/store'; import { api } from '../shared/api'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -10,7 +10,7 @@ export const configureAppStore = () => reducer: { [api.reducerPath]: api.reducer, pages: pagesReducer, - products: productsReducer, + products: productModel.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(api.middleware), diff --git a/src/frontend/src/entities/product/api/contracts.ts b/src/frontend/src/entities/product/api/contracts.ts index 75b128f69..27deb14a6 100644 --- a/src/frontend/src/entities/product/api/contracts.ts +++ b/src/frontend/src/entities/product/api/contracts.ts @@ -1,3 +1,17 @@ +export interface Product { + id: number; + name: string; + caloriesCost: number; + defaultQuantity: number; + categoryId: number; + categoryName: string; +} + +export interface GetProductsResponse { + productItems: Product[]; + totalProductsCount: number; +} + export interface GetProductsRequest { pageNumber: number; pageSize: number; diff --git a/src/frontend/src/entities/product/api/productApi.ts b/src/frontend/src/entities/product/api/productApi.ts index 9990fa45a..fc093fd79 100644 --- a/src/frontend/src/entities/product/api/productApi.ts +++ b/src/frontend/src/entities/product/api/productApi.ts @@ -1,6 +1,5 @@ import { api } from '@/shared/api'; import { createUrl } from '@/shared/lib'; -import { type ProductsResponse } from '../../../features/products/types'; import { type CreateProductResponse, type CreateProductRequest, @@ -8,11 +7,12 @@ import { type EditProductRequest, type GetProductsRequest, type ProductSelectOption, + type GetProductsResponse, } from './contracts'; export const productApi = api.injectEndpoints({ endpoints: builder => ({ - getProducts: builder.query({ + getProducts: builder.query({ query: request => createUrl('/api/v1/products', { ...request }), providesTags: ['product'], }), diff --git a/src/frontend/src/entities/product/lib/index.ts b/src/frontend/src/entities/product/lib/index.ts index 29a8f98b2..de2cd8f87 100644 --- a/src/frontend/src/entities/product/lib/index.ts +++ b/src/frontend/src/entities/product/lib/index.ts @@ -1,3 +1,6 @@ +export * from './mapping'; export * from './useAutocompleteData'; export * from './useAutocompleteInput'; export * from './useFormValues'; +export * from './useProducts'; +export * from './useCheckedProductIds'; diff --git a/src/frontend/src/entities/product/lib/mapping.ts b/src/frontend/src/entities/product/lib/mapping.ts new file mode 100644 index 000000000..09f2adc0a --- /dev/null +++ b/src/frontend/src/entities/product/lib/mapping.ts @@ -0,0 +1,30 @@ +import { type Product, type GetProductsRequest } from '../api'; +import { type FormValues, type ProductItemsFilter } from '../model'; + +export const mapToGetProductsRequest = ({ + pageNumber, + pageSize, + productSearchName, + category, +}: ProductItemsFilter): GetProductsRequest => ({ + pageNumber, + pageSize, + productSearchName, + categoryId: category?.id, +}); + +export const mapToProductFormData = ({ + name, + caloriesCost, + defaultQuantity, + categoryId, + categoryName, +}: Product): FormValues => ({ + name, + caloriesCost, + defaultQuantity, + category: { + id: categoryId, + name: categoryName, + }, +}); diff --git a/src/frontend/src/entities/product/lib/useCheckedProductIds.ts b/src/frontend/src/entities/product/lib/useCheckedProductIds.ts new file mode 100644 index 000000000..62c0f43fe --- /dev/null +++ b/src/frontend/src/entities/product/lib/useCheckedProductIds.ts @@ -0,0 +1,9 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector, type RootState } from '@/app/store'; +import { type ProductsState } from '../model'; + +const selectProducts = (state: RootState): ProductsState => state.products; + +const selectCheckedProductIds = createSelector(selectProducts, state => state.checkedProductIds); + +export const useCheckedProductIds = (): number[] => useAppSelector(selectCheckedProductIds); diff --git a/src/frontend/src/entities/product/lib/useProducts.ts b/src/frontend/src/entities/product/lib/useProducts.ts new file mode 100644 index 000000000..1521cc165 --- /dev/null +++ b/src/frontend/src/entities/product/lib/useProducts.ts @@ -0,0 +1,33 @@ +import { createDraftSafeSelector, createSelector } from '@reduxjs/toolkit'; +import { type RootState, useAppSelector } from '@/app/store'; +import { type Product, productApi } from '@/entities/product'; +import { type ProductsState } from '../model'; +import { mapToGetProductsRequest } from './mapping'; + +const selectProducts = (state: RootState): ProductsState => state.products; + +const selectProductsFilter = createDraftSafeSelector(selectProducts, products => products.filter); + +const selectProductsQueryArg = createSelector(selectProductsFilter, mapToGetProductsRequest); + +interface Result { + data: Product[]; + totalCount: number; + isLoading: boolean; + isFetching: boolean; + isChanged: boolean; +} + +export const useProducts = (): Result => { + const getProductsQueryArg = useAppSelector(selectProductsQueryArg); + + return productApi.useGetProductsQuery(getProductsQueryArg, { + selectFromResult: ({ data, isLoading, isFetching, isSuccess }) => ({ + data: data?.productItems ?? [], + totalCount: data?.totalProductsCount ?? 0, + isLoading, + isFetching, + isChanged: !isFetching && isSuccess, + }), + }); +}; diff --git a/src/frontend/src/entities/product/model/index.ts b/src/frontend/src/entities/product/model/index.ts index 01f2823e3..e92a4bb79 100644 --- a/src/frontend/src/entities/product/model/index.ts +++ b/src/frontend/src/entities/product/model/index.ts @@ -1,3 +1,4 @@ export * from './constants'; export * from './types'; export * from './validation'; +export * from './productSlice'; diff --git a/src/frontend/src/features/products/store/actions.test.ts b/src/frontend/src/entities/product/model/productSlice.actions.test.ts similarity index 97% rename from src/frontend/src/features/products/store/actions.test.ts rename to src/frontend/src/entities/product/model/productSlice.actions.test.ts index 96b706c82..b3ff73b87 100644 --- a/src/frontend/src/features/products/store/actions.test.ts +++ b/src/frontend/src/entities/product/model/productSlice.actions.test.ts @@ -1,7 +1,6 @@ import { configureAppStore } from '@/app/store'; import { type SelectOption } from '@/shared/types'; -import { actions } from './slice'; -import { type ProductItemsFilter } from './types'; +import { type ProductItemsFilter, actions } from './productSlice'; const { productChecked, productsChecked, productsUnchecked, productUnchecked } = actions; diff --git a/src/frontend/src/features/products/store/slice.ts b/src/frontend/src/entities/product/model/productSlice.ts similarity index 87% rename from src/frontend/src/features/products/store/slice.ts rename to src/frontend/src/entities/product/model/productSlice.ts index 167b4b8a1..a841ffc1d 100644 --- a/src/frontend/src/features/products/store/slice.ts +++ b/src/frontend/src/entities/product/model/productSlice.ts @@ -1,7 +1,13 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { productApi } from '@/entities/product'; -import { type SelectOption } from '@/shared/types'; -import { type ProductItemsFilter } from './types'; +import { type ItemsFilterBase, type SelectOption } from '@/shared/types'; + +export interface ProductItemsFilter extends ItemsFilterBase { + pageNumber: number; + pageSize: number; + productSearchName?: string; + category: SelectOption | null; +} export interface ProductsState { checkedProductIds: number[]; @@ -18,7 +24,7 @@ const initialState: ProductsState = { }, }; -const productsSlice = createSlice({ +const productSlice = createSlice({ name: 'products', initialState, reducers: { @@ -72,6 +78,4 @@ const productsSlice = createSlice({ }), }); -export const actions = productsSlice.actions; - -export default productsSlice.reducer; +export const { actions, reducer } = productSlice; diff --git a/src/frontend/src/features/product/addEdit/index.ts b/src/frontend/src/features/product/addEdit/index.ts new file mode 100644 index 000000000..5ecdd1f34 --- /dev/null +++ b/src/frontend/src/features/product/addEdit/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/frontend/src/features/product/addEdit/lib/index.ts b/src/frontend/src/features/product/addEdit/lib/index.ts new file mode 100644 index 000000000..69e42e1be --- /dev/null +++ b/src/frontend/src/features/product/addEdit/lib/index.ts @@ -0,0 +1 @@ +export * from './mapping'; diff --git a/src/frontend/src/features/product/addEdit/lib/mapping.ts b/src/frontend/src/features/product/addEdit/lib/mapping.ts new file mode 100644 index 000000000..6ce8d2e05 --- /dev/null +++ b/src/frontend/src/features/product/addEdit/lib/mapping.ts @@ -0,0 +1,27 @@ +import { + type EditProductRequest, + type CreateProductRequest, + type productModel, +} from '@/entities/product'; + +export const mapToCreateProductRequest = ( + categoryId: number, + { name, caloriesCost, defaultQuantity }: productModel.FormValues, +): CreateProductRequest => ({ + name, + caloriesCost, + defaultQuantity, + categoryId, +}); + +export const mapToEditProductRequest = ( + id: number, + categoryId: number, + { name, caloriesCost, defaultQuantity }: productModel.FormValues, +): EditProductRequest => ({ + id, + name, + caloriesCost, + defaultQuantity, + categoryId, +}); diff --git a/src/frontend/src/features/products/components/CreateProduct.tsx b/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx similarity index 74% rename from src/frontend/src/features/products/components/CreateProduct.tsx rename to src/frontend/src/features/product/addEdit/ui/AddProduct.tsx index 8077d5623..00bb2f91f 100644 --- a/src/frontend/src/features/products/components/CreateProduct.tsx +++ b/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx @@ -1,20 +1,15 @@ import AddIcon from '@mui/icons-material/Add'; import { IconButton, Tooltip } from '@mui/material'; import { type FC, useEffect, useState } from 'react'; -import { useAppSelector } from '@/app/store'; import { categoryLib } from '@/entities/category'; import { productApi, productLib, type productModel } from '@/entities/product'; -import { toCreateProductRequest } from '../mapping'; -import { useProducts } from '../model'; -import { selectProductsQueryArg } from '../selectors'; +import { mapToCreateProductRequest } from '../lib'; import { ProductInputDialog } from './ProductInputDialog'; -const CreateProduct: FC = () => { +export const AddProduct: FC = () => { const [isDialogOpened, setIsDialogOpened] = useState(false); - const getProductsQueryArg = useAppSelector(selectProductsQueryArg); - const getProductsQuery = productApi.useGetProductsQuery(getProductsQueryArg); const categorySelect = categoryLib.useCategorySelectData(); - const products = useProducts(); + const products = productLib.useProducts(); const [createProduct, createProductRequest] = productApi.useCreateProductMutation(); const { values: product } = productLib.useFormValues(); @@ -30,7 +25,7 @@ const CreateProduct: FC = () => { const handleDialogSubmit = (formData: productModel.FormValues): void => { if (formData.category) { - const request = toCreateProductRequest(formData.category.id, formData); + const request = mapToCreateProductRequest(formData.category.id, formData); void createProduct(request); } }; @@ -46,7 +41,7 @@ const CreateProduct: FC = () => { @@ -67,5 +62,3 @@ const CreateProduct: FC = () => { ); }; - -export default CreateProduct; diff --git a/src/frontend/src/features/product/addEdit/ui/EditProduct.tsx b/src/frontend/src/features/product/addEdit/ui/EditProduct.tsx new file mode 100644 index 000000000..98a2429ab --- /dev/null +++ b/src/frontend/src/features/product/addEdit/ui/EditProduct.tsx @@ -0,0 +1,56 @@ +import { type ReactElement, type FC, useState, useEffect, useMemo } from 'react'; +import { categoryLib } from '@/entities/category'; +import { type Product, productApi, productLib, type productModel } from '@/entities/product'; +import { mapToEditProductRequest } from '../lib'; +import { ProductInputDialog } from './ProductInputDialog'; + +interface Props { + product: Product; + renderTrigger: (openDialog: () => void) => ReactElement; +} + +export const EditProduct: FC = ({ product, renderTrigger }) => { + const [isEditDialogOpened, setIsEditDialogOpened] = useState(false); + const [editProduct, editProductRequest] = productApi.useEditProductMutation(); + const products = productLib.useProducts(); + const productFormData = useMemo(() => productLib.mapToProductFormData(product), [product]); + const categorySelect = categoryLib.useCategorySelectData(); + + useEffect(() => { + if (editProductRequest.isSuccess && products.isChanged) { + setIsEditDialogOpened(false); + } + }, [editProductRequest.isSuccess, products.isChanged]); + + const handleEditClick = (): void => { + setIsEditDialogOpened(true); + }; + + const handleEditDialogSubmit = (formData: productModel.FormValues): void => { + if (formData.category) { + const request = mapToEditProductRequest(product.id, formData.category.id, formData); + void editProduct(request); + } + }; + + const handleEditDialogClose = (): void => { + setIsEditDialogOpened(false); + }; + + return ( + <> + {renderTrigger(handleEditClick)} + + + ); +}; diff --git a/src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.fixture.tsx b/src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.fixture.tsx similarity index 100% rename from src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.fixture.tsx rename to src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.fixture.tsx diff --git a/src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.test.tsx b/src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.test.tsx similarity index 100% rename from src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.test.tsx rename to src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.test.tsx diff --git a/src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.tsx b/src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.tsx similarity index 100% rename from src/frontend/src/features/products/components/ProductInputDialog/ProductInputDialog.tsx rename to src/frontend/src/features/product/addEdit/ui/ProductInputDialog/ProductInputDialog.tsx diff --git a/src/frontend/src/features/products/components/ProductInputDialog/index.ts b/src/frontend/src/features/product/addEdit/ui/ProductInputDialog/index.ts similarity index 100% rename from src/frontend/src/features/products/components/ProductInputDialog/index.ts rename to src/frontend/src/features/product/addEdit/ui/ProductInputDialog/index.ts diff --git a/src/frontend/src/features/product/addEdit/ui/index.ts b/src/frontend/src/features/product/addEdit/ui/index.ts new file mode 100644 index 000000000..61d98ecbb --- /dev/null +++ b/src/frontend/src/features/product/addEdit/ui/index.ts @@ -0,0 +1,2 @@ +export * from './AddProduct'; +export * from './EditProduct'; diff --git a/src/frontend/src/features/products/components/DeleteProductsDialog.tsx b/src/frontend/src/features/products/components/DeleteProductsDialog.tsx index 7c046965f..cfb60b44a 100644 --- a/src/frontend/src/features/products/components/DeleteProductsDialog.tsx +++ b/src/frontend/src/features/products/components/DeleteProductsDialog.tsx @@ -6,11 +6,8 @@ import { type SetStateAction, type FormEventHandler, } from 'react'; -import { useAppSelector } from '@/app/store'; -import { productApi } from '@/entities/product'; +import { productApi, productLib } from '@/entities/product'; import { Button, AppDialog } from '@/shared/ui'; -import { useProducts } from '../model'; -import { selectCheckedProductIds } from '../selectors'; interface DeleteProductsDialogProps { isOpened: boolean; @@ -21,9 +18,9 @@ const DeleteProductsDialog: FC = ({ isOpened: isDialogOpened, setIsOpened: setIsDialogOpened, }) => { - const checkedProductIds = useAppSelector(selectCheckedProductIds); + const checkedProductIds = productLib.useCheckedProductIds(); + const products = productLib.useProducts(); const [deleteProducts, deleteProductRequest] = productApi.useDeleteProductsMutation(); - const products = useProducts(); useEffect(() => { if (deleteProductRequest.isSuccess && products.isChanged) { diff --git a/src/frontend/src/features/products/components/ProductsTable/ProductsTable.tsx b/src/frontend/src/features/products/components/ProductsTable/ProductsTable.tsx index a100b059a..c57efe5cc 100644 --- a/src/frontend/src/features/products/components/ProductsTable/ProductsTable.tsx +++ b/src/frontend/src/features/products/components/ProductsTable/ProductsTable.tsx @@ -9,7 +9,7 @@ import { Typography, } from '@mui/material'; import { type FC, type ReactElement } from 'react'; -import { type Product } from '../../types'; +import { type Product } from '@/entities/product'; import ProductsTableRow from '../ProductsTableRow'; interface ProductsTableProps { diff --git a/src/frontend/src/features/products/components/ProductsTablePagination.tsx b/src/frontend/src/features/products/components/ProductsTablePagination.tsx index f1cb77f8d..fcd311387 100644 --- a/src/frontend/src/features/products/components/ProductsTablePagination.tsx +++ b/src/frontend/src/features/products/components/ProductsTablePagination.tsx @@ -2,33 +2,29 @@ import { TablePagination } from '@mui/material'; import { type FC } from 'react'; import { useDispatch } from 'react-redux'; import { useAppSelector } from '@/app/store'; -import { productApi } from '@/entities/product'; -import { selectProductsQueryArg } from '../selectors'; -import { pageNumberChanged, pageSizeChanged } from '../store'; +import { productLib, productModel } from '@/entities/product'; const ProductsTablePagination: FC = () => { - const getProductsQueryArg = useAppSelector(selectProductsQueryArg); - const getProductsQuery = productApi.useGetProductsQuery(getProductsQueryArg); - const totalProductsCount = getProductsQuery.data?.totalProductsCount ?? 0; + const products = productLib.useProducts(); const { pageNumber, pageSize } = useAppSelector(state => state.products.filter); const dispatch = useDispatch(); function handleChangePage( - event: React.MouseEvent | null, + _: React.MouseEvent | null, pageIndex: number, ): void { - dispatch(pageNumberChanged(pageIndex + 1)); + dispatch(productModel.actions.pageNumberChanged(pageIndex + 1)); } function handleChangeRowsPerPage(event: React.ChangeEvent): void { const newPageSize = Number(event.target.value); - dispatch(pageSizeChanged(newPageSize)); + dispatch(productModel.actions.pageSizeChanged(newPageSize)); } return ( = ({ product }: ProductsTableRowProps) => { - const [isEditDialogOpened, setIsEditDialogOpened] = useState(false); - const [editProduct, editProductRequest] = productApi.useEditProductMutation(); - const categorySelect = categoryLib.useCategorySelectData(); const dispatch = useAppDispatch(); - const checkedProductIds = useAppSelector(selectCheckedProductIds); + const checkedProductIds = productLib.useCheckedProductIds(); const isChecked = checkedProductIds.some(id => id === product.id); - const products = useProducts(); - const productFormData = useMemo(() => toProductFormData(product), [product]); - - useEffect(() => { - if (editProductRequest.isSuccess && products.isChanged) { - setIsEditDialogOpened(false); - } - }, [editProductRequest.isSuccess, products.isChanged]); - - const handleEditClick = (): void => { - setIsEditDialogOpened(true); - }; - - const handleEditDialogSubmit = (formData: productModel.FormValues): void => { - if (formData.category) { - const request = toEditProductRequest(product.id, formData.category.id, formData); - void editProduct(request); - } - }; - - const handleEditDialogClose = (): void => { - setIsEditDialogOpened(false); - }; const handleCheckedChange = (): void => { if (isChecked) { - dispatch(productUnchecked(product.id)); + dispatch(productModel.actions.productUnchecked(product.id)); } else { - dispatch(productChecked(product.id)); + dispatch(productModel.actions.productChecked(product.id)); } }; return ( - <> - - - - - {product.name} - - {product.caloriesCost} - - - {product.defaultQuantity} - - - {product.categoryName} - - - - - - - - - - - - - + + + + + {product.name} + + {product.caloriesCost} + + + {product.defaultQuantity} + + + {product.categoryName} + + + ( + + + + + + + + )} + /> + + ); }; diff --git a/src/frontend/src/features/products/components/ProductsTableToolbar.tsx b/src/frontend/src/features/products/components/ProductsTableToolbar.tsx index 00d7a8d4a..baf3effa6 100644 --- a/src/frontend/src/features/products/components/ProductsTableToolbar.tsx +++ b/src/frontend/src/features/products/components/ProductsTableToolbar.tsx @@ -1,15 +1,14 @@ import DeleteIcon from '@mui/icons-material/Delete'; import { Box, IconButton, Stack, Tooltip, Typography } from '@mui/material'; import { useState, type FC } from 'react'; -import { useAppSelector } from '@/app/store'; -import { selectCheckedProductIds } from '../selectors'; -import CreateProduct from './CreateProduct'; +import { productLib } from '@/entities/product'; +import { AddProduct } from '@/features/product/addEdit'; import DeleteProductsDialog from './DeleteProductsDialog'; import { SearchByCategory } from './SearchByCategory'; import { SearchByName } from './SearchByName'; const ProductsTableToolbar: FC = () => { - const checkedProductIds = useAppSelector(selectCheckedProductIds); + const checkedProductIds = productLib.useCheckedProductIds(); const [isDeleteDialogOpened, setIsDeleteDialogOpened] = useState(false); const isSelectionActive = checkedProductIds.length > 0; @@ -54,7 +53,7 @@ const ProductsTableToolbar: FC = () => { > - + )} diff --git a/src/frontend/src/features/products/components/SearchByCategory.tsx b/src/frontend/src/features/products/components/SearchByCategory.tsx index 9594a85a1..df615c2ee 100644 --- a/src/frontend/src/features/products/components/SearchByCategory.tsx +++ b/src/frontend/src/features/products/components/SearchByCategory.tsx @@ -2,8 +2,8 @@ import { Divider, MenuItem, TextField, Typography } from '@mui/material'; import { type FC, type ChangeEventHandler } from 'react'; import { useAppDispatch, useAppSelector } from '@/app/store'; import { categoryLib } from '@/entities/category'; +import { productModel } from '@/entities/product'; import { type SelectOption } from '@/shared/types'; -import { filterByCategoryChanged } from '../store'; import * as styles from '../styles'; const ANY_CATEGORY_VALUE = ' '; @@ -22,7 +22,7 @@ export const SearchByCategory: FC = () => { const handleChange: ChangeEventHandler = event => { const selectedCategory = findSelectedCategory(event.target.value); - dispatch(filterByCategoryChanged(selectedCategory)); + dispatch(productModel.actions.filterByCategoryChanged(selectedCategory)); }; return ( diff --git a/src/frontend/src/features/products/components/SearchByName.tsx b/src/frontend/src/features/products/components/SearchByName.tsx index 379bd931f..e2c712ecb 100644 --- a/src/frontend/src/features/products/components/SearchByName.tsx +++ b/src/frontend/src/features/products/components/SearchByName.tsx @@ -10,7 +10,7 @@ import { } from 'react'; import { useDebounce } from 'use-debounce'; import { useAppDispatch, useAppSelector } from '@/app/store'; -import { productSearchNameChanged } from '../store'; +import { productModel } from '@/entities/product'; import * as styles from '../styles'; const DEBOUNCE_QUERY_DELAY = 500; @@ -30,12 +30,12 @@ export const SearchByName: FC = () => { } if (query === '') { - dispatch(productSearchNameChanged(query)); + dispatch(productModel.actions.productSearchNameChanged(query)); return; } if (debouncedQuery.length >= DEBOUNCE_QUERY_LENGTH_THRESHOLD) { - dispatch(productSearchNameChanged(debouncedQuery)); + dispatch(productModel.actions.productSearchNameChanged(debouncedQuery)); } }, [queryTouched, query, debouncedQuery, dispatch]); diff --git a/src/frontend/src/features/products/index.ts b/src/frontend/src/features/products/index.ts index d202fd8f3..a3820983e 100644 --- a/src/frontend/src/features/products/index.ts +++ b/src/frontend/src/features/products/index.ts @@ -1,4 +1 @@ export * from './routes'; -export * from './types'; -export * from './mapping'; -export * from './model'; diff --git a/src/frontend/src/features/products/mapping.ts b/src/frontend/src/features/products/mapping.ts deleted file mode 100644 index b0e069561..000000000 --- a/src/frontend/src/features/products/mapping.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - type GetProductsRequest, - type CreateProductRequest, - type EditProductRequest, - type productModel, -} from '@/entities/product'; -import { type ProductItemsFilter } from './store'; -import { type Product } from './types'; - -export const toProductFormData = ({ - name, - caloriesCost, - defaultQuantity, - categoryId, - categoryName, -}: Product): productModel.FormValues => ({ - name, - caloriesCost, - defaultQuantity, - category: { - id: categoryId, - name: categoryName, - }, -}); - -export const toGetProductsRequest = ({ - pageNumber, - pageSize, - productSearchName, - category, -}: ProductItemsFilter): GetProductsRequest => ({ - pageNumber, - pageSize, - productSearchName, - categoryId: category?.id, -}); - -export const toCreateProductRequest = ( - categoryId: number, - { name, caloriesCost, defaultQuantity }: productModel.FormValues, -): CreateProductRequest => ({ - name, - caloriesCost, - defaultQuantity, - categoryId, -}); - -export const toEditProductRequest = ( - id: number, - categoryId: number, - { name, caloriesCost, defaultQuantity }: productModel.FormValues, -): EditProductRequest => ({ - id, - name, - caloriesCost, - defaultQuantity, - categoryId, -}); diff --git a/src/frontend/src/features/products/model/index.ts b/src/frontend/src/features/products/model/index.ts deleted file mode 100644 index a53137e03..000000000 --- a/src/frontend/src/features/products/model/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useProducts'; diff --git a/src/frontend/src/features/products/model/useProducts.ts b/src/frontend/src/features/products/model/useProducts.ts deleted file mode 100644 index ce4c1cd21..000000000 --- a/src/frontend/src/features/products/model/useProducts.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useAppSelector } from '@/app/store'; -import { productApi } from '@/entities/product'; -import { selectProductsQueryArg } from '../selectors'; -import { type Product } from '../types'; - -interface Result { - data: Product[]; - isFetching: boolean; - isChanged: boolean; -} - -export const useProducts = (): Result => { - const getProductsQueryArg = useAppSelector(selectProductsQueryArg); - - return productApi.useGetProductsQuery(getProductsQueryArg, { - selectFromResult: ({ data, isFetching, isSuccess }) => ({ - data: data?.productItems ?? [], - isFetching, - isChanged: !isFetching && isSuccess, - }), - }); -}; diff --git a/src/frontend/src/features/products/routes/Products.tsx b/src/frontend/src/features/products/routes/Products.tsx index e321bedd7..c334e8e73 100644 --- a/src/frontend/src/features/products/routes/Products.tsx +++ b/src/frontend/src/features/products/routes/Products.tsx @@ -2,34 +2,31 @@ import { Paper, Typography } from '@mui/material'; import { visuallyHidden } from '@mui/utils'; import { useEffect, type FC } from 'react'; import { useAppDispatch, useAppSelector } from '@/app/store'; +import { type Product, productLib, productModel } from '@/entities/product'; import { LoadingContainer } from '@/shared/ui'; import ProductsTable from '../components/ProductsTable'; import ProductsTablePagination from '../components/ProductsTablePagination'; import ProductsTableToolbar from '../components/ProductsTableToolbar'; -import { useProducts } from '../model'; -import { selectCheckedProductIds } from '../selectors'; -import { productsUnchecked, productsChecked, filterReset } from '../store'; -import { type Product } from '../types'; const Products: FC = () => { - const products = useProducts(); - const checkedProductIds = useAppSelector(selectCheckedProductIds); + const products = productLib.useProducts(); + const checkedProductIds = productLib.useCheckedProductIds(); const filterChanged = useAppSelector(state => state.products.filter.changed); const dispatch = useAppDispatch(); useEffect(() => { return () => { if (filterChanged) { - dispatch(filterReset()); + dispatch(productModel.actions.filterReset()); } }; }, [dispatch, filterChanged]); const handleCheckedProductsChange = (products: Product[], newCheckedIds: number[]): void => { if (newCheckedIds.length > 0) { - dispatch(productsUnchecked(products.map(p => p.id))); + dispatch(productModel.actions.productsUnchecked(products.map(p => p.id))); } else { - dispatch(productsChecked(products.map(p => p.id))); + dispatch(productModel.actions.productsChecked(products.map(p => p.id))); } }; diff --git a/src/frontend/src/features/products/selectors/index.ts b/src/frontend/src/features/products/selectors/index.ts deleted file mode 100644 index 13dfbe7b7..000000000 --- a/src/frontend/src/features/products/selectors/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createDraftSafeSelector, createSelector } from '@reduxjs/toolkit'; -import { type RootState } from '@/app/store'; -import { toGetProductsRequest } from '../mapping'; -import { type ProductsState } from '../store/slice'; - -const selectProducts = (state: RootState): ProductsState => state.products; - -const selectProductsFilter = createDraftSafeSelector(selectProducts, products => products.filter); - -export const selectProductsQueryArg = createSelector(selectProductsFilter, toGetProductsRequest); - -export const selectCheckedProductIds = createSelector( - selectProducts, - state => state.checkedProductIds, -); diff --git a/src/frontend/src/features/products/store/index.ts b/src/frontend/src/features/products/store/index.ts deleted file mode 100644 index b4718906c..000000000 --- a/src/frontend/src/features/products/store/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import reducer, { actions } from './slice'; - -export const { - productChecked, - productUnchecked, - productsChecked, - productsUnchecked, - pageNumberChanged, - pageSizeChanged, - productSearchNameChanged, - filterByCategoryChanged, - filterReset, -} = actions; - -export * from './types'; - -export default reducer; diff --git a/src/frontend/src/features/products/store/types.ts b/src/frontend/src/features/products/store/types.ts deleted file mode 100644 index 3ce1191e1..000000000 --- a/src/frontend/src/features/products/store/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ItemsFilterBase, type SelectOption } from '@/shared/types'; - -export interface ProductItemsFilter extends ItemsFilterBase { - pageNumber: number; - pageSize: number; - productSearchName?: string; - category: SelectOption | null; -} diff --git a/src/frontend/src/features/products/types/index.ts b/src/frontend/src/features/products/types/index.ts deleted file mode 100644 index a521e53b0..000000000 --- a/src/frontend/src/features/products/types/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface Product { - id: number; - name: string; - caloriesCost: number; - defaultQuantity: number; - categoryId: number; - categoryName: string; -} - -export interface ProductsResponse { - productItems: Product[]; - totalProductsCount: number; -} diff --git a/src/frontend/src/pages/ui/ProductsPage.tsx b/src/frontend/src/pages/ui/ProductsPage.tsx index 6697e7e6b..53d042cc0 100644 --- a/src/frontend/src/pages/ui/ProductsPage.tsx +++ b/src/frontend/src/pages/ui/ProductsPage.tsx @@ -1,12 +1,12 @@ import { type FC } from 'react'; import { store } from '@/app/store'; -import { productApi } from '@/entities/product'; -import { Products, toGetProductsRequest } from '@/features/products'; +import { productApi, productLib } from '@/entities/product'; +import { Products } from '@/features/products'; import { PrivateLayout } from '@/widgets/layout'; import { ok, withAuthStatusCheck } from '../lib'; export const loader = withAuthStatusCheck(async () => { - const getProductsRequest = toGetProductsRequest(store.getState().products.filter); + const getProductsRequest = productLib.mapToGetProductsRequest(store.getState().products.filter); await store.dispatch(productApi.endpoints.getProducts.initiate(getProductsRequest)); return ok(); }); diff --git a/src/frontend/tests/mockApi/products/products.handlers.ts b/src/frontend/tests/mockApi/products/products.handlers.ts index 5877941ef..fdfb1eb15 100644 --- a/src/frontend/tests/mockApi/products/products.handlers.ts +++ b/src/frontend/tests/mockApi/products/products.handlers.ts @@ -4,10 +4,10 @@ import { type CreateProductRequest, type CreateProductResponse, type EditProductRequest, + type GetProductsResponse, } from '@/entities/product'; import { API_URL } from '@/shared/config'; import { type SelectOption } from '@/shared/types'; -import { type ProductsResponse } from 'src/features/products'; import { DelayedHttpResponse } from '../DelayedHttpResponse'; import * as productsService from './products.service'; @@ -29,7 +29,7 @@ export const handlers: HttpHandler[] = [ const totalProductsCount = productsService.count(); const categoryNamesMap = productsService.getCategoryNames(products); - const response: ProductsResponse = { + const response: GetProductsResponse = { productItems: products.map(({ id, name, caloriesCost, defaultQuantity, categoryId }) => ({ id, name, diff --git a/src/frontend/tests/render/TestEnvironment.tsx b/src/frontend/tests/render/TestEnvironment.tsx index 2d7d67a55..95e046d41 100644 --- a/src/frontend/tests/render/TestEnvironment.tsx +++ b/src/frontend/tests/render/TestEnvironment.tsx @@ -1,7 +1,7 @@ import { type PropsWithChildren, type FC, useEffect } from 'react'; import { useSubmit } from 'react-router-dom'; import { useAppDispatch } from '@/app/store'; -import { pageSizeChanged } from 'src/features/products/store'; +import { productModel } from '@/entities/product'; interface TestEnvironmentProps { signOutAfterMilliseconds?: number; @@ -32,7 +32,7 @@ const TestEnvironment: FC> = ({ useEffect(() => { if (pageSizeOverride != null) { - dispatch(pageSizeChanged(pageSizeOverride)); + dispatch(productModel.actions.pageSizeChanged(pageSizeOverride)); } }, [dispatch, pageSizeOverride]); From d98d1c15af1fe172007b8f6c176cca21fe885f8a Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Sat, 11 May 2024 22:23:28 +0300 Subject: [PATCH 2/2] Changed add product dialog title and submit text --- src/frontend/src/features/product/addEdit/ui/AddProduct.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx b/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx index 00bb2f91f..ab11b4aeb 100644 --- a/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx +++ b/src/frontend/src/features/product/addEdit/ui/AddProduct.tsx @@ -42,7 +42,6 @@ export const AddProduct: FC = () => { size="large" onClick={handleCreate} disabled={products.isLoading || createProductRequest.isLoading} - aria-label="Open create product dialog" > @@ -50,8 +49,8 @@ export const AddProduct: FC = () => {