Skip to content

Commit cff78ed

Browse files
committed
fix: propagate filament search pagination fix from PR846
- Multi-field debounced fetch replaces single-field server search - Remove redundant matchesSearch() and visibleDataSource local filter - Consolidate to localSearchTerm + allSearchResults state - Consistent with PR846 filament selector fix (e2388ac)
1 parent e81b670 commit cff78ed

1 file changed

Lines changed: 86 additions & 43 deletions

File tree

client/src/pages/printing/filamentSelectModal.tsx

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useNavigate } from "react-router";
66
import { FilteredQueryColumn, SortedColumn, SpoolIconColumn } from "../../components/column";
77
import { useSpoolmanFilamentNames, useSpoolmanMaterials, useSpoolmanVendors } from "../../components/otherModels";
88
import { removeUndefined } from "../../utils/filtering";
9+
import { getAPIURL } from "../../utils/url";
910
import { TableState } from "../../utils/saveload";
1011
import { IFilament } from "../filaments/model";
1112

@@ -25,15 +26,6 @@ function collapseFilament(element: IFilament): IFilamentCollapsed {
2526
}
2627

2728
// Keep the quick search local to the currently loaded page instead of changing the server-side query contract.
28-
function matchesSearch(filament: IFilamentCollapsed, searchTerm: string): boolean {
29-
const needle = searchTerm.trim().toLowerCase();
30-
if (needle.length === 0) {
31-
return true;
32-
}
33-
34-
const haystacks = [String(filament.id), filament["vendor.name"] ?? "", filament.name ?? "", filament.material ?? ""];
35-
return haystacks.some((value) => value.toLowerCase().includes(needle));
36-
}
3729

3830
const MIN_TABLE_SCROLL_Y = 180;
3931
const TABLE_BOTTOM_GAP = 16;
@@ -43,20 +35,16 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
4335
const [selectedItems, setSelectedItems] = useState<number[]>([]);
4436
const [messageApi, contextHolder] = message.useMessage();
4537
const navigate = useNavigate();
46-
const [searchValue, setSearchValue] = useState("");
47-
const [serverSearchValue, setServerSearchValue] = useState("");
4838
const [tableScrollY, setTableScrollY] = useState<number>(300);
39+
const [localSearchTerm, setLocalSearchTerm] = useState("");
40+
const [allSearchResults, setAllSearchResults] = useState<IFilamentCollapsed[]>([]);
41+
const [isSearching, setIsSearching] = useState(false);
4942
const rootRef = useRef<HTMLDivElement | null>(null);
5043
const tableContainerRef = useRef<HTMLDivElement | null>(null);
5144

5245
const { tableProps, sorters, filters, setFilters, currentPage, pageSize, setCurrentPage, setPageSize } =
5346
useTable<IFilamentCollapsed>({
5447
resource: "filament",
55-
meta: {
56-
queryParams: {
57-
...(serverSearchValue.trim().length > 0 ? { search: serverSearchValue.trim() } : {}),
58-
},
59-
},
6048
syncWithLocation: false,
6149
pagination: {
6250
mode: "server",
@@ -85,17 +73,26 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
8573
pagination: { currentPage, pageSize },
8674
};
8775

88-
const dataSource: IFilamentCollapsed[] = useMemo(
89-
() => (tableProps.dataSource || []).map((record) => ({ ...record })),
90-
[tableProps.dataSource],
91-
);
92-
// Keep typing responsive by narrowing the current page immediately even while the backend query is in flight.
93-
const visibleDataSource = useMemo(
94-
() => dataSource.filter((filament) => matchesSearch(filament, searchValue)),
95-
[dataSource, searchValue],
96-
);
76+
const dataSource: IFilamentCollapsed[] = useMemo(() => {
77+
if (localSearchTerm.trim()) {
78+
// Paginate search results
79+
const startIndex = (currentPage - 1) * pageSize;
80+
const endIndex = startIndex + pageSize;
81+
return allSearchResults.slice(startIndex, endIndex);
82+
}
83+
return (tableProps.dataSource || []).map((record) => ({ ...record }));
84+
}, [localSearchTerm, allSearchResults, currentPage, pageSize, tableProps.dataSource]);
85+
86+
const currentSearchValue = localSearchTerm;
9787
const selectedSet = useMemo(() => new Set(selectedItems), [selectedItems]);
9888

89+
const paginationTotal = useMemo(() => {
90+
if (localSearchTerm.trim()) {
91+
return allSearchResults.length;
92+
}
93+
return tableProps.pagination ? (tableProps.pagination.total ?? 0) : 0;
94+
}, [localSearchTerm, allSearchResults.length, tableProps.pagination]);
95+
9996
useEffect(() => {
10097
const computeScrollHeight = () => {
10198
if (!tableContainerRef.current) {
@@ -131,7 +128,6 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
131128
};
132129
}, []);
133130

134-
const paginationTotal = tableProps.pagination ? (tableProps.pagination.total ?? 0) : 0;
135131
const handlePageChange = useCallback(
136132
(page: number, nextPageSize?: number) => {
137133
if (typeof nextPageSize === "number" && nextPageSize !== pageSize) {
@@ -145,17 +141,67 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
145141
setPageSize(size);
146142
setCurrentPage(1);
147143
}, []);
148-
const applySearchFilter = useCallback((nextSearch: string) => {
149-
setServerSearchValue(nextSearch.trim());
150-
setCurrentPage(1);
151-
}, []);
144+
// When search term changes, fetch from multiple fields and combine results
145+
useEffect(() => {
146+
if (!localSearchTerm.trim()) {
147+
setAllSearchResults([]);
148+
setFilters(
149+
(filters ?? []).filter(
150+
(f) => !("field" in f) || !["name", "vendor.name", "material", "article_number"].includes(f.field as string),
151+
),
152+
"replace",
153+
);
154+
return;
155+
}
156+
157+
const fetchMultiFieldResults = async () => {
158+
setIsSearching(true);
159+
try {
160+
const searchTerm = localSearchTerm.trim();
161+
const fields = ["name", "vendor.name", "material", "article_number"];
162+
const results = new Map<number, IFilamentCollapsed>();
163+
164+
// Fetch from each field with a reasonable limit per field
165+
// Using 150 per field balances performance with coverage
166+
for (const field of fields) {
167+
try {
168+
const params = new URLSearchParams({
169+
[field]: searchTerm,
170+
limit: "150",
171+
offset: "0",
172+
});
173+
const response = await fetch(`${getAPIURL()}/filament?${params}`);
174+
if (response.ok) {
175+
const data = (await response.json()) as IFilament[];
176+
data.forEach((item) => {
177+
if (!results.has(item.id)) {
178+
results.set(item.id, collapseFilament(item));
179+
}
180+
});
181+
}
182+
} catch (error) {
183+
console.error(`Search failed for field ${field}:`, error);
184+
}
185+
}
186+
187+
setAllSearchResults(Array.from(results.values()));
188+
setCurrentPage(1);
189+
} finally {
190+
setIsSearching(false);
191+
}
192+
};
193+
194+
// Debounce search to avoid excessive API calls while typing
195+
const timer = setTimeout(fetchMultiFieldResults, 300);
196+
return () => clearTimeout(timer);
197+
}, [localSearchTerm, setFilters, setCurrentPage]);
152198

153-
// Bulk toggles only touch the rows currently visible after search and paging.
199+
// Bulk toggles only touch the rows currently visible after paging and server-side filtering.
154200
const selectUnselectFiltered = useCallback(
155201
(select: boolean) => {
156202
setSelectedItems((prevSelected) => {
157203
const nextSelected = new Set(prevSelected);
158-
visibleDataSource.forEach((filament) => {
204+
dataSource.forEach((filament) => {
159205
if (select) {
160206
nextSelected.add(filament.id);
161207
} else {
@@ -165,7 +211,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
165211
return Array.from(nextSelected);
166212
});
167213
},
168-
[visibleDataSource],
214+
[dataSource],
169215
);
170216

171217
const handleSelectItem = useCallback((item: number) => {
@@ -174,10 +220,9 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
174220
);
175221
}, []);
176222

177-
const isAllFilteredSelected =
178-
visibleDataSource.length > 0 && visibleDataSource.every((filament) => selectedSet.has(filament.id));
223+
const isAllFilteredSelected = dataSource.length > 0 && dataSource.every((filament) => selectedSet.has(filament.id));
179224
const isSomeButNotAllFilteredSelected =
180-
visibleDataSource.some((filament) => selectedSet.has(filament.id)) && !isAllFilteredSelected;
225+
dataSource.some((filament) => selectedSet.has(filament.id)) && !isAllFilteredSelected;
181226

182227
const commonProps = {
183228
t,
@@ -227,17 +272,16 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
227272
<Col xs={24} md={12}>
228273
<Input.Search
229274
placeholder={resolvedSearchPlaceholder}
230-
value={searchValue}
275+
value={currentSearchValue}
231276
allowClear
232277
enterButton
278+
loading={isSearching}
233279
onChange={(event) => {
234280
const value = event.target.value;
235-
setSearchValue(value);
236-
applySearchFilter(value);
281+
setLocalSearchTerm(value);
237282
}}
238283
onSearch={(value) => {
239-
setSearchValue(value);
240-
applySearchFilter(value);
284+
setLocalSearchTerm(value);
241285
}}
242286
/>
243287
</Col>
@@ -246,8 +290,7 @@ const FilamentSelectModal = ({ description, onPrint, searchPlaceholder }: Props)
246290
<Col flex="none">
247291
<Button
248292
onClick={() => {
249-
setSearchValue("");
250-
setServerSearchValue("");
293+
setLocalSearchTerm("");
251294
setFilters([], "replace");
252295
setCurrentPage(1);
253296
}}

0 commit comments

Comments
 (0)