From 15ffd318b9e3150dcad443c3cdb4ce1d3c8d8c06 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 2 May 2023 21:45:56 -0700 Subject: [PATCH] fixes some bugs with multiple filtering & keywords. Switched away from the search endpoint and added keyword searching to the list parts endpoint --- .../src/components/PartsGrid2Memoized.js | 34 ++++---- .../ClientApp/src/pages/LowInventory.js | 33 ++++--- .../Binner.Web/ClientApp/src/pages/Search.js | 85 +++++++++---------- .../EntityFrameworkStorageProvider.cs | 18 ++++ .../Binner.Common/Integrations/DigikeyApi.cs | 2 + .../Library/Binner.Model/PaginatedRequest.cs | 5 ++ 6 files changed, 107 insertions(+), 70 deletions(-) diff --git a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js index 7bb99202..c319282f 100644 --- a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js +++ b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js @@ -32,10 +32,6 @@ export default function PartsGrid2Memoized(props) { const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); - var byParam = searchParams.get("by"); - var valueParam = searchParams.get("value"); - const by = byParam?.split(',') || []; - const byValue = valueParam?.split(',') || []; const getViewPreference = (preferenceName) => { return getLocalData(preferenceName, { settingsName: 'partsGridViewPreferences', location }) @@ -218,8 +214,8 @@ export default function PartsGrid2Memoized(props) { }; const indexOfBy = (filterBy) => { - for(let i = 0; i < by.length; i++) { - if (by[i] === filterBy) { + for(let i = 0; i < props.by.length; i++) { + if (props.by[i] === filterBy) { return i; } } @@ -227,16 +223,16 @@ export default function PartsGrid2Memoized(props) { }; const createFilterBy = (filterByToAdd) => { - const newFilterBy = [...by]; + const newFilterBy = [...props.by]; - if (!by.includes(filterByToAdd)) + if (!newFilterBy.includes(filterByToAdd)) newFilterBy.push(filterByToAdd); return newFilterBy.join(','); }; const createFilterByValue = (filterByValueToAdd) => { - const newFilterByValue = [...byValue]; - if (!by.includes(filterByValueToAdd)) + const newFilterByValue = [...props.byValue]; + if (!newFilterByValue.includes(filterByValueToAdd)) newFilterByValue.push(filterByValueToAdd); return newFilterByValue.join(','); }; @@ -246,7 +242,7 @@ export default function PartsGrid2Memoized(props) { e.preventDefault(); e.stopPropagation(); if (part[propertyName]) { - const url = `${props.visitUrl}?by=${createFilterBy(propertyName)}&value=${createFilterByValue(part[propertyName])}`; + const url = `${props.visitUrl}?by=${createFilterBy(propertyName)}&value=${createFilterByValue(part[propertyName])}&keyword=${props.keyword}`; navigate(url); } }; @@ -281,7 +277,7 @@ export default function PartsGrid2Memoized(props) {
handleSelfLink(e, row.original, columnName)}>
{getIconForPart(row.original)}
- handleSelfLink(e, row.original, columnName)}> + handleSelfLink(e, row.original, columnName)}> {row.original.partType}
@@ -294,9 +290,9 @@ export default function PartsGrid2Memoized(props) { return {...def, Cell: ({row}) => (
handleSelfLink(e, row.original, columnName)}> - {by.includes(columnName) && byValue[indexOfBy(columnName, row.original[columnName])]?.toString() === row.original[columnName]?.toString() + {props.by.includes(columnName) && props.byValue[indexOfBy(columnName, row.original[columnName])]?.toString() === row.original[columnName]?.toString() ? {row.original[columnName]} - : handleSelfLink(e, row.original, columnName)}> + : handleSelfLink(e, row.original, columnName)}> {row.original[columnName]} } @@ -482,7 +478,10 @@ PartsGrid2Memoized.propTypes = { /** The link to use when clicking on items to filter by */ visitUrl: PropTypes.string, /** Provides a function to get the default state */ - onInit: PropTypes.func + onInit: PropTypes.func, + keyword: PropTypes.string, + by: PropTypes.array, + byValue: PropTypes.array }; PartsGrid2Memoized.defaultProps = { @@ -494,5 +493,8 @@ PartsGrid2Memoized.defaultProps = { totalRecords: 0, editable: true, visitable: true, - visitUrl: '/inventory' + visitUrl: '/inventory', + keyword: '', + by: [], + byValue: [] }; diff --git a/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js b/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js index 4b92ae74..8c24198d 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js +++ b/Binner/Binner.Web/ClientApp/src/pages/LowInventory.js @@ -42,10 +42,12 @@ export function LowInventory (props) { const totalPages = Math.ceil(data.totalItems / results); setTotalRecords(data.totalItems); let newData = []; - if (reset) - newData = [...pageOfData]; - else - newData = [...parts, ...pageOfData]; + if (pageOfData) { + if (reset) + newData = [...pageOfData]; + else + newData = [...parts, ...pageOfData]; + } setParts(newData); setPage(page); setTotalPages(totalPages); @@ -74,6 +76,10 @@ export function LowInventory (props) { }; }, [byParam, valueParam, initComplete]); + useEffect(() => { + loadParts(1, true, filterBy, filterByValue, pageSize, sortBy, sortDirection); + }, [filterBy, filterByValue]); + const handleNextPage = async (e, page) => { await loadParts(page, true); }; @@ -94,11 +100,16 @@ export function LowInventory (props) { } setFilterBy(newFilterBy); setFilterByValue(newFilterByValue); - // go - if (newFilterBy.length > 0) - props.history(`/lowstock?by=${newFilterBy.join(',')}&value=${newFilterByValue.join(',')}`); - else - props.history('/lowstock'); + + // replace the browser url + let newBrowserUrl = '/lowstock'; + if (newFilterBy.length > 0 || newFilterByValue.length > 0) { + newBrowserUrl += '?'; + if (newFilterBy.length > 0) + newBrowserUrl += `by=${newFilterBy.join(',')}&value=${newFilterByValue.join(',')}`; + } + window.history.pushState(null, null, newBrowserUrl); + setRenderIsDirty(!renderIsDirty); }; const handlePageSizeChange = async (e, pageSize) => { @@ -128,10 +139,12 @@ export function LowInventory (props) { onPageSizeChange={handlePageSizeChange} onSortChange={handleSortChange} onInit={handleInit} + by={filterBy} + byValue={filterByValue} name='partsGrid' visitUrl="/lowstock" >{t('message.noMatchingResults', "No matching results.")}); - }, [renderIsDirty, parts, page, totalPages, totalRecords, loading]); + }, [renderIsDirty, parts, page, totalPages, totalRecords, loading, filterBy, filterByValue]); return (
diff --git a/Binner/Binner.Web/ClientApp/src/pages/Search.js b/Binner/Binner.Web/ClientApp/src/pages/Search.js index b1f2a221..82fef36a 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Search.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Search.js @@ -10,7 +10,6 @@ import PartsGrid2Memoized from "../components/PartsGrid2Memoized"; import { fetchApi } from "../common/fetchApi"; import { FormHeader } from "../components/FormHeader"; import { BarcodeScannerInput } from "../components/BarcodeScannerInput"; -import { CatchingPokemonSharp } from "@mui/icons-material"; export function Search(props) { const DebounceTimeMs = 400; @@ -90,16 +89,20 @@ export function Search(props) { } }; - const loadParts = async (page, reset = false, by = filterBy, byValue = filterByValue, results = pageSize, orderBy = sortBy, orderDirection = sortDirection) => { + const loadParts = async (page, reset = false, by = filterBy, byValue = filterByValue, results = pageSize, orderBy = sortBy, orderDirection = sortDirection, keyword = null) => { const response = await fetchApi( - `api/part/list?orderBy=${orderBy || ""}&direction=${orderDirection || ""}&results=${results}&page=${page}&by=${by?.join(',')}&value=${byValue?.join(',')}` + `api/part/list?orderBy=${orderBy || ""}&direction=${orderDirection || ""}&results=${results}&page=${page}&keyword=${keyword || ""}&by=${by?.join(',')}&value=${byValue?.join(',')}` ); const { data } = response; const pageOfData = data.items; const totalPages = Math.ceil(data.totalItems / results); let newData = []; - if (reset) newData = [...pageOfData]; - else newData = [...parts, ...pageOfData]; + if (pageOfData) { + if (reset) + newData = [...pageOfData]; + else + newData = [...parts, ...pageOfData]; + } setParts(newData); setPage(page); setTotalPages(totalPages); @@ -109,7 +112,7 @@ export function Search(props) { return response; }; - const search = useCallback(async (keyword) => { + const search = useCallback(async (keyword, by = filterBy, byValue = filterByValue) => { Search.abortController.abort(); // Cancel the previous request Search.abortController = new AbortController(); // if there's a keyword we should clear binning (because they use different endpoints) @@ -151,52 +154,33 @@ export function Search(props) { }, [renderIsDirty]); const searchDebounced = useMemo(() => debounce(search, DebounceTimeMs), []); + const loadPartsDebounced = useMemo(() => debounce(loadParts, DebounceTimeMs), []); useEffect(() => { if (pageSize === -1) return; - if (keywordParam !== keyword) { - if (keywordParam && keywordParam.length > 0) { - // if there's a keyword we should clear binning (because they use different endpoints) - setKeyword(keywordParam); - setFilterBy([]); - setFilterByValue([]); - setPage(1); - search(keywordParam); - } else if (by && by.length > 0) { - // likewise, clear keyword if we're in a bin search - setFilterBy(by); - setFilterByValue(byValue); - setKeyword(""); - setPage(1); - loadParts(page, true, by, byValue, pageSize); - } else { - setPage(1); - loadParts(page, true, [], [], pageSize); - } - } else { - if (keyword && keyword.length > 0) search(keyword); - else loadParts(page); - } - + setPage(1); + setKeyword(keywordParam || ""); + setFilterBy(byParam?.split(',') || []); + setFilterByValue(valueParam?.split(',') || []); + loadParts(1, true, by, byValue, pageSize, sortBy, sortDirection, keywordParam || ""); return () => { Search.abortController.abort(); }; }, [byParam, valueParam, keywordParam, initComplete]); + useEffect(() => { + loadPartsDebounced(1, true, filterBy, filterByValue, pageSize, sortBy, sortDirection, keyword || ""); + }, [filterBy, filterByValue, keyword]); + const handlePartClick = (e, part) => { props.history(`/inventory/${encodeURIComponent(part.partNumber)}`); }; const handleNextPage = async (e, page) => { - await loadParts(page, true); + await loadParts(page, true, filterBy, filterByValue, pageSize, sortBy, sortDirection, keyword || ""); }; const handleSearch = (e, control) => { - if (control.value && control.value.length > 0) { - searchDebounced(control.value); - } else { - loadParts(page, true); - } setKeyword(control.value); }; @@ -213,15 +197,25 @@ export function Search(props) { setFilterBy(newFilterBy); setFilterByValue(newFilterByValue); // go - if (newFilterBy.length > 0) - props.history(`/inventory?by=${newFilterBy.join(',')}&value=${newFilterByValue.join(',')}`); - else - props.history('/inventory'); + + // replace the browser url + let newBrowserUrl = '/inventory'; + if (newFilterBy.length > 0 || newFilterByValue.length > 0 || keyword.length > 0) { + newBrowserUrl += '?'; + if (newFilterBy.length > 0) + newBrowserUrl += `by=${newFilterBy.join(',')}&value=${newFilterByValue.join(',')}`; + if (newFilterBy.length > 0 && keyword.length > 0) + newBrowserUrl += `&`; + if (keyword.length > 0) + newBrowserUrl += `keyword=${keyword}`; + } + window.history.pushState(null, null, newBrowserUrl); + setRenderIsDirty(!renderIsDirty); }; const handlePageSizeChange = async (e, pageSize) => { setPageSize(pageSize); - await loadParts(page, true, filterBy, filterByValue, pageSize); + await loadParts(page, true, filterBy, filterByValue, pageSize, sortBy, sortDirection, keyword || ""); }; const handleSortChange = async (sortBy, sortDirection) => { @@ -229,7 +223,7 @@ export function Search(props) { const newSortDirection = sortDirection || "Descending"; setSortBy(newSortBy); setSortDirection(newSortDirection); - return await loadParts(page, true, filterBy, filterByValue, pageSize, newSortBy, newSortDirection); + return await loadParts(page, true, filterBy, filterByValue, pageSize, newSortBy, newSortDirection, keyword || ""); }; const renderPartsTable = useMemo(() => { @@ -244,9 +238,12 @@ export function Search(props) { onPageSizeChange={handlePageSizeChange} onSortChange={handleSortChange} onInit={handleInit} + by={filterBy} + byValue={filterByValue} + keyword={keyword} name="partsGrid" >{t('message.noMatchingResults', "No matching results.")}); - }, [renderIsDirty, parts, page, totalPages, totalRecords, loading]); + }, [renderIsDirty, parts, page, totalPages, totalRecords, loading, filterBy, filterByValue]); return ( @@ -264,7 +261,7 @@ export function Search(props) { focus placeholder={t('page.search.search', "Search")} icon="search" - value={keyword} + value={keyword || ""} onChange={handleSearch} id="keyword" name="keyword" diff --git a/Binner/Data/Binner.StorageProvider.EntityFrameworkCore/EntityFrameworkStorageProvider.cs b/Binner/Data/Binner.StorageProvider.EntityFrameworkCore/EntityFrameworkStorageProvider.cs index c3fcfd3b..536aa4c9 100644 --- a/Binner/Data/Binner.StorageProvider.EntityFrameworkCore/EntityFrameworkStorageProvider.cs +++ b/Binner/Data/Binner.StorageProvider.EntityFrameworkCore/EntityFrameworkStorageProvider.cs @@ -1031,6 +1031,24 @@ public async Task GetOrCreatePartTypeAsync(PartType partType, IUserCon } } + // finally, do any keyword filtering + if (!string.IsNullOrEmpty(request.Keyword)) + { + entitiesQueryable = entitiesQueryable.Where(x => + EF.Functions.Like(x.PartNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.ManufacturerPartNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.Description, '%' + request.Keyword + '%') + || EF.Functions.Like(x.Manufacturer, '%' + request.Keyword + '%') + || EF.Functions.Like(x.Keywords, '%' + request.Keyword + '%') + || EF.Functions.Like(x.DigiKeyPartNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.MouserPartNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.ArrowPartNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.Location, '%' + request.Keyword + '%') + || EF.Functions.Like(x.BinNumber, '%' + request.Keyword + '%') + || EF.Functions.Like(x.BinNumber2, '%' + request.Keyword + '%') + ); + } + if (additionalPredicate != null) entitiesQueryable = entitiesQueryable.Where(additionalPredicate); diff --git a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs index 31bda7bd..a229d968 100644 --- a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs +++ b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs @@ -720,6 +720,8 @@ private string GetPower(string power) private string GetResistance(string resistance) { var val = new String(resistance.Where(x => Char.IsDigit(x) || Char.IsPunctuation(x)).ToArray()); + if (string.IsNullOrEmpty(val)) + val = "0"; var unitsParsed = resistance.Replace(val, "").ToLower(); var units = "ohms"; switch (unitsParsed) diff --git a/Binner/Library/Binner.Model/PaginatedRequest.cs b/Binner/Library/Binner.Model/PaginatedRequest.cs index 30861834..78ede2fb 100644 --- a/Binner/Library/Binner.Model/PaginatedRequest.cs +++ b/Binner/Library/Binner.Model/PaginatedRequest.cs @@ -38,5 +38,10 @@ public class PaginatedRequest : ISortable, IPaginated /// Property value to filter by /// public string? Value { get; set; } + + /// + /// Keyword to filter by + /// + public string? Keyword { get; set; } } }