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' ;
2
3
import { useAppSelector } from 'app/store/storeHooks' ;
3
4
import { IAINoContentFallback } from 'common/components/IAIImageFallback' ;
4
5
import {
@@ -7,53 +8,65 @@ import {
7
8
selectWorkflowOrderDirection ,
8
9
selectWorkflowSearchTerm ,
9
10
} from 'features/nodes/store/workflowSlice' ;
10
- import { useEffect , useMemo , useState } from 'react' ;
11
+ import { memo , useCallback , useMemo , useRef } from 'react' ;
11
12
import { 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' ;
13
16
import { useDebounce } from 'use-debounce' ;
14
17
15
- import { WorkflowLibraryPagination } from './WorkflowLibraryPagination' ;
16
18
import { WorkflowListItem } from './WorkflowListItem' ;
17
19
18
- const PER_PAGE = 6 ;
20
+ const PER_PAGE = 3 ;
19
21
20
- export const WorkflowList = ( ) => {
21
- const searchTerm = useAppSelector ( selectWorkflowSearchTerm ) ;
22
- const { t } = useTranslation ( ) ;
23
-
24
- const [ page , setPage ] = useState ( 0 ) ;
22
+ const useInfiniteQueryAry = ( ) => {
25
23
const categories = useAppSelector ( selectWorkflowCategories ) ;
26
24
const orderBy = useAppSelector ( selectWorkflowOrderBy ) ;
27
25
const direction = useAppSelector ( selectWorkflowOrderDirection ) ;
28
26
const query = useAppSelector ( selectWorkflowSearchTerm ) ;
29
27
const [ debouncedQuery ] = useDebounce ( query , 500 ) ;
30
28
31
- useEffect ( ( ) => {
32
- setPage ( 0 ) ;
33
- } , [ categories , query ] ) ;
34
-
35
- const queryArg = useMemo < Parameters < typeof useListWorkflowsQuery > [ 0 ] > ( ( ) => {
29
+ const queryArg = useMemo ( ( ) => {
36
30
return {
37
- page,
31
+ page : 0 ,
38
32
per_page : PER_PAGE ,
39
33
order_by : orderBy ,
40
34
direction,
41
35
categories,
42
36
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
+ } ;
45
42
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
+ ) ;
47
60
48
61
if ( isLoading ) {
49
62
return (
50
- < Flex alignItems = "center" justifyContent = "center" p = { 20 } >
63
+ < Flex alignItems = "center" justifyContent = "center" w = "full" h = "full" >
51
64
< Spinner />
52
65
</ Flex >
53
66
) ;
54
67
}
55
68
56
- if ( ! data ?. items . length ) {
69
+ if ( items . length === 0 ) {
57
70
return (
58
71
< IAINoContentFallback
59
72
fontSize = "sm"
@@ -65,15 +78,96 @@ export const WorkflowList = () => {
65
78
}
66
79
67
80
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
+ />
78
87
) ;
79
88
} ;
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