feat(ui): make select and relationship field placeholder configurable (#12253)

### What?
Allows to overwrite the default placeholder text of select and
relationship fields.

### Why?
The default placeholder text is generic. In some scenarios a custom
placeholder can guide the user better.

### How?
Adds a new property `admin.placeholder` to relationship and select field
which allows to define an alternative text or translation function for
the placeholder. The placeholder is used in the form fields as well as
in the filter options.

![Screenshot 2025-04-29 at 15 28
54](https://github.com/user-attachments/assets/d83d60c8-d4f6-41b7-951c-9f21c238afd8)
![Screenshot 2025-04-29 at 15 28
19](https://github.com/user-attachments/assets/d2263cf1-6042-4072-b5a9-e10af5f380bb)

---------

Co-authored-by: Dan Ribbens <DanRibbens@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Tobias Odendahl
2025-05-01 21:17:47 +02:00
committed by GitHub
parent 78d3af7dc9
commit e5683913b4
15 changed files with 138 additions and 16 deletions

View File

@@ -0,0 +1,41 @@
import type { CollectionConfig } from 'payload'
import { placeholderCollectionSlug } from '../slugs.js'
export const Placeholder: CollectionConfig = {
slug: placeholderCollectionSlug,
fields: [
{
name: 'defaultSelect',
type: 'select',
options: [
{
label: 'Option 1',
value: 'option1',
},
],
},
{
name: 'placeholderSelect',
type: 'select',
options: [{ label: 'Option 1', value: 'option1' }],
admin: {
placeholder: 'Custom placeholder',
},
},
{
name: 'defaultRelationship',
type: 'relationship',
relationTo: 'posts',
},
{
name: 'placeholderRelationship',
type: 'relationship',
relationTo: 'posts',
admin: {
placeholder: 'Custom placeholder',
},
},
],
versions: true,
}

View File

@@ -1,7 +1,6 @@
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { Array } from './collections/Array.js'
import { BaseListFilter } from './collections/BaseListFilter.js'
@@ -19,6 +18,7 @@ import { CollectionHidden } from './collections/Hidden.js'
import { ListDrawer } from './collections/ListDrawer.js'
import { CollectionNoApiView } from './collections/NoApiView.js'
import { CollectionNotInView } from './collections/NotInView.js'
import { Placeholder } from './collections/Placeholder.js'
import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js'
import { UploadTwoCollection } from './collections/UploadTwo.js'
@@ -43,7 +43,8 @@ import {
protectedCustomNestedViewPath,
publicCustomViewPath,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
admin: {
importMap: {
@@ -165,6 +166,7 @@ export default buildConfigWithDefaults({
BaseListFilter,
with300Documents,
ListDrawer,
Placeholder,
],
globals: [
GlobalHidden,

View File

@@ -1,11 +1,10 @@
import type { Page } from '@playwright/test'
import type { User as PayloadUser } from 'payload'
import { expect, test } from '@playwright/test'
import { mapAsync } from 'payload'
import * as qs from 'qs-esm'
import type { Config, Geo, Post, User } from '../../payload-types.js'
import type { Config, Geo, Post } from '../../payload-types.js'
import {
ensureCompilationIsDone,
@@ -21,6 +20,7 @@ import {
customViews1CollectionSlug,
geoCollectionSlug,
listDrawerSlug,
placeholderCollectionSlug,
postsCollectionSlug,
with300DocumentsSlug,
} from '../../slugs.js'
@@ -64,6 +64,7 @@ describe('List View', () => {
let customViewsUrl: AdminUrlUtil
let with300DocumentsUrl: AdminUrlUtil
let withListViewUrl: AdminUrlUtil
let placeholderUrl: AdminUrlUtil
let user: any
let serverURL: string
@@ -87,7 +88,7 @@ describe('List View', () => {
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -1408,6 +1409,66 @@ describe('List View', () => {
).toHaveText('Title')
})
})
describe('placeholder', () => {
test('should display placeholder in filter options', async () => {
await page.goto(
`${placeholderUrl.list}${qs.stringify(
{
where: {
or: [
{
and: [
{
defaultSelect: {
equals: '',
},
},
{
placeholderSelect: {
equals: '',
},
},
{
defaultRelationship: {
equals: '',
},
},
{
placeholderRelationship: {
equals: '',
},
},
],
},
],
},
},
{ addQueryPrefix: true },
)}`,
)
const conditionValueSelects = page.locator('#list-controls-where .condition__value')
await expect(conditionValueSelects.nth(0)).toHaveText('Select a value')
await expect(conditionValueSelects.nth(1)).toHaveText('Custom placeholder')
await expect(conditionValueSelects.nth(2)).toHaveText('Select a value')
await expect(conditionValueSelects.nth(3)).toHaveText('Custom placeholder')
})
})
test('should display placeholder in edit view', async () => {
await page.goto(placeholderUrl.create)
await expect(page.locator('#field-defaultSelect .rs__placeholder')).toHaveText('Select a value')
await expect(page.locator('#field-placeholderSelect .rs__placeholder')).toHaveText(
'Custom placeholder',
)
await expect(page.locator('#field-defaultRelationship .rs__placeholder')).toHaveText(
'Select a value',
)
await expect(page.locator('#field-placeholderRelationship .rs__placeholder')).toHaveText(
'Custom placeholder',
)
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -14,6 +14,7 @@ export const noApiViewCollectionSlug = 'collection-no-api-view'
export const disableDuplicateSlug = 'disable-duplicate'
export const disableCopyToLocale = 'disable-copy-to-locale'
export const uploadCollectionSlug = 'uploads'
export const placeholderCollectionSlug = 'placeholder'
export const uploadTwoCollectionSlug = 'uploads-two'
export const customFieldsSlug = 'custom-fields'