Files
payload/packages/ui/src/fields/Select/Input.tsx
Jacob Fletcher f75d62c79b feat: select field filter options (#12487)
It is a common pattern to dynamically show and validate a select field's
options based on various criteria such as the current user or underlying
document.

Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g.
a city/state selector
 
While this is already possible to do with a custom `validate` function,
the user can still view and select the forbidden option...unless you
_also_ wired up a custom component.

Now, you can define `filterOptions` on select fields.

This behaves similarly to the existing `filterOptions` property on
relationship and upload fields, except the return value of this function
is simply an array of options, not a query constraint. The result of
this function will determine what is shown to the user and what is
validated on the server.

Here's an example:

```ts
{
  name: 'select',
  type: 'select',
  options: [
    {
      label: 'One',
      value: 'one',
    },
    {
      label: 'Two',
      value: 'two',
    },
    {
      label: 'Three',
      value: 'three',
    },
  ],
  filterOptions: ({ options, data }) =>
    data.disallowOption1
      ? options.filter(
          (option) => (typeof option === 'string' ? options : option.value) !== 'one',
        )
      : options,
}
```
2025-05-22 15:54:12 -04:00

146 lines
4.3 KiB
TypeScript

'use client'
import type { LabelFunction, OptionObject, StaticDescription, StaticLabel } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import type { ReactSelectAdapterProps } from '../../elements/ReactSelect/types.js'
import { ReactSelect } from '../../elements/ReactSelect/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { FieldDescription } from '../../fields/FieldDescription/index.js'
import { FieldError } from '../../fields/FieldError/index.js'
import { FieldLabel } from '../../fields/FieldLabel/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { fieldBaseClass } from '../shared/index.js'
import './index.scss'
export type SelectInputProps = {
readonly AfterInput?: React.ReactNode
readonly BeforeInput?: React.ReactNode
readonly className?: string
readonly Description?: React.ReactNode
readonly description?: StaticDescription
readonly Error?: React.ReactNode
readonly filterOption?: ReactSelectAdapterProps['filterOption']
readonly hasMany?: boolean
readonly isClearable?: boolean
readonly isSortable?: boolean
readonly Label?: React.ReactNode
readonly label?: StaticLabel
readonly localized?: boolean
readonly name: string
readonly onChange?: ReactSelectAdapterProps['onChange']
readonly onInputChange?: ReactSelectAdapterProps['onInputChange']
readonly options?: OptionObject[]
readonly path: string
readonly placeholder?: LabelFunction | string
readonly readOnly?: boolean
readonly required?: boolean
readonly showError?: boolean
readonly style?: React.CSSProperties
readonly value?: string | string[]
}
export const SelectInput: React.FC<SelectInputProps> = (props) => {
const {
AfterInput,
BeforeInput,
className,
Description,
description,
Error,
filterOption,
hasMany = false,
isClearable = true,
isSortable = true,
label,
Label,
localized,
onChange,
onInputChange,
options,
path,
placeholder,
readOnly,
required,
showError,
style,
value,
} = props
const { i18n } = useTranslation()
let valueToRender
if (hasMany && Array.isArray(value)) {
valueToRender = value.map((val) => {
const matchingOption = options.find((option) => option.value === val)
return {
label: matchingOption ? getTranslation(matchingOption.label, i18n) : val,
value: matchingOption?.value ?? val,
}
})
} else if (value) {
const matchingOption = options.find((option) => option.value === value)
valueToRender = {
label: matchingOption ? getTranslation(matchingOption.label, i18n) : value,
value: matchingOption?.value ?? value,
}
} else {
// If value is not present then render nothing, allowing select fields to reset to their initial 'Select an option' state
valueToRender = null
}
return (
<div
className={[
fieldBaseClass,
'select',
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={style}
>
<RenderCustomComponent
CustomComponent={Label}
Fallback={
<FieldLabel label={label} localized={localized} path={path} required={required} />
}
/>
<div className={`${fieldBaseClass}__wrap`}>
<RenderCustomComponent
CustomComponent={Error}
Fallback={<FieldError path={path} showError={showError} />}
/>
{BeforeInput}
<ReactSelect
disabled={readOnly}
filterOption={filterOption}
isClearable={isClearable}
isMulti={hasMany}
isSortable={isSortable}
onChange={onChange}
onInputChange={onInputChange}
options={options.map((option) => ({
...option,
label: getTranslation(option.label, i18n),
}))}
placeholder={placeholder}
showError={showError}
value={valueToRender as OptionObject}
/>
{AfterInput}
</div>
<RenderCustomComponent
CustomComponent={Description}
Fallback={<FieldDescription description={description} path={path} />}
/>
</div>
)
}