Skip to content

fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields#16131

Open
lcnogueira wants to merge 5 commits intopayloadcms:mainfrom
lcnogueira:fix/relationship-drawer-filter-options-re-evaluation
Open

fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields#16131
lcnogueira wants to merge 5 commits intopayloadcms:mainfrom
lcnogueira:fix/relationship-drawer-filter-options-re-evaluation

Conversation

@lcnogueira
Copy link
Copy Markdown
Contributor

@lcnogueira lcnogueira commented Apr 1, 2026

What?

When a relationship field uses admin.appearance: 'drawer' with a filterOptions function that depends on sibling field values, the filter stops being applied after the user selects a value and reopens the drawer. This is specific to the drawer appearance — the default select appearance works correctly.

Why?

Three bugs compound to produce the issue:

Bug 1 — ListView caching (DrawerContent.tsx)

DrawerContent caches the rendered server list in ListView state and only calls refresh() (which re-fetches with the current filterOptions) when ListView is null. On first open, ListView is null so the fetch runs correctly. On any subsequent open, ListView is already set, so refresh() is skipped and the stale cached list is shown — meaning filterOptions is never re-evaluated against the updated form state.

Bug 2 — id filter overwrite (Input.tsx)

listDrawerFilterOptions combines the user's filterOptions result with a not_in exclusion for already-selected values using object spread. When both produce an id condition (e.g. id: { not_equals: sibling1Id } from filterOptions and id: { not_in: [selectedId] } from the exclusion logic), the spread silently overwrites the user's condition. This means the user's filter is dropped from the server query once a value is selected in the field.

Bug 3 — MemoizedDrawer remount on filterOptions change (index.tsx)

MemoizedDrawer in useListDrawer had filterOptions in its useMemo dependency array. After a value is selected, listDrawerFilterOptions in Input.tsx produces a new object reference (with the selected value excluded via not_in). This causes MemoizedDrawer to receive a new function reference, which React treats as a new component type — unmounting and remounting ListDrawerContent. This resets its selectedOption state back to the first collection, breaking the polymorphic drawer filter entirely.

How?

Fix 1 — Clear ListView on drawer close

Added an effect that resets ListView to undefined and restores the loading state whenever the drawer closes. This ensures every subsequent open triggers a fresh refresh() call with the latest filterOptions.

Also added an isOpen guard to the existing !ListView effect to prevent a wasted server round-trip when refresh is recreated as a side effect of isOpen changing to false.

// existing effect — added isOpen guard
useEffect(() => {
  if (!ListView && isOpen) {
    void refresh({ slug: selectedOption?.value })
  }
}, [refresh, ListView, selectedOption.value, isOpen])

// new effect — invalidate cache on close
useEffect(() => {
  if (!isOpen) {
    setListView(undefined)
    setIsLoading(true)
  }
}, [isOpen])

Fix 2 — Merge id conditions with and logic

Replaced the object spread in listDrawerFilterOptions with hoistQueryParamsToAnd (already used elsewhere in the codebase) so both id conditions are preserved:

// before — silently overwrites id: { not_equals: ... } with id: { not_in: [...] }
[relation]: {
  ...(typeof filterOptions?.[relation] === 'object' ? filterOptions[relation] : {}),
  ...(valuesByRelation[relation] ? { id: { not_in: valuesByRelation[relation] } } : {}),
}

// after — combines both conditions with and
[relation]: hoistQueryParamsToAnd(existingFilter, { id: { not_in: valuesByRelation[relation] } })

Fix 3 — Stabilize MemoizedDrawer with a ref

Used a useRef to hold the latest filterOptions value, updated synchronously during render. The ref itself (not ref.current) is captured in the memoized component, so each render reads the current value without recreating the function. filterOptions is removed from the useMemo dependency array, preventing remounts:

const filterOptionsRef = useRef(filterOptions)
filterOptionsRef.current = filterOptions

const MemoizedDrawer = useMemo(() => {
  return (props) => (
    <ListDrawer
      {...props}
      filterOptions={filterOptionsRef.current}
      // ...
    />
  )
}, [drawerSlug, collectionSlugs, uploads, closeDrawer, selectedCollection]) // filterOptions removed

Fixes #15971

@lcnogueira lcnogueira marked this pull request as draft April 1, 2026 18:30
@lcnogueira lcnogueira changed the title fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields [WIP] fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields Apr 1, 2026
@lcnogueira lcnogueira changed the title [WIP] fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields fix(ui): re-evaluate filterOptions on subsequent drawer opens for relationship fields Apr 1, 2026
@lcnogueira lcnogueira marked this pull request as ready for review April 2, 2026 12:19
…anges

Use a ref to hold the latest filterOptions in useListDrawer so that
updates to filterOptions (e.g. after selecting a value) do not recreate
MemoizedDrawer. A new function reference causes React to unmount and
remount ListDrawerContent, resetting selectedOption state — which broke
the polymorphic relationship list drawer filter.
@lcnogueira lcnogueira force-pushed the fix/relationship-drawer-filter-options-re-evaluation branch from 0791ec1 to 0f06c30 Compare April 2, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fields with "appearance: drawer" do not re run the filter options after the user sets a value

1 participant