@@ -12,7 +12,7 @@ import {
1212useIcon ,
1313wrapperToControlItem ,
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: {
225225IconType ?:"OnlyAntd" | "All" | "default" | undefined ;
226226} ) => {
227227const 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 > > ( [ ] ) ;
235233const [ page , setPage ] = useState ( 1 ) ;
236234const [ 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
243245const onChangeRef = useRef ( props . onChange ) ;
244246onChangeRef . 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+
246257const onChangeIcon = useCallback (
247258( key :string , value :string , url :string ) => {
248259onChangeRef . current ( key , value , url ) ;
249260setVisible ( 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+
254271setLoading ( 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 (
284307uuid :string ,
285308downloadUrl :string ,
286309callback :( 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 ) ;
297320setDownloading ( false ) ;
298321}
299- }
322+ } , [ ] ) ;
300323
301- const fetchDownloadUrl = async ( uuid :string , preview :string ) => {
324+ const fetchDownloadUrl = useCallback ( async ( uuid :string , preview :string ) => {
302325try {
303326setDownloading ( true ) ;
304327const result = await getAssetLinks ( uuid , {
305328format :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 ) => {
309332setDownloading ( false ) ;
310333onChangeIcon ( uuid , assetUrl , preview ) ;
311334} ) ;
312335} catch ( error ) {
313- console . error ( error ) ;
336+ console . error ( 'Error fetching download URL:' , error ) ;
314337setDownloading ( false ) ;
315338}
316- }
339+ } , [ props . assetType , downloadAsset , onChangeIcon ] ) ;
317340
318- const handleChange = ( e :{ target :{ value :any ; } ; } ) => {
341+ const handleChange = useCallback ( ( e :{ target :{ value :any ; } ; } ) => {
319342const query = e . target . value ;
320343setSearchText ( query ) ; // Update search text immediately
321344
@@ -324,9 +347,15 @@ export const IconPicker = (props: {
324347} else {
325348setSearchResults ( [ ] ) ; // 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
331360const 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
415443const popupTitle = useMemo ( ( ) => {
416444if ( props . assetType === AssetType . ILLUSTRATION ) return trans ( "iconScout.searchImage" ) ;
417445if ( props . assetType === AssetType . LOTTIE ) return trans ( "iconScout.searchAnimation" ) ;
418446return 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
445476return (
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) {
558567return class IconscoutControl extends SimpleComp < IconScoutAsset > {
559568readonly IGNORABLE_DEFAULT_VALUE = false ;
569+
560570protected getDefaultValue ( ) :IconScoutAsset {
561571return {
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}