fix(ui): executes filterOptions on the server (#5335)

This commit is contained in:
Jacob Fletcher
2024-03-14 16:53:24 -04:00
committed by GitHub
parent bff83f1785
commit f85e96acac
21 changed files with 112 additions and 151 deletions

View File

@@ -1,4 +1,5 @@
import type { ClientValidate, Field } from '../../fields/config/types.js' import type { ClientValidate, Field } from '../../fields/config/types.js'
import type { Where } from '../../types/index.js'
export type Data = { export type Data = {
[key: string]: any [key: string]: any
@@ -11,11 +12,16 @@ export type Row = {
id: string id: string
} }
export type FilterOptionsResult = {
[relation: string]: Where | boolean
}
export type FormField = { export type FormField = {
disableFormData?: boolean disableFormData?: boolean
errorMessage?: string errorMessage?: string
errorPaths?: Set<string> errorPaths?: Set<string>
fieldSchema?: Field fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue: unknown initialValue: unknown
passesCondition?: boolean passesCondition?: boolean
rows?: Row[] rows?: Row[]

View File

@@ -23,9 +23,10 @@ export type {
DescriptionComponent, DescriptionComponent,
DescriptionFunction, DescriptionFunction,
} from './forms/FieldDescription.js' } from './forms/FieldDescription.js'
export type { Data, FormField, FormState, Row } from './forms/Form.js' export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
export type { LabelProps } from './forms/Label.js' export type { LabelProps } from './forms/Label.js'
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js' export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
export type { export type {
AdminViewComponent, AdminViewComponent,
AdminViewProps, AdminViewProps,

View File

@@ -32,7 +32,8 @@ export type ServerOnlyGlobalAdminProperties = keyof Pick<
export type ServerOnlyLivePreviewProperties = keyof Pick<LivePreviewConfig, 'url'> export type ServerOnlyLivePreviewProperties = keyof Pick<LivePreviewConfig, 'url'>
export type ServerOnlyFieldProperties = export type ServerOnlyFieldProperties =
| 'editor' | 'editor' // This is a `richText` only property
| 'filterOptions' // This is a `relationship` and `upload` only property
| 'label' | 'label'
| keyof Pick<FieldBase, 'access' | 'defaultValue' | 'hooks' | 'validate'> | keyof Pick<FieldBase, 'access' | 'defaultValue' | 'hooks' | 'validate'>
@@ -79,8 +80,8 @@ export const sanitizeField = (f: Field) => {
'validate', 'validate',
'defaultValue', 'defaultValue',
'label', 'label',
// This is a `richText` only property 'filterOptions', // This is a `relationship` and `upload` only property
'editor', 'editor', // This is a `richText` only property
// `fields` // `fields`
// `blocks` // `blocks`
// `tabs` // `tabs`

View File

@@ -1,64 +0,0 @@
import type { FilterOptions } from 'payload/types'
import equal from 'deep-equal'
import { useEffect } from 'react'
import type { FilterOptionsResult } from '../../forms/fields/Relationship/types.js'
import { useAllFormFields } from '../../forms/Form/context.js'
import getSiblingData from '../../forms/Form/getSiblingData.js'
import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues.js'
import { getFilterOptionsQuery } from '../../forms/fields/getFilterOptionsQuery.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
type Args = {
filterOptions: FilterOptions
filterOptionsResult: FilterOptionsResult
path: string
relationTo: string | string[]
setFilterOptionsResult: (optionFilters: FilterOptionsResult) => void
}
export const GetFilterOptions = ({
filterOptions,
filterOptionsResult,
path,
relationTo,
setFilterOptionsResult,
}: Args): null => {
const [fields] = useAllFormFields()
const { id } = useDocumentInfo()
const { user } = useAuth()
useEffect(() => {
const data = reduceFieldsToValues(fields, true)
const siblingData = getSiblingData(fields, path)
const getFilterOptions = async () => {
const newFilterOptionsResult = await getFilterOptionsQuery(filterOptions, {
id,
data,
relationTo,
siblingData,
user,
})
if (!equal(newFilterOptionsResult, filterOptionsResult)) {
setFilterOptionsResult(newFilterOptionsResult)
}
}
getFilterOptions()
}, [
fields,
filterOptions,
id,
relationTo,
user,
path,
filterOptionsResult,
setFilterOptionsResult,
])
return null
}

View File

@@ -1,9 +1,7 @@
import type { SanitizedCollectionConfig } from 'payload/types' import type { FilterOptionsResult, SanitizedCollectionConfig } from 'payload/types'
import type React from 'react' import type React from 'react'
import type { HTMLAttributes } from 'react' import type { HTMLAttributes } from 'react'
import type { FilterOptionsResult } from '../../forms/fields/Relationship/types.js'
export type ListDrawerProps = { export type ListDrawerProps = {
collectionSlugs: string[] collectionSlugs: string[]
customHeader?: React.ReactNode customHeader?: React.ReactNode

View File

@@ -21,6 +21,8 @@ export { RenderFields } from '../forms/RenderFields/index.js'
export { useRowLabel } from '../forms/RowLabel/Context/index.js' export { useRowLabel } from '../forms/RowLabel/Context/index.js'
export { default as FormSubmit } from '../forms/Submit/index.js' export { default as FormSubmit } from '../forms/Submit/index.js'
export { default as Submit } from '../forms/Submit/index.js' export { default as Submit } from '../forms/Submit/index.js'
export { buildStateFromSchema } from '../forms/buildStateFromSchema/index.js'
export type { BuildFormStateArgs } from '../forms/buildStateFromSchema/index.js'
export { default as SectionTitle } from '../forms/fields/Blocks/SectionTitle/index.js' export { default as SectionTitle } from '../forms/fields/Blocks/SectionTitle/index.js'
export { CheckboxInput } from '../forms/fields/Checkbox/Input.js' export { CheckboxInput } from '../forms/fields/Checkbox/Input.js'
export { default as Checkbox } from '../forms/fields/Checkbox/index.js' export { default as Checkbox } from '../forms/fields/Checkbox/index.js'
@@ -38,16 +40,14 @@ export { default as Text } from '../forms/fields/Text/index.js'
export type { Props as TextFieldProps } from '../forms/fields/Text/types.js' export type { Props as TextFieldProps } from '../forms/fields/Text/types.js'
export { type TextAreaInputProps, TextareaInput } from '../forms/fields/Textarea/Input.js' export { type TextAreaInputProps, TextareaInput } from '../forms/fields/Textarea/Input.js'
export { default as Textarea } from '../forms/fields/Textarea/index.js' export { default as Textarea } from '../forms/fields/Textarea/index.js'
export { UploadInput, type UploadInputProps } from '../forms/fields/Upload/Input.js' export { UploadInput, type UploadInputProps } from '../forms/fields/Upload/Input.js'
export { default as UploadField } from '../forms/fields/Upload/index.js' export { default as UploadField } from '../forms/fields/Upload/index.js'
export { fieldTypes } from '../forms/fields/index.js' export { fieldTypes } from '../forms/fields/index.js'
export { fieldBaseClass } from '../forms/fields/shared.js' export { fieldBaseClass } from '../forms/fields/shared.js'
export { useField } from '../forms/useField/index.js' export { useField } from '../forms/useField/index.js'
export type { FieldType, Options } from '../forms/useField/types.js' export type { FieldType, Options } from '../forms/useField/types.js'
export { default as buildStateFromSchema } from '../forms/utilities/buildStateFromSchema/index.js'
export type { BuildFormStateArgs } from '../forms/utilities/buildStateFromSchema/index.js'
export { withCondition } from '../forms/withCondition/index.js' export { withCondition } from '../forms/withCondition/index.js'
export { buildComponentMap } from '../utilities/buildComponentMap/index.js' export { buildComponentMap } from '../utilities/buildComponentMap/index.js'

View File

@@ -11,6 +11,7 @@ import ObjectIdImport from 'bson-objectid'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types' import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types'
import { getDefaultValue } from 'payload/utilities' import { getDefaultValue } from 'payload/utilities'
import { getFilterOptionsQuery } from './getFilterOptionsQuery.js'
import { iterateFields } from './iterateFields.js' import { iterateFields } from './iterateFields.js'
const ObjectId = (ObjectIdImport.default || const ObjectId = (ObjectIdImport.default ||
@@ -92,6 +93,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
const validate = operation === 'update' ? field.validate : undefined const validate = operation === 'update' ? field.validate : undefined
const fieldState: FormField = { const fieldState: FormField = {
errorPaths: new Set(), errorPaths: new Set(),
fieldSchema: includeSchema ? field : undefined, fieldSchema: includeSchema ? field : undefined,
@@ -375,6 +377,18 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
} }
case 'relationship': { case 'relationship': {
if (typeof field.filterOptions === 'function') {
const query = await getFilterOptionsQuery(field.filterOptions, {
id,
data: fullData,
relationTo: field.relationTo,
siblingData: data,
user: req.user,
})
fieldState.filterOptions = query
}
if (field.hasMany) { if (field.hasMany) {
const relationshipValue = Array.isArray(valueWithDefault) const relationshipValue = Array.isArray(valueWithDefault)
? valueWithDefault.map((relationship) => { ? valueWithDefault.map((relationship) => {
@@ -433,6 +447,18 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
} }
case 'upload': { case 'upload': {
if (typeof field.filterOptions === 'function') {
const query = await getFilterOptionsQuery(field.filterOptions, {
id,
data: fullData,
relationTo: field.relationTo,
siblingData: data,
user: req.user,
})
fieldState.filterOptions = query
}
const relationshipValue = const relationshipValue =
valueWithDefault && typeof valueWithDefault === 'object' && 'id' in valueWithDefault valueWithDefault && typeof valueWithDefault === 'object' && 'id' in valueWithDefault
? valueWithDefault.id ? valueWithDefault.id

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import buildStateFromSchema from './index.js' import buildStateFromSchema from './index.js'
describe('Form - buildStateFromSchema', () => { describe('Form - buildStateFromSchema', () => {

View File

@@ -5,8 +5,11 @@ export const getFilterOptionsQuery = async (
options: Omit<FilterOptionsProps, 'relationTo'> & { relationTo: string | string[] }, options: Omit<FilterOptionsProps, 'relationTo'> & { relationTo: string | string[] },
): Promise<{ [collection: string]: Where }> => { ): Promise<{ [collection: string]: Where }> => {
const { relationTo } = options const { relationTo } = options
const relations = Array.isArray(relationTo) ? relationTo : [relationTo] const relations = Array.isArray(relationTo) ? relationTo : [relationTo]
const query = {} const query = {}
if (typeof filterOptions !== 'undefined') { if (typeof filterOptions !== 'undefined') {
await Promise.all( await Promise.all(
relations.map(async (relation) => { relations.map(async (relation) => {
@@ -14,9 +17,11 @@ export const getFilterOptionsQuery = async (
typeof filterOptions === 'function' typeof filterOptions === 'function'
? await filterOptions({ ...options, relationTo: relation }) ? await filterOptions({ ...options, relationTo: relation })
: filterOptions : filterOptions
if (query[relation] === true) { if (query[relation] === true) {
query[relation] = {} query[relation] = {}
} }
// this is an ugly way to prevent results from being returned // this is an ugly way to prevent results from being returned
if (query[relation] === false) { if (query[relation] === false) {
query[relation] = { id: { exists: false } } query[relation] = { id: { exists: false } }
@@ -24,5 +29,6 @@ export const getFilterOptionsQuery = async (
}), }),
) )
} }
return query return query
} }

View File

@@ -24,7 +24,7 @@ export type BuildFormStateArgs = {
schemaPath: string schemaPath: string
} }
const buildStateFromSchema = async (args: Args): Promise<FormState> => { export const buildStateFromSchema = async (args: Args): Promise<FormState> => {
const { id, data: fullData = {}, fieldSchema, operation, preferences, req } = args const { id, data: fullData = {}, fieldSchema, operation, preferences, req } = args
if (fieldSchema) { if (fieldSchema) {
@@ -49,5 +49,3 @@ const buildStateFromSchema = async (args: Args): Promise<FormState> => {
return {} return {}
} }
export default buildStateFromSchema

View File

@@ -7,9 +7,8 @@ import qs from 'qs'
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react' import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js' import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js'
import type { FilterOptionsResult, GetResults, Option, Props, Value } from './types.js' import type { GetResults, Option, Props, Value } from './types.js'
import { GetFilterOptions } from '../../../elements/GetFilterOptions/index.js'
import ReactSelect from '../../../elements/ReactSelect/index.js' import ReactSelect from '../../../elements/ReactSelect/index.js'
import { useDebouncedCallback } from '../../../hooks/useDebouncedCallback.js' import { useDebouncedCallback } from '../../../hooks/useDebouncedCallback.js'
import { useAuth } from '../../../providers/Auth/index.js' import { useAuth } from '../../../providers/Auth/index.js'
@@ -57,7 +56,6 @@ const Relationship: React.FC<Props> = (props) => {
const relationTo = 'relationTo' in props ? props?.relationTo : undefined const relationTo = 'relationTo' in props ? props?.relationTo : undefined
const hasMany = 'hasMany' in props ? props?.hasMany : undefined const hasMany = 'hasMany' in props ? props?.hasMany : undefined
const filterOptions = 'filterOptions' in props ? props?.filterOptions : undefined
const sortOptions = 'sortOptions' in props ? props?.sortOptions : undefined const sortOptions = 'sortOptions' in props ? props?.sortOptions : undefined
const isSortable = 'isSortable' in props ? props?.isSortable : true const isSortable = 'isSortable' in props ? props?.isSortable : true
const allowCreate = 'allowCreate' in props ? props?.allowCreate : true const allowCreate = 'allowCreate' in props ? props?.allowCreate : true
@@ -71,7 +69,6 @@ const Relationship: React.FC<Props> = (props) => {
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1) const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1)
const [lastLoadedPage, setLastLoadedPage] = useState<Record<string, number>>({}) const [lastLoadedPage, setLastLoadedPage] = useState<Record<string, number>>({})
const [errorLoading, setErrorLoading] = useState('') const [errorLoading, setErrorLoading] = useState('')
const [filterOptionsResult, setFilterOptionsResult] = useState<FilterOptionsResult>()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [hasLoadedFirstPage, setHasLoadedFirstPage] = useState(false) const [hasLoadedFirstPage, setHasLoadedFirstPage] = useState(false)
@@ -87,7 +84,9 @@ const Relationship: React.FC<Props> = (props) => {
[validate, required], [validate, required],
) )
const { initialValue, path, setValue, showError, value } = useField<Value | Value[]>({ const { filterOptions, initialValue, path, setValue, showError, value } = useField<
Value | Value[]
>({
path: pathFromProps || name, path: pathFromProps || name,
validate: memoizedValidate, validate: memoizedValidate,
}) })
@@ -123,7 +122,8 @@ const Relationship: React.FC<Props> = (props) => {
if (!errorLoading) { if (!errorLoading) {
await relationsToFetch.reduce(async (priorRelation, relation) => { await relationsToFetch.reduce(async (priorRelation, relation) => {
const relationFilterOption = filterOptionsResult?.[relation] const relationFilterOption = filterOptions?.[relation]
let lastLoadedPageToUse let lastLoadedPageToUse
if (search !== searchArg) { if (search !== searchArg) {
lastLoadedPageToUse = 1 lastLoadedPageToUse = 1
@@ -246,7 +246,7 @@ const Relationship: React.FC<Props> = (props) => {
lastLoadedPage, lastLoadedPage,
collections, collections,
locale, locale,
filterOptionsResult, filterOptions,
serverURL, serverURL,
sortOptions, sortOptions,
api, api,
@@ -362,7 +362,7 @@ const Relationship: React.FC<Props> = (props) => {
setEnableWordBoundarySearch(!isIdOnly) setEnableWordBoundarySearch(!isIdOnly)
}, [relationTo, collections]) }, [relationTo, collections])
// When (`relationTo` || `filterOptionsResult` || `locale`) changes, reset component // When (`relationTo` || `filterOptions` || `locale`) changes, reset component
// Note - effect should not run on first run // Note - effect should not run on first run
useEffect(() => { useEffect(() => {
if (firstRun.current) { if (firstRun.current) {
@@ -374,7 +374,7 @@ const Relationship: React.FC<Props> = (props) => {
setLastFullyLoadedRelation(-1) setLastFullyLoadedRelation(-1)
setLastLoadedPage({}) setLastLoadedPage({})
setHasLoadedFirstPage(false) setHasLoadedFirstPage(false)
}, [relationTo, filterOptionsResult, locale]) }, [relationTo, filterOptions, locale])
const onSave = useCallback<DocumentDrawerProps['onSave']>( const onSave = useCallback<DocumentDrawerProps['onSave']>(
(args) => { (args) => {
@@ -435,15 +435,6 @@ const Relationship: React.FC<Props> = (props) => {
> >
{Error} {Error}
{Label} {Label}
<GetFilterOptions
{...{
filterOptions,
filterOptionsResult,
path,
relationTo,
setFilterOptionsResult,
}}
/>
{!errorLoading && ( {!errorLoading && (
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<ReactSelect <ReactSelect

View File

@@ -1,7 +1,6 @@
import type { I18n } from '@payloadcms/translations' import type { I18n } from '@payloadcms/translations'
import type { SanitizedCollectionConfig } from 'payload/types' import type { SanitizedCollectionConfig } from 'payload/types'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import type { Where } from 'payload/types'
import type { FormFieldBase } from '../shared.js' import type { FormFieldBase } from '../shared.js'
@@ -59,7 +58,3 @@ export type GetResults = (args: {
sort?: boolean sort?: boolean
value?: Value | Value[] value?: Value | Value[]
}) => Promise<void> }) => Promise<void>
export type FilterOptionsResult = {
[relation: string]: Where | boolean
}

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import type { SanitizedCollectionConfig, UploadField } from 'payload/types' import type { FilterOptionsResult, SanitizedCollectionConfig, UploadField } from 'payload/types'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js' import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../../elements/ListDrawer/types.js' import type { ListDrawerProps } from '../../../elements/ListDrawer/types.js'
import type { FilterOptionsResult } from '../Relationship/types.js'
import { Button } from '../../../elements/Button/index.js' import { Button } from '../../../elements/Button/index.js'
import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js' import { useDocumentDrawer } from '../../../elements/DocumentDrawer/index.js'
@@ -23,7 +22,7 @@ const baseClass = 'upload'
export type UploadInputProps = FormFieldBase & { export type UploadInputProps = FormFieldBase & {
api?: string api?: string
collection?: SanitizedCollectionConfig collection?: SanitizedCollectionConfig
filterOptions?: UploadField['filterOptions'] filterOptions?: FilterOptionsResult
onChange?: (e) => void onChange?: (e) => void
relationTo?: UploadField['relationTo'] relationTo?: UploadField['relationTo']
serverURL?: string serverURL?: string
@@ -34,12 +33,12 @@ export type UploadInputProps = FormFieldBase & {
export const UploadInput: React.FC<UploadInputProps> = (props) => { export const UploadInput: React.FC<UploadInputProps> = (props) => {
const { const {
Description, Description,
Error, Error,
Label: LabelFromProps, Label: LabelFromProps,
api = '/api', api = '/api',
className, className,
collection, collection,
filterOptions,
label, label,
onChange, onChange,
readOnly, readOnly,
@@ -59,7 +58,6 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
const [file, setFile] = useState(undefined) const [file, setFile] = useState(undefined)
const [missingFile, setMissingFile] = useState(false) const [missingFile, setMissingFile] = useState(false)
const [collectionSlugs] = useState([collection?.slug]) const [collectionSlugs] = useState([collection?.slug])
const [filterOptionsResult, setFilterOptionsResult] = useState<FilterOptionsResult>()
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({ const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
collectionSlug: collectionSlugs[0], collectionSlug: collectionSlugs[0],
@@ -67,7 +65,7 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
const [ListDrawer, ListDrawerToggler, { closeDrawer: closeListDrawer }] = useListDrawer({ const [ListDrawer, ListDrawerToggler, { closeDrawer: closeListDrawer }] = useListDrawer({
collectionSlugs, collectionSlugs,
filterOptions: filterOptionsResult, filterOptions,
}) })
useEffect(() => { useEffect(() => {
@@ -131,15 +129,6 @@ export const UploadInput: React.FC<UploadInputProps> = (props) => {
width, width,
}} }}
> >
{/* <GetFilterOptions
{...{
filterOptions,
filterOptionsResult,
path,
relationTo,
setFilterOptionsResult,
}}
/> */}
{Error} {Error}
{Label} {Label}
{collection?.upload && ( {collection?.upload && (

View File

@@ -15,7 +15,6 @@ const Upload: React.FC<Props> = (props) => {
Error, Error,
Label: LabelFromProps, Label: LabelFromProps,
className, className,
filterOptions,
label, label,
path: pathFromProps, path: pathFromProps,
readOnly, readOnly,
@@ -45,7 +44,7 @@ const Upload: React.FC<Props> = (props) => {
[validate, required], [validate, required],
) )
const { path, setValue, showError, value } = useField<string>({ const { filterOptions, path, setValue, showError, value } = useField<string>({
path: pathFromProps, path: pathFromProps,
validate: memoizedValidate, validate: memoizedValidate,
}) })

View File

@@ -1,4 +1,4 @@
import type { FieldPermissions, User } from 'payload/auth' import type { User } from 'payload/auth'
import type { Locale, SanitizedLocalizationConfig } from 'payload/config' import type { Locale, SanitizedLocalizationConfig } from 'payload/config'
import type { import type {
ArrayField, ArrayField,
@@ -51,6 +51,21 @@ export type FormFieldBase = {
validate?: Validate validate?: Validate
width?: string width?: string
} & ( } & (
| {
// For `array` fields
label?: RowLabel
labels?: ArrayField['labels']
maxRows?: ArrayField['maxRows']
minRows?: ArrayField['minRows']
}
| {
// For `blocks` fields
blocks?: ReducedBlock[]
labels?: BlockField['labels']
maxRows?: BlockField['maxRows']
minRows?: BlockField['minRows']
slug?: string
}
| { | {
// For `code` fields // For `code` fields
editorOptions?: CodeField['admin']['editorOptions'] editorOptions?: CodeField['admin']['editorOptions']
@@ -68,11 +83,25 @@ export type FormFieldBase = {
// For `json` fields // For `json` fields
editorOptions?: JSONField['admin']['editorOptions'] editorOptions?: JSONField['admin']['editorOptions']
} }
| {
// For `number` fields
hasMany?: boolean
max?: number
maxRows?: number
min?: number
step?: number
}
| { | {
// For `radio` fields // For `radio` fields
layout?: 'horizontal' | 'vertical' layout?: 'horizontal' | 'vertical'
options?: Option[] options?: Option[]
} }
| {
// For `relationship` fields
allowCreate?: RelationshipField['admin']['allowCreate']
relationTo?: RelationshipField['relationTo']
sortOptions?: RelationshipField['admin']['sortOptions']
}
| { | {
// For `richText` fields // For `richText` fields
richTextComponentMap?: Map<string, MappedField[] | React.ReactNode> richTextComponentMap?: Map<string, MappedField[] | React.ReactNode>
@@ -90,36 +119,6 @@ export type FormFieldBase = {
// For `upload` fields // For `upload` fields
relationTo?: UploadField['relationTo'] relationTo?: UploadField['relationTo']
} }
| {
allowCreate?: RelationshipField['admin']['allowCreate']
filterOptions?: RelationshipField['filterOptions']
// For `relationship` fields
relationTo?: RelationshipField['relationTo']
sortOptions?: RelationshipField['admin']['sortOptions']
}
| {
blocks?: ReducedBlock[]
labels?: BlockField['labels']
maxRows?: BlockField['maxRows']
minRows?: BlockField['minRows']
// For `blocks` fields
slug?: string
}
| {
hasMany?: boolean
max?: number
maxRows?: number
min?: number
// For `number` fields
step?: number
}
| {
label?: RowLabel
labels?: ArrayField['labels']
maxRows?: ArrayField['maxRows']
// For `array` fields
minRows?: ArrayField['minRows']
}
| { | {
tabs?: MappedTab[] tabs?: MappedTab[]
} }

View File

@@ -41,6 +41,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
const { getData, getDataByPath, getSiblingData, setModified } = useForm() const { getData, getDataByPath, getSiblingData, setModified } = useForm()
const filterOptions = field?.filterOptions
const value = field?.value as T const value = field?.value as T
const initialValue = field?.initialValue as T const initialValue = field?.initialValue as T
const valid = typeof field?.valid === 'boolean' ? field.valid : true const valid = typeof field?.valid === 'boolean' ? field.valid : true
@@ -79,6 +80,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
const result: FieldType<T> = useMemo( const result: FieldType<T> = useMemo(
() => ({ () => ({
errorMessage: field?.errorMessage, errorMessage: field?.errorMessage,
filterOptions,
formProcessing: processing, formProcessing: processing,
formSubmitted: submitted, formSubmitted: submitted,
initialValue, initialValue,
@@ -106,6 +108,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
schemaPath, schemaPath,
readOnly, readOnly,
permissions, permissions,
filterOptions,
], ],
) )

View File

@@ -1,4 +1,4 @@
import type { ClientValidate, FieldPermissions, Row } from 'payload/types' import type { ClientValidate, FieldPermissions, FilterOptionsResult, Row } from 'payload/types'
export type Options = { export type Options = {
disableFormData?: boolean disableFormData?: boolean
@@ -12,6 +12,7 @@ export type Options = {
export type FieldType<T> = { export type FieldType<T> = {
errorMessage?: string errorMessage?: string
filterOptions?: FilterOptionsResult
formProcessing: boolean formProcessing: boolean
formSubmitted: boolean formSubmitted: boolean
initialValue?: T initialValue?: T

View File

@@ -1,6 +1,6 @@
import type { FormState, SanitizedConfig } from 'payload/types' import type { FormState, SanitizedConfig } from 'payload/types'
import type { BuildFormStateArgs } from '../forms/utilities/buildStateFromSchema/index.js' import type { BuildFormStateArgs } from '../forms/buildStateFromSchema/index.js'
export const getFormState = async (args: { export const getFormState = async (args: {
apiRoute: SanitizedConfig['routes']['api'] apiRoute: SanitizedConfig['routes']['api']

View File

@@ -21,6 +21,17 @@ export const PostsCollection: CollectionConfig = {
{ {
name: 'relationship', name: 'relationship',
type: 'relationship', type: 'relationship',
filterOptions: ({ id }) => {
return {
where: [
{
id: {
not_equals: id,
},
},
],
}
},
relationTo: ['posts'], relationTo: ['posts'],
}, },
{ {

View File

@@ -88,7 +88,7 @@
"./packages/graphql/src" "./packages/graphql/src"
], ],
"@payload-config": [ "@payload-config": [
"./test/access-control/config.ts" "./test/_community/config.ts"
] ]
} }
}, },