diff --git a/src/frontend/.pnp.cjs b/src/frontend/.pnp.cjs index ad71e55e7..4ec19c3ea 100644 --- a/src/frontend/.pnp.cjs +++ b/src/frontend/.pnp.cjs @@ -79,6 +79,7 @@ const RAW_RUNTIME_STATE = ["redux-mock-store", "npm:1.5.4"],\ ["redux-thunk", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:2.4.2"],\ ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"],\ + ["use-debounce", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:10.0.0"],\ ["vite", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:5.0.3"],\ ["vitest", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:0.34.6"],\ ["vitest-preview", "npm:0.0.1"]\ @@ -5641,6 +5642,7 @@ const RAW_RUNTIME_STATE = ["redux-mock-store", "npm:1.5.4"],\ ["redux-thunk", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:2.4.2"],\ ["typescript", "patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441"],\ + ["use-debounce", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:10.0.0"],\ ["vite", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:5.0.3"],\ ["vitest", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:0.34.6"],\ ["vitest-preview", "npm:0.0.1"]\ @@ -9790,6 +9792,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["use-debounce", [\ + ["npm:10.0.0", {\ + "packageLocation": "./.yarn/cache/use-debounce-npm-10.0.0-04f7df41a1-b0fd28112a.zip/node_modules/use-debounce/",\ + "packageDependencies": [\ + ["use-debounce", "npm:10.0.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:10.0.0", {\ + "packageLocation": "./.yarn/__virtual__/use-debounce-virtual-91239e7c08/0/cache/use-debounce-npm-10.0.0-04f7df41a1-b0fd28112a.zip/node_modules/use-debounce/",\ + "packageDependencies": [\ + ["use-debounce", "virtual:a98f04960cc7eaa90b3b4031434bf51078616b854270c891bf505dc7080da50229384e574995251e6688b61b7cd512851213f0347ece1285c879b26dde9cc6b6#npm:10.0.0"],\ + ["@types/react", "npm:18.2.39"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["use-sync-external-store", [\ ["npm:1.2.0", {\ "packageLocation": "./.yarn/cache/use-sync-external-store-npm-1.2.0-44f75d2564-a676216aff.zip/node_modules/use-sync-external-store/",\ diff --git a/src/frontend/.yarn/cache/use-debounce-npm-10.0.0-04f7df41a1-b0fd28112a.zip b/src/frontend/.yarn/cache/use-debounce-npm-10.0.0-04f7df41a1-b0fd28112a.zip new file mode 100644 index 000000000..d753830b2 Binary files /dev/null and b/src/frontend/.yarn/cache/use-debounce-npm-10.0.0-04f7df41a1-b0fd28112a.zip differ diff --git a/src/frontend/package.json b/src/frontend/package.json index 069b3993f..014cbd983 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -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", diff --git a/src/frontend/src/features/__shared__/hooks/index.ts b/src/frontend/src/features/__shared__/hooks/index.ts index 5ebfc08cd..7ede9ac0d 100644 --- a/src/frontend/src/features/__shared__/hooks/index.ts +++ b/src/frontend/src/features/__shared__/hooks/index.ts @@ -4,5 +4,3 @@ import usePopover from './usePopover'; import useRouterId from './useRouterId'; export { useAppSelector, useAppDispatch, useRouterId, usePopover }; - -export * from './inputHooks'; diff --git a/src/frontend/src/features/__shared__/hooks/inputHooks.ts b/src/frontend/src/features/__shared__/hooks/inputHooks.ts deleted file mode 100644 index 4e02ea523..000000000 --- a/src/frontend/src/features/__shared__/hooks/inputHooks.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type TextFieldProps } from '@mui/material'; -import createInputHook from './createInputHook'; -import createValidatedInputHook from './createValidatedInputHook'; - -export const useTextInput = createInputHook((value, setValue) => ({ - value, - onChange: event => { - setValue(event.target.value); - }, -})); - -export const useValidatedTextInput = createValidatedInputHook(useTextInput); diff --git a/src/frontend/src/features/products/components/ProductsFilter.tsx b/src/frontend/src/features/products/components/ProductsFilter.tsx index 75190a823..aabc96aac 100644 --- a/src/frontend/src/features/products/components/ProductsFilter.tsx +++ b/src/frontend/src/features/products/components/ProductsFilter.tsx @@ -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): void => { - dispatch(productSearchNameChanged(event.target.value)); - }; - const handleCategoryChange = (value: SelectOption | null): void => { setCategory(value); dispatch(filterByCategoryChanged(value)); @@ -51,14 +34,6 @@ const ProductsFilter: FC = () => { return ( - { - 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 ( - - {productSearchName && ( - - } - label={productSearchName} - onDelete={handleProductSearchNameClear} - /> - - )} - {category && ( - - } - label={category.name} - onDelete={handleCategoryClear} - /> - - )} - + + } + label={category.name} + onDelete={handleCategoryClear} + /> + ); }; diff --git a/src/frontend/src/features/products/components/ProductsTableToolbar.tsx b/src/frontend/src/features/products/components/ProductsTableToolbar.tsx index 50a2ce86d..377620484 100644 --- a/src/frontend/src/features/products/components/ProductsTableToolbar.tsx +++ b/src/frontend/src/features/products/components/ProductsTableToolbar.tsx @@ -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, @@ -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); @@ -67,24 +66,7 @@ const ProductsTableToolbar: FC = () => { direction={{ xs: 'column', sm: 'row' }} alignItems={{ xs: 'flex-start', sm: 'center' }} > - - - - ), - }} - sx={theme => ({ - minWidth: '200px', - width: '100%', - [theme.breakpoints.up('sm')]: { - width: '200px', - }, - })} - /> + { + 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 = event => { + setQuery(event.target.value); + }; + + return ( + + + + ), + }} + sx={theme => ({ + minWidth: '200px', + width: '100%', + [theme.breakpoints.up('sm')]: { + width: '200px', + }, + })} + /> + ); +}; + +export default SearchByName; diff --git a/src/frontend/src/features/products/routes/Products.test.tsx b/src/frontend/src/features/products/routes/Products.test.tsx index b424b14c0..fbdd93824 100644 --- a/src/frontend/src/features/products/routes/Products.test.tsx +++ b/src/frontend/src/features/products/routes/Products.test.tsx @@ -153,22 +153,14 @@ test('products can be filtered by category', async () => { }); test('products can be filtered by name', async () => { - render(); + 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(); + 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 () => { @@ -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)); diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 78dd6c8e8..df0f42296 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -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" @@ -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"