payloadcms / payloadcms/payload

filterOptions on a virtual: 'path.to.field' relationship/upload field is never applied in the list view's Where Builder filter dropdown

Open Beginner friendly
#18,215 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: ui Bug status: needs-triage v3
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

To Reproduce

  1. Define a collection with a relationship (or upload) field that is virtual: 'someRelation.someField' (i.e. a virtual field aliasing a field on a related collection, not virtual: true), and that also defines a filterOptions function, e.g.:
{
  name: 'region',
  type: 'relationship',
  relationTo: 'regions',
  virtual: 'customer.region',
  filterOptions: async ({ req }) => {
    // return a Where that should restrict the option list, e.g. scoped to the current user
    return { id: { in: await getAllowedRegionIds(req) } }
  },
}
  1. Go to the collection's list view, open the "Where" filter builder, add a condition on this field, and open its value dropdown.
  2. Observe the network request made by the value dropdown (GET /api/<relationTo>?...) — it has no where constraint at all, and the dropdown shows every document in the related collection, ignoring filterOptions.
  3. For comparison, a non-virtual relationship field with the same filterOptions function on the same collection filters correctly.

Describe the bug

resolveAllFilterOptions (packages/next/src/views/List/resolveAllFilterOptions.ts) computes the map key it stores each field's resolved filterOptions result under as:

const fieldPath = fieldAffectsData(field)
  ? pathPrefix ? `${pathPrefix}.${field.name}` : field.name
  : pathPrefix

This always uses field.name, with no special case for virtual: string fields.

But reduceFieldsToOptions (packages/ui/src/utilities/reduceFieldsToOptions.tsx), which builds the field path actually used by the Where Builder's Condition component (both for the field-select dropdown's value and for the key looked up in the resolved-filter-options map), does special-case virtual: string fields — it uses the virtual path itself instead of the field's own name:

if ('virtual' in field && typeof field.virtual === 'string') {
  pathPrefix = pathPrefix ? `${pathPrefix}.${field.virtual}` : field.virtual
  if (fieldAffectsData(field)) {
    shouldIgnoreFieldName = true // fieldPath ends up being just pathPrefix, i.e. field.virtual
  }
}

So for a field like region with virtual: 'customer.region':

  • resolveAllFilterOptions stores the resolved Where under the key "region".
  • reduceFieldsToOptions / Condition looks it up under the key "customer.region".

The lookup (resolvedFilterOptions?.get(fieldPath)) always misses, Condition passes filterOptions: undefined down to DefaultFilterRelationshipFilter, and RelationshipFilter silently fetches the related collection with no where constraint at all — the filter is dropped with no error or warning.

This appears to affect any collection with a virtual: 'path' relationship/upload field that also uses filterOptions, regardless of database adapter.

Expected behavior

The value dropdown for a virtual: 'path' field with filterOptions should apply the same restriction that document-level relationship pickers for that field already correctly apply (that path works because it calls the field's filterOptions function directly, not through resolveAllFilterOptions's map).

Proposed fix

Make resolveAllFilterOptions's path computation match reduceFieldsToOptions's: when a field's virtual is a string and the field affects data, key the resolved map entry by the virtual path (composed with any pathPrefix) instead of field.name. We have a minimal patch doing exactly this that we've verified fixes the issue locally — happy to open a PR if useful.

Payload Version

payload@3.88.0, @payloadcms/next@3.88.0, @payloadcms/ui@3.88.0, Next.js 16.3.4 (Turbopack), Postgres adapter.

Link to the code that reproduces this issue

https://github.com/tramck/payload-virtual-filteroptions-repro

Reproduction Steps
  1. Clone the reproduction, then pnpm install && pnpm dev (no .env needed — the config has fallbacks, an onInit seed creates the data, and autoLogin is enabled).
  2. Open http://localhost:3000/admin and go to the Orders list view.
  3. Click FiltersAdd Filter.
  4. Set the field to Direct Region and open the value dropdown — ✅ only Allowed Region is offered.
  5. Change the field to Virtual Region and open the value dropdown — ❌ Allowed Region, Blocked Region A and Blocked Region B are all offered.
Which area(s) are affected?

area: ui

Environment Info
> payload-virtual-filteroptions-repro@1.0.0 payload /Users/travis/Documents/payload-virtual-filteroptions-repro
> cross-env NODE_OPTIONS=--no-deprecation payload info


Binaries:
  Node: 26.0.0
  npm: 11.12.1
  Yarn: N/A
  pnpm: 10.33.0
Relevant Packages:
  payload: 3.90.1
  next: 16.3.3
  @payloadcms/db-sqlite: 3.90.1
  @payloadcms/drizzle: 3.90.1
  @payloadcms/graphql: 3.90.1
  @payloadcms/next/utilities: 3.90.1
  @payloadcms/richtext-lexical: 3.90.1
  @payloadcms/translations: 3.90.1
  @payloadcms/ui/shared: 3.90.1
  react: 19.2.6
  react-dom: 19.2.6
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:18:49 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6000
  Available memory (MB): 16384
  Available CPU cores: 8

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/next/src/views/List/resolveAllFilterOptions.ts and compare its field-path handling with packages/ui/src/utilities/reduceFieldsToOptions.tsx. Run the linked reproduction and verify the Direct Region and Virtual Region dropdowns. Done means the virtual field's dropdown applies its filterOptions restriction and no longer lists blocked regions.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.