@@ -6,6 +6,7 @@ import { useNavigate } from "react-router";
66import { FilteredQueryColumn , SortedColumn , SpoolIconColumn } from "../../components/column" ;
77import { useSpoolmanFilamentNames , useSpoolmanMaterials , useSpoolmanVendors } from "../../components/otherModels" ;
88import { removeUndefined } from "../../utils/filtering" ;
9+ import { getAPIURL } from "../../utils/url" ;
910import { TableState } from "../../utils/saveload" ;
1011import { 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
3830const MIN_TABLE_SCROLL_Y = 180 ;
3931const 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