@@ -12,7 +12,7 @@ import {
12
12
useIcon ,
13
13
wrapperToControlItem ,
14
14
} from "lowcoder-design" ;
15
- import { memo , ReactNode , useCallback , useMemo , useRef , useState } from "react" ;
15
+ import { ReactNode , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
16
16
import styled from "styled-components" ;
17
17
import Popover from "antd/es/popover" ;
18
18
import { CloseIcon , SearchIcon } from "icons" ;
@@ -225,62 +225,85 @@ export const IconPicker = (props: {
225
225
IconType ?: "OnlyAntd" | "All" | "default" | undefined ;
226
226
} ) => {
227
227
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 > > ( [ ] ) ;
235
233
const [ page , setPage ] = useState ( 1 ) ;
236
234
const [ hasMore , setHasMore ] = useState ( true ) ;
235
+ const abortControllerRef = useRef < AbortController | null > ( null ) ;
236
+ const { subscriptions } = useSimpleSubscriptionContext ( ) ;
237
237
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 ]
241
243
) ;
242
244
243
245
const onChangeRef = useRef ( props . onChange ) ;
244
246
onChangeRef . current = props . onChange ;
245
247
248
+ // Cleanup function for async operations
249
+ useEffect ( ( ) => {
250
+ return ( ) => {
251
+ if ( abortControllerRef . current ) {
252
+ abortControllerRef . current . abort ( ) ;
253
+ }
254
+ } ;
255
+ } , [ ] ) ;
256
+
246
257
const onChangeIcon = useCallback (
247
258
( key : string , value : string , url : string ) => {
248
259
onChangeRef . current ( key , value , url ) ;
249
260
setVisible ( false ) ;
250
- } , [ ]
261
+ } ,
262
+ [ ]
251
263
) ;
252
264
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
+
254
271
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
+ ] ) ;
255
289
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 ] ) ;
282
305
283
- const downloadAsset = async (
306
+ const downloadAsset = useCallback ( async (
284
307
uuid : string ,
285
308
downloadUrl : string ,
286
309
callback : ( assetUrl : string ) => void ,
@@ -293,29 +316,29 @@ export const IconPicker = (props: {
293
316
} ) ;
294
317
}
295
318
} catch ( error ) {
296
- console . error ( error ) ;
319
+ console . error ( 'Error downloading asset:' , error ) ;
297
320
setDownloading ( false ) ;
298
321
}
299
- }
322
+ } , [ ] ) ;
300
323
301
- const fetchDownloadUrl = async ( uuid : string , preview : string ) => {
324
+ const fetchDownloadUrl = useCallback ( async ( uuid : string , preview : string ) => {
302
325
try {
303
326
setDownloading ( true ) ;
304
327
const result = await getAssetLinks ( uuid , {
305
328
format : props . assetType === AssetType . LOTTIE ? 'lottie' : 'svg' ,
306
329
} ) ;
307
330
308
- downloadAsset ( uuid , result . download_url , ( assetUrl : string ) => {
331
+ await downloadAsset ( uuid , result . download_url , ( assetUrl : string ) => {
309
332
setDownloading ( false ) ;
310
333
onChangeIcon ( uuid , assetUrl , preview ) ;
311
334
} ) ;
312
335
} catch ( error ) {
313
- console . error ( error ) ;
336
+ console . error ( 'Error fetching download URL:' , error ) ;
314
337
setDownloading ( false ) ;
315
338
}
316
- }
339
+ } , [ props . assetType , downloadAsset , onChangeIcon ] ) ;
317
340
318
- const handleChange = ( e : { target : { value : any ; } ; } ) => {
341
+ const handleChange = useCallback ( ( e : { target : { value : any ; } ; } ) => {
319
342
const query = e . target . value ;
320
343
setSearchText ( query ) ; // Update search text immediately
321
344
@@ -324,9 +347,15 @@ export const IconPicker = (props: {
324
347
} else {
325
348
setSearchResults ( [ ] ) ; // Clear results if input is too short
326
349
}
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
+ ) ;
330
359
331
360
const rowRenderer = useCallback (
332
361
( { index, key, style } : ListRowProps ) => {
@@ -408,39 +437,41 @@ export const IconPicker = (props: {
408
437
</ IconRow >
409
438
) ;
410
439
} ,
411
- [ columnNum , mediaPackSubscription , props . assetType , fetchDownloadUrl ]
440
+ [ columnNum , mediaPackSubscription , props . assetType , fetchDownloadUrl , searchResults ]
412
441
) ;
413
-
414
442
415
443
const popupTitle = useMemo ( ( ) => {
416
444
if ( props . assetType === AssetType . ILLUSTRATION ) return trans ( "iconScout.searchImage" ) ;
417
445
if ( props . assetType === AssetType . LOTTIE ) return trans ( "iconScout.searchAnimation" ) ;
418
446
return trans ( "iconScout.searchIcon" ) ;
419
447
} , [ props . assetType ] ) ;
420
448
421
- const MemoizedIconList = memo ( ( {
422
- searchResults,
423
- rowRenderer,
424
- onScroll,
425
- columnNum,
449
+ const handleScroll = useCallback ( ( {
450
+ clientHeight,
451
+ scrollHeight,
452
+ scrollTop,
426
453
} : {
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 ;
431
457
} ) => {
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 ] ) ;
444
475
445
476
return (
446
477
< Popover
@@ -471,11 +502,6 @@ export const IconPicker = (props: {
471
502
/>
472
503
< StyledSearchIcon />
473
504
</ SearchDiv >
474
- { loading && (
475
- < Flex align = "center" justify = "center" style = { { flex : 1 } } >
476
- < Spin indicator = { < LoadingOutlined style = { { fontSize : 25 } } spin /> } />
477
- </ Flex >
478
- ) }
479
505
< Spin spinning = { downloading } indicator = { < LoadingOutlined style = { { fontSize : 25 } } /> } >
480
506
{ ! loading && Boolean ( searchText ) && ! Boolean ( searchResults ?. length ) && (
481
507
< Flex align = "center" justify = "center" style = { { flex : 1 } } >
@@ -484,33 +510,16 @@ export const IconPicker = (props: {
484
510
</ Typography . Text >
485
511
</ Flex >
486
512
) }
487
- { ! loading && Boolean ( searchText ) && Boolean ( searchResults ?. length ) && (
513
+ { Boolean ( searchText ) && Boolean ( searchResults ?. length ) && (
488
514
< 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 }
512
516
</ IconListWrapper >
513
517
) }
518
+ { loading && (
519
+ < Flex align = "center" justify = "center" style = { { flex : 1 } } >
520
+ < Spin indicator = { < LoadingOutlined style = { { fontSize : 25 } } spin /> } />
521
+ </ Flex >
522
+ ) }
514
523
</ Spin >
515
524
</ PopupContainer >
516
525
</ Draggable >
@@ -557,11 +566,12 @@ export function IconscoutControl(
557
566
) {
558
567
return class IconscoutControl extends SimpleComp < IconScoutAsset > {
559
568
readonly IGNORABLE_DEFAULT_VALUE = false ;
569
+
560
570
protected getDefaultValue ( ) : IconScoutAsset {
561
571
return {
562
- uuid : '' ,
563
- value : '' ,
564
- preview : '' ,
572
+ uuid : "" ,
573
+ value : "" ,
574
+ preview : "" ,
565
575
} ;
566
576
}
567
577
@@ -586,5 +596,5 @@ export function IconscoutControl(
586
596
</ ControlPropertyViewWrapper >
587
597
) ;
588
598
}
589
- }
599
+ } ;
590
600
}
0 commit comments