Skip to content

Commit

Permalink
Implemented new products search by name
Browse files Browse the repository at this point in the history
  • Loading branch information
pkirilin committed Dec 30, 2023
1 parent f4063bc commit 1e79a9b
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 116 deletions.
24 changes: 24 additions & 0 deletions src/frontend/.pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
3 changes: 2 additions & 1 deletion src/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"react-ga4": "^2.1.0",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"redux": "^4.2.0"
"redux": "^4.2.0",
"use-debounce": "^10.0.0"
},
"devDependencies": {
"@mswjs/data": "^0.16.1",
Expand Down
2 changes: 0 additions & 2 deletions src/frontend/src/features/__shared__/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ import usePopover from './usePopover';
import useRouterId from './useRouterId';

export { useAppSelector, useAppDispatch, useRouterId, usePopover };

export * from './inputHooks';
12 changes: 0 additions & 12 deletions src/frontend/src/features/__shared__/hooks/inputHooks.ts

This file was deleted.

33 changes: 4 additions & 29 deletions src/frontend/src/features/products/components/ProductsFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,24 @@
import { Box, Button, Paper, TextField } from '@mui/material';
import { type FC, type FocusEvent, useEffect, useState } from 'react';
import { Box, Button, Paper } from '@mui/material';
import { type FC, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { CategorySelect, categoriesApi } from 'src/features/categories';
import { type SelectOption } from 'src/types';
import { useAppSelector, useValidatedTextInput } from '../../__shared__/hooks';
import { useAppSelector } from '../../__shared__/hooks';
import { useFilterStyles } from '../../__shared__/styles';
import { filterByCategoryChanged, filterReset, productSearchNameChanged } from '../store';
import { filterByCategoryChanged, filterReset } from '../store';

const ProductsFilter: FC = () => {
const classes = useFilterStyles();

const filterProductName = useAppSelector(state => state.products.filter.productSearchName ?? '');
const filterCategory = useAppSelector(state => state.products.filter.category);
const filterChanged = useAppSelector(state => state.products.filter.changed);
const [getCategories, categoriesRequest] = categoriesApi.useLazyGetCategorySelectOptionsQuery();

const dispatch = useDispatch();

const [, setProductSearchName, bindProductSearchName] = useValidatedTextInput(filterProductName, {
validate: productName => productName.length >= 0 && productName.length <= 50,
errorHelperText: 'Product search name is invalid',
});

const [category, setCategory] = useState(filterCategory);

useEffect(() => {
setProductSearchName(filterProductName);
}, [filterProductName, setProductSearchName]);

useEffect(() => {
setCategory(filterCategory);
}, [filterCategory]);

const handleProductSearchNameBlur = (event: FocusEvent<HTMLInputElement>): void => {
dispatch(productSearchNameChanged(event.target.value));
};

const handleCategoryChange = (value: SelectOption | null): void => {
setCategory(value);
dispatch(filterByCategoryChanged(value));
Expand All @@ -51,14 +34,6 @@ const ProductsFilter: FC = () => {

return (
<Box component={Paper} className={classes.root}>
<TextField
{...bindProductSearchName()}
label="Search by name"
placeholder="Enter product name"
fullWidth
margin="normal"
onBlur={handleProductSearchNameBlur}
/>
<CategorySelect
label="Filter by category"
placeholder="Select a category"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
import CategoryIcon from '@mui/icons-material/Category';
import SearchIcon from '@mui/icons-material/Search';
import { Box, Chip, Tooltip } from '@mui/material';
import { Chip, Tooltip } from '@mui/material';
import { type FC } from 'react';
import { useAppDispatch, useAppSelector } from 'src/store';
import { useFilterAppliedParamsStyles } from '../../__shared__/styles';
import { filterByCategoryChanged, productSearchNameChanged } from '../store';
import { filterByCategoryChanged } from '../store';

const ProductsFilterAppliedParams: FC = () => {
const classes = useFilterAppliedParamsStyles();
const productSearchName = useAppSelector(state => state.products.filter.productSearchName);
const category = useAppSelector(state => state.products.filter.category);
const dispatch = useAppDispatch();

if (!productSearchName && !category) {
return null;
}

const handleProductSearchNameClear = (): void => {
dispatch(productSearchNameChanged(''));
};

const handleCategoryClear = (): void => {
dispatch(filterByCategoryChanged(null));
};

if (!category) {
return null;
}

return (
<Box className={classes.root}>
{productSearchName && (
<Tooltip title="Applied filter: product search name">
<Chip
variant="outlined"
icon={<SearchIcon />}
label={productSearchName}
onDelete={handleProductSearchNameClear}
/>
</Tooltip>
)}
{category && (
<Tooltip title="Applied filter: category">
<Chip
variant="outlined"
icon={<CategoryIcon />}
label={category.name}
onDelete={handleCategoryClear}
/>
</Tooltip>
)}
</Box>
<Tooltip title="Applied filter: category">
<Chip
variant="outlined"
icon={<CategoryIcon />}
label={category.name}
onDelete={handleCategoryClear}
/>
</Tooltip>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import DeleteIcon from '@mui/icons-material/Delete';
import FilterListIcon from '@mui/icons-material/FilterList';
import SearchIcon from '@mui/icons-material/Search';
import {
Box,
IconButton,
InputAdornment,
MenuItem,
Popover,
Stack,
Expand All @@ -18,6 +16,7 @@ import { selectCheckedProductIds } from '../selectors';
import CreateProduct from './CreateProduct';
import DeleteProductsDialog from './DeleteProductsDialog';
import ProductsFilter from './ProductsFilter';
import SearchByName from './SearchByName';

const ProductsTableToolbar: FC = () => {
const checkedProductIds = useAppSelector(selectCheckedProductIds);
Expand Down Expand Up @@ -67,24 +66,7 @@ const ProductsTableToolbar: FC = () => {
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'flex-start', sm: 'center' }}
>
<TextField
size="small"
placeholder="Search by name"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
sx={theme => ({
minWidth: '200px',
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '200px',
},
})}
/>
<SearchByName />
<TextField
size="small"
label="Category"
Expand Down
48 changes: 48 additions & 0 deletions src/frontend/src/features/products/components/SearchByName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import SearchIcon from '@mui/icons-material/Search';
import { TextField, InputAdornment } from '@mui/material';
import { useState, type FC, type ChangeEventHandler, useEffect } from 'react';
import { useDebounce } from 'use-debounce';
import { useAppDispatch } from 'src/store';
import { validateProductName } from 'src/utils/validation';
import { productSearchNameChanged } from '../store';

const SearchByName: FC = () => {
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebounce(query, 500);
const dispatch = useAppDispatch();

useEffect(() => {
if (debouncedQuery === '' || validateProductName(debouncedQuery)) {
dispatch(productSearchNameChanged(debouncedQuery));
}
}, [debouncedQuery, dispatch]);

const handleQueryChange: ChangeEventHandler<HTMLInputElement> = event => {
setQuery(event.target.value);
};

return (
<TextField
size="small"
placeholder="Search by name"
value={query}
onChange={handleQueryChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
sx={theme => ({
minWidth: '200px',
width: '100%',
[theme.breakpoints.up('sm')]: {
width: '200px',
},
})}
/>
);
};

export default SearchByName;
21 changes: 5 additions & 16 deletions src/frontend/src/features/products/routes/Products.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,22 +153,14 @@ test('products can be filtered by category', async () => {
});

test('products can be filtered by name', async () => {
render(<Products />);
const user = userEvent.setup();

await waitForElementToBeRemoved(screen.queryByRole('progressbar'));
await userEvent.click(screen.getByLabelText(/open products filter/i));

const filterPopup = within(screen.getByRole('presentation'));
const productName = filterPopup.getByPlaceholderText(/product name/i);
await userEvent.type(productName, 'bre');
await userEvent.click(document.body);
render(<Products />);
const searchField = await screen.findByPlaceholderText(/search by name/i);
await user.type(searchField, 'bre');

await waitFor(() => {
expect(screen.queryByText(/rice/i)).not.toBeInTheDocument();
});
const filterChip = screen.getByLabelText(/applied filter: product search name/i);
expect(within(filterChip).queryByText(/bre/i)).toBeVisible();
expect(screen.getByText(/bread/i));
expect(screen.queryByText(/rice/i)).not.toBeInTheDocument();
});

test('products filter can be reset', async () => {
Expand All @@ -178,12 +170,9 @@ test('products filter can be reset', async () => {
await userEvent.click(screen.getByLabelText(/open products filter/i));

const filterPopup = within(screen.getByRole('presentation'));
const productName = filterPopup.getByPlaceholderText(/product name/i);
const category = filterPopup.getByPlaceholderText(/category/i);
await userEvent.type(productName, 'sfdsfwfegegrw');
await userEvent.click(category);
await userEvent.click(within(await screen.findByRole('listbox')).getByText(/dairy/i));
await waitFor(() => expect(screen.getByText(/no products found/i)));
await userEvent.click(filterPopup.getByRole('button', { name: /reset/i }));

expect(screen.getByText(/bread/i));
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4400,6 +4400,7 @@ __metadata:
redux-mock-store: "npm:^1.5.4"
redux-thunk: "npm:^2.4.1"
typescript: "npm:5.2.2"
use-debounce: "npm:^10.0.0"
vite: "npm:^5.0.3"
vitest: "npm:^0.34.6"
vitest-preview: "npm:^0.0.1"
Expand Down Expand Up @@ -8017,6 +8018,15 @@ __metadata:
languageName: node
linkType: hard

"use-debounce@npm:^10.0.0":
version: 10.0.0
resolution: "use-debounce@npm:10.0.0"
peerDependencies:
react: ">=16.8.0"
checksum: b0fd28112aa3d7b5333f64e845aa1a4b5223bae7f0800fcb496d4796816bc9490bc3bb050c8c104d32262f7cffa06c6145455cc6054add4fdd8dccf2be52f0c9
languageName: node
linkType: hard

"use-sync-external-store@npm:^1.0.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
Expand Down

0 comments on commit 1e79a9b

Please sign in to comment.