1- import { Flex , Grid , GridItem , Spinner } from '@invoke-ai/ui-library' ;
1+ import { Button , Flex , Grid , GridItem , Spacer , Spinner } from '@invoke-ai/ui-library' ;
2+ import { EMPTY_ARRAY } from 'app/store/constants' ;
23import { useAppSelector } from 'app/store/storeHooks' ;
34import { IAINoContentFallback } from 'common/components/IAIImageFallback' ;
45import {
@@ -7,53 +8,65 @@ import {
78 selectWorkflowOrderDirection ,
89 selectWorkflowSearchTerm ,
910} from 'features/nodes/store/workflowSlice' ;
10- import { useEffect , useMemo , useState } from 'react' ;
11+ import { memo , useCallback , useMemo , useRef } from 'react' ;
1112import { useTranslation } from 'react-i18next' ;
12- import { useListWorkflowsQuery } from 'services/api/endpoints/workflows' ;
13+ import type { useListWorkflowsQuery } from 'services/api/endpoints/workflows' ;
14+ import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows' ;
15+ import type { S } from 'services/api/types' ;
1316import { useDebounce } from 'use-debounce' ;
1417
15- import { WorkflowLibraryPagination } from './WorkflowLibraryPagination' ;
1618import { WorkflowListItem } from './WorkflowListItem' ;
1719
18- const PER_PAGE = 6 ;
20+ const PER_PAGE = 3 ;
1921
20- export const WorkflowList = ( ) => {
21- const searchTerm = useAppSelector ( selectWorkflowSearchTerm ) ;
22- const { t } = useTranslation ( ) ;
23-
24- const [ page , setPage ] = useState ( 0 ) ;
22+ const useInfiniteQueryAry = ( ) => {
2523 const categories = useAppSelector ( selectWorkflowCategories ) ;
2624 const orderBy = useAppSelector ( selectWorkflowOrderBy ) ;
2725 const direction = useAppSelector ( selectWorkflowOrderDirection ) ;
2826 const query = useAppSelector ( selectWorkflowSearchTerm ) ;
2927 const [ debouncedQuery ] = useDebounce ( query , 500 ) ;
3028
31- useEffect ( ( ) => {
32- setPage ( 0 ) ;
33- } , [ categories , query ] ) ;
34-
35- const queryArg = useMemo < Parameters < typeof useListWorkflowsQuery > [ 0 ] > ( ( ) => {
29+ const queryArg = useMemo ( ( ) => {
3630 return {
37- page,
31+ page : 0 ,
3832 per_page : PER_PAGE ,
3933 order_by : orderBy ,
4034 direction,
4135 categories,
4236 query : debouncedQuery ,
43- } ;
44- } , [ direction , orderBy , page , categories , debouncedQuery ] ) ;
37+ } satisfies Parameters < typeof useListWorkflowsQuery > [ 0 ] ;
38+ } , [ orderBy , direction , categories , debouncedQuery ] ) ;
39+
40+ return queryArg ;
41+ } ;
4542
46- const { data, isLoading } = useListWorkflowsQuery ( queryArg ) ;
43+ const queryOptions = {
44+ selectFromResult : ( { data, ...rest } ) => {
45+ return {
46+ items : data ?. pages . map ( ( { items } ) => items ) . flat ( ) ?? EMPTY_ARRAY ,
47+ ...rest ,
48+ } as const ;
49+ } ,
50+ } satisfies Parameters < typeof useListWorkflowsInfiniteInfiniteQuery > [ 1 ] ;
51+
52+ export const WorkflowList = ( ) => {
53+ const searchTerm = useAppSelector ( selectWorkflowSearchTerm ) ;
54+ const { t } = useTranslation ( ) ;
55+ const queryArg = useInfiniteQueryAry ( ) ;
56+ const { items, isFetching, isLoading, fetchNextPage, hasNextPage } = useListWorkflowsInfiniteInfiniteQuery (
57+ queryArg ,
58+ queryOptions
59+ ) ;
4760
4861 if ( isLoading ) {
4962 return (
50- < Flex alignItems = "center" justifyContent = "center" p = { 20 } >
63+ < Flex alignItems = "center" justifyContent = "center" w = "full" h = "full" >
5164 < Spinner />
5265 </ Flex >
5366 ) ;
5467 }
5568
56- if ( ! data ?. items . length ) {
69+ if ( items . length === 0 ) {
5770 return (
5871 < IAINoContentFallback
5972 fontSize = "sm"
@@ -65,15 +78,96 @@ export const WorkflowList = () => {
6578 }
6679
6780 return (
68- < Flex flexDir = "column" gap = { 6 } >
69- < Grid templateColumns = "repeat(2, minmax(200px, 3fr))" templateRows = "1fr 1fr 1fr" gap = { 4 } >
70- { data ?. items . map ( ( workflow ) => (
71- < GridItem key = { workflow . workflow_id } >
72- < WorkflowListItem workflow = { workflow } key = { workflow . workflow_id } />
73- </ GridItem >
74- ) ) }
75- </ Grid >
76- < WorkflowLibraryPagination page = { page } setPage = { setPage } data = { data } />
77- </ Flex >
81+ < WorkflowListContent
82+ items = { items }
83+ hasNextPage = { hasNextPage }
84+ fetchNextPage = { fetchNextPage }
85+ isFetching = { isFetching }
86+ />
7887 ) ;
7988} ;
89+
90+ const WorkflowListContent = memo (
91+ ( {
92+ items,
93+ hasNextPage,
94+ isFetching,
95+ fetchNextPage,
96+ } : {
97+ items : S [ 'WorkflowRecordListItemWithThumbnailDTO' ] [ ] ;
98+ hasNextPage : boolean ;
99+ isFetching : boolean ;
100+ fetchNextPage : ReturnType < typeof useListWorkflowsInfiniteInfiniteQuery > [ 'fetchNextPage' ] ;
101+ } ) => {
102+ const { t } = useTranslation ( ) ;
103+ const ref = useRef < HTMLDivElement > ( null ) ;
104+
105+ const onScroll = useCallback ( ( ) => {
106+ if ( ! hasNextPage || isFetching ) {
107+ return ;
108+ }
109+ const el = ref . current ;
110+ if ( ! el ) {
111+ return ;
112+ }
113+ const { scrollTop, scrollHeight, clientHeight } = el ;
114+ if ( Math . abs ( scrollHeight - ( scrollTop + clientHeight ) ) <= 1 ) {
115+ fetchNextPage ( ) ;
116+ }
117+ } , [ hasNextPage , isFetching , fetchNextPage ] ) ;
118+
119+ const loadMore = useCallback ( ( ) => {
120+ if ( ! hasNextPage || isFetching ) {
121+ return ;
122+ }
123+ const el = ref . current ;
124+ if ( ! el ) {
125+ return ;
126+ }
127+ fetchNextPage ( ) ;
128+ } , [ hasNextPage , isFetching , fetchNextPage ] ) ;
129+
130+ // // TODO(psyche): this causes an infinite loop, the scrollIntoView triggers the onScroll which triggers the
131+ // // fetchNextPage which triggers the scrollIntoView again...
132+ // useEffect(() => {
133+ // const el = ref.current;
134+ // if (!el) {
135+ // return;
136+ // }
137+
138+ // const observer = new MutationObserver(() => {
139+ // el.querySelector(':scope > :last-child')?.scrollIntoView({ behavior: 'smooth' });
140+ // });
141+
142+ // observer.observe(el, { childList: true });
143+
144+ // return () => {
145+ // observer.disconnect();
146+ // };
147+ // }, []);
148+
149+ return (
150+ < Flex flexDir = "column" gap = { 4 } flex = { 1 } minH = { 0 } >
151+ < Grid
152+ ref = { ref }
153+ templateColumns = "repeat(auto-fit, minmax(340px, 3fr))"
154+ gridAutoFlow = "dense"
155+ gap = { 4 }
156+ overflow = "scroll"
157+ onScroll = { onScroll }
158+ >
159+ { items . map ( ( workflow ) => (
160+ < GridItem id = { `grid-${ workflow . workflow_id } ` } key = { workflow . workflow_id } >
161+ < WorkflowListItem workflow = { workflow } key = { workflow . workflow_id } />
162+ </ GridItem >
163+ ) ) }
164+ </ Grid >
165+ < Spacer />
166+ < Button onClick = { loadMore } isDisabled = { ! hasNextPage } isLoading = { isFetching } >
167+ { t ( 'nodes.loadMore' ) }
168+ </ Button >
169+ </ Flex >
170+ ) ;
171+ }
172+ ) ;
173+ WorkflowListContent . displayName = 'WorkflowListContent' ;
0 commit comments