Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Product addEdit feature folder #102

Merged
merged 2 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/frontend/src/app/store.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/src/entities/product/api/contracts.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/entities/product/api/productApi.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { api } from '@/shared/api';
import { createUrl } from '@/shared/lib';
import { type ProductsResponse } from '../../../features/products/types';
import {
type CreateProductResponse,
type CreateProductRequest,
type DeleteProductsRequest,
type EditProductRequest,
type GetProductsRequest,
type ProductSelectOption,
type GetProductsResponse,
} from './contracts';

export const productApi = api.injectEndpoints({
endpoints: builder => ({
getProducts: builder.query<ProductsResponse, GetProductsRequest>({
getProducts: builder.query<GetProductsResponse, GetProductsRequest>({
query: request => createUrl('/api/v1/products', { ...request }),
providesTags: ['product'],
}),
Expand Down
3 changes: 3 additions & 0 deletions src/frontend/src/entities/product/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './mapping';
export * from './useAutocompleteData';
export * from './useAutocompleteInput';
export * from './useFormValues';
export * from './useProducts';
export * from './useCheckedProductIds';
30 changes: 30 additions & 0 deletions src/frontend/src/entities/product/lib/mapping.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
9 changes: 9 additions & 0 deletions src/frontend/src/entities/product/lib/useCheckedProductIds.ts
Original file line number Diff line number Diff line change
@@ -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);
33 changes: 33 additions & 0 deletions src/frontend/src/entities/product/lib/useProducts.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
});
};
1 change: 1 addition & 0 deletions src/frontend/src/entities/product/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './constants';
export * from './types';
export * from './validation';
export * from './productSlice';
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -18,7 +24,7 @@ const initialState: ProductsState = {
},
};

const productsSlice = createSlice({
const productSlice = createSlice({
name: 'products',
initialState,
reducers: {
Expand Down Expand Up @@ -72,6 +78,4 @@ const productsSlice = createSlice({
}),
});

export const actions = productsSlice.actions;

export default productsSlice.reducer;
export const { actions, reducer } = productSlice;
1 change: 1 addition & 0 deletions src/frontend/src/features/product/addEdit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ui';
1 change: 1 addition & 0 deletions src/frontend/src/features/product/addEdit/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './mapping';
27 changes: 27 additions & 0 deletions src/frontend/src/features/product/addEdit/lib/mapping.ts
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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);
}
};
Expand All @@ -46,17 +41,16 @@ const CreateProduct: FC = () => {
<IconButton
size="large"
onClick={handleCreate}
disabled={getProductsQuery.isLoading || createProductRequest.isLoading}
aria-label="Open create product dialog"
disabled={products.isLoading || createProductRequest.isLoading}
>
<AddIcon />
</IconButton>
</span>
</Tooltip>
<ProductInputDialog
opened={isDialogOpened}
title="Create product"
submitText="Create"
title="New product"
submitText="Add"
isLoading={createProductRequest.isLoading || products.isFetching}
product={product}
categories={categorySelect.data}
Expand All @@ -67,5 +61,3 @@ const CreateProduct: FC = () => {
</>
);
};

export default CreateProduct;
56 changes: 56 additions & 0 deletions src/frontend/src/features/product/addEdit/ui/EditProduct.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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)}
<ProductInputDialog
opened={isEditDialogOpened}
title="Edit product"
submitText="Save"
isLoading={editProductRequest.isLoading || products.isFetching}
product={productFormData}
categories={categorySelect.data}
categoriesLoading={categorySelect.isLoading}
onSubmit={handleEditDialogSubmit}
onClose={handleEditDialogClose}
/>
</>
);
};
2 changes: 2 additions & 0 deletions src/frontend/src/features/product/addEdit/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './AddProduct';
export * from './EditProduct';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,9 +18,9 @@ const DeleteProductsDialog: FC<DeleteProductsDialogProps> = ({
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading