Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/ui/src/elements/ListDrawer/DrawerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,17 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
)

useEffect(() => {
if (!ListView) {
if (!ListView && isOpen) {
void refresh({ slug: selectedOption?.value })
}
}, [refresh, ListView, selectedOption.value])
}, [refresh, ListView, selectedOption.value, isOpen])

useEffect(() => {
if (!isOpen) {
setListView(undefined)
setIsLoading(true)
}
}, [isOpen])

const onCreateNew = useCallback(
({ doc }) => {
Expand Down
12 changes: 9 additions & 3 deletions packages/ui/src/elements/ListDrawer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'
import { useModal } from '@faceless-ui/modal'
import React, { useCallback, useEffect, useId, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'

import type { ListDrawerProps, ListTogglerProps, UseListDrawer } from './types.js'

Expand Down Expand Up @@ -119,20 +119,26 @@ export const useListDrawer: UseListDrawer = ({
openModal(drawerSlug)
}, [drawerSlug, openModal])

// Use a ref so that filterOptions updates don't recreate MemoizedDrawer.
// A new MemoizedDrawer reference causes React to unmount/remount ListDrawerContent,
// which resets its internal state (e.g. selectedOption) — breaking polymorphic drawers.
const filterOptionsRef = useRef(filterOptions)
filterOptionsRef.current = filterOptions

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

const MemoizedDrawerToggler = useMemo(() => {
return (props) => <ListDrawerToggler {...props} drawerSlug={drawerSlug} />
Expand Down
20 changes: 9 additions & 11 deletions packages/ui/src/fields/Relationship/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
} from 'payload'

import { dequal } from 'dequal/lite'
import { formatAdminURL, wordBoundariesRegex } from 'payload/shared'
import { formatAdminURL, hoistQueryParamsToAnd, wordBoundariesRegex } from 'payload/shared'
import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'

Expand Down Expand Up @@ -128,18 +128,16 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
}, {})

;(Array.isArray(relationTo) ? relationTo : [relationTo]).forEach((relation) => {
const existingFilter =
filterOptions?.[relation] && typeof filterOptions[relation] === 'object'
? filterOptions[relation]
: {}

newFilterOptions = {
...(newFilterOptions || {}),
[relation]: {
...(typeof filterOptions?.[relation] === 'object' ? filterOptions[relation] : {}),
...(valuesByRelation[relation]
? {
id: {
not_in: valuesByRelation[relation],
},
}
: {}),
},
[relation]: valuesByRelation[relation]
? hoistQueryParamsToAnd(existingFilter, { id: { not_in: valuesByRelation[relation] } })
: existingFilter,
}
})
}
Expand Down
32 changes: 31 additions & 1 deletion test/fields/collections/Relationship/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import {
} from '../../../__helpers/e2e/helpers.js'
import { AdminUrlUtil } from '../../../__helpers/shared/adminUrlUtil.js'
import { assertToastErrors } from '../../../__helpers/shared/assertToastErrors.js'
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../../../__helpers/shared/clearAndSeed/reInitializeDB.js'
import { initPayloadE2ENoConfig } from '../../../__helpers/shared/initPayloadE2ENoConfig.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
import { relationshipFieldsSlug, textFieldsSlug } from '../../slugs.js'

Expand Down Expand Up @@ -1058,6 +1058,36 @@ describe('relationship', () => {
).toHaveText('new text')
})

test('should re-evaluate `filterOptions` on subsequent drawer opens when a value has already been set', async () => {
const doc1 = await createTextFieldDoc({ text: 'filterBySibling doc 1' })
const doc2 = await createTextFieldDoc({ text: 'filterBySibling doc 2' })
await loadCreatePage()

const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')

// Select doc1 in sibling1 drawer
await page.locator('#field-relationshipDrawerFilterBySibling1').click()
await expect(listDrawerContent).toBeVisible()
await listDrawerContent.getByText(doc1.text, { exact: true }).click()
await expect(listDrawerContent).toBeHidden()

// First open of sibling2 drawer: doc1 should be filtered out by filterOptions
await page.locator('#field-relationshipDrawerFilterBySibling2').click()
await expect(listDrawerContent).toBeVisible()
await expect(listDrawerContent.getByText(doc1.text)).toBeHidden()
await expect(listDrawerContent.getByText(doc2.text)).toBeVisible()

// Select doc2 and close drawer
await listDrawerContent.getByText(doc2.text, { exact: true }).click()
await expect(listDrawerContent).toBeHidden()

// Second open: filterOptions must be re-evaluated
// Without the fix, the cached ListView reappears and doc1 would be visible again
await page.locator('#field-relationshipDrawerFilterBySibling2').click()
await expect(listDrawerContent).toBeVisible()
await expect(listDrawerContent.getByText(doc1.text)).toBeHidden()
})

describe('A11y', () => {
test.fixme('Create view should have no accessibility violations', async ({}, testInfo) => {
await page.goto(url.create)
Expand Down
21 changes: 21 additions & 0 deletions test/fields/collections/Relationship/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,27 @@ const RelationshipFields: CollectionConfig = {
}
},
},
{
name: 'relationshipDrawerFilterBySibling1',
admin: { appearance: 'drawer' },
type: 'relationship',
relationTo: 'text-fields',
},
{
name: 'relationshipDrawerFilterBySibling2',
admin: { appearance: 'drawer' },
type: 'relationship',
relationTo: 'text-fields',
filterOptions: ({ siblingData }) => {
const sibling1 = siblingData?.relationshipDrawerFilterBySibling1 as string | undefined

if (sibling1) {
return { id: { not_equals: sibling1 } }
}

return true
},
},
],
slug: relationshipFieldsSlug,
}
Expand Down
4 changes: 4 additions & 0 deletions test/fields/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,8 @@ export interface RelationshipField {
relationTo: 'text-fields';
value: string | TextField;
} | null;
relationshipDrawerFilterBySibling1?: (string | null) | TextField;
relationshipDrawerFilterBySibling2?: (string | null) | TextField;
updatedAt: string;
createdAt: string;
}
Expand Down Expand Up @@ -3327,6 +3329,8 @@ export interface RelationshipFieldsSelect<T extends boolean = true> {
relationshipDrawerHasManyPolymorphic?: T;
relationshipDrawerWithAllowCreateFalse?: T;
relationshipDrawerWithFilterOptions?: T;
relationshipDrawerFilterBySibling1?: T;
relationshipDrawerFilterBySibling2?: T;
updatedAt?: T;
createdAt?: T;
}
Expand Down
Loading