88 * SPDX-License-Identifier: EPL-2.0
99 ********************************************************************************/
1010
11- import { FunctionComponent , useContext , useEffect , useRef , useState } from 'react' ;
11+ import { FunctionComponent , useContext , useEffect , useMemo } from 'react' ;
1212import InfiniteScroll from 'react-infinite-scroller' ;
1313import { Box , Grid , CircularProgress , Container } from '@mui/material' ;
1414import { ExtensionListItem } from './extension-list-item' ;
15- import { isError , SearchEntry , SearchResult } from '../../extension-registry-types' ;
1615import { ExtensionFilter } from '../../extension-registry-service' ;
17- import { debounce } from '../../utils' ;
1816import { DelayedLoadIndicator } from '../../components/delayed-load-indicator' ;
1917import { MainContext } from '../../context' ;
18+ import { useSearch } from '../../hooks/extension-list/use-search' ;
2019
2120export const ExtensionList : FunctionComponent < ExtensionListProps > = props => {
22- const abortController = useRef < AbortController > ( new AbortController ( ) ) ;
23- const cancellationToken = useRef < { timeout ?: number } > ( { } ) ;
24- const enableLoadMore = useRef ( false ) ;
25- const lastRequestedPage = useRef ( 0 ) ;
26- const pageOffset = useRef ( 0 ) ;
27- const filterSize = useRef ( props . filter . size ?? 10 ) ;
28- const context = useContext ( MainContext ) ;
29- const [ extensions , setExtensions ] = useState < SearchEntry [ ] > ( [ ] ) ;
30- const [ extensionKeys , setExtensionKeys ] = useState < Set < string > > ( new Set < string > ( ) ) ;
31- const [ appliedFilter , setAppliedFilter ] = useState < ExtensionFilter > ( ) ;
32- const [ hasMore , setHasMore ] = useState < boolean > ( false ) ;
33- const [ loading , setLoading ] = useState < boolean > ( true ) ;
21+ const { handleError } = useContext ( MainContext ) ;
3422
35- useEffect ( ( ) => {
36- enableLoadMore . current = true ;
37- return ( ) => {
38- abortController . current . abort ( ) ;
39- clearTimeout ( cancellationToken . current . timeout ) ;
40- enableLoadMore . current = false ;
41- } ;
42- } , [ ] ) ;
43-
44- useEffect ( ( ) => {
45- filterSize . current = props . filter . size ?? filterSize . current ;
46- debounce (
47- async ( ) => {
48- try {
49- const result = await context . service . search ( abortController . current , props . filter ) ;
50- if ( isError ( result ) ) {
51- throw result ;
52- }
53-
54- const searchResult = result as SearchResult ;
55- props . onUpdate ( searchResult . totalSize ) ;
56- const actualSize = searchResult . extensions . length ;
57- pageOffset . current = lastRequestedPage . current ;
58- const extensionKeys = new Set < string > ( ) ;
59- for ( const ext of searchResult . extensions ) {
60- extensionKeys . add ( `${ ext . namespace } .${ ext . name } ` ) ;
61- }
23+ const { data, isFetching, fetchNextPage, hasNextPage, error } = useSearch ( props . filter ) ;
6224
63- setExtensions ( searchResult . extensions ) ;
64- setExtensionKeys ( extensionKeys ) ;
65- setAppliedFilter ( props . filter ) ;
66- setHasMore ( actualSize < searchResult . totalSize && actualSize > 0 ) ;
67- } catch ( err ) {
68- context . handleError ( err ) ;
69- } finally {
70- setLoading ( false ) ;
71- }
72- } ,
73- cancellationToken . current ,
74- props . debounceTime
75- ) ;
76- } , [ props . filter . category , props . filter . query , props . filter . sortBy , props . filter . sortOrder , props . debounceTime ] ) ;
25+ const extensions = useMemo ( ( ) => data ?. pages . flatMap ( p => p . extensions ) ?? [ ] , [ data ] ) ;
26+ const totalSize = useMemo ( ( ) => data ?. pages [ 0 ] ?. totalSize ?? 0 , [ data ] ) ;
7727
78- const loadMore = async ( p : number ) : Promise < void > => {
79- setLoading ( true ) ;
80- setHasMore ( false ) ;
81- lastRequestedPage . current = p ;
82- const filter = copyFilter ( appliedFilter as ExtensionFilter ) ;
83- if ( ! isSameFilter ( props . filter , filter ) ) {
84- return ;
85- }
86- try {
87- filter . offset = ( p - pageOffset . current ) * filterSize . current ;
88- const result = await context . service . search ( abortController . current , filter ) ;
89- if ( isError ( result ) ) {
90- throw result ;
91- }
92-
93- const newExtensions : SearchEntry [ ] = [ ] ;
94- const newExtensionKeys = new Set < string > ( ) ;
95- newExtensions . push ( ...extensions ) ;
96- extensionKeys . forEach ( ( key ) => newExtensionKeys . add ( key ) ) ;
97- const searchResult = result as SearchResult ;
98- if ( enableLoadMore . current && isSameFilter ( props . filter , filter ) ) {
99- // Check for duplicate keys to avoid problems due to asynchronous user edit / loadMore call
100- for ( const ext of searchResult . extensions ) {
101- const key = `${ ext . namespace } .${ ext . name } ` ;
102- if ( ! extensionKeys . has ( key ) ) {
103- newExtensions . push ( ext ) ;
104- newExtensionKeys . add ( key ) ;
105- }
106- }
107-
108- setExtensions ( newExtensions ) ;
109- setExtensionKeys ( newExtensionKeys ) ;
110- setHasMore ( extensions . length < searchResult . totalSize && searchResult . extensions . length > 0 ) ;
111- }
112- } catch ( err ) {
113- context . handleError ( err ) ;
114- } finally {
115- setLoading ( false ) ;
116- }
117- } ;
28+ useEffect ( ( ) => {
29+ props . onUpdate ( totalSize ) ;
30+ } , [ totalSize ] ) ;
11831
119- const isSameFilter = ( f1 : ExtensionFilter , f2 : ExtensionFilter ) : boolean => {
120- return f1 . category === f2 . category && f1 . query === f2 . query && f1 . sortBy === f2 . sortBy && f1 . sortOrder === f2 . sortOrder ;
121- } ;
32+ useEffect ( ( ) => {
33+ if ( error ) handleError ( error ) ;
34+ } , [ error ] ) ;
12235
123- const copyFilter = ( f : ExtensionFilter ) : ExtensionFilter => {
124- return {
125- query : f . query ,
126- category : f . category || '' ,
127- size : f . size ,
128- offset : f . offset ,
129- sortBy : f . sortBy ,
130- sortOrder : f . sortOrder
131- } ;
132- } ;
36+ const loadMore = ( ) : Promise < void > => fetchNextPage ( ) . then ( ( ) => undefined ) ;
13337
13438 const extensionList = extensions . map ( ( ext , idx ) => (
13539 < ExtensionListItem
13640 idx = { idx }
13741 extension = { ext }
138- filterSize = { filterSize . current }
42+ filterSize = { props . filter . size ?? 10 }
13943 key = { `${ ext . namespace } .${ ext . name } ` } />
14044 ) ) ;
14145
@@ -144,10 +48,10 @@ export const ExtensionList: FunctionComponent<ExtensionListProps> = props => {
14448 </ Box > ;
14549
14650 return < >
147- < DelayedLoadIndicator loading = { loading } />
51+ < DelayedLoadIndicator loading = { isFetching } />
14852 < InfiniteScroll
14953 loadMore = { loadMore }
150- hasMore = { hasMore }
54+ hasMore = { ! ! hasNextPage }
15155 loader = { loader }
15256 threshold = { 200 } >
15357 < Container maxWidth = 'xl' >
@@ -163,4 +67,4 @@ export interface ExtensionListProps {
16367 filter : ExtensionFilter ;
16468 debounceTime : number ;
16569 onUpdate : ( resultNumber : number ) => void ;
166- }
70+ }
0 commit comments