@@ -8,11 +8,12 @@ import type { Option } from '../../../ReactSelect/types.js'
8
8
import type { Props , ValueWithRelation } from './types.js'
9
9
10
10
import { useDebounce } from '../../../../hooks/useDebounce.js'
11
+ import { useEffectEvent } from '../../../../hooks/useEffectEvent.js'
11
12
import { useConfig } from '../../../../providers/Config/index.js'
12
13
import { useTranslation } from '../../../../providers/Translation/index.js'
13
14
import { ReactSelect } from '../../../ReactSelect/index.js'
14
- import './index.scss'
15
15
import optionsReducer from './optionsReducer.js'
16
+ import './index.scss'
16
17
17
18
const baseClass = 'condition-value-relationship'
18
19
@@ -37,22 +38,32 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
37
38
const hasMultipleRelations = Array . isArray ( relationTo )
38
39
const [ options , dispatchOptions ] = useReducer ( optionsReducer , [ ] )
39
40
const [ search , setSearch ] = useState ( '' )
41
+ const debouncedSearch = useDebounce ( search , 300 )
40
42
const [ errorLoading , setErrorLoading ] = useState ( '' )
41
43
const [ hasLoadedFirstOptions , setHasLoadedFirstOptions ] = useState ( false )
42
- const debouncedSearch = useDebounce ( search , 300 )
43
44
const { i18n, t } = useTranslation ( )
44
- const relationSlugs = hasMultipleRelations ? relationTo : [ relationTo ]
45
45
46
- const initialRelationMap = ( ) => {
47
- const map : Map < string , number > = new Map ( )
48
- relationSlugs . forEach ( ( relation ) => {
49
- map . set ( relation , 1 )
50
- } )
51
- return map
52
- }
46
+ const relationSlugs = hasMultipleRelations ? relationTo : [ relationTo ]
53
47
54
- const nextPageByRelationshipRef = React . useRef < Map < string , number > > ( initialRelationMap ( ) )
55
- const partiallyLoadedRelationshipSlugs = React . useRef < string [ ] > ( relationSlugs )
48
+ const loadedRelationships = React . useRef <
49
+ Map <
50
+ string ,
51
+ {
52
+ hasLoadedAll : boolean
53
+ nextPage : number
54
+ }
55
+ >
56
+ > (
57
+ new Map (
58
+ relationSlugs . map ( ( relation ) => [
59
+ relation ,
60
+ {
61
+ hasLoadedAll : false ,
62
+ nextPage : 1 ,
63
+ } ,
64
+ ] ) ,
65
+ ) ,
66
+ )
56
67
57
68
const addOptions = useCallback (
58
69
( data , relation ) => {
@@ -62,28 +73,31 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
62
73
[ hasMultipleRelations , i18n , getEntityConfig ] ,
63
74
)
64
75
65
- const loadRelationOptions = React . useCallback (
76
+ const loadOptions = useEffectEvent (
66
77
async ( {
67
78
abortController,
68
79
relationSlug,
69
80
} : {
70
81
abortController : AbortController
71
82
relationSlug : string
72
83
} ) => {
73
- if ( relationSlug && partiallyLoadedRelationshipSlugs . current . includes ( relationSlug ) ) {
84
+ const loadedRelationship = loadedRelationships . current . get ( relationSlug )
85
+
86
+ if ( relationSlug && ! loadedRelationship . hasLoadedAll ) {
74
87
const collection = getEntityConfig ( {
75
88
collectionSlug : relationSlug ,
76
89
} )
90
+
77
91
const fieldToSearch = collection ?. admin ?. useAsTitle || 'id'
78
- const pageIndex = nextPageByRelationshipRef . current . get ( relationSlug )
79
92
80
93
const where : Where = {
81
94
and : [ ] ,
82
95
}
96
+
83
97
const query = {
84
98
depth : 0 ,
85
99
limit : maxResultsPerRequest ,
86
- page : pageIndex ,
100
+ page : loadedRelationship . nextPage ,
87
101
select : {
88
102
[ fieldToSearch ] : true ,
89
103
} ,
@@ -116,38 +130,43 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
116
130
addOptions ( data , relationSlug )
117
131
118
132
if ( data . nextPage ) {
119
- nextPageByRelationshipRef . current . set ( relationSlug , data . nextPage )
133
+ loadedRelationships . current . set ( relationSlug , {
134
+ hasLoadedAll : false ,
135
+ nextPage : data . nextPage ,
136
+ } )
120
137
} else {
121
- partiallyLoadedRelationshipSlugs . current =
122
- partiallyLoadedRelationshipSlugs . current . filter (
123
- ( partiallyLoadedRelation ) => partiallyLoadedRelation !== relationSlug ,
124
- )
138
+ loadedRelationships . current . set ( relationSlug , {
139
+ hasLoadedAll : true ,
140
+ nextPage : null ,
141
+ } )
125
142
}
126
143
}
127
144
} else {
128
145
setErrorLoading ( t ( 'error:unspecific' ) )
129
146
}
130
147
} catch ( e ) {
131
148
if ( ! abortController . signal . aborted ) {
132
- console . error ( e )
149
+ console . error ( e ) // eslint-disable-line no-console
133
150
}
134
151
}
135
152
}
136
153
137
154
setHasLoadedFirstOptions ( true )
138
155
} ,
139
- [ addOptions , api , debouncedSearch , getEntityConfig , i18n . language , serverURL , t ] ,
140
156
)
141
157
142
- const loadMoreOptions = React . useCallback ( ( ) => {
143
- if ( partiallyLoadedRelationshipSlugs . current . length > 0 ) {
158
+ const handleScrollToBottom = React . useCallback ( ( ) => {
159
+ const relationshipToLoad = loadedRelationships . current . entries ( ) . next ( ) . value
160
+
161
+ if ( relationshipToLoad [ 0 ] && ! relationshipToLoad [ 1 ] . hasLoadedAll ) {
144
162
const abortController = new AbortController ( )
145
- void loadRelationOptions ( {
163
+
164
+ void loadOptions ( {
146
165
abortController,
147
- relationSlug : partiallyLoadedRelationshipSlugs . current [ 0 ] ,
166
+ relationSlug : relationshipToLoad [ 0 ] ,
148
167
} )
149
168
}
150
- } , [ loadRelationOptions ] )
169
+ } , [ ] )
151
170
152
171
const findOptionsByValue = useCallback ( ( ) : Option | Option [ ] => {
153
172
if ( value ) {
@@ -206,15 +225,28 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
206
225
return undefined
207
226
} , [ hasMany , hasMultipleRelations , value , options ] )
208
227
209
- const handleInputChange = ( input : string ) => {
210
- if ( input !== search ) {
211
- dispatchOptions ( { type : 'CLEAR' , i18n, required : false } )
212
- const relationSlug = partiallyLoadedRelationshipSlugs . current [ 0 ]
213
- partiallyLoadedRelationshipSlugs . current = relationSlugs
214
- nextPageByRelationshipRef . current . set ( relationSlug , 1 )
215
- setSearch ( input )
216
- }
217
- }
228
+ const handleInputChange = useCallback (
229
+ ( input : string ) => {
230
+ if ( input !== search ) {
231
+ dispatchOptions ( { type : 'CLEAR' , i18n, required : false } )
232
+
233
+ const relationSlugs = Array . isArray ( relationTo ) ? relationTo : [ relationTo ]
234
+
235
+ loadedRelationships . current = new Map (
236
+ relationSlugs . map ( ( relation ) => [
237
+ relation ,
238
+ {
239
+ hasLoadedAll : false ,
240
+ nextPage : 1 ,
241
+ } ,
242
+ ] ) ,
243
+ )
244
+
245
+ setSearch ( input )
246
+ }
247
+ } ,
248
+ [ i18n , relationTo , search ] ,
249
+ )
218
250
219
251
const addOptionByID = useCallback (
220
252
async ( id , relation ) => {
@@ -239,19 +271,37 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
239
271
)
240
272
241
273
/**
242
- * 1. Trigger initial relationship options fetch
243
- * 2. When search changes, loadRelationOptions will
244
- * fire off again
274
+ * When `relationTo` changes externally, reset the options and reload them from scratch
275
+ * The `loadOptions` dependency is a useEffectEvent which has no dependencies of its own
276
+ * This means we can safely depend on it without it triggering this effect to run
277
+ * This is useful because this effect should _only_ run when `relationTo` changes
245
278
*/
246
279
useEffect ( ( ) => {
247
280
const relations = Array . isArray ( relationTo ) ? relationTo : [ relationTo ]
281
+
282
+ loadedRelationships . current = new Map (
283
+ relations . map ( ( relation ) => [
284
+ relation ,
285
+ {
286
+ hasLoadedAll : false ,
287
+ nextPage : 1 ,
288
+ } ,
289
+ ] ) ,
290
+ )
291
+
292
+ dispatchOptions ( { type : 'CLEAR' , i18n, required : false } )
293
+ setHasLoadedFirstOptions ( false )
294
+
248
295
const abortControllers : AbortController [ ] = [ ]
296
+
249
297
relations . forEach ( ( relation ) => {
250
298
const abortController = new AbortController ( )
251
- void loadRelationOptions ( {
299
+
300
+ void loadOptions ( {
252
301
abortController,
253
302
relationSlug : relation ,
254
303
} )
304
+
255
305
abortControllers . push ( abortController )
256
306
} )
257
307
@@ -266,11 +316,10 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
266
316
}
267
317
} )
268
318
}
269
- } , [ i18n , loadRelationOptions , relationTo ] )
319
+ } , [ i18n , relationTo , debouncedSearch ] )
270
320
271
321
/**
272
- * Load any options that were not returned
273
- * in the first 10 of each relation fetch
322
+ * Load any other options that might exist in the value that were not loaded already
274
323
*/
275
324
useEffect ( ( ) => {
276
325
if ( value && hasLoadedFirstOptions ) {
@@ -327,6 +376,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
327
376
onChange ( null )
328
377
return
329
378
}
379
+
330
380
if ( hasMany && Array . isArray ( selected ) ) {
331
381
onChange (
332
382
selected
@@ -352,7 +402,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
352
402
}
353
403
} }
354
404
onInputChange = { handleInputChange }
355
- onMenuScrollToBottom = { loadMoreOptions }
405
+ onMenuScrollToBottom = { handleScrollToBottom }
356
406
options = { options }
357
407
placeholder = { t ( 'general:selectValue' ) }
358
408
value = { valueToRender }
0 commit comments