@@ -12,7 +12,7 @@ import {
1212 useIcon ,
1313 wrapperToControlItem ,
1414} from "lowcoder-design" ;
15- import { memo , ReactNode , useCallback , useMemo , useRef , useState } from "react" ;
15+ import { ReactNode , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
1616import styled from "styled-components" ;
1717import Popover from "antd/es/popover" ;
1818import { CloseIcon , SearchIcon } from "icons" ;
@@ -225,62 +225,85 @@ export const IconPicker = (props: {
225225 IconType ?: "OnlyAntd" | "All" | "default" | undefined ;
226226} ) => {
227227 const draggableRef = useRef < HTMLDivElement > ( null ) ;
228- const [ visible , setVisible ] = useState ( false )
229- const [ loading , setLoading ] = useState ( false )
230- const [ downloading , setDownloading ] = useState ( false )
231- const [ searchText , setSearchText ] = useState < string > ( '' )
232- const [ searchResults , setSearchResults ] = useState < Array < any > > ( [ ] ) ;
233- const { subscriptions } = useSimpleSubscriptionContext ( ) ;
234-
228+ const [ visible , setVisible ] = useState ( false ) ;
229+ const [ loading , setLoading ] = useState ( false ) ;
230+ const [ downloading , setDownloading ] = useState ( false ) ;
231+ const [ searchText , setSearchText ] = useState < string > ( '' ) ;
232+ const [ searchResults , setSearchResults ] = useState < Array < any > > ( [ ] ) ;
235233 const [ page , setPage ] = useState ( 1 ) ;
236234 const [ hasMore , setHasMore ] = useState ( true ) ;
235+ const abortControllerRef = useRef < AbortController | null > ( null ) ;
236+ const { subscriptions } = useSimpleSubscriptionContext ( ) ;
237237
238-
239- const mediaPackSubscription = subscriptions . find (
240- sub => sub . product === SubscriptionProductsEnum . MEDIAPACKAGE && sub . status === 'active'
238+ const mediaPackSubscription = useMemo ( ( ) =>
239+ subscriptions . find (
240+ sub => sub . product === SubscriptionProductsEnum . MEDIAPACKAGE && sub . status === 'active'
241+ ) ,
242+ [ subscriptions ]
241243 ) ;
242244
243245 const onChangeRef = useRef ( props . onChange ) ;
244246 onChangeRef . current = props . onChange ;
245247
248+ // Cleanup function for async operations
249+ useEffect ( ( ) => {
250+ return ( ) => {
251+ if ( abortControllerRef . current ) {
252+ abortControllerRef . current . abort ( ) ;
253+ }
254+ } ;
255+ } , [ ] ) ;
256+
246257 const onChangeIcon = useCallback (
247258 ( key : string , value : string , url : string ) => {
248259 onChangeRef . current ( key , value , url ) ;
249260 setVisible ( false ) ;
250- } , [ ]
261+ } ,
262+ [ ]
251263 ) ;
252264
253- const fetchResults = async ( query : string , pageNum : number = 1 ) => {
265+ const fetchResults = useCallback ( async ( query : string , pageNum : number = 1 ) => {
266+ if ( abortControllerRef . current ) {
267+ abortControllerRef . current . abort ( ) ;
268+ }
269+ abortControllerRef . current = new AbortController ( ) ;
270+
254271 setLoading ( true ) ;
272+ try {
273+ const [ freeResult , premiumResult ] = await Promise . all ( [
274+ searchAssets ( {
275+ ...IconScoutSearchParams ,
276+ asset : props . assetType ,
277+ price : 'free' ,
278+ query,
279+ page : pageNum ,
280+ } ) ,
281+ searchAssets ( {
282+ ...IconScoutSearchParams ,
283+ asset : props . assetType ,
284+ price : 'premium' ,
285+ query,
286+ page : pageNum ,
287+ } )
288+ ] ) ;
255289
256- const freeResult = await searchAssets ( {
257- ...IconScoutSearchParams ,
258- asset : props . assetType ,
259- price : 'free' ,
260- query,
261- page : pageNum ,
262- } ) ;
263-
264- const premiumResult = await searchAssets ( {
265- ...IconScoutSearchParams ,
266- asset : props . assetType ,
267- price : 'premium' ,
268- query,
269- page : pageNum ,
270- } ) ;
271-
272- const combined = [ ...freeResult . data , ...premiumResult . data ] ;
273- const isLastPage = combined . length < IconScoutSearchParams . per_page * 2 ;
274-
275- setSearchResults ( prev =>
276- pageNum === 1 ? combined : [ ...prev , ...combined ]
277- ) ;
278- setHasMore ( ! isLastPage ) ;
279- setLoading ( false ) ;
280- } ;
281-
290+ const combined = [ ...freeResult . data , ...premiumResult . data ] ;
291+ const isLastPage = combined . length < IconScoutSearchParams . per_page * 2 ;
292+
293+ setSearchResults ( prev =>
294+ pageNum === 1 ? combined : [ ...prev , ...combined ]
295+ ) ;
296+ setHasMore ( ! isLastPage ) ;
297+ } catch ( error : any ) {
298+ if ( error . name !== 'AbortError' ) {
299+ console . error ( 'Error fetching results:' , error ) ;
300+ }
301+ } finally {
302+ setLoading ( false ) ;
303+ }
304+ } , [ props . assetType ] ) ;
282305
283- const downloadAsset = async (
306+ const downloadAsset = useCallback ( async (
284307 uuid : string ,
285308 downloadUrl : string ,
286309 callback : ( assetUrl : string ) => void ,
@@ -293,29 +316,29 @@ export const IconPicker = (props: {
293316 } ) ;
294317 }
295318 } catch ( error ) {
296- console . error ( error ) ;
319+ console . error ( 'Error downloading asset:' , error ) ;
297320 setDownloading ( false ) ;
298321 }
299- }
322+ } , [ ] ) ;
300323
301- const fetchDownloadUrl = async ( uuid : string , preview : string ) => {
324+ const fetchDownloadUrl = useCallback ( async ( uuid : string , preview : string ) => {
302325 try {
303326 setDownloading ( true ) ;
304327 const result = await getAssetLinks ( uuid , {
305328 format : props . assetType === AssetType . LOTTIE ? 'lottie' : 'svg' ,
306329 } ) ;
307330
308- downloadAsset ( uuid , result . download_url , ( assetUrl : string ) => {
331+ await downloadAsset ( uuid , result . download_url , ( assetUrl : string ) => {
309332 setDownloading ( false ) ;
310333 onChangeIcon ( uuid , assetUrl , preview ) ;
311334 } ) ;
312335 } catch ( error ) {
313- console . error ( error ) ;
336+ console . error ( 'Error fetching download URL:' , error ) ;
314337 setDownloading ( false ) ;
315338 }
316- }
339+ } , [ props . assetType , downloadAsset , onChangeIcon ] ) ;
317340
318- const handleChange = ( e : { target : { value : any ; } ; } ) => {
341+ const handleChange = useCallback ( ( e : { target : { value : any ; } ; } ) => {
319342 const query = e . target . value ;
320343 setSearchText ( query ) ; // Update search text immediately
321344
@@ -324,9 +347,15 @@ export const IconPicker = (props: {
324347 } else {
325348 setSearchResults ( [ ] ) ; // Clear results if input is too short
326349 }
327- } ;
328-
329- const debouncedFetchResults = useMemo ( ( ) => debounce ( fetchResults , 700 ) , [ ] ) ;
350+ } , [ ] ) ;
351+
352+ const debouncedFetchResults = useMemo (
353+ ( ) => debounce ( ( query : string ) => {
354+ setPage ( 1 ) ;
355+ fetchResults ( query , 1 ) ;
356+ } , 700 ) ,
357+ [ fetchResults ]
358+ ) ;
330359
331360 const rowRenderer = useCallback (
332361 ( { index, key, style } : ListRowProps ) => {
@@ -408,39 +437,41 @@ export const IconPicker = (props: {
408437 </ IconRow >
409438 ) ;
410439 } ,
411- [ columnNum , mediaPackSubscription , props . assetType , fetchDownloadUrl ]
440+ [ columnNum , mediaPackSubscription , props . assetType , fetchDownloadUrl , searchResults ]
412441 ) ;
413-
414442
415443 const popupTitle = useMemo ( ( ) => {
416444 if ( props . assetType === AssetType . ILLUSTRATION ) return trans ( "iconScout.searchImage" ) ;
417445 if ( props . assetType === AssetType . LOTTIE ) return trans ( "iconScout.searchAnimation" ) ;
418446 return trans ( "iconScout.searchIcon" ) ;
419447 } , [ props . assetType ] ) ;
420448
421- const MemoizedIconList = memo ( ( {
422- searchResults,
423- rowRenderer,
424- onScroll,
425- columnNum,
449+ const handleScroll = useCallback ( ( {
450+ clientHeight,
451+ scrollHeight,
452+ scrollTop,
426453 } : {
427- searchResults : any [ ] ;
428- rowRenderer : ( props : ListRowProps ) => React . ReactNode ;
429- onScroll : ( params : { clientHeight : number ; scrollHeight : number ; scrollTop : number } ) => void ;
430- columnNum : number ;
454+ clientHeight : number ;
455+ scrollHeight : number ;
456+ scrollTop : number ;
431457 } ) => {
432- return (
433- < IconList
434- width = { 550 }
435- height = { 400 }
436- rowHeight = { 140 }
437- rowCount = { Math . ceil ( searchResults . length / columnNum ) }
438- rowRenderer = { rowRenderer }
439- onScroll = { onScroll }
440- />
441- ) ;
442- } ) ;
443-
458+ if ( hasMore && ! loading && scrollHeight - scrollTop <= clientHeight + 10 ) {
459+ const nextPage = page + 1 ;
460+ setPage ( nextPage ) ;
461+ fetchResults ( searchText , nextPage ) ;
462+ }
463+ } , [ hasMore , loading , page , searchText , fetchResults ] ) ;
464+
465+ const memoizedIconListElement = useMemo ( ( ) => (
466+ < IconList
467+ width = { 550 }
468+ height = { 400 }
469+ rowHeight = { 140 }
470+ rowCount = { Math . ceil ( searchResults . length / columnNum ) }
471+ rowRenderer = { rowRenderer }
472+ onScroll = { handleScroll }
473+ />
474+ ) , [ searchResults . length , rowRenderer , handleScroll , columnNum ] ) ;
444475
445476 return (
446477 < Popover
@@ -471,11 +502,6 @@ export const IconPicker = (props: {
471502 />
472503 < StyledSearchIcon />
473504 </ SearchDiv >
474- { loading && (
475- < Flex align = "center" justify = "center" style = { { flex : 1 } } >
476- < Spin indicator = { < LoadingOutlined style = { { fontSize : 25 } } spin /> } />
477- </ Flex >
478- ) }
479505 < Spin spinning = { downloading } indicator = { < LoadingOutlined style = { { fontSize : 25 } } /> } >
480506 { ! loading && Boolean ( searchText ) && ! Boolean ( searchResults ?. length ) && (
481507 < Flex align = "center" justify = "center" style = { { flex : 1 } } >
@@ -484,33 +510,16 @@ export const IconPicker = (props: {
484510 </ Typography . Text >
485511 </ Flex >
486512 ) }
487- { ! loading && Boolean ( searchText ) && Boolean ( searchResults ?. length ) && (
513+ { Boolean ( searchText ) && Boolean ( searchResults ?. length ) && (
488514 < IconListWrapper >
489-
490- < IconList
491- width = { 550 }
492- height = { 400 }
493- rowHeight = { 140 }
494- rowCount = { Math . ceil ( searchResults . length / columnNum ) }
495- rowRenderer = { rowRenderer }
496- onScroll = { ( {
497- clientHeight,
498- scrollHeight,
499- scrollTop,
500- } : {
501- clientHeight : number ;
502- scrollHeight : number ;
503- scrollTop : number ;
504- } ) => {
505- if ( hasMore && ! loading && scrollHeight - scrollTop <= clientHeight + 10 ) {
506- const nextPage = page + 1 ;
507- setPage ( nextPage ) ;
508- fetchResults ( searchText , nextPage ) ;
509- }
510- } }
511- />
515+ { memoizedIconListElement }
512516 </ IconListWrapper >
513517 ) }
518+ { loading && (
519+ < Flex align = "center" justify = "center" style = { { flex : 1 } } >
520+ < Spin indicator = { < LoadingOutlined style = { { fontSize : 25 } } spin /> } />
521+ </ Flex >
522+ ) }
514523 </ Spin >
515524 </ PopupContainer >
516525 </ Draggable >
@@ -557,11 +566,12 @@ export function IconscoutControl(
557566) {
558567 return class IconscoutControl extends SimpleComp < IconScoutAsset > {
559568 readonly IGNORABLE_DEFAULT_VALUE = false ;
569+
560570 protected getDefaultValue ( ) : IconScoutAsset {
561571 return {
562- uuid : '' ,
563- value : '' ,
564- preview : '' ,
572+ uuid : "" ,
573+ value : "" ,
574+ preview : "" ,
565575 } ;
566576 }
567577
@@ -586,5 +596,5 @@ export function IconscoutControl(
586596 </ ControlPropertyViewWrapper >
587597 ) ;
588598 }
589- }
599+ } ;
590600}
0 commit comments