Skip to content

Commit cda5657

Browse files
fixed assets loading on scroll
1 parent c11dcb5 commit cda5657

File tree

1 file changed

+116
-106
lines changed

1 file changed

+116
-106
lines changed

client/packages/lowcoder/src/comps/controls/iconscoutControl.tsx

Lines changed: 116 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
1616
import styled from "styled-components";
1717
import Popover from "antd/es/popover";
1818
import { 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

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy