refactor(ui): improve relationship field option loading reliability using queues (#12653)

This PR uses the new `useQueue` hook for relationship react-select field
for loading options. This will reduce flakiness in our CI and ensure the
following:
- most recently triggered options loading request will not have its
result overwritten by a previous, delayed request
- reduce unnecessary, parallel requests - outdated requests are
discarded from the queue if a newer request exist
This commit is contained in:
Alessio Gravili
2025-06-02 14:33:41 -07:00
committed by GitHub
parent c639c5f278
commit 30dd9a23a3
2 changed files with 144 additions and 139 deletions

View File

@@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState }
import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../elements/ListDrawer/types.js'
import type { ReactSelectAdapterProps } from '../../elements/ReactSelect/types.js'
import type { GetResults, HasManyValueUnion, Option, RelationshipInputProps } from './types.js'
import type { HasManyValueUnion, Option, RelationshipInputProps, UpdateResults } from './types.js'
import { AddNewRelation } from '../../elements/AddNewRelation/index.js'
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
@@ -21,6 +21,7 @@ import { FieldError } from '../../fields/FieldError/index.js'
import { FieldLabel } from '../../fields/FieldLabel/index.js'
import { useDebouncedCallback } from '../../hooks/useDebouncedCallback.js'
import { useEffectEvent } from '../../hooks/useEffectEvent.js'
import { useQueues } from '../../hooks/useQueues.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useLocale } from '../../providers/Locale/index.js'
@@ -31,8 +32,8 @@ import { createRelationMap } from './createRelationMap.js'
import { findOptionsByValue } from './findOptionsByValue.js'
import { optionsReducer } from './optionsReducer.js'
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
import { SingleValue } from './select-components/SingleValue/index.js'
import './index.scss'
import { SingleValue } from './select-components/SingleValue/index.js'
const baseClass = 'relationship'
@@ -94,6 +95,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
const [enableWordBoundarySearch, setEnableWordBoundarySearch] = useState(false)
const [menuIsOpen, setMenuIsOpen] = useState(false)
const hasLoadedFirstPageRef = useRef(false)
const { queueTask } = useQueues()
const [options, dispatchOptions] = useReducer(optionsReducer, [])
@@ -173,8 +175,8 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
const openDrawerWhenRelationChanges = useRef(false)
const getResults: GetResults = useCallback(
async ({
const updateResults: UpdateResults = useCallback(
({
filterOptions,
hasMany: hasManyArg,
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
@@ -187,6 +189,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
if (!permissions) {
return
}
queueTask(async () => {
const lastFullyLoadedRelationToUse =
typeof lastFullyLoadedRelationArg !== 'undefined' ? lastFullyLoadedRelationArg : -1
@@ -333,9 +336,11 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
onSuccess()
}
}
})
},
[
permissions,
queueTask,
relationTo,
errorLoading,
search,
@@ -353,7 +358,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
const updateSearch = useDebouncedCallback<{ search: string } & HasManyValueUnion>(
({ hasMany: hasManyArg, search: searchArg, value }) => {
void getResults({
updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
search: searchArg,
@@ -593,8 +598,8 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
[config.routes.admin],
)
const getResultsEffectEvent: GetResults = useEffectEvent(async (args) => {
return await getResults(args)
const updateResultsEffectEvent: UpdateResults = useEffectEvent((args) => {
return updateResults(args)
})
// When (`relationTo` || `filterOptions` || `locale`) changes, reset component
@@ -605,7 +610,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
// re-fetch options
if (hasLoadedFirstPageRef.current && menuIsOpen) {
setIsLoading(true)
void getResultsEffectEvent({
void updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
@@ -777,7 +782,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
setMenuIsOpen(true)
if (!hasLoadedFirstPageRef.current) {
setIsLoading(true)
void getResults({
updateResultsEffectEvent({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
@@ -798,7 +803,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
}
}}
onMenuScrollToBottom={() => {
void getResults({
updateResultsEffectEvent({
filterOptions,
lastFullyLoadedRelation,
lastLoadedPage,

View File

@@ -78,7 +78,7 @@ export type HasManyValueUnion =
value?: PolymorphicRelationValue[]
}
export type GetResults = (
export type UpdateResults = (
args: {
filterOptions?: FilterOptionsResult
lastFullyLoadedRelation?: number
@@ -87,7 +87,7 @@ export type GetResults = (
search?: string
sort?: boolean
} & HasManyValueUnion,
) => Promise<void>
) => void
export type RelationshipInputProps = {
readonly AfterInput?: React.ReactNode