fix(ui): fixed many bugs in the WhereBuilder relationship select menu (#10553)

Following https://github.com/payloadcms/payload/pull/10551, I found and
fixed a handful more bugs:

- When writing to the input, the results that were already there were
not cleaned, causing incorrect results to appear.
- the scroll was causing an infinite loop that showed repeated elements
- optimization: only the required field is selected (not required)
- refs are set to the initial value to avoid a state where nothing can
be searched
This commit is contained in:
Germán Jabloñski
2025-01-20 16:14:49 -03:00
committed by GitHub
parent 2d70269c56
commit 56667cdc8d
7 changed files with 125 additions and 18 deletions

View File

@@ -76,18 +76,17 @@ export const RelationshipField: React.FC<Props> = (props) => {
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
const pageIndex = nextPageByRelationshipRef.current.get(relationSlug)
const query: {
depth?: number
limit?: number
page?: number
where: Where
} = {
const where: Where = {
and: [],
}
const query = {
depth: 0,
limit: maxResultsPerRequest,
page: pageIndex,
where: {
and: [],
select: {
[fieldToSearch]: true,
},
where,
}
if (debouncedSearch) {
@@ -115,15 +114,13 @@ export const RelationshipField: React.FC<Props> = (props) => {
if (data.docs.length > 0) {
addOptions(data, relationSlug)
if (!debouncedSearch) {
if (data.nextPage) {
nextPageByRelationshipRef.current.set(relationSlug, data.nextPage)
} else {
partiallyLoadedRelationshipSlugs.current =
partiallyLoadedRelationshipSlugs.current.filter(
(partiallyLoadedRelation) => partiallyLoadedRelation !== relationSlug,
)
}
if (data.nextPage) {
nextPageByRelationshipRef.current.set(relationSlug, data.nextPage)
} else {
partiallyLoadedRelationshipSlugs.current =
partiallyLoadedRelationshipSlugs.current.filter(
(partiallyLoadedRelation) => partiallyLoadedRelation !== relationSlug,
)
}
}
} else {
@@ -209,7 +206,9 @@ export const RelationshipField: React.FC<Props> = (props) => {
}, [hasMany, hasMultipleRelations, value, options])
const handleInputChange = (input: string) => {
dispatchOptions({ type: 'CLEAR', i18n, required: false })
const relationSlug = partiallyLoadedRelationshipSlugs.current[0]
partiallyLoadedRelationshipSlugs.current = relationSlugs
nextPageByRelationshipRef.current.set(relationSlug, 1)
setSearch(input)
}

View File

@@ -0,0 +1,21 @@
import type { CollectionConfig } from 'payload'
import { with300DocumentsSlug } from '../slugs.js'
export const with300Documents: CollectionConfig = {
slug: with300DocumentsSlug,
admin: {
useAsTitle: 'text',
},
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'selfRelation',
type: 'relationship',
relationTo: with300DocumentsSlug,
},
],
}

View File

@@ -19,6 +19,7 @@ import { CollectionNotInView } from './collections/NotInView.js'
import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js'
import { Users } from './collections/Users.js'
import { with300Documents } from './collections/With300Documents.js'
import { CustomGlobalViews1 } from './globals/CustomViews1.js'
import { CustomGlobalViews2 } from './globals/CustomViews2.js'
import { Global } from './globals/Global.js'
@@ -155,6 +156,7 @@ export default buildConfigWithDefaults({
Geo,
DisableDuplicate,
BaseListFilter,
with300Documents,
],
globals: [
GlobalHidden,

View File

@@ -16,7 +16,12 @@ import {
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
import { customAdminRoutes } from '../../shared.js'
import { customViews1CollectionSlug, geoCollectionSlug, postsCollectionSlug } from '../../slugs.js'
import {
customViews1CollectionSlug,
geoCollectionSlug,
postsCollectionSlug,
with300DocumentsSlug,
} from '../../slugs.js'
const { beforeAll, beforeEach, describe } = test
@@ -48,6 +53,7 @@ describe('List View', () => {
let postsUrl: AdminUrlUtil
let baseListFiltersUrl: AdminUrlUtil
let customViewsUrl: AdminUrlUtil
let with300DocumentsUrl: AdminUrlUtil
let serverURL: string
let adminRoutes: ReturnType<typeof getRoutes>
@@ -65,6 +71,7 @@ describe('List View', () => {
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
with300DocumentsUrl = new AdminUrlUtil(serverURL, with300DocumentsSlug)
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
@@ -608,6 +615,35 @@ describe('List View', () => {
})
})
describe('WhereBuilder', () => {
test('should render where builder', async () => {
await page.goto(
`${with300DocumentsUrl.list}?limit=10&page=1&where%5Bor%5D%5B0%5D%5Band%5D%5B0%5D%5BselfRelation%5D%5Bequals%5D=null`,
)
const valueField = page.locator('.condition__value')
await valueField.click()
await page.keyboard.type('4')
const options = page.getByRole('option')
expect(options).toHaveCount(10)
for (const option of await options.all()) {
expect(option).toHaveText('4')
}
await page.keyboard.press('Backspace')
await page.keyboard.type('5')
expect(options).toHaveCount(10)
for (const option of await options.all()) {
expect(option).toHaveText('5')
}
// await options.last().scrollIntoViewIfNeeded()
await options.first().hover()
// three times because react-select is not very reliable
await page.mouse.wheel(0, 50)
await page.mouse.wheel(0, 50)
await page.mouse.wheel(0, 50)
expect(options).toHaveCount(20)
})
})
describe('table columns', () => {
test('should hide field column when field.hidden is true', async () => {
await page.goto(postsUrl.list)

View File

@@ -27,6 +27,7 @@ export interface Config {
geo: Geo;
'disable-duplicate': DisableDuplicate;
'base-list-filters': BaseListFilter;
with300documents: With300Document;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -49,6 +50,7 @@ export interface Config {
geo: GeoSelect<false> | GeoSelect<true>;
'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>;
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
with300documents: With300DocumentsSelect<false> | With300DocumentsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -374,6 +376,17 @@ export interface BaseListFilter {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "with300documents".
*/
export interface With300Document {
id: string;
text?: string | null;
selfRelation?: (string | null) | With300Document;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -444,6 +457,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'base-list-filters';
value: string | BaseListFilter;
} | null)
| ({
relationTo: 'with300documents';
value: string | With300Document;
} | null);
globalSlug?: string | null;
user: {
@@ -734,6 +751,16 @@ export interface BaseListFiltersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "with300documents_select".
*/
export interface With300DocumentsSelect<T extends boolean = true> {
text?: T;
selfRelation?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".

View File

@@ -11,6 +11,7 @@ import {
noApiViewCollectionSlug,
postsCollectionSlug,
usersCollectionSlug,
with300DocumentsSlug,
} from './slugs.js'
export const seed = async (_payload) => {
@@ -117,6 +118,26 @@ export const seed = async (_payload) => {
],
false,
)
// delete all with300Documents
await _payload.delete({
collection: with300DocumentsSlug,
where: {},
})
// Create 300 documents of with300Documents
const manyDocumentsPromises: Promise<unknown>[] = Array.from({ length: 300 }, (_, i) => {
const index = (i + 1).toString().padStart(3, '0')
return _payload.create({
collection: with300DocumentsSlug,
data: {
id: index,
text: `document ${index}`,
},
})
})
await Promise.all([...manyDocumentsPromises])
}
export async function clearAndSeedEverything(_payload: Payload) {

View File

@@ -48,3 +48,4 @@ export const globalSlugs = [
hiddenGlobalSlug,
noApiViewGlobalSlug,
]
export const with300DocumentsSlug = 'with300documents'