diff --git a/packages/plugin-import-export/src/components/FieldsToExport/index.tsx b/packages/plugin-import-export/src/components/FieldsToExport/index.tsx index 1a33f1fe1..67461e167 100644 --- a/packages/plugin-import-export/src/components/FieldsToExport/index.tsx +++ b/packages/plugin-import-export/src/components/FieldsToExport/index.tsx @@ -20,8 +20,7 @@ const baseClass = 'fields-to-export' export const FieldsToExport: SelectFieldClientComponent = (props) => { const { id } = useDocumentInfo() - const { path } = props - const { setValue, value } = useField({ path }) + const { setValue, value } = useField() const { value: collectionSlug } = useField({ path: 'collectionSlug' }) const { getEntityConfig } = useConfig() const { collection } = useImportExport() diff --git a/packages/plugin-import-export/src/components/SortBy/index.tsx b/packages/plugin-import-export/src/components/SortBy/index.tsx index 75d7cbb29..0d06a2116 100644 --- a/packages/plugin-import-export/src/components/SortBy/index.tsx +++ b/packages/plugin-import-export/src/components/SortBy/index.tsx @@ -20,12 +20,12 @@ const baseClass = 'sort-by-fields' export const SortBy: SelectFieldClientComponent = (props) => { const { id } = useDocumentInfo() - const { path } = props - const { setValue, value } = useField({ path }) + const { setValue, value } = useField() const { value: collectionSlug } = useField({ path: 'collectionSlug' }) const { query } = useListQuery() const { getEntityConfig } = useConfig() const { collection } = useImportExport() + const [displayedValue, setDisplayedValue] = useState<{ id: string label: ReactNode diff --git a/packages/plugin-import-export/src/components/WhereField/index.tsx b/packages/plugin-import-export/src/components/WhereField/index.tsx index 02c1d3db3..68f4daf65 100644 --- a/packages/plugin-import-export/src/components/WhereField/index.tsx +++ b/packages/plugin-import-export/src/components/WhereField/index.tsx @@ -11,6 +11,7 @@ export const WhereField: React.FC = () => { const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({ path: 'selectionToUse', }) + const { setValue } = useField({ path: 'where' }) const { selectAll, selected } = useSelection() const { query } = useListQuery() diff --git a/packages/plugin-multi-tenant/src/components/TenantField/index.client.tsx b/packages/plugin-multi-tenant/src/components/TenantField/index.client.tsx index 4f6605e60..b0a8bf0b9 100644 --- a/packages/plugin-multi-tenant/src/components/TenantField/index.client.tsx +++ b/packages/plugin-multi-tenant/src/components/TenantField/index.client.tsx @@ -16,8 +16,8 @@ type Props = { } & RelationshipFieldClientProps export const TenantField = (args: Props) => { - const { debug, path, unique } = args - const { setValue, value } = useField({ path }) + const { debug, unique } = args + const { setValue, value } = useField() const { options, selectedTenantID, setPreventRefreshOnChange, setTenant } = useTenantSelection() const hasSetValueRef = React.useRef(false) diff --git a/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx index 5a544a9f3..7b93ff377 100644 --- a/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx +++ b/packages/plugin-seo/src/fields/MetaDescription/MetaDescriptionComponent.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FieldType, Options } from '@payloadcms/ui' +import type { FieldType } from '@payloadcms/ui' import type { TextareaFieldClientProps } from 'payload' import { @@ -38,7 +38,6 @@ export const MetaDescriptionComponent: React.FC = (props) required, }, hasGenerateDescriptionFn, - path, readOnly, } = props @@ -58,12 +57,14 @@ export const MetaDescriptionComponent: React.FC = (props) const maxLength = maxLengthFromProps || maxLengthDefault const minLength = minLengthFromProps || minLengthDefault - const { customComponents, errorMessage, setValue, showError, value }: FieldType = - useField({ - path, - } as Options) - - const { AfterInput, BeforeInput, Label } = customComponents ?? {} + const { + customComponents: { AfterInput, BeforeInput, Label } = {}, + errorMessage, + path, + setValue, + showError, + value, + }: FieldType = useField() const regenerateDescription = useCallback(async () => { if (!hasGenerateDescriptionFn) { diff --git a/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx b/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx index 987c4a204..acfc2aaef 100644 --- a/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx +++ b/packages/plugin-seo/src/fields/MetaImage/MetaImageComponent.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FieldType, Options } from '@payloadcms/ui' +import type { FieldType } from '@payloadcms/ui' import type { UploadFieldClientProps } from 'payload' import { @@ -30,9 +30,8 @@ export const MetaImageComponent: React.FC = (props) => { const { field: { label, localized, relationTo, required }, hasGenerateImageFn, - path, readOnly, - } = props || {} + } = props const { config: { @@ -42,10 +41,14 @@ export const MetaImageComponent: React.FC = (props) => { getEntityConfig, } = useConfig() - const field: FieldType = useField({ ...props, path } as Options) - const { customComponents } = field - - const { Error, Label } = customComponents ?? {} + const { + customComponents: { Error, Label } = {}, + filterOptions, + path, + setValue, + showError, + value, + }: FieldType = useField() const { t } = useTranslation() @@ -53,8 +56,6 @@ export const MetaImageComponent: React.FC = (props) => { const { getData } = useForm() const docInfo = useDocumentInfo() - const { setValue, showError, value } = field - const regenerateImage = useCallback(async () => { if (!hasGenerateImageFn) { return @@ -174,7 +175,7 @@ export const MetaImageComponent: React.FC = (props) => { api={api} collection={collection} Error={Error} - filterOptions={field.filterOptions} + filterOptions={filterOptions} onChange={(incomingImage) => { if (incomingImage !== null) { if (typeof incomingImage === 'object') { diff --git a/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx b/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx index e1496ca63..4d7e57b11 100644 --- a/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx +++ b/packages/plugin-seo/src/fields/MetaTitle/MetaTitleComponent.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FieldType, Options } from '@payloadcms/ui' +import type { FieldType } from '@payloadcms/ui' import type { TextFieldClientProps } from 'payload' import { @@ -33,9 +33,8 @@ export const MetaTitleComponent: React.FC = (props) => { const { field: { label, maxLength: maxLengthFromProps, minLength: minLengthFromProps, required }, hasGenerateTitleFn, - path, readOnly, - } = props || {} + } = props const { t } = useTranslation() @@ -46,8 +45,14 @@ export const MetaTitleComponent: React.FC = (props) => { }, } = useConfig() - const field: FieldType = useField({ path } as Options) - const { customComponents: { AfterInput, BeforeInput, Label } = {} } = field + const { + customComponents: { AfterInput, BeforeInput, Label } = {}, + errorMessage, + path, + setValue, + showError, + value, + }: FieldType = useField() const locale = useLocale() const { getData } = useForm() @@ -56,8 +61,6 @@ export const MetaTitleComponent: React.FC = (props) => { const minLength = minLengthFromProps || minLengthDefault const maxLength = maxLengthFromProps || maxLengthDefault - const { errorMessage, setValue, showError, value } = field - const regenerateTitle = useCallback(async () => { if (!hasGenerateTitleFn) { return diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 65edb6ede..1f5bc2825 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -36,7 +36,6 @@ const RichTextComponent: React.FC< editorConfig, field, field: { - name, admin: { className, description, readOnly: readOnlyFromAdmin } = {}, label, localized, @@ -48,7 +47,6 @@ const RichTextComponent: React.FC< } = props const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin - const path = pathFromProps ?? name const editDepth = useEditDepth() @@ -70,11 +68,12 @@ const RichTextComponent: React.FC< customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled: disabledFromField, initialValue, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/richtext-slate/src/field/RichText.tsx b/packages/richtext-slate/src/field/RichText.tsx index 1effc7eb0..fbcb1df13 100644 --- a/packages/richtext-slate/src/field/RichText.tsx +++ b/packages/richtext-slate/src/field/RichText.tsx @@ -28,7 +28,6 @@ import type { LoadedSlateFieldProps } from './types.js' import { defaultRichTextValue } from '../data/defaultValue.js' import { richTextValidate } from '../data/validation.js' import { listTypes } from './elements/listTypes.js' -import './index.scss' import { hotkeys } from './hotkeys.js' import { toggleLeaf } from './leaves/toggle.js' import { withEnterBreakOut } from './plugins/withEnterBreakOut.js' @@ -37,6 +36,7 @@ import { ElementButtonProvider } from './providers/ElementButtonProvider.js' import { ElementProvider } from './providers/ElementProvider.js' import { LeafButtonProvider } from './providers/LeafButtonProvider.js' import { LeafProvider } from './providers/LeafProvider.js' +import './index.scss' const baseClass = 'rich-text' @@ -66,7 +66,6 @@ const RichTextField: React.FC = (props) => { validate = richTextValidate, } = props - const path = pathFromProps ?? name const schemaPath = schemaPathFromProps ?? name const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin @@ -97,11 +96,12 @@ const RichTextField: React.FC = (props) => { customComponents: { Description, Error, Label } = {}, disabled: disabledFromField, initialValue, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/elements/QueryPresets/fields/ColumnsField/index.tsx b/packages/ui/src/elements/QueryPresets/fields/ColumnsField/index.tsx index 78570d667..6c21780e5 100644 --- a/packages/ui/src/elements/QueryPresets/fields/ColumnsField/index.tsx +++ b/packages/ui/src/elements/QueryPresets/fields/ColumnsField/index.tsx @@ -11,9 +11,8 @@ import './index.scss' export const QueryPresetsColumnField: JSONFieldClientComponent = ({ field: { label, required }, - path, }) => { - const { value } = useField({ path }) + const { path, value } = useField() return (
diff --git a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx index 564cbe82b..a44fd868b 100644 --- a/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx +++ b/packages/ui/src/elements/QueryPresets/fields/WhereField/index.tsx @@ -88,9 +88,8 @@ const transformWhereToNaturalLanguage = ( export const QueryPresetsWhereField: JSONFieldClientComponent = ({ field: { label, required }, - path, }) => { - const { value } = useField({ path }) + const { path, value } = useField() const { collectionSlug } = useListQuery() const { getEntityConfig } = useConfig() diff --git a/packages/ui/src/fields/Array/index.tsx b/packages/ui/src/fields/Array/index.tsx index ed2501903..954937599 100644 --- a/packages/ui/src/fields/Array/index.tsx +++ b/packages/ui/src/fields/Array/index.tsx @@ -46,7 +46,7 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { required, }, forceRender = false, - path, + path: pathFromProps, permissions, readOnly, schemaPath: schemaPathFromProps, @@ -113,13 +113,14 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, errorPaths, + path, rows = [], showError, valid, value, } = useField({ hasRows: true, - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Blocks/index.tsx b/packages/ui/src/fields/Blocks/index.tsx index 0dea39644..99a857808 100644 --- a/packages/ui/src/fields/Blocks/index.tsx +++ b/packages/ui/src/fields/Blocks/index.tsx @@ -48,7 +48,7 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { minRows: minRowsProp, required, }, - path, + path: pathFromProps, permissions, readOnly, schemaPath: schemaPathFromProps, @@ -101,13 +101,14 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, errorPaths, + path, rows = [], showError, valid, value, } = useField({ hasRows: true, - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Checkbox/index.tsx b/packages/ui/src/fields/Checkbox/index.tsx index e3fc493cf..2a685e6ea 100644 --- a/packages/ui/src/fields/Checkbox/index.tsx +++ b/packages/ui/src/fields/Checkbox/index.tsx @@ -39,7 +39,7 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => { } = {} as CheckboxFieldClientProps['field'], onChange: onChangeFromProps, partialChecked, - path, + path: pathFromProps, readOnly, validate, } = props @@ -60,12 +60,13 @@ const CheckboxFieldComponent: CheckboxFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ disableFormData, - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Code/index.tsx b/packages/ui/src/fields/Code/index.tsx index 5a9f38e45..143df342e 100644 --- a/packages/ui/src/fields/Code/index.tsx +++ b/packages/ui/src/fields/Code/index.tsx @@ -31,7 +31,7 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => { required, }, onMount, - path, + path: pathFromProps, readOnly, validate, } = props @@ -48,11 +48,12 @@ const CodeFieldComponent: CodeFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/DateTime/index.tsx b/packages/ui/src/fields/DateTime/index.tsx index da801b937..4afbc3c91 100644 --- a/packages/ui/src/fields/DateTime/index.tsx +++ b/packages/ui/src/fields/DateTime/index.tsx @@ -33,7 +33,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { required, timezone, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -59,11 +59,12 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Email/index.tsx b/packages/ui/src/fields/Email/index.tsx index 305ad5b28..6cce6d753 100644 --- a/packages/ui/src/fields/Email/index.tsx +++ b/packages/ui/src/fields/Email/index.tsx @@ -16,8 +16,8 @@ import { withCondition } from '../../forms/withCondition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { FieldLabel } from '../FieldLabel/index.js' import { mergeFieldStyles } from '../mergeFieldStyles.js' -import './index.scss' import { fieldBaseClass } from '../shared/index.js' +import './index.scss' const EmailFieldComponent: EmailFieldClientComponent = (props) => { const { @@ -33,7 +33,7 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => { localized, required, } = {} as EmailFieldClientProps['field'], - path, + path: pathFromProps, readOnly, validate, } = props @@ -52,11 +52,12 @@ const EmailFieldComponent: EmailFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Hidden/index.tsx b/packages/ui/src/fields/Hidden/index.tsx index 90c4e0f98..3938e354d 100644 --- a/packages/ui/src/fields/Hidden/index.tsx +++ b/packages/ui/src/fields/Hidden/index.tsx @@ -8,14 +8,15 @@ import { useField } from '../../forms/useField/index.js' import { withCondition } from '../../forms/withCondition/index.js' /** + * Renders an input with `type="hidden"`. * This is mainly used to save a value on the form that is not visible to the user. * For example, this sets the `ìd` property of a block in the Blocks field. */ const HiddenFieldComponent: React.FC = (props) => { - const { disableModifyingForm = true, path, value: valueFromProps } = props + const { disableModifyingForm = true, path: pathFromProps, value: valueFromProps } = props - const { setValue, value } = useField({ - path, + const { path, setValue, value } = useField({ + potentiallyStalePath: pathFromProps, }) useEffect(() => { diff --git a/packages/ui/src/fields/JSON/index.tsx b/packages/ui/src/fields/JSON/index.tsx index 75754cb18..c0b80a670 100644 --- a/packages/ui/src/fields/JSON/index.tsx +++ b/packages/ui/src/fields/JSON/index.tsx @@ -28,7 +28,7 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { localized, required, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -50,11 +50,12 @@ const JSONFieldComponent: JSONFieldClientComponent = (props) => { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, initialValue, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Join/index.tsx b/packages/ui/src/fields/Join/index.tsx index 1a75eff97..43610cb40 100644 --- a/packages/ui/src/fields/Join/index.tsx +++ b/packages/ui/src/fields/Join/index.tsx @@ -131,7 +131,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => { on, required, }, - path, + path: pathFromProps, } = props const { id: docID, docConfig } = useDocumentInfo() @@ -140,10 +140,11 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, + path, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, }) const filterOptions: null | Where = useMemo(() => { diff --git a/packages/ui/src/fields/Number/index.tsx b/packages/ui/src/fields/Number/index.tsx index 98be6bee3..c7f135a26 100644 --- a/packages/ui/src/fields/Number/index.tsx +++ b/packages/ui/src/fields/Number/index.tsx @@ -38,7 +38,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => { required, }, onChange: onChangeFromProps, - path, + path: pathFromProps, readOnly, validate, } = props @@ -57,11 +57,12 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Point/index.tsx b/packages/ui/src/fields/Point/index.tsx index 3d3ec40a9..dbe86849f 100644 --- a/packages/ui/src/fields/Point/index.tsx +++ b/packages/ui/src/fields/Point/index.tsx @@ -26,7 +26,7 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => { localized, required, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -45,11 +45,12 @@ export const PointFieldComponent: PointFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value = [null, null], } = useField<[number, number]>({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/RadioGroup/index.tsx b/packages/ui/src/fields/RadioGroup/index.tsx index bb5746bda..0542d55b9 100644 --- a/packages/ui/src/fields/RadioGroup/index.tsx +++ b/packages/ui/src/fields/RadioGroup/index.tsx @@ -34,7 +34,7 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => { required, } = {} as RadioFieldClientProps['field'], onChange: onChangeFromProps, - path, + path: pathFromProps, readOnly, validate, value: valueFromProps, @@ -54,11 +54,12 @@ const RadioGroupFieldComponent: RadioFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value: valueFromContext, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Relationship/index.tsx b/packages/ui/src/fields/Relationship/index.tsx index 85a897f2d..449c1e137 100644 --- a/packages/ui/src/fields/Relationship/index.tsx +++ b/packages/ui/src/fields/Relationship/index.tsx @@ -64,7 +64,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) => relationTo, required, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -112,11 +112,12 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) => disabled, filterOptions, initialValue, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Select/index.tsx b/packages/ui/src/fields/Select/index.tsx index f49af6fe6..b8b67af5f 100644 --- a/packages/ui/src/fields/Select/index.tsx +++ b/packages/ui/src/fields/Select/index.tsx @@ -46,7 +46,7 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => { required, }, onChange: onChangeFromProps, - path, + path: pathFromProps, readOnly, validate, } = props @@ -65,11 +65,12 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Tabs/index.tsx b/packages/ui/src/fields/Tabs/index.tsx index 5b7e9b6bf..b8be36c96 100644 --- a/packages/ui/src/fields/Tabs/index.tsx +++ b/packages/ui/src/fields/Tabs/index.tsx @@ -269,15 +269,13 @@ function TabContent({ parentIndexPath, parentPath, parentSchemaPath, - path, permissions, readOnly, }: ActiveTabProps) { const { i18n } = useTranslation() - const { customComponents: { AfterInput, BeforeInput, Description, Field } = {} } = useField({ - path, - }) + const { customComponents: { AfterInput, BeforeInput, Description, Field } = {}, path } = + useField() if (Field) { return Field diff --git a/packages/ui/src/fields/Text/index.tsx b/packages/ui/src/fields/Text/index.tsx index afa9aaa53..637b5efe4 100644 --- a/packages/ui/src/fields/Text/index.tsx +++ b/packages/ui/src/fields/Text/index.tsx @@ -32,7 +32,7 @@ const TextFieldComponent: TextFieldClientComponent = (props) => { required, }, inputRef, - path, + path: pathFromProps, readOnly, validate, } = props @@ -55,11 +55,12 @@ const TextFieldComponent: TextFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Textarea/index.tsx b/packages/ui/src/fields/Textarea/index.tsx index f27695f36..3a3ffba6b 100644 --- a/packages/ui/src/fields/Textarea/index.tsx +++ b/packages/ui/src/fields/Textarea/index.tsx @@ -29,7 +29,7 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => { minLength, required, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -61,11 +61,12 @@ const TextareaFieldComponent: TextareaFieldClientComponent = (props) => { const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/fields/Upload/index.tsx b/packages/ui/src/fields/Upload/index.tsx index 681e5c18e..9fc43b203 100644 --- a/packages/ui/src/fields/Upload/index.tsx +++ b/packages/ui/src/fields/Upload/index.tsx @@ -28,7 +28,7 @@ export function UploadComponent(props: UploadFieldClientProps) { relationTo, required, }, - path, + path: pathFromProps, readOnly, validate, } = props @@ -50,11 +50,12 @@ export function UploadComponent(props: UploadFieldClientProps) { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, filterOptions, + path, setValue, showError, value, } = useField({ - path, + potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) diff --git a/packages/ui/src/forms/RenderFields/context.ts b/packages/ui/src/forms/RenderFields/context.ts new file mode 100644 index 000000000..d6d9da067 --- /dev/null +++ b/packages/ui/src/forms/RenderFields/context.ts @@ -0,0 +1,14 @@ +import React from 'react' + +export const FieldPathContext = React.createContext(undefined) + +export const useFieldPath = () => { + const context = React.useContext(FieldPathContext) + + if (!context) { + // swallow the error, not all fields are wrapped in a FieldPathContext + return undefined + } + + return context +} diff --git a/packages/ui/src/forms/RenderFields/index.tsx b/packages/ui/src/forms/RenderFields/index.tsx index bf447d951..ed662e202 100644 --- a/packages/ui/src/forms/RenderFields/index.tsx +++ b/packages/ui/src/forms/RenderFields/index.tsx @@ -7,8 +7,9 @@ import type { RenderFieldsProps } from './types.js' import { RenderIfInViewport } from '../../elements/RenderIfInViewport/index.js' import { useOperation } from '../../providers/Operation/index.js' -import { RenderField } from './RenderField.js' +import { FieldPathContext } from './context.js' import './index.scss' +import { RenderField } from './RenderField.js' const baseClass = 'render-fields' @@ -90,18 +91,19 @@ export const RenderFields: React.FC = (props) => { }) return ( - + + + ) })} diff --git a/packages/ui/src/forms/useField/index.tsx b/packages/ui/src/forms/useField/index.tsx index f3fc737df..987b81995 100644 --- a/packages/ui/src/forms/useField/index.tsx +++ b/packages/ui/src/forms/useField/index.tsx @@ -23,14 +23,27 @@ import { useFormProcessing, useFormSubmitted, } from '../Form/context.js' +import { useFieldPath } from '../RenderFields/context.js' /** * Get and set the value of a form field. * * @see https://payloadcms.com/docs/admin/react-hooks#usefield */ -export const useField = (options: Options): FieldType => { - const { disableFormData = false, hasRows, path, validate } = options +export const useField = (options?: Options): FieldType => { + const { + disableFormData = false, + hasRows, + path: pathFromOptions, + potentiallyStalePath, + validate, + } = options || {} + + const pathFromContext = useFieldPath() + + // This is a workaround for stale props given to server rendered components. + // See the notes in the `potentiallyStalePath` type definition for more details. + const path = pathFromOptions || pathFromContext || potentiallyStalePath const submitted = useFormSubmitted() const processing = useFormProcessing() diff --git a/packages/ui/src/forms/useField/types.ts b/packages/ui/src/forms/useField/types.ts index 2f43cbab6..e91e60e8b 100644 --- a/packages/ui/src/forms/useField/types.ts +++ b/packages/ui/src/forms/useField/types.ts @@ -3,7 +3,27 @@ import type { FieldState, FilterOptionsResult, Row, Validate } from 'payload' export type Options = { disableFormData?: boolean hasRows?: boolean - path: string + /** + * If `path` is provided to this hook, it will be used outright. This is useful when calling this hook directly within a custom component. + * Otherwise, the field will attempt to get the path from the `FieldPathContext` via the `useFieldPath` hook. + * If still not found, the `potentiallyStalePath` arg will be used. See the note below about why this is important. + */ + path?: string + /** + * Custom server components receive a static `path` prop at render-time, leading to temporarily stale paths when re-ordering rows in form state. + * This is because when manipulating rows, field paths change in form state, but the prop remains the same until the component is re-rendered on the server. + * This causes the component to temporarily point to the wrong field in form state until the server responds with a freshly rendered component. + * To prevent this, fields are wrapped with a `FieldPathContext` which is guaranteed to be up-to-date. + * The `path` prop that Payload's default fields receive, then, are sent into this hook as the `potentiallyStalePath` arg. + * This ensures that: + * 1. Custom components that use this hook directly will still respect the `path` prop as top priority. + * 2. Custom server components that blindly spread their props into default Payload fields still prefer the dynamic path from context. + * 3. Components that render default Payload fields directly do not require a `FieldPathProvider`, e.g. the email field in the account view. + */ + potentiallyStalePath?: string + /** + * Client-side validation function fired when the form is submitted. + */ validate?: Validate } @@ -17,6 +37,7 @@ export type FieldType = { formProcessing: boolean formSubmitted: boolean initialValue?: T + path: string readOnly?: boolean rows?: Row[] setValue: (val: unknown, disableModifyingForm?: boolean) => void diff --git a/test/field-error-states/e2e.spec.ts b/test/field-error-states/e2e.spec.ts index 3398d1b8f..49bb09b55 100644 --- a/test/field-error-states/e2e.spec.ts +++ b/test/field-error-states/e2e.spec.ts @@ -57,7 +57,10 @@ describe('Field Error States', () => { // add third child array await page.locator('#parentArray-row-0 .collapsible__content .array-field__add-row').click() + + // remove the row await page.locator('#parentArray-0-childArray-row-2 .array-actions__button').click() + await page .locator('#parentArray-0-childArray-row-2 .array-actions__action.array-actions__remove') .click() @@ -68,6 +71,7 @@ describe('Field Error States', () => { '#parentArray-row-0 > .collapsible > .collapsible__toggle-wrap .array-field__row-error-pill', { state: 'hidden', timeout: 500 }, ) + expect(errorPill).toBeNull() }) @@ -77,13 +81,11 @@ describe('Field Error States', () => { await saveDocAndAssert(page, '#action-save-draft') }) - // eslint-disable-next-line playwright/expect-expect test('should validate drafts when enabled', async () => { await page.goto(validateDraftsOn.create) await saveDocAndAssert(page, '#action-save-draft', 'error') }) - // eslint-disable-next-line playwright/expect-expect test('should show validation errors when validate and autosave are enabled', async () => { await page.goto(validateDraftsOnAutosave.create) await page.locator('#field-title').fill('valid') diff --git a/test/fields-relationship/PrePopulateFieldUI/index.tsx b/test/fields-relationship/PopulateFieldButton/index.tsx similarity index 91% rename from test/fields-relationship/PrePopulateFieldUI/index.tsx rename to test/fields-relationship/PopulateFieldButton/index.tsx index 1ed66a3c2..c9d8b51e9 100644 --- a/test/fields-relationship/PrePopulateFieldUI/index.tsx +++ b/test/fields-relationship/PopulateFieldButton/index.tsx @@ -4,12 +4,12 @@ import * as React from 'react' import { collection1Slug } from '../slugs.js' -export const PrePopulateFieldUI: React.FC<{ +export const PopulateFieldButton: React.FC<{ hasMany?: boolean hasMultipleRelations?: boolean path?: string targetFieldPath: string -}> = ({ hasMany = true, hasMultipleRelations = false, path, targetFieldPath }) => { +}> = ({ hasMany = true, hasMultipleRelations = false, targetFieldPath }) => { const { setValue } = useField({ path: targetFieldPath }) const addDefaults = React.useCallback(() => { diff --git a/test/fields-relationship/collections/UpdatedExternally/index.ts b/test/fields-relationship/collections/UpdatedExternally/index.ts index 8d208a400..8e2cd9f5e 100644 --- a/test/fields-relationship/collections/UpdatedExternally/index.ts +++ b/test/fields-relationship/collections/UpdatedExternally/index.ts @@ -19,7 +19,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = { admin: { components: { Field: { - path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI', + path: '/PopulateFieldButton/index.js#PopulateFieldButton', clientProps: { hasMany: false, hasMultipleRelations: false, @@ -50,7 +50,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = { admin: { components: { Field: { - path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI', + path: '/PopulateFieldButton/index.js#PopulateFieldButton', clientProps: { hasMultipleRelations: false, targetFieldPath: 'relationHasMany', @@ -80,7 +80,7 @@ export const RelationshipUpdatedExternally: CollectionConfig = { admin: { components: { Field: { - path: '/PrePopulateFieldUI/index.js#PrePopulateFieldUI', + path: '/PopulateFieldButton/index.js#PopulateFieldButton', clientProps: { hasMultipleRelations: true, targetFieldPath: 'relationToManyHasMany', diff --git a/test/form-state/collections/Posts/index.ts b/test/form-state/collections/Posts/index.ts index 14550604f..54183c26f 100644 --- a/test/form-state/collections/Posts/index.ts +++ b/test/form-state/collections/Posts/index.ts @@ -83,6 +83,10 @@ export const PostsCollection: CollectionConfig = { }, }, }, + { + name: 'defaultTextField', + type: 'text', + }, ], }, ], diff --git a/test/form-state/e2e.spec.ts b/test/form-state/e2e.spec.ts index 596cb061d..d44b812ec 100644 --- a/test/form-state/e2e.spec.ts +++ b/test/form-state/e2e.spec.ts @@ -213,7 +213,37 @@ test.describe('Form State', () => { }) }) - test('new rows should contain default values', async () => { + test('should not render stale values for server components while form state is in flight', async () => { + await page.goto(postsUrl.create) + + await page.locator('#field-array .array-field__add-row').click() + await page.locator('#field-array #array-row-0 #field-array__0__customTextField').fill('1') + + await page.locator('#field-array .array-field__add-row').click() + await page.locator('#field-array #array-row-1 #field-array__1__customTextField').fill('2') + + // block the next form state request from firing to ensure the field remains in stale state + await page.route(postsUrl.create, async (route) => { + if (route.request().method() === 'POST' && route.request().url() === postsUrl.create) { + await route.abort() + } + + await route.continue() + }) + + // remove the first row + await page.locator('#field-array #array-row-0 .array-actions__button').click() + + await page + .locator('#field-array #array-row-0 .array-actions__action.array-actions__remove') + .click() + + await expect( + page.locator('#field-array #array-row-0 #field-array__0__customTextField'), + ).toHaveValue('2') + }) + + test('should queue onChange functions', async () => { await page.goto(postsUrl.create) await page.locator('#field-array .array-field__add-row').click() await expect( diff --git a/test/form-state/payload-types.ts b/test/form-state/payload-types.ts index 72b04f150..dd00cd42e 100644 --- a/test/form-state/payload-types.ts +++ b/test/form-state/payload-types.ts @@ -144,6 +144,7 @@ export interface Post { array?: | { customTextField?: string | null; + defaultTextField?: string | null; id?: string | null; }[] | null; @@ -254,6 +255,7 @@ export interface PostsSelect { | T | { customTextField?: T; + defaultTextField?: T; id?: T; }; updatedAt?: T; diff --git a/tsconfig.base.json b/tsconfig.base.json index c9793d25c..e2b64e3bc 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,7 +31,7 @@ } ], "paths": { - "@payload-config": ["./test/_community/config.ts"], + "@payload-config": ["./test/form-state/config.ts"], "@payloadcms/admin-bar": ["./packages/admin-bar/src"], "@payloadcms/live-preview": ["./packages/live-preview/src"], "@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],