chore(next): overhauls field rendering strategy (#4924)

This commit is contained in:
Jacob Fletcher
2024-01-26 14:12:41 -05:00
committed by GitHub
parent b8e7b9c8b3
commit 369a1a8ad9
106 changed files with 1972 additions and 2895 deletions

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const AfterInput: React.FC = () => {
return <div>This is a custom `AfterInput` component</div>
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const BeforeInput: React.FC = () => {
return <div>This is a custom `BeforeInput` component</div>
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const CustomDescription: React.FC = () => {
return <div>This is a custom `Description` component</div>
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const CustomField: React.FC = () => {
return <div>This is a custom `Field` component</div>
}

View File

@@ -0,0 +1,5 @@
import React from 'react'
export const CustomLabel: React.FC = () => {
return <div>This is a custom `Label` component</div>
}

View File

@@ -1,5 +1,10 @@
import { CollectionConfig } from 'payload/types' import { CollectionConfig } from 'payload/types'
import { CustomView } from './CustomView' import { CustomView } from './CustomView'
import { BeforeInput } from './BeforeInput'
import { AfterInput } from './AfterInput'
import { CustomField } from './CustomField'
import { CustomDescription } from './CustomDescription'
import { CustomLabel } from './CustomLabel'
export const Pages: CollectionConfig = { export const Pages: CollectionConfig = {
slug: 'pages', slug: 'pages',
@@ -24,11 +29,47 @@ export const Pages: CollectionConfig = {
drafts: true, drafts: true,
}, },
fields: [ fields: [
{
name: 'titleWithCustomComponents',
label: 'Title With Custom Components',
type: 'text',
required: true,
admin: {
description: CustomDescription,
components: {
beforeInput: [BeforeInput],
afterInput: [AfterInput],
Label: CustomLabel,
},
},
},
{ {
name: 'title', name: 'title',
label: 'Title', label: 'Title',
type: 'text', type: 'text',
required: true, required: true,
admin: {
description: 'This is a description',
},
},
{
name: 'titleWithCustomField',
label: 'Title With Custom Field',
type: 'text',
admin: {
components: {
Field: CustomField,
},
},
},
{
name: 'sidebarTitle',
label: 'Sidebar Title',
type: 'text',
required: true,
admin: {
position: 'sidebar',
},
}, },
{ {
name: 'enableConditionalField', name: 'enableConditionalField',
@@ -118,7 +159,9 @@ export const Pages: CollectionConfig = {
required: true, required: true,
}, },
{ {
label: ({ data }) => `This is ${data?.title || 'Untitled'}`, // TODO: fix this
// label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
label: 'Hello',
type: 'collapsible', type: 'collapsible',
admin: { admin: {
initCollapsed: true, initCollapsed: true,
@@ -145,6 +188,20 @@ export const Pages: CollectionConfig = {
}, },
], ],
}, },
// {
// name: 'array',
// label: 'Array',
// type: 'array',
// required: true,
// fields: [
// {
// name: 'arrayText',
// label: 'Array Text',
// type: 'text',
// required: true,
// },
// ],
// },
{ {
label: 'Tabs', label: 'Tabs',
type: 'tabs', type: 'tabs',

View File

@@ -52,7 +52,7 @@ export const LoginForm: React.FC<{
> >
<FormLoadingOverlayToggle action="loading" name="login-form" /> <FormLoadingOverlayToggle action="loading" name="login-form" />
<div className={`${baseClass}__inputWrap`}> <div className={`${baseClass}__inputWrap`}>
<Email admin={{ autoComplete: 'email' }} label={t('general:email')} name="email" required /> <Email autoComplete="email" label={t('general:email')} name="email" required />
<Password autoComplete="off" label={t('general:password')} name="password" required /> <Password autoComplete="off" label={t('general:password')} name="password" required />
</div> </div>
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link> <Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>

View File

@@ -1,6 +1,5 @@
export type ErrorProps = { export type ErrorProps = {
alignCaret?: 'center' | 'left' | 'right' alignCaret?: 'center' | 'left' | 'right'
message?: string message?: string
path: string
showError?: boolean showError?: boolean
} }

View File

@@ -1,5 +1,4 @@
type Args<T = unknown> = { type Args<T = unknown> = {
path: string
value?: T value?: T
} }

View File

@@ -1,8 +1,5 @@
import type { I18n } from '@payloadcms/translations'
export type LabelProps = { export type LabelProps = {
htmlFor?: string htmlFor?: string
i18n: I18n
label?: JSX.Element | Record<string, string> | false | string label?: JSX.Element | Record<string, string> | false | string
required?: boolean required?: boolean
} }

View File

@@ -1,16 +1,18 @@
'use client'
import React from 'react' import React from 'react'
import type { CollectionPermission, GlobalPermission, User } from 'payload/auth' import type { CollectionPermission, GlobalPermission, User } from 'payload/auth'
import type { Description, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types' import type { Description, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types'
import type { FieldTypes, Locale } from 'payload/config' import type { Locale } from 'payload/config'
import RenderFields from '../../forms/RenderFields' import RenderFields from '../../forms/RenderFields'
import { filterFields } from '../../forms/RenderFields/filterFields'
import { Gutter } from '../Gutter' import { Gutter } from '../Gutter'
import './index.scss' import { Document } from 'payload/types'
import { Document, FieldWithPath } from 'payload/types'
import { FormState } from '../../forms/Form/types' import { FormState } from '../../forms/Form/types'
import { I18n } from '@payloadcms/translations' import { useTranslation } from '../../providers/Translation'
import { createFieldMap } from '../../forms/RenderFields/createFieldMap'
import './index.scss'
const baseClass = 'document-fields' const baseClass = 'document-fields'
@@ -18,8 +20,6 @@ export const DocumentFields: React.FC<{
AfterFields?: React.ReactNode AfterFields?: React.ReactNode
BeforeFields?: React.ReactNode BeforeFields?: React.ReactNode
description?: Description description?: Description
fieldTypes: FieldTypes
fields: FieldWithPath[]
forceSidebarWrap?: boolean forceSidebarWrap?: boolean
hasSavePermission: boolean hasSavePermission: boolean
docPermissions: CollectionPermission | GlobalPermission docPermissions: CollectionPermission | GlobalPermission
@@ -27,17 +27,13 @@ export const DocumentFields: React.FC<{
data: Document data: Document
formState: FormState formState: FormState
user: User user: User
i18n: I18n
payload: Payload
locale?: Locale locale?: Locale
config: SanitizedConfig fieldMap?: ReturnType<typeof createFieldMap>
}> = (props) => { }> = (props) => {
const { const {
AfterFields, AfterFields,
BeforeFields, BeforeFields,
description, description,
fieldTypes,
fields,
forceSidebarWrap, forceSidebarWrap,
hasSavePermission, hasSavePermission,
docPermissions, docPermissions,
@@ -45,27 +41,15 @@ export const DocumentFields: React.FC<{
data, data,
formState, formState,
user, user,
i18n,
payload,
locale, locale,
config, fieldMap,
} = props } = props
const mainFields = filterFields({ const { i18n } = useTranslation()
fieldSchema: fields,
fieldTypes,
filter: (field) => !field?.admin?.position || field?.admin?.position !== 'sidebar',
permissions: docPermissions.fields,
readOnly: !hasSavePermission,
})
const sidebarFields = filterFields({ const mainFields = fieldMap.filter(({ isSidebar }) => !isSidebar)
fieldSchema: fields,
fieldTypes, const sidebarFields = fieldMap.filter(({ isSidebar }) => isSidebar)
filter: (field) => field?.admin?.position === 'sidebar',
permissions: docPermissions.fields,
readOnly: !hasSavePermission,
})
const hasSidebarFields = sidebarFields && sidebarFields.length > 0 const hasSidebarFields = sidebarFields && sidebarFields.length > 0
@@ -89,23 +73,17 @@ export const DocumentFields: React.FC<{
</div> </div>
)} )}
</header> </header>
{BeforeFields || null} {BeforeFields}
<RenderFields <RenderFields
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldTypes={fieldTypes}
fields={mainFields}
// permissions={permissions.fields} // permissions={permissions.fields}
readOnly={!hasSavePermission} readOnly={!hasSavePermission}
data={data} data={data}
formState={formState}
user={user}
i18n={i18n}
payload={payload}
docPreferences={docPreferences} docPreferences={docPreferences}
config={config}
locale={locale} locale={locale}
fieldMap={mainFields}
/> />
{AfterFields || null} {AfterFields}
</Gutter> </Gutter>
</div> </div>
{hasSidebarFields && ( {hasSidebarFields && (
@@ -113,17 +91,11 @@ export const DocumentFields: React.FC<{
<div className={`${baseClass}__sidebar`}> <div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-fields`}> <div className={`${baseClass}__sidebar-fields`}>
<RenderFields <RenderFields
fieldTypes={fieldTypes}
fields={sidebarFields}
// permissions={permissions.fields} // permissions={permissions.fields}
readOnly={!hasSavePermission} readOnly={!hasSavePermission}
data={data} data={data}
formState={formState}
user={user}
i18n={i18n}
payload={payload}
locale={locale} locale={locale}
config={config} fieldMap={sidebarFields}
/> />
</div> </div>
</div> </div>

View File

@@ -14,9 +14,9 @@ export { default as RadioGroupInput } from '../forms/field-types/RadioGroup'
export { default as Label } from '../forms/Label' export { default as Label } from '../forms/Label'
export { default as Submit } from '../forms/Submit' export { default as Submit } from '../forms/Submit'
export { default as Checkbox } from '../forms/field-types/Checkbox' export { default as Checkbox } from '../forms/field-types/Checkbox'
export { CheckboxInput } from '../forms/field-types/Checkbox/Input' export { default as CheckboxInput } from '../forms/field-types/Checkbox'
export { default as Select } from '../forms/field-types/Select' export { default as Select } from '../forms/field-types/Select'
export { default as SelectInput } from '../forms/field-types/Select/Input' export { default as SelectInput } from '../forms/field-types/Select'
export { default as Number } from '../forms/field-types/Number' export { default as Number } from '../forms/field-types/Number'
export { useAllFormFields } from '../forms/Form/context' export { useAllFormFields } from '../forms/Form/context'
export { default as reduceFieldsToValues } from '../forms/Form/reduceFieldsToValues' export { default as reduceFieldsToValues } from '../forms/Form/reduceFieldsToValues'

View File

@@ -3,26 +3,25 @@ import React from 'react'
import { Tooltip } from '../../elements/Tooltip' import { Tooltip } from '../../elements/Tooltip'
import type { ErrorProps } from 'payload/types' import type { ErrorProps } from 'payload/types'
import { useFormFields } from '../Form/context' import { useFormFields, useFormSubmitted } from '../Form/context'
import './index.scss' import './index.scss'
const baseClass = 'field-error' const baseClass = 'field-error'
const Error: React.FC<ErrorProps> = (props) => { const Error: React.FC<ErrorProps> = (props) => {
const { const { alignCaret = 'right', message: messageFromProps, showError: showErrorFromProps } = props
alignCaret = 'right',
message: messageFromProps,
path,
showError: showErrorFromProps,
} = props
// TODO: get path from context
const path = ''
const hasSubmitted = useFormSubmitted()
const field = useFormFields(([fields]) => fields[path]) const field = useFormFields(([fields]) => fields[path])
const { valid, errorMessage } = field || {} const { valid, errorMessage } = field || {}
const message = messageFromProps || errorMessage const message = messageFromProps || errorMessage
const showMessage = showErrorFromProps || !valid const showMessage = showErrorFromProps || (hasSubmitted && !valid)
if (showMessage) { if (showMessage) {
return ( return (

View File

@@ -1,20 +1,19 @@
'use client'
import React from 'react' import React from 'react'
import type { Props } from './types' import type { Props } from './types'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { isComponent } from './types' import { useTranslation } from '../../providers/Translation'
import './index.scss' import './index.scss'
const baseClass = 'field-description' const baseClass = 'field-description'
const FieldDescription: React.FC<Props> = (props) => { const FieldDescription: React.FC<Props> = (props) => {
const { className, description, marginPlacement, path, value, i18n } = props const { className, description, marginPlacement } = props
if (isComponent(description)) { const { i18n } = useTranslation()
const Description = description
return <Description path={path} value={value} />
}
if (description) { if (description) {
return ( return (
@@ -27,12 +26,7 @@ const FieldDescription: React.FC<Props> = (props) => {
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
{typeof description === 'function' {getTranslation(description, i18n)}
? description({
path,
value,
})
: getTranslation(description, i18n)}
</div> </div>
) )
} }

View File

@@ -1,16 +1,6 @@
import { I18n } from '@payloadcms/translations'
import { Description, DescriptionComponent } from 'payload/types'
import React from 'react'
export type Props = { export type Props = {
className?: string className?: string
description?: Description description?: string
marginPlacement?: 'bottom' | 'top' marginPlacement?: 'bottom' | 'top'
path?: string
value?: unknown value?: unknown
i18n: I18n
}
export function isComponent(description: Description): description is DescriptionComponent {
return React.isValidElement(description)
} }

View File

@@ -0,0 +1,12 @@
@import '../../scss/styles.scss';
.field-description {
display: flex;
color: var(--theme-elevation-400);
margin-top: calc(var(--base) / 4);
&--margin-bottom {
margin-top: 0;
margin-bottom: calc(var(--base) / 2);
}
}

View File

@@ -0,0 +1,18 @@
'use client'
import React from 'react'
const FieldPathContext = React.createContext<string>('')
export const FieldPathProvider: React.FC<{
path: string
children: React.ReactNode
}> = (props) => {
const { children, path } = props
return <FieldPathContext.Provider value={path}>{children}</FieldPathContext.Provider>
}
export const useFieldPath = () => {
const path = React.useContext(FieldPathContext)
return path
}

View File

@@ -0,0 +1,6 @@
export type Props = {
className?: string
description?: string
marginPlacement?: 'bottom' | 'top'
value?: unknown
}

View File

@@ -1,12 +1,16 @@
'use client'
import React from 'react' import React from 'react'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { LabelProps } from 'payload/types' import { LabelProps } from 'payload/types'
import { useTranslation } from '../..'
import './index.scss' import './index.scss'
const Label: React.FC<LabelProps> = (props) => { const Label: React.FC<LabelProps> = (props) => {
const { htmlFor, label, required = false, i18n } = props const { htmlFor, label, required = false } = props
const { i18n } = useTranslation()
if (label) { if (label) {
return ( return (

View File

@@ -6,7 +6,7 @@ import { Banner } from '../../elements/Banner'
import { useConfig } from '../../providers/Config' import { useConfig } from '../../providers/Config'
import { useLocale } from '../../providers/Locale' import { useLocale } from '../../providers/Locale'
import { useForm } from '../Form/context' import { useForm } from '../Form/context'
import { CheckboxInput } from '../field-types/Checkbox/Input' import CheckboxInput from '../field-types/Checkbox'
type NullifyLocaleFieldProps = { type NullifyLocaleFieldProps = {
fieldValue?: [] | null | number fieldValue?: [] | null | number

View File

@@ -0,0 +1,13 @@
'use client'
import React from 'react'
import { FieldPathProvider, useFieldPath } from '../FieldPathProvider'
export const RenderField: React.FC<{
name?: string
Field: React.ReactNode
}> = (props) => {
const { name, Field } = props
const pathFromContext = useFieldPath()
const path = `${pathFromContext ? `${pathFromContext}.` : ''}${name || ''}`
return <FieldPathProvider path={path}>{Field}</FieldPathProvider>
}

View File

@@ -0,0 +1,231 @@
import React, { Fragment } from 'react'
import type { FieldPermissions } from 'payload/auth'
import type { Field, FieldWithPath, TabsField } from 'payload/types'
import DefaultError from '../Error'
import DefaultLabel from '../Label'
import DefaultDescription from '../FieldDescription'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
import { fieldTypes } from '../field-types'
import { FormFieldBase } from '../field-types/shared'
export type ReducedField = {
type: keyof typeof fieldTypes
Field: React.ReactNode
fieldIsPresentational: boolean
fieldPermissions: FieldPermissions
isFieldAffectingData: boolean
name: string
readOnly: boolean
isSidebar: boolean
subfields?: ReducedField[]
tabs?: ReducedTab[]
}
export type ReducedTab = {
name?: string
label: TabsField['tabs'][0]['label']
subfields?: ReducedField[]
}
export const createFieldMap = (args: {
fieldSchema: FieldWithPath[]
filter?: (field: Field) => boolean
operation?: 'create' | 'update'
permissions?:
| {
[field: string]: FieldPermissions
}
| FieldPermissions
readOnly?: boolean
parentPath?: string
}): ReducedField[] => {
const {
fieldSchema,
filter,
operation = 'update',
permissions,
readOnly: readOnlyOverride,
parentPath,
} = args
return fieldSchema.reduce((acc, field): ReducedField[] => {
const fieldIsPresentational = fieldIsPresentationalOnly(field)
let FieldComponent = field.admin?.components?.Field || fieldTypes[field.type]
if (fieldIsPresentational || (!field?.hidden && field?.admin?.disabled !== true)) {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
if (field.admin && 'hidden' in field.admin && field?.admin?.hidden) {
FieldComponent = fieldTypes.hidden
}
const isFieldAffectingData = fieldAffectsData(field)
const path = `${parentPath ? `${parentPath}.` : ''}${
field.path || (isFieldAffectingData && 'name' in field ? field.name : '')
}`
const fieldPermissions = isFieldAffectingData ? permissions?.[field.name] : permissions
// if the user cannot read the field, then filter it out
if (fieldPermissions?.read?.permission === false) {
return acc
}
// readOnly from field config
let readOnly = field.admin && 'readOnly' in field.admin ? field.admin.readOnly : undefined
// if parent field is readOnly
// but this field is `readOnly: false`
// the field should be editable
if (readOnlyOverride && readOnly !== false) readOnly = true
// unless the user does not pass access control
if (fieldPermissions?.[operation]?.permission === false) {
readOnly = true
}
const LabelComponent =
(field.admin?.components &&
'Label' in field.admin.components &&
field.admin?.components?.Label) ||
DefaultLabel
// TODO: fix this for basic function types, i.e. not React.ComponentType
const Label = (
<LabelComponent
htmlFor="TODO"
// TODO: fix types
// @ts-ignore-next-line
label={'label' in field ? field.label : null}
required={'required' in field ? field.required : undefined}
/>
)
const ErrorComponent =
(field.admin?.components &&
'Error' in field.admin.components &&
field.admin?.components?.Error) ||
DefaultError
const Error = <ErrorComponent />
const DescriptionComponent =
('description' in field.admin &&
field.admin.description &&
typeof field.admin.description === 'function' &&
(field.admin.description as React.FC<any>)) ||
DefaultDescription
const Description = (
<DescriptionComponent
description={
'description' in field.admin && typeof field.admin?.description === 'string'
? field.admin.description
: undefined
}
/>
)
const BeforeInput = field.admin?.components &&
'beforeInput' in field.admin?.components &&
Array.isArray(field.admin.components.beforeInput) && (
<Fragment>
{field.admin.components.beforeInput.map((Component, i) => (
<Component key={i} />
))}
</Fragment>
)
const AfterInput = 'components' in field.admin &&
'afterInput' in field.admin.components &&
Array.isArray(field.admin.components.afterInput) && (
<Fragment>
{field.admin.components.afterInput.map((Component, i) => (
<Component key={i} />
))}
</Fragment>
)
// Group, Array, and Collapsible fields have nested fields
const nestedFieldMap =
'fields' in field &&
field.fields &&
Array.isArray(field.fields) &&
createFieldMap({
fieldSchema: field.fields,
filter,
operation,
permissions,
readOnly: readOnlyOverride,
parentPath: path,
})
// Tabs
const tabs =
'tabs' in field &&
field.tabs &&
Array.isArray(field.tabs) &&
field.tabs.map((tab) => {
const tabFieldMap = createFieldMap({
fieldSchema: tab.fields,
filter,
operation,
permissions,
readOnly: readOnlyOverride,
parentPath: path,
})
return {
name: 'name' in tab ? tab.name : undefined,
label: 'label' in tab ? tab.label : undefined,
subfields: tabFieldMap,
}
})
// TODO: these types can get cleaned up
const fieldComponentProps: FormFieldBase = {
Error,
Label,
BeforeInput,
AfterInput,
Description,
fieldMap: nestedFieldMap,
className: 'className' in field.admin ? field?.admin?.className : undefined,
style: 'style' in field.admin ? field?.admin?.style : undefined,
width: 'width' in field.admin ? field?.admin?.width : undefined,
// TODO: fix types
// label: 'label' in field ? field.label : undefined,
step: 'step' in field.admin ? field.admin.step : undefined,
hasMany: 'hasMany' in field ? field.hasMany : undefined,
maxRows: 'maxRows' in field ? field.maxRows : undefined,
min: 'min' in field ? field.min : undefined,
max: 'max' in field ? field.max : undefined,
options: 'options' in field ? field.options : undefined,
tabs,
}
const Field = <FieldComponent {...fieldComponentProps} />
const reducedField: ReducedField = {
name: 'name' in field ? field.name : '',
type: field.type,
Field,
fieldIsPresentational,
fieldPermissions,
isFieldAffectingData,
readOnly,
isSidebar: field.admin?.position === 'sidebar',
subfields: nestedFieldMap,
tabs,
}
if (FieldComponent) {
acc.push(reducedField)
}
}
}
return acc
}, [])
}

View File

@@ -1,87 +0,0 @@
import type React from 'react'
import type { FieldPermissions } from 'payload/auth'
import type { Field, FieldWithPath } from 'payload/types'
import type { FieldTypes } from 'payload/config'
import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types'
export type ReducedField = {
FieldComponent: React.ComponentType<any>
field: FieldWithPath
fieldIsPresentational: boolean
fieldPermissions: FieldPermissions
isFieldAffectingData: boolean
name: string
readOnly: boolean
}
export const filterFields = (args: {
fieldSchema: FieldWithPath[]
fieldTypes: FieldTypes
filter: (field: Field) => boolean
operation?: 'create' | 'update'
permissions?:
| {
[field: string]: FieldPermissions
}
| FieldPermissions
readOnly?: boolean
}): ReducedField[] => {
const {
fieldSchema,
fieldTypes,
filter,
operation = 'update',
permissions,
readOnly: readOnlyOverride,
} = args
return fieldSchema.reduce((acc, field): ReducedField[] => {
const fieldIsPresentational = fieldIsPresentationalOnly(field)
let FieldComponent = fieldTypes[field.type]
if (fieldIsPresentational || (!field?.hidden && field?.admin?.disabled !== true)) {
if ((filter && typeof filter === 'function' && filter(field)) || !filter) {
if (field.admin && 'hidden' in field.admin && field?.admin?.hidden) {
FieldComponent = fieldTypes.hidden
}
const isFieldAffectingData = fieldAffectsData(field)
const fieldPermissions = isFieldAffectingData ? permissions?.[field.name] : permissions
// if the user cannot read the field, then filter it out
if (fieldPermissions?.read?.permission === false) {
return acc
}
// readOnly from field config
let readOnly = field.admin && 'readOnly' in field.admin ? field.admin.readOnly : undefined
// if parent field is readOnly
// but this field is `readOnly: false`
// the field should be editable
if (readOnlyOverride && readOnly !== false) readOnly = true
// unless the user does not pass access control
if (fieldPermissions?.[operation]?.permission === false) {
readOnly = true
}
if (FieldComponent) {
acc.push({
name: 'name' in field ? field.name : '',
FieldComponent,
field,
fieldIsPresentational,
fieldPermissions,
isFieldAffectingData,
readOnly,
})
}
}
}
return acc
}, [])
}

View File

@@ -1,53 +1,23 @@
'use client'
import React from 'react' import React from 'react'
import { fieldAffectsData } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import type { Props } from './types' import type { Props } from './types'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent' import { useTranslation } from '../../providers/Translation'
import { FormFieldBase } from '../field-types/shared' import { RenderField } from './RenderField'
import { filterFields } from './filterFields'
import './index.scss' import './index.scss'
const baseClass = 'render-fields' const baseClass = 'render-fields'
const RenderFields: React.FC<Props> = (props) => { const RenderFields: React.FC<Props> = (props) => {
const { const { className, margins, fieldMap } = props
className,
fieldTypes, const { i18n } = useTranslation()
forceRender,
margins,
data,
user,
formState,
i18n,
payload,
docPreferences,
locale,
config,
} = props
if (!i18n) { if (!i18n) {
console.error('Need to implement i18n when calling RenderFields') console.error('Need to implement i18n when calling RenderFields')
} }
let fieldsToRender = 'fields' in props ? props?.fields : null if (fieldMap) {
if (!fieldsToRender && 'fieldSchema' in props) {
const { fieldSchema, fieldTypes, filter, permissions, readOnly: readOnlyOverride } = props
fieldsToRender = filterFields({
fieldSchema,
fieldTypes,
filter,
operation: props?.operation,
permissions,
readOnly: readOnlyOverride,
})
}
if (fieldsToRender) {
return ( return (
<div <div
className={[ className={[
@@ -59,71 +29,9 @@ const RenderFields: React.FC<Props> = (props) => {
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
{fieldsToRender?.map((reducedField, fieldIndex) => { {fieldMap?.map(({ Field, name }, fieldIndex) => (
const { <RenderField key={fieldIndex} name={name} Field={Field} />
FieldComponent, ))}
field,
fieldIsPresentational,
fieldPermissions,
isFieldAffectingData,
readOnly,
} = reducedField
const path = field.path || (isFieldAffectingData && 'name' in field ? field.name : '')
const fieldState = formState?.[path]
if (fieldIsPresentational) {
return <FieldComponent key={fieldIndex} />
}
// TODO: type this, i.e. `componentProps: FieldComponentProps`
const componentProps: FormFieldBase & Record<string, any> = {
...field,
admin: {
...(field.admin || {}),
readOnly,
},
fieldTypes,
forceRender,
indexPath: 'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
path,
permissions: fieldPermissions,
data,
user,
formState,
valid: fieldState?.valid,
errorMessage: fieldState?.errorMessage,
i18n,
payload,
docPreferences,
locale,
config,
}
if (field) {
return (
<RenderCustomComponent
CustomComponent={field?.admin?.components?.Field}
DefaultComponent={FieldComponent}
componentProps={componentProps}
key={fieldIndex}
/>
)
}
return (
<div className="missing-field" key={fieldIndex}>
{i18n
? i18n.t('error:noMatchedField', {
label: fieldAffectsData(field)
? getTranslation(field.label || field.name, i18n)
: field.path,
})
: 'Need to implement i18n when calling RenderFields'}
</div>
)
})}
</div> </div>
) )
} }

View File

@@ -1,25 +1,15 @@
import type { FieldPermissions, User } from 'payload/auth' import type { FieldPermissions, User } from 'payload/auth'
import type { import type { Document, DocumentPreferences, Field } from 'payload/types'
Document,
DocumentPreferences,
Field,
FieldWithPath,
Payload,
SanitizedConfig,
} from 'payload/types'
import type { ReducedField } from './filterFields'
import { FormState } from '../Form/types' import { FormState } from '../Form/types'
import { FieldTypes, Locale } from 'payload/config' import { Locale } from 'payload/config'
import { I18n } from '@payloadcms/translations' import { createFieldMap } from './createFieldMap'
export type Props = { export type Props = {
className?: string className?: string
fieldTypes: FieldTypes
forceRender?: boolean forceRender?: boolean
margins?: 'small' | false margins?: 'small' | false
data?: Document data?: Document
formState: FormState fieldMap: ReturnType<typeof createFieldMap>
user: User
docPreferences?: DocumentPreferences docPreferences?: DocumentPreferences
permissions?: permissions?:
| { | {
@@ -27,20 +17,9 @@ export type Props = {
} }
| FieldPermissions | FieldPermissions
readOnly?: boolean readOnly?: boolean
i18n: I18n
payload: Payload
locale?: Locale locale?: Locale
config: SanitizedConfig } & {
} & (
| {
// FormState to be filtered by the component
fieldSchema: FieldWithPath[]
filter?: (field: Field) => boolean filter?: (field: Field) => boolean
indexPath?: string indexPath?: string
operation?: 'create' | 'update' operation?: 'create' | 'update'
} }
| {
// Pre-filtered fields to be simply rendered
fields: ReducedField[]
}
)

View File

@@ -1,25 +1,29 @@
import type { Field, TabAsField } from 'payload/types' import type { TabAsField } from 'payload/types'
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types' import { tabHasName } from 'payload/types'
import { createFieldMap } from '../RenderFields/createFieldMap'
export const buildPathSegments = (parentPath: string, fieldSchema: Field[]): string[] => { export const buildPathSegments = (
const pathNames = fieldSchema.reduce((acc, subField) => { parentPath: string,
if (fieldHasSubFields(subField) && fieldAffectsData(subField)) { fieldMap: ReturnType<typeof createFieldMap>,
): string[] => {
const pathNames = fieldMap.reduce((acc, subField) => {
if (subField.subfields && subField.isFieldAffectingData) {
// group, block, array // group, block, array
acc.push(parentPath ? `${parentPath}.${subField.name}.` : `${subField.name}.`) acc.push(parentPath ? `${parentPath}.${subField.name}.` : `${subField.name}.`)
} else if (fieldHasSubFields(subField)) { } else if (subField.subfields) {
// rows, collapsibles, unnamed-tab // rows, collapsibles, unnamed-tab
acc.push(...buildPathSegments(parentPath, subField.fields)) acc.push(...buildPathSegments(parentPath, subField.subfields))
} else if (subField.type === 'tabs') { } else if (subField.type === 'tabs') {
// tabs // tabs
subField.tabs.forEach((tab: TabAsField) => { subField.tabs.forEach((tab) => {
let tabPath = parentPath let tabPath = parentPath
if (tabHasName(tab)) { if ('name' in tab) {
tabPath = parentPath ? `${parentPath}.${tab.name}` : tab.name tabPath = parentPath ? `${parentPath}.${tab.name}` : tab.name
} }
acc.push(...buildPathSegments(tabPath, tab.fields)) acc.push(...buildPathSegments(tabPath, tab.subfields))
}) })
} else if (fieldAffectsData(subField)) { } else if (subField.isFieldAffectingData) {
// text, number, date, etc. // text, number, date, etc.
acc.push(parentPath ? `${parentPath}.${subField.name}` : subField.name) acc.push(parentPath ? `${parentPath}.${subField.name}` : subField.name)
} }

View File

@@ -4,19 +4,25 @@ import React from 'react'
import useThrottledEffect from '../../hooks/useThrottledEffect' import useThrottledEffect from '../../hooks/useThrottledEffect'
import { useAllFormFields, useFormSubmitted } from '../Form/context' import { useAllFormFields, useFormSubmitted } from '../Form/context'
import { getFieldStateFromPaths } from './getFieldStateFromPaths' import { getFieldStateFromPaths } from './getFieldStateFromPaths'
import { buildPathSegments } from './buildPathSegments'
import { createFieldMap } from '../RenderFields/createFieldMap'
type TrackSubSchemaErrorCountProps = { type TrackSubSchemaErrorCountProps = {
pathSegments?: string[] path: string
fieldMap?: ReturnType<typeof createFieldMap>
setErrorCount: (count: number) => void setErrorCount: (count: number) => void
} }
export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({ export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
pathSegments, path,
fieldMap,
setErrorCount, setErrorCount,
}) => { }) => {
const [formState] = useAllFormFields() const [formState] = useAllFormFields()
const hasSubmitted = useFormSubmitted() const hasSubmitted = useFormSubmitted()
const pathSegments = buildPathSegments(path, fieldMap)
useThrottledEffect( useThrottledEffect(
() => { () => {
if (hasSubmitted) { if (hasSubmitted) {
@@ -25,7 +31,7 @@ export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
} }
}, },
250, 250,
[formState, hasSubmitted, pathSegments], [formState, hasSubmitted, fieldMap],
) )
return null return null

View File

@@ -20,7 +20,7 @@ import './index.scss'
const baseClass = 'array-field' const baseClass = 'array-field'
type ArrayRowProps = UseDraggableSortableReturn & type ArrayRowProps = UseDraggableSortableReturn &
Pick<Props, 'fieldTypes' | 'fields' | 'indexPath' | 'labels' | 'path' | 'permissions'> & { Pick<Props, 'indexPath' | 'labels' | 'path' | 'permissions'> & {
CustomRowLabel?: RowLabelType CustomRowLabel?: RowLabelType
addRow: (rowIndex: number) => void addRow: (rowIndex: number) => void
duplicateRow: (rowIndex: number) => void duplicateRow: (rowIndex: number) => void
@@ -34,13 +34,12 @@ type ArrayRowProps = UseDraggableSortableReturn &
rowIndex: number rowIndex: number
setCollapse: (rowID: string, collapsed: boolean) => void setCollapse: (rowID: string, collapsed: boolean) => void
} }
export const ArrayRow: React.FC<ArrayRowProps> = ({ export const ArrayRow: React.FC<ArrayRowProps> = ({
CustomRowLabel, CustomRowLabel,
addRow, addRow,
attributes, attributes,
duplicateRow, duplicateRow,
fieldTypes,
fields,
forceRender = false, forceRender = false,
hasMaxRows, hasMaxRows,
indexPath, indexPath,
@@ -57,6 +56,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
setCollapse, setCollapse,
setNodeRef, setNodeRef,
transform, transform,
fieldMap,
}) => { }) => {
const path = `${parentPath}.${rowIndex}` const path = `${parentPath}.${rowIndex}`
const { i18n } = useTranslation() const { i18n } = useTranslation()
@@ -115,26 +115,21 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
path={path} path={path}
rowNumber={rowIndex + 1} rowNumber={rowIndex + 1}
/> />
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage />} {fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage i18n={i18n} />}
</div> </div>
} }
onToggle={(collapsed) => setCollapse(row.id, collapsed)} onToggle={(collapsed) => setCollapse(row.id, collapsed)}
> >
<HiddenInput name={`${path}.id`} value={row.id} /> <HiddenInput name={`${path}.id`} value={row.id} />
[RenderFields] <RenderFields
{/* <RenderFields
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldSchema={fields.map((field) => ({ fieldMap={fieldMap}
...field,
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender} forceRender={forceRender}
indexPath={indexPath} indexPath={indexPath}
margins="small" margins="small"
permissions={permissions?.fields} permissions={permissions?.fields}
readOnly={readOnly} readOnly={readOnly}
/> */} />
</Collapsible> </Collapsible>
</div> </div>
) )

View File

@@ -14,8 +14,6 @@ import { ErrorPill } from '../../../elements/ErrorPill'
import { useConfig } from '../../../providers/Config' import { useConfig } from '../../../providers/Config'
import { useDocumentInfo } from '../../../providers/DocumentInfo' import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { useLocale } from '../../../providers/Locale' import { useLocale } from '../../../providers/Locale'
import Error from '../../Error'
import FieldDescription from '../../FieldDescription'
import { useForm, useFormSubmitted } from '../../Form/context' import { useForm, useFormSubmitted } from '../../Form/context'
import { NullifyLocaleField } from '../../NullifyField' import { NullifyLocaleField } from '../../NullifyField'
import useField from '../../useField' import useField from '../../useField'
@@ -28,26 +26,23 @@ const baseClass = 'array-field'
const ArrayFieldType: React.FC<Props> = (props) => { const ArrayFieldType: React.FC<Props> = (props) => {
const { const {
name, name,
admin: { className, components, condition, description, readOnly }, className,
fieldTypes, readOnly,
fields,
forceRender = false, forceRender = false,
indexPath, indexPath,
localized, localized,
maxRows,
minRows,
path: pathFromProps, path: pathFromProps,
permissions, permissions,
required, required,
validate, validate,
Error,
Label,
Description,
fieldMap,
} = props } = props
const path = pathFromProps || name const minRows = 'minRows' in props ? props.minRows : 0
const maxRows = 'maxRows' in props ? props.maxRows : undefined
// eslint-disable-next-line react/destructuring-assignment
const label = props?.label ?? props?.labels?.singular
const CustomRowLabel = components?.RowLabel || undefined
const { setDocFieldPreferences } = useDocumentInfo() const { setDocFieldPreferences } = useDocumentInfo()
const { addFieldRow, dispatchFields, removeFieldRow, setModified } = useForm() const { addFieldRow, dispatchFields, removeFieldRow, setModified } = useForm()
@@ -86,20 +81,20 @@ const ArrayFieldType: React.FC<Props> = (props) => {
) )
const { const {
errorMessage,
rows = [], rows = [],
showError, showError,
valid, valid,
value, value,
path,
} = useField<number>({ } = useField<number>({
hasRows: true, hasRows: true,
path, path: pathFromProps || name,
validate: memoizedValidate, validate: memoizedValidate,
}) })
const addRow = useCallback( const addRow = useCallback(
async (rowIndex: number) => { async (rowIndex: number) => {
await addFieldRow({ path, rowIndex }) await addFieldRow({ path, rowIndex, fieldMap })
setModified(true) setModified(true)
setTimeout(() => { setTimeout(() => {
@@ -173,17 +168,13 @@ const ArrayFieldType: React.FC<Props> = (props) => {
.join(' ')} .join(' ')}
id={`field-${path.replace(/\./g, '__')}`} id={`field-${path.replace(/\./g, '__')}`}
> >
{showError && ( {showError && <div className={`${baseClass}__error-wrap`}>{Error}</div>}
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
</div>
)}
<header className={`${baseClass}__header`}> <header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}> <div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-content`}> <div className={`${baseClass}__header-content`}>
<h3 className={`${baseClass}__title`}>{getTranslation(label || name, i18n)}</h3> <h3 className={`${baseClass}__title`}>{Label}</h3>
{fieldHasErrors && fieldErrorCount > 0 && ( {fieldHasErrors && fieldErrorCount > 0 && (
<ErrorPill count={fieldErrorCount} withMessage /> <ErrorPill count={fieldErrorCount} withMessage i18n={i18n} />
)} )}
</div> </div>
{rows.length > 0 && ( {rows.length > 0 && (
@@ -209,14 +200,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
</ul> </ul>
)} )}
</div> </div>
<FieldDescription {Description}
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
/>
</header> </header>
<NullifyLocaleField fieldValue={value} localized={localized} path={path} /> <NullifyLocaleField fieldValue={value} localized={localized} path={path} />
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && ( {(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
<DraggableSortable <DraggableSortable
@@ -230,10 +215,9 @@ const ArrayFieldType: React.FC<Props> = (props) => {
<ArrayRow <ArrayRow
{...draggableSortableItemProps} {...draggableSortableItemProps}
CustomRowLabel={CustomRowLabel} CustomRowLabel={CustomRowLabel}
fieldMap={fieldMap}
addRow={addRow} addRow={addRow}
duplicateRow={duplicateRow} duplicateRow={duplicateRow}
fieldTypes={fieldTypes}
fields={fields}
forceRender={forceRender} forceRender={forceRender}
hasMaxRows={hasMaxRows} hasMaxRows={hasMaxRows}
indexPath={indexPath} indexPath={indexPath}

View File

@@ -1,12 +1,11 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth' import type { FieldPermissions } from 'payload/auth'
import type { ArrayField } from 'payload/types' import { FormFieldBase } from '../shared'
export type Props = Omit<ArrayField, 'type'> & { export type Props = FormFieldBase & {
fieldTypes: FieldTypes
forceRender?: boolean forceRender?: boolean
indexPath: string indexPath: string
label: false | string label: false | string
path?: string path?: string
permissions: FieldPermissions permissions: FieldPermissions
name?: string
} }

View File

@@ -1,89 +0,0 @@
'use client'
import React, { Fragment, useCallback } from 'react'
import useField from '../../../useField'
import { Validate } from 'payload/types'
import { Check } from '../../../../icons/Check'
import { Line } from '../../../../icons/Line'
type CheckboxInputProps = {
'aria-label'?: string
checked?: boolean
className?: string
id?: string
inputRef?: React.MutableRefObject<HTMLInputElement>
label?: string
name?: string
onChange?: (value: boolean) => void
readOnly?: boolean
required?: boolean
path?: string
validate?: Validate
partialChecked?: boolean
iconClassName?: string
}
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
id,
name,
'aria-label': ariaLabel,
checked: checkedFromProps,
className,
iconClassName,
inputRef,
onChange: onChangeFromProps,
readOnly,
required,
path,
validate,
partialChecked,
} = props
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { setValue, value } = useField({
// disableFormData,
path,
validate: memoizedValidate,
})
const onToggle = useCallback(() => {
if (!readOnly) {
setValue(!value)
if (typeof onChangeFromProps === 'function') onChangeFromProps(!value)
}
}, [onChangeFromProps, readOnly, setValue, value])
const checked = checkedFromProps || Boolean(value)
return (
<Fragment>
<input
className={className}
aria-label={ariaLabel}
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={id}
name={name}
onInput={onToggle}
ref={inputRef}
type="checkbox"
required={required}
/>
<span
className={[iconClassName, !value && partialChecked ? 'check' : 'partial']
.filter(Boolean)
.join(' ')}
>
{value && <Check />}
{!value && partialChecked && <Line />}
</span>
</Fragment>
)
}

View File

@@ -1,30 +0,0 @@
'use client'
import React from 'react'
import { useFormFields } from '../../../Form/context'
export const CheckboxWrapper: React.FC<{
path: string
children: React.ReactNode
readOnly?: boolean
baseClass?: string
}> = (props) => {
const { path, children, readOnly, baseClass } = props
const field = useFormFields(([fields]) => fields[path])
const { value: checked } = field || {}
return (
<div
className={[
baseClass,
checked && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
)
}

View File

@@ -1,15 +1,13 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { Props } from './types' import type { Props } from './types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import { fieldBaseClass } from '../shared' import { fieldBaseClass } from '../shared'
import { CheckboxInput } from './Input'
import DefaultLabel from '../../Label'
import { CheckboxWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { Validate } from 'payload/types'
import useField from '../../useField'
import { Check } from '../../../icons/Check'
import { Line } from '../../../icons/Line'
import './index.scss' import './index.scss'
@@ -19,38 +17,56 @@ export const inputBaseClass = 'checkbox-input'
const Checkbox: React.FC<Props> = (props) => { const Checkbox: React.FC<Props> = (props) => {
const { const {
name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
readOnly, readOnly,
style, style,
width, width,
} = {},
disableFormData,
label,
path: pathFromProps,
required, required,
valid = true, validate,
errorMessage, BeforeInput,
value, AfterInput,
i18n, Label,
Error,
Description,
onChange: onChangeFromProps,
partialChecked,
checked: checkedFromProps,
disableFormData,
id,
path: pathFromProps,
name,
} = props } = props
const path = pathFromProps || name const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const fieldID = `field-${path.replace(/\./g, '__')}` const { setValue, value, showError, path } = useField({
disableFormData,
validate: memoizedValidate,
path: pathFromProps || name,
})
const ErrorComp = Error || DefaultError const onToggle = useCallback(() => {
const LabelComp = Label || DefaultLabel if (!readOnly) {
setValue(!value)
if (typeof onChangeFromProps === 'function') onChangeFromProps(!value)
}
}, [onChangeFromProps, readOnly, setValue, value])
const checked = checkedFromProps || Boolean(value)
const fieldID = id || `field-${path?.replace(/\./g, '__')}`
return ( return (
<div <div
className={[ className={[
fieldBaseClass, fieldBaseClass,
baseClass, baseClass,
!valid && 'error', showError && 'error',
className, className,
value && `${baseClass}--checked`, value && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`, readOnly && `${baseClass}--read-only`,
@@ -62,26 +78,42 @@ const Checkbox: React.FC<Props> = (props) => {
width, width,
}} }}
> >
<div className={`${baseClass}__error-wrap`}> <div className={`${baseClass}__error-wrap`}>{Error}</div>
<ErrorComp alignCaret="left" path={path} /> <div
</div> className={[
<CheckboxWrapper path={path} readOnly={readOnly} baseClass={inputBaseClass}> inputBaseClass,
checked && `${inputBaseClass}--checked`,
readOnly && `${inputBaseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${inputBaseClass}__input`}> <div className={`${inputBaseClass}__input`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<CheckboxInput <input
aria-label=""
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={fieldID} id={fieldID}
label={getTranslation(label || name, i18n)}
name={path} name={path}
readOnly={readOnly} onInput={onToggle}
// ref={inputRef}
type="checkbox"
required={required} required={required}
path={path}
iconClassName={`${inputBaseClass}__icon`}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} <span
className={[`${inputBaseClass}__icon`, !value && partialChecked ? 'check' : 'partial']
.filter(Boolean)
.join(' ')}
>
{value && <Check />}
{!value && partialChecked && <Line />}
</span>
{AfterInput}
</div> </div>
{label && <LabelComp htmlFor={fieldID} label={label} required={required} i18n={i18n} />} {Label}
</CheckboxWrapper> </div>
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {Description}
</div> </div>
) )
} }

View File

@@ -1,11 +1,11 @@
import type { CheckboxField } from 'payload/types'
import type { I18n } from '@payloadcms/translations'
import type { FormFieldBase } from '../shared' import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<CheckboxField, 'type'> & {
disableFormData?: boolean disableFormData?: boolean
onChange?: (val: boolean) => void onChange?: (val: boolean) => void
i18n: I18n partialChecked?: boolean
value?: boolean checked?: boolean
id?: string
path?: string
name?: string
} }

View File

@@ -1,50 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import useField from '../../../useField'
import { CodeField, Validate } from 'payload/types'
import { CodeEditor } from '../../../../elements/CodeEditor'
const prismToMonacoLanguageMap = {
js: 'javascript',
ts: 'typescript',
}
export const CodeInput: React.FC<{
path: string
required?: boolean
placeholder?: Record<string, string> | string
readOnly?: boolean
name?: string
validate?: Validate
language?: string
editorOptions?: CodeField['admin']['editorOptions']
}> = (props) => {
const { name, readOnly, path: pathFromProps, required, validate, language, editorOptions } = props
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)
const { setValue, value } = useField({
path,
validate: memoizedValidate,
})
return (
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}
options={editorOptions}
readOnly={readOnly}
value={(value as string) || ''}
/>
)
}

View File

@@ -1,38 +0,0 @@
'use client'
import React from 'react'
import { useFormFields } from '../../../Form/context'
import { fieldBaseClass } from '../../shared'
import './index.scss'
const baseClass = 'code-field'
export const CodeInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { width, className, style, path, children, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, baseClass, className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,4 +1,4 @@
@import '../../../../scss/styles.scss'; @import '../../../scss/styles.scss';
.code-field { .code-field {
position: relative; position: relative;

View File

@@ -1,59 +1,87 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { CodeEditor } from '../../../elements/CodeEditor'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import { withCondition } from '../../withCondition'
import DefaultError from '../../Error' import './index.scss'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label' const prismToMonacoLanguageMap = {
import { CodeInputWrapper } from './Wrapper' js: 'javascript',
import { CodeInput } from './Input' ts: 'typescript',
}
const baseClass = 'code-field'
const Code: React.FC<Props> = (props) => { const Code: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label } = {},
description,
editorOptions,
language,
readOnly, readOnly,
style, style,
width, width,
} = {},
label,
path: pathFromProps, path: pathFromProps,
required, required,
i18n, Error,
value, Label,
Description,
BeforeInput,
AfterInput,
validate,
} = props } = props
const ErrorComp = Error || DefaultError const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
const LabelComp = Label || DefaultLabel const language = 'language' in props ? props.language : 'javascript'
const path = pathFromProps || name const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)
const { setValue, value, path, showError } = useField({
path: pathFromProps || name,
validate: memoizedValidate,
})
return ( return (
<CodeInputWrapper <div
className={className} className={[
path={path} fieldBaseClass,
readOnly={readOnly} baseClass,
style={style} className,
width={width} showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} /> {Label}
<CodeInput <div>
path={path} {BeforeInput}
required={required} <CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}
options={editorOptions}
readOnly={readOnly} readOnly={readOnly}
name={name} value={(value as string) || ''}
language={language}
editorOptions={editorOptions}
/> />
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {AfterInput}
</CodeInputWrapper> </div>
{Description}
</div>
) )
} }
export default Code export default withCondition(Code)

View File

@@ -1,8 +1,6 @@
import type { CodeField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<CodeField, 'type'> & {
path?: string path?: string
value?: string name?: string
} }

View File

@@ -1,79 +0,0 @@
'use client'
import React, { Fragment, useCallback, useState } from 'react'
import type { DocumentPreferences } from 'payload/types'
import { Collapsible } from '../../../../elements/Collapsible'
import { ErrorPill } from '../../../../elements/ErrorPill'
import { useDocumentInfo } from '../../../../providers/DocumentInfo'
import { usePreferences } from '../../../../providers/Preferences'
import { useTranslation } from '../../../..'
import { WatchChildErrors } from '../../../WatchChildErrors'
export const CollapsibleInput: React.FC<{
initCollapsed?: boolean
children: React.ReactNode
path: string
baseClass: string
RowLabel?: React.ReactNode
fieldPreferencesKey?: string
pathSegments?: string[]
}> = (props) => {
const { initCollapsed, children, path, baseClass, RowLabel, fieldPreferencesKey, pathSegments } =
props
const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo()
const [errorCount, setErrorCount] = useState(0)
const { i18n } = useTranslation()
const onToggle = useCallback(
async (newCollapsedState: boolean) => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
collapsed: newCollapsedState,
},
},
}
: {
fields: {
...(existingPreferences?.fields || {}),
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
},
},
}),
})
},
[preferencesKey, fieldPreferencesKey, getPreference, setPreference, path],
)
return (
<Fragment>
<WatchChildErrors pathSegments={pathSegments} setErrorCount={setErrorCount} />
<Collapsible
className={`${baseClass}__collapsible`}
collapsibleStyle={errorCount > 0 ? 'error' : 'default'}
header={
<div className={`${baseClass}__row-label-wrap`}>
{RowLabel}
{errorCount > 0 && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
}
initCollapsed={initCollapsed}
onToggle={onToggle}
>
{children}
</Collapsible>
</Fragment>
)
}

View File

@@ -1,36 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
const baseClass = 'collapsible-field'
export const CollapsibleFieldWrapper: React.FC<{
className?: string
path: string
children: React.ReactNode
id?: string
}> = (props) => {
const { children, className, path, id } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[
fieldBaseClass,
baseClass,
className,
!valid ? `${baseClass}--has-error` : `${baseClass}--has-no-error`,
]
.filter(Boolean)
.join(' ')}
id={id}
>
{children}
</div>
)
}

View File

@@ -1,14 +1,20 @@
import React from 'react' 'use client'
import React, { Fragment, useCallback, useEffect, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
import FieldDescription from '../../FieldDescription' import { Collapsible } from '../../../elements/Collapsible'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath' import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { usePreferences } from '../../../providers/Preferences'
import { useFormSubmitted } from '../../Form/context'
import RenderFields from '../../RenderFields' import RenderFields from '../../RenderFields'
import { CollapsibleFieldWrapper } from './Wrapper' import { withCondition } from '../../withCondition'
import { CollapsibleInput } from './Input' import { fieldBaseClass } from '../shared'
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState' import { DocumentPreferences } from 'payload/types'
import { RowLabel } from '../../RowLabel' import { useFieldPath } from '../../FieldPathProvider'
import { WatchChildErrors } from '../../WatchChildErrors'
import { ErrorPill } from '../../../elements/ErrorPill'
import { useTranslation } from '../../../providers/Translation'
import './index.scss' import './index.scss'
@@ -16,70 +22,116 @@ const baseClass = 'collapsible-field'
const CollapsibleField: React.FC<Props> = (props) => { const CollapsibleField: React.FC<Props> = (props) => {
const { const {
admin: { className, description, initCollapsed: initCollapsedFromProps, readOnly }, className,
fieldTypes, readOnly,
fields, path: pathFromProps,
indexPath,
label,
path,
permissions, permissions,
i18n, Description,
config, Error,
payload, fieldMap,
user, Label,
formState,
docPreferences,
} = props } = props
const { fieldState: nestedFieldState, pathSegments } = getNestedFieldState({ const pathFromContext = useFieldPath()
formState, const path = pathFromProps || pathFromContext
path,
fieldSchema: fields, const { i18n } = useTranslation()
const initCollapsed = 'initCollapsed' in props ? props.initCollapsed : false
const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo()
const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>()
const fieldPreferencesKey = `collapsible-${path.replace(/\./g, '__')}`
const [errorCount, setErrorCount] = useState(0)
const submitted = useFormSubmitted()
const fieldHasErrors = errorCount > 0 && submitted
const onToggle = useCallback(
async (newCollapsedState: boolean) => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
collapsed: newCollapsedState,
},
},
}
: {
fields: {
...(existingPreferences?.fields || {}),
[fieldPreferencesKey]: {
...existingPreferences?.fields?.[fieldPreferencesKey],
collapsed: newCollapsedState,
},
},
}),
}) })
},
const fieldPreferencesKey = `collapsible-${indexPath.replace(/\./g, '__')}` [preferencesKey, fieldPreferencesKey, getPreference, setPreference, path],
const initCollapsed = Boolean(
docPreferences
? docPreferences?.fields?.[path || fieldPreferencesKey]?.collapsed
: initCollapsedFromProps,
) )
useEffect(() => {
const fetchInitialState = async () => {
const preferences = await getPreference(preferencesKey)
if (preferences) {
const initCollapsedFromPref = path
? preferences?.fields?.[path]?.collapsed
: preferences?.fields?.[fieldPreferencesKey]?.collapsed
setCollapsedOnMount(Boolean(initCollapsedFromPref))
} else {
setCollapsedOnMount(typeof initCollapsed === 'boolean' ? initCollapsed : false)
}
}
fetchInitialState()
}, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed, path])
if (typeof collapsedOnMount !== 'boolean') return null
return ( return (
<CollapsibleFieldWrapper <Fragment>
className={className} <WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
path={path} <div
className={[
fieldBaseClass,
baseClass,
className,
fieldHasErrors ? `${baseClass}--has-error` : `${baseClass}--has-no-error`,
]
.filter(Boolean)
.join(' ')}
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`} id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
> >
<CollapsibleInput <Collapsible
initCollapsed={initCollapsed} className={`${baseClass}__collapsible`}
baseClass={baseClass} collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
RowLabel={<RowLabel data={formState} label={label} path={path} i18n={i18n} />} header={
path={path} <div className={`${baseClass}__row-label-wrap`}>
fieldPreferencesKey={fieldPreferencesKey} {Label}
pathSegments={pathSegments} {fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
}
initCollapsed={collapsedOnMount}
onToggle={onToggle}
> >
<RenderFields <RenderFields
fieldSchema={fields.map((field) => ({ fieldMap={fieldMap}
...field,
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender forceRender
indexPath={indexPath} indexPath={path}
margins="small" margins="small"
permissions={permissions} permissions={permissions}
readOnly={readOnly} readOnly={readOnly}
i18n={i18n}
config={config}
payload={payload}
formState={nestedFieldState}
user={user}
/> />
</CollapsibleInput> </Collapsible>
<FieldDescription description={description} path={path} i18n={i18n} /> {Description}
</CollapsibleFieldWrapper> </div>
</Fragment>
) )
} }
export default CollapsibleField export default withCondition(CollapsibleField)

View File

@@ -1,10 +1,8 @@
import type { FieldTypes } from 'payload/config' import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth' import type { FieldPermissions } from 'payload/auth'
import type { CollapsibleField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<CollapsibleField, 'type'> & {
fieldTypes: FieldTypes fieldTypes: FieldTypes
indexPath: string indexPath: string
permissions: FieldPermissions permissions: FieldPermissions

View File

@@ -1,47 +0,0 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import type { DateField, Validate } from 'payload/types'
import { useTranslation } from '../../../providers/Translation'
import React, { useCallback } from 'react'
import DatePicker from '../../../elements/DatePicker'
import useField from '../../useField'
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
datePickerProps?: DateField['admin']['date']
path: string
placeholder?: Record<string, string> | string
readOnly?: boolean
style?: React.CSSProperties
width?: string
}
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
const { path, readOnly, placeholder, datePickerProps, style, width, validate, required } = props
const { i18n } = useTranslation()
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField<Date>({
path,
validate: memoizedValidate,
})
return (
<DatePicker
{...datePickerProps}
onChange={(incomingDate) => {
if (!readOnly) setValue(incomingDate?.toISOString() || null)
}}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly}
value={value}
/>
)
}

View File

@@ -1,38 +1,52 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { DateTimeInput } from './Input'
import './index.scss'
import FieldDescription from '../../FieldDescription'
import { fieldBaseClass } from '../shared' import { fieldBaseClass } from '../shared'
import DefaultLabel from '../../Label' import DatePickerField from '../../../elements/DatePicker'
import DefaultError from '../../Error' import { getTranslation } from '@payloadcms/translations'
import { Validate } from 'payload/types'
import useField from '../../useField'
import { useTranslation } from '../../../providers/Translation'
import './index.scss'
const baseClass = 'date-time-field' const baseClass = 'date-time-field'
const DateTime: React.FC<Props> = (props) => { const DateTime: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { beforeInput, afterInput, Label, Error },
date,
description,
placeholder, placeholder,
readOnly, readOnly,
style, style,
width, width,
} = {},
label,
path: pathFromProps, path: pathFromProps,
required, required,
Error,
Label,
BeforeInput,
AfterInput,
Description,
validate,
} = props } = props
const path = pathFromProps || name const datePickerProps = 'date' in props ? props.date : {}
const ErrorComp = Error || DefaultError const { i18n } = useTranslation()
const LabelComp = Label || DefaultLabel
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { setValue, showError, value, path } = useField<Date>({
path: pathFromProps || name,
validate: memoizedValidate,
})
return ( return (
<div <div
@@ -40,7 +54,7 @@ const DateTime: React.FC<Props> = (props) => {
fieldBaseClass, fieldBaseClass,
baseClass, baseClass,
className, className,
// showError && `${baseClass}--has-error`, showError && `${baseClass}--has-error`,
readOnly && 'read-only', readOnly && 'read-only',
] ]
.filter(Boolean) .filter(Boolean)
@@ -50,26 +64,22 @@ const DateTime: React.FC<Props> = (props) => {
width, width,
}} }}
> >
<div className={`${baseClass}__error-wrap`}> <div className={`${baseClass}__error-wrap`}>{Error}</div>
{/* <ErrorComp {Label}
message={errorMessage}
showError={showError}
/> */}
</div>
<LabelComp htmlFor={path} label={label} required={required} />
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}> <div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<DateTimeInput <DatePickerField
datePickerProps={date} {...datePickerProps}
placeholder={placeholder} onChange={(incomingDate) => {
if (!readOnly) setValue(incomingDate?.toISOString() || null)
}}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly} readOnly={readOnly}
path={path} value={value}
style={style}
width={width}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div> </div>
<FieldDescription description={description} path={path} /> {Description}
</div> </div>
) )
} }

View File

@@ -1,5 +1,6 @@
import type { DateField } from 'payload/types' import { FormFieldBase } from '../shared'
export type Props = Omit<DateField, 'type'> & { export type Props = FormFieldBase & {
path: string path: string
name?: string
} }

View File

@@ -1,61 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../../useField'
import { useTranslation } from '../../../../providers/Translation'
import { Validate } from 'payload/types'
export const EmailInput: React.FC<{
name: string
autoComplete?: string
readOnly?: boolean
path: string
required?: boolean
placeholder?: Record<string, string> | string
validate?: Validate
}> = (props) => {
const {
name,
autoComplete,
readOnly,
path: pathFromProps,
required,
validate,
placeholder,
} = props
const { i18n } = useTranslation()
const path = pathFromProps || name
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const {
// errorMessage,
setValue,
// showError,
value,
} = useField({
path,
validate: memoizedValidate,
})
return (
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/>
)
}

View File

@@ -1,34 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const EmailInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, hasMany, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'email', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,67 +1,77 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { EmailInput } from './Input'
import { EmailInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { useTranslation } from '../../../providers/Translation'
import { Validate } from 'payload/types'
import useField from '../../useField'
import { getTranslation } from '@payloadcms/translations'
import './index.scss' import './index.scss'
export const Email: React.FC<Props> = (props) => { export const Email: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {}, path: pathFromProps,
description,
autoComplete, autoComplete,
readOnly, readOnly,
style, style,
width, width,
} = {}, Error,
label, Label,
path: pathFromProps, BeforeInput,
AfterInput,
Description,
required, required,
i18n, validate,
value, placeholder,
} = props } = props
const path = pathFromProps || name const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError const memoizedValidate: Validate = useCallback(
const LabelComp = Label || DefaultLabel (value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { setValue, showError, value, path } = useField({
validate: memoizedValidate,
path: pathFromProps || name,
})
return ( return (
<EmailInputWrapper <div
className={className} className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
readOnly={readOnly} .filter(Boolean)
style={style} .join(' ')}
width={width} style={{
path={path} ...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp {Label}
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<div> <div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<EmailInput <input
name={name}
autoComplete={autoComplete} autoComplete={autoComplete}
readOnly={readOnly} disabled={Boolean(readOnly)}
path={path} id={`field-${path.replace(/\./g, '__')}`}
required={required} name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div>
{Description}
</div> </div>
<FieldDescription description={description} path={path} i18n={i18n} value={value} />
</EmailInputWrapper>
) )
} }

View File

@@ -1,14 +1,7 @@
import type { EmailField } from 'payload/types'
import type { FormFieldBase } from '../shared' import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<EmailField, 'type'> & {
path?: string
value?: string
}
export type InputProps = Omit<EmailField, 'type' | 'admin'> & {
autoComplete?: EmailField['admin']['autoComplete']
readOnly?: EmailField['admin']['readOnly']
path?: string path?: string
name?: string
autoComplete?: string
} }

View File

@@ -1,21 +0,0 @@
'use client'
import React, { Fragment } from 'react'
import { ErrorPill, useTranslation } from '../../../..'
import { WatchChildErrors } from '../../../WatchChildErrors'
export const GroupFieldErrors: React.FC<{
pathSegments: string[]
}> = (props) => {
const { pathSegments } = props
const [errorCount, setErrorCount] = React.useState(0)
const { i18n } = useTranslation()
return (
<Fragment>
<WatchChildErrors pathSegments={pathSegments} setErrorCount={setErrorCount} />{' '}
<ErrorPill count={errorCount} i18n={i18n} withMessage />
</Fragment>
)
}

View File

@@ -1,59 +0,0 @@
'use client'
import React from 'react'
import { useCollapsible } from '../../../../elements/Collapsible/provider'
import { useFormSubmitted } from '../../../Form/context'
import { useRow } from '../../Row/provider'
import { useTabs } from '../../Tabs/provider'
import { fieldBaseClass } from '../../shared'
import { useGroup } from '../provider'
import { GroupField } from 'payload/types'
const baseClass = 'group-field'
export const GroupWrapper: React.FC<
Pick<GroupField['admin'], 'className' | 'hideGutter' | 'style' | 'width'> & {
name: string
children: React.ReactNode
path?: string
}
> = (props) => {
const { name, className, hideGutter = false, style, width, path: pathFromProps, children } = props
const isWithinCollapsible = useCollapsible()
const isWithinGroup = useGroup()
const isWithinRow = useRow()
const isWithinTab = useTabs()
const submitted = useFormSubmitted()
const [errorCount] = React.useState(undefined)
const groupHasErrors = submitted && errorCount > 0
const path = pathFromProps || name
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
return (
<div
className={[
fieldBaseClass,
baseClass,
isTopLevel && `${baseClass}--top-level`,
isWithinCollapsible && `${baseClass}--within-collapsible`,
isWithinGroup && `${baseClass}--within-group`,
isWithinRow && `${baseClass}--within-row`,
isWithinTab && `${baseClass}--within-tab`,
!hideGutter && isWithinGroup && `${baseClass}--gutter`,
groupHasErrors && `${baseClass}--has-error`,
className,
]
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,102 +1,80 @@
import React from 'react' 'use client'
import React, { Fragment } from 'react'
import type { Props } from './types' import type { Props } from './types'
import FieldDescription from '../../FieldDescription'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields' import RenderFields from '../../RenderFields'
import { GroupProvider } from './provider' import { GroupProvider, useGroup } from './provider'
import { GroupWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState' import { useCollapsible } from '../../../elements/Collapsible/provider'
import { GroupFieldErrors } from './Errors' import { useRow } from '../Row/provider'
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments' import { useTabs } from '../Tabs/provider'
import { useFormSubmitted } from '../../../forms/Form/context'
import { fieldBaseClass } from '../shared'
import { useFieldPath } from '../../FieldPathProvider'
import { WatchChildErrors } from '../../WatchChildErrors'
import { ErrorPill } from '../../../elements/ErrorPill'
import { useTranslation } from '../../../providers/Translation'
import './index.scss' import './index.scss'
const baseClass = 'group-field' const baseClass = 'group-field'
const Group: React.FC<Props> = (props) => { const Group: React.FC<Props> = (props) => {
const { const { className, style, width, fieldMap, Description, hideGutter, Label } = props
name,
admin: { description, className, hideGutter = false, readOnly, style, width },
fieldTypes,
fields,
forceRender = false,
indexPath,
label,
path: pathFromProps,
permissions,
formState,
user,
i18n,
payload,
config,
value,
} = props
const path = pathFromProps || name const path = useFieldPath()
const { fieldState: nestedFieldState } = getNestedFieldState({ const { i18n } = useTranslation()
formState, const hasSubmitted = useFormSubmitted()
path, const isWithinCollapsible = useCollapsible()
fieldSchema: fields, const isWithinGroup = useGroup()
}) const isWithinRow = useRow()
const isWithinTab = useTabs()
const [errorCount, setErrorCount] = React.useState(undefined)
const fieldHasErrors = errorCount > 0 && hasSubmitted
const fieldSchema = fields.map((subField) => ({ const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
...subField,
path: createNestedFieldPath(path, subField),
}))
const pathSegments = buildPathSegments(path, fieldSchema)
return ( return (
<GroupWrapper <Fragment>
name={name} <WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
path={path} <div
className={className} className={[
hideGutter={hideGutter} fieldBaseClass,
style={style} baseClass,
width={width} isTopLevel && `${baseClass}--top-level`,
isWithinCollapsible && `${baseClass}--within-collapsible`,
isWithinGroup && `${baseClass}--within-group`,
isWithinRow && `${baseClass}--within-row`,
isWithinTab && `${baseClass}--within-tab`,
!hideGutter && isWithinGroup && `${baseClass}--gutter`,
fieldHasErrors && `${baseClass}--has-error`,
className,
]
.filter(Boolean)
.join(' ')}
id={`field-${path?.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
> >
<GroupProvider> <GroupProvider>
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__header`}> <div className={`${baseClass}__header`}>
{(label || description) && ( {(Label || Description) && (
<header> <header>
{label && ( {Label}
<h3 className={`${baseClass}__title`}> {Description}
{typeof label === 'string' ? label : 'Group Title'}
{/* {getTranslation(label, i18n)} */}
</h3>
)}
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
i18n={i18n}
/>
</header> </header>
)} )}
<GroupFieldErrors pathSegments={pathSegments} /> {fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div> </div>
<RenderFields <RenderFields fieldMap={fieldMap} />
fieldSchema={fieldSchema}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.fields}
readOnly={readOnly}
user={user}
formState={nestedFieldState}
i18n={i18n}
payload={payload}
config={config}
/>
</div> </div>
</GroupProvider> </GroupProvider>
</GroupWrapper> </div>
</Fragment>
) )
} }

View File

@@ -1,13 +1,9 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth' import type { FieldPermissions } from 'payload/auth'
import type { GroupField } from 'payload/types'
import type { FormFieldBase } from '../shared' import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<GroupField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean forceRender?: boolean
indexPath: string indexPath: string
permissions: FieldPermissions permissions: FieldPermissions
value: Record<string, unknown> hideGutter?: boolean
} }

View File

@@ -1,32 +0,0 @@
'use client'
import React, { useEffect } from 'react'
import useField from '../../../useField'
export const HiddenInput: React.FC<{
path: string
value: unknown
disableModifyingForm?: boolean
}> = (props) => {
const { path, value: valueFromProps, disableModifyingForm } = props
const { setValue, value } = useField({
path,
})
useEffect(() => {
if (valueFromProps !== undefined) {
setValue(valueFromProps, disableModifyingForm)
}
}, [valueFromProps, setValue, disableModifyingForm])
return (
<input
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="hidden"
value={(value as string) || ''}
/>
)
}

View File

@@ -1,18 +1,39 @@
import React from 'react' 'use client'
import React, { useEffect } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { HiddenInput } from './Input'
import useField from '../../useField'
import { withCondition } from '../../withCondition'
/** /**
* This is mainly used to save a value on the form that is not visible to the user. * 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. * For example, this sets the `ìd` property of a block in the Blocks field.
*/ */
const HiddenField: React.FC<Props> = (props) => { const HiddenInput: React.FC<Props> = (props) => {
const { name, disableModifyingForm = true, path: pathFromProps, value } = props const { name, disableModifyingForm = true, path: pathFromProps, value: valueFromProps } = props
const path = pathFromProps || name const path = pathFromProps || name
return <HiddenInput path={path} value={value} disableModifyingForm={disableModifyingForm} /> const { setValue, value } = useField({
path,
})
useEffect(() => {
if (valueFromProps !== undefined) {
setValue(valueFromProps, disableModifyingForm)
}
}, [valueFromProps, setValue, disableModifyingForm])
return (
<input
id={`field-${path?.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="hidden"
value={(value as string) || ''}
/>
)
} }
export default HiddenField export default withCondition(HiddenInput)

View File

@@ -1,70 +0,0 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import { JSONField, Validate } from 'payload/types'
import { CodeEditor } from '../../../../elements/CodeEditor'
import useField from '../../../useField'
export const JSONInput: React.FC<{
path: string
required?: boolean
min?: number
max?: number
placeholder?: Record<string, string> | string
readOnly?: boolean
step?: number
hasMany?: boolean
name?: string
validate?: Validate
editorOptions?: JSONField['admin']['editorOptions']
}> = (props) => {
const { name, path: pathFromProps, required, validate, readOnly, editorOptions } = props
const path = pathFromProps || name
const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
const [hasLoadedValue, setHasLoadedValue] = useState(false)
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, jsonError, required })
},
[validate, required],
)
const { initialValue, setValue, value } = useField<string>({
path,
validate: memoizedValidate,
})
const handleChange = useCallback(
(val) => {
if (readOnly) return
setStringValue(val)
try {
setValue(JSON.parse(val))
setJsonError(undefined)
} catch (e) {
setJsonError(e)
}
},
[readOnly, setValue, setStringValue],
)
useEffect(() => {
if (hasLoadedValue) return
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
setHasLoadedValue(true)
}, [initialValue, value])
return (
<CodeEditor
defaultLanguage="json"
onChange={handleChange}
options={editorOptions}
readOnly={readOnly}
value={stringValue}
/>
)
}

View File

@@ -1,38 +0,0 @@
'use client'
import React from 'react'
import { useFormFields } from '../../../Form/context'
import { fieldBaseClass } from '../../shared'
import './index.scss'
const baseClass = 'json-field'
export const JSONInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { width, className, style, path, children, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, baseClass, className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,4 +1,4 @@
@import '../../../../scss/styles.scss'; @import '../../../scss/styles.scss';
.json-field { .json-field {
position: relative; position: relative;

View File

@@ -1,56 +1,107 @@
import React from 'react' 'use client'
import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
import DefaultError from '../../Error' import { fieldBaseClass } from '../shared'
import FieldDescription from '../../FieldDescription' import { CodeEditor } from '../../../elements/CodeEditor'
import DefaultLabel from '../../Label' import { Validate } from 'payload/types'
import { JSONInputWrapper } from './Wrapper' import useField from '../../useField'
import { JSONInput } from './Input' import { withCondition } from '../../withCondition'
import './index.scss'
const baseClass = 'json-field'
const JSONField: React.FC<Props> = (props) => { const JSONField: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label } = {},
description,
editorOptions,
readOnly, readOnly,
style, style,
width, width,
} = {},
label,
path: pathFromProps, path: pathFromProps,
Error,
Label,
Description,
BeforeInput,
AfterInput,
validate,
required, required,
i18n,
value,
} = props } = props
const ErrorComp = Error || DefaultError const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
const [hasLoadedValue, setHasLoadedValue] = useState(false)
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, jsonError, required })
},
[validate, required],
)
const { initialValue, setValue, value, path, showError } = useField<string>({
path: pathFromProps || name,
validate: memoizedValidate,
})
const handleChange = useCallback(
(val) => {
if (readOnly) return
setStringValue(val)
try {
setValue(JSON.parse(val))
setJsonError(undefined)
} catch (e) {
setJsonError(e)
}
},
[readOnly, setValue, setStringValue],
)
useEffect(() => {
if (hasLoadedValue) return
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
setHasLoadedValue(true)
}, [initialValue, value])
return ( return (
<JSONInputWrapper <div
className={className} className={[
path={path} fieldBaseClass,
readOnly={readOnly} baseClass,
width={width} className,
style={style} showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} /> {Label}
<JSONInput <div>
path={path} {BeforeInput}
required={required} <CodeEditor
defaultLanguage="json"
onChange={handleChange}
options={editorOptions}
readOnly={readOnly} readOnly={readOnly}
editorOptions={editorOptions} value={stringValue}
/> />
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {AfterInput}
</JSONInputWrapper> </div>
{Description}
</div>
) )
} }
export default JSONField export default withCondition(JSONField)

View File

@@ -1,8 +1,6 @@
import type { JSONField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<JSONField, 'type'> & {
path?: string path?: string
value?: string name?: string
} }

View File

@@ -1,120 +0,0 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from '../../../../providers/Translation'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../../useField'
import { Validate } from 'payload/types'
export const NumberInput: React.FC<{
path: string
required?: boolean
min?: number
max?: number
placeholder?: Record<string, string> | string
readOnly?: boolean
step?: number
hasMany?: boolean
name?: string
validate?: Validate
}> = (props) => {
const {
name,
placeholder,
readOnly,
step,
hasMany,
max,
min,
path: pathFromProps,
required,
validate,
} = props
const { i18n } = useTranslation()
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, max, min, required })
},
[validate, min, max, required],
)
const { errorMessage, setValue, showError, value } = useField<number | number[]>({
path,
validate: memoizedValidate,
})
const handleChange = useCallback(
(e) => {
const val = parseFloat(e.target.value)
if (Number.isNaN(val)) {
setValue('')
} else {
setValue(val)
}
},
[setValue],
)
const [valueToRender, setValueToRender] = useState<
{ id: string; label: string; value: { value: number } }[]
>([]) // Only for hasMany
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = []
} else if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => Number(option.value?.value || option.value))
} else {
newValue = [Number(selectedOption.value?.value || selectedOption.value)]
}
setValue(newValue)
}
},
[readOnly, setValue],
)
// useEffect update valueToRender:
useEffect(() => {
if (hasMany && Array.isArray(value)) {
setValueToRender(
value.map((val, index) => {
return {
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
label: `${val}`,
value: {
toString: () => `${val}${index}`,
value: (val as any)?.value || val,
}, // You're probably wondering, why the hell is this done that way? Well, React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
}
}),
)
}
}, [value, hasMany])
return (
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
)
}

View File

@@ -1,41 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const NumberInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, hasMany, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[
fieldBaseClass,
'number',
className,
!valid && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,62 +1,128 @@
import React from 'react' 'use client'
import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { isNumber } from 'payload/utilities' import { isNumber } from 'payload/utilities'
import ReactSelect from '../../../elements/ReactSelect' import ReactSelect from '../../../elements/ReactSelect'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { NumberInput } from './Input'
import { NumberInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '../../../providers/Translation'
import useField from '../../useField'
import { Option } from '../../../elements/ReactSelect/types'
import './index.scss' import './index.scss'
const NumberField: React.FC<Props> = (props) => { const NumberField: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder, placeholder,
readOnly, readOnly,
step,
style, style,
width, width,
} = {},
hasMany,
label,
maxRows,
min,
path: pathFromProps, path: pathFromProps,
required, required,
valid = true, Error,
i18n, Label,
value, Description,
BeforeInput,
AfterInput,
validate,
} = props } = props
const ErrorComp = Error || DefaultError const max = 'max' in props ? props.max : Infinity
const LabelComp = Label || DefaultLabel const min = 'min' in props ? props.min : -Infinity
const step = 'step' in props ? props.step : 1
const hasMany = 'hasMany' in props ? props.hasMany : false
const maxRows = 'maxRows' in props ? props.maxRows : Infinity
const path = pathFromProps || name const { i18n } = useTranslation()
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, max, min, required })
},
[validate, min, max, required],
)
const { setValue, showError, value, path } = useField<number | number[]>({
path: pathFromProps || name,
validate: memoizedValidate,
})
const handleChange = useCallback(
(e) => {
const val = parseFloat(e.target.value)
if (Number.isNaN(val)) {
setValue('')
} else {
setValue(val)
}
},
[setValue],
)
const [valueToRender, setValueToRender] = useState<
{ id: string; label: string; value: { value: number } }[]
>([]) // Only for hasMany
const handleHasManyChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = []
} else if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => Number(option.value?.value || option.value))
} else {
newValue = [Number(selectedOption.value?.value || selectedOption.value)]
}
setValue(newValue)
}
},
[readOnly, setValue],
)
// useEffect update valueToRender:
useEffect(() => {
if (hasMany && Array.isArray(value)) {
setValueToRender(
value.map((val, index) => {
return {
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
label: `${val}`,
value: {
toString: () => `${val}${index}`,
value: (val as any)?.value || val,
}, // You're probably wondering, why the hell is this done that way? Well, React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
}
}),
)
}
}, [value, hasMany])
return ( return (
<NumberInputWrapper <div
className={className} className={[
readOnly={readOnly} fieldBaseClass,
hasMany={hasMany} 'number',
style={style} className,
width={width} showError && 'error',
path={path} readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp {Label}
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{hasMany ? ( {hasMany ? (
<ReactSelect <ReactSelect
className={`field-${path.replace(/\./g, '__')}`} className={`field-${path.replace(/\./g, '__')}`}
@@ -81,27 +147,32 @@ const NumberField: React.FC<Props> = (props) => {
// onChange={handleHasManyChange} // onChange={handleHasManyChange}
options={[]} options={[]}
// placeholder={t('general:enterAValue')} // placeholder={t('general:enterAValue')}
showError={!valid} showError={showError}
// value={valueToRender as Option[]} value={valueToRender as Option[]}
/> />
) : ( ) : (
<div> <div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<NumberInput <input
path={path} disabled={readOnly}
required={required} id={`field-${path.replace(/\./g, '__')}`}
min={min} name={path}
placeholder={placeholder} onChange={handleChange}
readOnly={readOnly} onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step} step={step}
hasMany={hasMany} type="number"
name={name} value={typeof value === 'number' ? value : ''}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div> </div>
)} )}
<FieldDescription description={description} value={value} i18n={i18n} /> {Description}
</NumberInputWrapper> </div>
) )
} }

View File

@@ -1,7 +1,7 @@
import type { NumberField } from 'payload/types' import type { NumberField } from 'payload/types'
import type { FormFieldBase } from '../shared' import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<NumberField, 'type'> & { path?: string
value?: number name?: string
} }

View File

@@ -1,42 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import useField from '../../../useField'
import { Validate } from 'payload/types'
export const PasswordInput: React.FC<{
name: string
autoComplete?: string
disabled?: boolean
path: string
required?: boolean
validate?: Validate
}> = (props) => {
const { name, autoComplete, disabled, path: pathFromProps, required, validate } = props
const path = pathFromProps || name
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { errorMessage, formProcessing, setValue, showError, value } = useField({
path,
validate: memoizedValidate,
})
return (
<input
autoComplete={autoComplete}
disabled={formProcessing || disabled}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="password"
value={(value as string) || ''}
/>
)
}

View File

@@ -1,33 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const PasswordInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
children: React.ReactNode
path: string
}> = (props) => {
const { className, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'password', className, !valid && 'error']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,41 +1,63 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import Error from '../../Error'
import Label from '../../Label'
import { PasswordInput } from './Input'
import { PasswordInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { Validate } from 'payload/types'
import useField from '../../useField'
import './index.scss' import './index.scss'
export const Password: React.FC<Props> = (props) => { export const Password: React.FC<Props> = (props) => {
const { const {
name,
autoComplete, autoComplete,
className, className,
disabled, disabled,
label,
path: pathFromProps,
required, required,
style, style,
width, width,
i18n, validate,
path: pathFromProps,
name,
Error,
Label,
} = props } = props
const path = pathFromProps || name const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { formProcessing, setValue, showError, value, path } = useField({
path: pathFromProps || name,
validate: memoizedValidate,
})
return ( return (
<PasswordInputWrapper className={className} style={style} width={width} path={path}> <div
<Error path={path} /> className={[fieldBaseClass, 'password', className, showError && 'error']
<Label .filter(Boolean)
htmlFor={`field-${path.replace(/\./g, '__')}`} .join(' ')}
label={label} style={{
required={required} ...style,
i18n={i18n} width,
}}
>
{Error}
{Label}
<input
autoComplete={autoComplete}
disabled={formProcessing || disabled}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
type="password"
value={(value as string) || ''}
/> />
<PasswordInput name={name} autoComplete={autoComplete} disabled={disabled} path={path} /> </div>
</PasswordInputWrapper>
) )
} }

View File

@@ -1,59 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import useField from '../../../useField'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '../../../../providers/Translation'
import { Validate } from 'payload/types'
export const PointInput: React.FC<{
path: string
placeholder?: Record<string, string> | string
step?: number
readOnly?: boolean
isLatitude?: boolean
validate?: Validate
required?: boolean
}> = (props) => {
const { path, placeholder, step, readOnly, isLatitude = true, validate, required } = props
const { i18n } = useTranslation()
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { setValue, value = [null, null] } = useField<[number, number]>({
path,
validate: memoizedValidate,
})
const handleChange = useCallback(
(e, index: 0 | 1) => {
let val = parseFloat(e.target.value)
if (Number.isNaN(val)) {
val = e.target.value
}
const coordinates = [...value]
coordinates[index] = val
setValue(coordinates)
},
[setValue, value],
)
return (
<input
disabled={readOnly}
id={`field-${isLatitude ? 'latitude' : 'longitude'}-${path.replace(/\./g, '__')}`}
name={`${path}.${isLatitude ? 'latitude' : 'longitude'}`}
onChange={(e) => handleChange(e, isLatitude ? 1 : 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={
value && typeof value[isLatitude ? 1 : 0] === 'number' ? value[isLatitude ? 1 : 0] : ''
}
/>
)
}

View File

@@ -1,34 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const PointInputWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
}> = (props) => {
const { children, className, style, width, path, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,13 +1,14 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import DefaultError from '../../Error' import useField from '../../useField'
import FieldDescription from '../../FieldDescription' import { fieldBaseClass } from '../shared'
import DefaultLabel from '../../Label' import { Validate } from 'payload/types'
import { NumberInputWrapper } from '../Number/Wrapper' import { useTranslation } from '../../../providers/Translation'
import { PointInput } from './Input' import { withCondition } from '../../withCondition'
import './index.scss' import './index.scss'
@@ -16,82 +17,111 @@ const baseClass = 'point'
const PointField: React.FC<Props> = (props) => { const PointField: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder, placeholder,
readOnly, readOnly,
step,
style, style,
width, width,
} = {},
label,
path: pathFromProps,
required, required,
i18n, validate,
i18n: { t }, path: pathFromProps,
value, Error,
BeforeInput,
AfterInput,
Label,
Description,
} = props } = props
const ErrorComp = Error || DefaultError const { i18n } = useTranslation()
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name const step = 'step' in props ? props.step : 1
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const {
setValue,
value = [null, null],
showError,
path,
} = useField<[number, number]>({
validate: memoizedValidate,
path: pathFromProps || name,
})
const handleChange = useCallback(
(e, index: 0 | 1) => {
let val = parseFloat(e.target.value)
if (Number.isNaN(val)) {
val = e.target.value
}
const coordinates = [...value]
coordinates[index] = val
setValue(coordinates)
},
[setValue, value],
)
return ( return (
<NumberInputWrapper <div
className={[className, baseClass].filter(Boolean).join(' ')} className={[
readOnly={readOnly} fieldBaseClass,
style={style} baseClass,
width={width} className,
path={path} showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<ul className={`${baseClass}__wrap`}> <ul className={`${baseClass}__wrap`}>
<li> <li>
<LabelComp {Label}
htmlFor={`field-longitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('fields:longitude')}`}
required={required}
i18n={i18n}
/>
<div className="input-wrapper"> <div className="input-wrapper">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<PointInput <input
path={path} disabled={readOnly}
placeholder={placeholder} id={`field-longitude-${path.replace(/\./g, '__')}`}
readOnly={readOnly} name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step} step={step}
required={required} type="number"
isLatitude={false} value={value && typeof value[0] === 'number' ? value[0] : ''}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div> </div>
</li> </li>
<li> <li>
<LabelComp {Label}
htmlFor={`field-latitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('fields:latitude')}`}
required={required}
i18n={i18n}
/>
<div className="input-wrapper"> <div className="input-wrapper">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<PointInput <input
path={path} disabled={readOnly}
placeholder={placeholder} id={`field-latitude-${path.replace(/\./g, '__')}`}
readOnly={readOnly} name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}
placeholder={getTranslation(placeholder, i18n)}
step={step} step={step}
required={required} type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div> </div>
</li> </li>
</ul> </ul>
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {Description}
</NumberInputWrapper> </div>
) )
} }
export default PointField export default withCondition(PointField)

View File

@@ -1,8 +1,6 @@
import type { NumberField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<NumberField, 'type'> & {
path?: string path?: string
value?: number name?: string
} }

View File

@@ -1,66 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import { Option, Validate, optionIsObject } from 'payload/types'
import useField from '../../../useField'
import { Radio } from '../Radio'
export const RadioGroupInput: React.FC<{
readOnly: boolean
path: string
required?: boolean
options: Option[]
name?: string
validate?: Validate
baseClass: string
}> = (props) => {
const { name, readOnly, path: pathFromProps, required, validate, options, baseClass } = props
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, options, required })
},
[validate, options, required],
)
const { setValue, value } = useField<string>({
path,
validate: memoizedValidate,
})
return (
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''
let optionLabel: string | Record<string, string> = ''
if (optionIsObject(option)) {
optionValue = option.value
optionLabel = option.label
} else {
optionValue = option
optionLabel = option
}
const isSelected = String(optionValue) === String(value)
const id = `field-${path}-${optionValue}`
return (
<li key={`${path} - ${optionValue}`}>
<Radio
id={id}
isSelected={isSelected}
onChange={readOnly ? undefined : setValue}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
/>
</li>
)
})}
</ul>
)
}

View File

@@ -4,7 +4,7 @@ import React from 'react'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { OptionObject } from 'payload/types' import { OptionObject } from 'payload/types'
import { OnChange } from '../types' import { OnChange } from '../types'
import { useTranslation } from '../../../..' import { useTranslation } from '../../../../providers/Translation'
import './index.scss' import './index.scss'

View File

@@ -1,46 +0,0 @@
@import '../../../../scss/styles.scss';
.radio-group {
ul {
list-style: none;
padding: 0;
margin: 0;
}
&--layout-horizontal {
ul {
display: flex;
flex-wrap: wrap;
}
li {
flex-shrink: 0;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
}
}
}
html[data-theme='light'] {
.radio-group {
&.error {
.radio-input__styled-radio {
@include lightInputError;
}
}
}
}
html[data-theme='dark'] {
.radio-group {
&.error {
.radio-input__styled-radio {
@include darkInputError;
}
}
}
}

View File

@@ -1,46 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
import { RadioField } from 'payload/types'
import './index.scss'
export const RadioGroupWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
baseClass?: string
layout?: RadioField['admin']['layout']
}> = (props) => {
const { children, className, style, width, path, readOnly, baseClass, layout } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[
fieldBaseClass,
baseClass,
className,
`${baseClass}--layout-${layout}`,
!valid && 'error',
readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -4,4 +4,47 @@
&__error-wrap { &__error-wrap {
position: relative; position: relative;
} }
&--layout-horizontal {
ul {
display: flex;
flex-wrap: wrap;
}
li {
flex-shrink: 0;
[dir='ltr'] & {
padding-right: $baseline;
}
[dir='rtl'] & {
padding-left: $baseline;
}
}
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
}
html[data-theme='light'] {
.radio-group {
&.error {
.radio-input__styled-radio {
@include lightInputError;
}
}
}
}
html[data-theme='dark'] {
.radio-group {
&.error {
.radio-input__styled-radio {
@include darkInputError;
}
}
}
} }

View File

@@ -1,13 +1,13 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { RadioGroupWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import FieldDescription from '../../FieldDescription' import { Radio } from './Radio'
import DefaultError from '../../Error' import { optionIsObject } from 'payload/types'
import DefaultLabel from '../../Label' import useField from '../../useField'
import { RadioGroupInput } from './Input' import { fieldBaseClass } from '../shared'
import './index.scss' import './index.scss'
@@ -16,45 +16,86 @@ const baseClass = 'radio-group'
const RadioGroup: React.FC<Props> = (props) => { const RadioGroup: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label } = {},
description,
layout = 'horizontal',
readOnly, readOnly,
style, style,
width, width,
} = {},
label,
options,
path: pathFromProps, path: pathFromProps,
Error,
Label,
Description,
validate,
required, required,
i18n,
value,
} = props } = props
const path = pathFromProps || name const options = 'options' in props ? props.options : []
const ErrorComp = Error || DefaultError const layout = 'layout' in props ? props.layout : 'horizontal'
const LabelComp = Label || DefaultLabel
const memoizedValidate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, options, required })
},
[validate, options, required],
)
const { setValue, value, path, showError } = useField<string>({
path: pathFromProps || name,
validate: memoizedValidate,
})
return ( return (
<RadioGroupWrapper <div
width={width} className={[
className={className} fieldBaseClass,
style={style} baseClass,
layout={layout} className,
path={path} `${baseClass}--layout-${layout}`,
readOnly={readOnly} showError && 'error',
baseClass={baseClass} readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<div className={`${baseClass}__error-wrap`}> <div className={`${baseClass}__error-wrap`}>{Error}</div>
<ErrorComp path={path} /> {Label}
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''
let optionLabel: string | Record<string, string> = ''
if (optionIsObject(option)) {
optionValue = option.value
optionLabel = option.label
} else {
optionValue = option
optionLabel = option
}
const isSelected = String(optionValue) === String(value)
const id = `field-${path}-${optionValue}`
return (
<li key={`${path} - ${optionValue}`}>
<Radio
id={id}
isSelected={isSelected}
onChange={readOnly ? undefined : setValue}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
/>
</li>
)
})}
</ul>
{Description}
</div> </div>
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
<RadioGroupInput path={path} options={options} readOnly={readOnly} baseClass={baseClass} />
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</RadioGroupWrapper>
) )
} }

View File

@@ -1,10 +1,9 @@
import type { RadioField } from 'payload/types' import type { RadioField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<RadioField, 'type'> & { name?: string
path?: string path?: string
value?: string
} }
export type OnChange<T = string> = (value: T) => void export type OnChange<T = string> = (value: T) => void

View File

@@ -1,8 +1,8 @@
'use client'
import React from 'react' import React from 'react'
import type { Props } from './types' import type { Props } from './types'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields' import RenderFields from '../../RenderFields'
import { fieldBaseClass } from '../shared' import { fieldBaseClass } from '../shared'
import { RowProvider } from './provider' import { RowProvider } from './provider'
@@ -13,41 +13,19 @@ import './index.scss'
const baseClass = 'row' const baseClass = 'row'
const Row: React.FC<Props> = (props) => { const Row: React.FC<Props> = (props) => {
const { const { className, readOnly, forceRender = false, indexPath, permissions, fieldMap } = props
admin: { className, readOnly },
fieldTypes,
fields,
forceRender = false,
indexPath,
path,
permissions,
formState,
user,
i18n,
payload,
config,
} = props
return ( return (
<RowProvider> <RowProvider>
<div className={[fieldBaseClass, baseClass, className].filter(Boolean).join(' ')}> <div className={[fieldBaseClass, baseClass, className].filter(Boolean).join(' ')}>
<RenderFields <RenderFields
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldSchema={fields.map((field) => ({ fieldMap={fieldMap}
...field,
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender} forceRender={forceRender}
indexPath={indexPath} indexPath={indexPath}
margins={false} margins={false}
permissions={permissions} permissions={permissions}
readOnly={readOnly} readOnly={readOnly}
formState={formState}
user={user}
i18n={i18n}
payload={payload}
config={config}
/> />
</div> </div>
</RowProvider> </RowProvider>

View File

@@ -1,10 +1,8 @@
import type { FieldTypes } from 'payload/config' import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth' import type { FieldPermissions } from 'payload/auth'
import type { RowField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<RowField, 'type'> & {
fieldTypes: FieldTypes fieldTypes: FieldTypes
forceRender?: boolean forceRender?: boolean
indexPath: string indexPath: string

View File

@@ -1,94 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import { useTranslation } from '../../../../providers/Translation'
import type { OptionObject, Validate } from 'payload/types'
import type { Option } from '../../../../elements/ReactSelect/types'
import { getTranslation } from '@payloadcms/translations'
import ReactSelect from '../../../../elements/ReactSelect'
import useField from '../../../useField'
const SelectInput: React.FC<{
readOnly: boolean
isClearable: boolean
hasMany: boolean
isSortable: boolean
options: OptionObject[]
path: string
validate?: Validate
required?: boolean
}> = ({ readOnly, isClearable, hasMany, isSortable, options, path, validate, required }) => {
const { i18n } = useTranslation()
const memoizedValidate: Validate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, hasMany, options, required })
},
[validate, required],
)
const { setValue, showError, value } = useField({
path,
validate: memoizedValidate,
})
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,
}
}
const onChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = null
} else if (hasMany) {
if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value)
} else {
newValue = []
}
} else {
newValue = selectedOption.value
}
setValue(newValue)
}
},
[readOnly, hasMany, setValue],
)
return (
<ReactSelect
disabled={readOnly}
isClearable={isClearable}
isMulti={hasMany}
isSortable={isSortable}
onChange={onChange}
options={options.map((option) => ({
...option,
label: getTranslation(option.label, i18n),
}))}
showError={showError}
value={valueToRender as Option}
/>
)
}
export default SelectInput

View File

@@ -1,35 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const SelectFieldWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'select', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,13 +1,14 @@
import React from 'react' 'use client'
import React, { useCallback, useEffect, useState } from 'react'
import type { Option, OptionObject } from 'payload/types' import type { Option, OptionObject, Validate } from 'payload/types'
import type { Props } from './types' import type { Props } from './types'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import SelectInput from './Input'
import { SelectFieldWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { getTranslation } from '@payloadcms/translations'
import { fieldBaseClass } from '../shared'
import useField from '../../useField'
import ReactSelect from '../../../elements/ReactSelect'
import { useTranslation } from '../../../providers/Translation'
import './index.scss' import './index.scss'
@@ -26,55 +27,120 @@ const formatOptions = (options: Option[]): OptionObject[] =>
export const Select: React.FC<Props> = (props) => { export const Select: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
description,
isClearable,
isSortable = true,
readOnly, readOnly,
style, style,
width, width,
components: { Error, Label } = {},
} = {},
hasMany,
label,
options,
path: pathFromProps, path: pathFromProps,
required, required,
i18n, Description,
value, Error,
Label,
BeforeInput,
AfterInput,
validate,
} = props } = props
const path = pathFromProps || name const optionsFromProps = 'options' in props ? props.options : []
const hasMany = 'hasMany' in props ? props.hasMany : false
const isClearable = 'isClearable' in props ? props.isClearable : true
const isSortable = 'isSortable' in props ? props.isSortable : true
const ErrorComp = Error || DefaultError const { i18n } = useTranslation()
const LabelComp = Label || DefaultLabel
const [options] = useState(formatOptions(optionsFromProps))
const memoizedValidate: Validate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, hasMany, options, required })
},
[validate, required],
)
const { setValue, value, showError, path } = useField({
path: pathFromProps || name,
validate: memoizedValidate,
})
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,
}
}
const onChange = useCallback(
(selectedOption) => {
if (!readOnly) {
let newValue
if (!selectedOption) {
newValue = null
} else if (hasMany) {
if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value)
} else {
newValue = []
}
} else {
newValue = selectedOption.value
}
setValue(newValue)
}
},
[readOnly, hasMany, setValue],
)
return ( return (
<SelectFieldWrapper <div
className={className} className={[
style={style} fieldBaseClass,
width={width} 'select',
path={path} className,
readOnly={readOnly} showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp {Label}
htmlFor={`field-${path.replace(/\./g, '__')}`} <div>
label={label} {BeforeInput}
required={required} <ReactSelect
i18n={i18n} disabled={readOnly}
/>
<SelectInput
readOnly={readOnly}
isClearable={isClearable} isClearable={isClearable}
hasMany={hasMany} isMulti={hasMany}
isSortable={isSortable} isSortable={isSortable}
options={formatOptions(options)} onChange={onChange}
path={path} options={options.map((option) => ({
...option,
label: getTranslation(option.label, i18n),
}))}
showError={showError}
value={valueToRender as OptionObject}
/> />
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {AfterInput}
</SelectFieldWrapper> </div>
{Description}
</div>
) )
} }

View File

@@ -1,8 +1,7 @@
import type { SelectField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<SelectField, 'type'> & {
path?: string path?: string
value?: string value?: string
name?: string
} }

View File

@@ -1,38 +1,40 @@
'use client' 'use client'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { NamedTab, Tab } from 'payload/types' import React, { useState } from 'react'
import React from 'react'
import { ErrorPill } from '../../../../elements/ErrorPill' import { ErrorPill } from '../../../../elements/ErrorPill'
import { WatchChildErrors } from '../../../WatchChildErrors' import { WatchChildErrors } from '../../../WatchChildErrors'
import { useTranslation } from '../../../..' import { useFormSubmitted, useTranslation } from '../../../..'
import { ReducedTab } from '../../../RenderFields/createFieldMap'
import './index.scss' import './index.scss'
type TabProps = {
isActive?: boolean
setIsActive: () => void
pathSegments: string[]
path: string
label: Tab['label']
name: NamedTab['name']
}
const baseClass = 'tabs-field__tab-button' const baseClass = 'tabs-field__tab-button'
export const TabComponent: React.FC<TabProps> = (props) => { type TabProps = {
const { isActive, setIsActive, pathSegments, name, label } = props isActive?: boolean
parentPath: string
setIsActive: () => void
tab: ReducedTab
}
export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsActive, tab }) => {
const { label, name } = tab
const { i18n } = useTranslation() const { i18n } = useTranslation()
const [errorCount, setErrorCount] = useState(undefined)
const hasName = 'name' in tab
const hasSubmitted = useFormSubmitted()
const [errorCount, setErrorCount] = React.useState(0) const path = `${parentPath ? `${parentPath}.` : ''}${'name' in tab ? name : ''}`
const fieldHasErrors = errorCount > 0 && hasSubmitted
const tabHasErrors = errorCount > 0
return ( return (
<React.Fragment>
<WatchChildErrors fieldMap={tab.subfields} path={path} setErrorCount={setErrorCount} />
<button <button
className={[ className={[
baseClass, baseClass,
tabHasErrors && `${baseClass}--has-error`, fieldHasErrors && `${baseClass}--has-error`,
isActive && `${baseClass}--active`, isActive && `${baseClass}--active`,
] ]
.filter(Boolean) .filter(Boolean)
@@ -40,9 +42,9 @@ export const TabComponent: React.FC<TabProps> = (props) => {
onClick={setIsActive} onClick={setIsActive}
type="button" type="button"
> >
<WatchChildErrors pathSegments={pathSegments} setErrorCount={setErrorCount} /> {label ? getTranslation(label, i18n) : hasName && name}
{label ? getTranslation(label, i18n) : name} {fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} />}
{tabHasErrors && <ErrorPill i18n={i18n} count={errorCount} />}
</button> </button>
</React.Fragment>
) )
} }

View File

@@ -1,30 +0,0 @@
'use client'
import React from 'react'
import { useCollapsible } from '../../../../elements/Collapsible/provider'
import { fieldBaseClass } from '../../shared'
const baseClass = 'tabs-field'
export const Wrapper: React.FC<{
className?: string
children: React.ReactNode
}> = (props) => {
const { className, children } = props
const isWithinCollapsible = useCollapsible()
return (
<div
className={[
fieldBaseClass,
className,
baseClass,
isWithinCollapsible && `${baseClass}--within-collapsible`,
]
.filter(Boolean)
.join(' ')}
>
{children}
</div>
)
}

View File

@@ -1,114 +1,117 @@
import React from 'react' 'use client'
import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types' import type { Props } from './types'
import FieldDescription from '../../FieldDescription' import { useCollapsible } from '../../../elements/Collapsible/provider'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath' import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { usePreferences } from '../../../providers/Preferences'
import RenderFields from '../../RenderFields' import RenderFields from '../../RenderFields'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { TabsProvider } from './provider' import { TabsProvider } from './provider'
import { TabComponent } from './Tab' import { TabComponent } from './Tab'
import { Wrapper } from './Wrapper'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { DocumentPreferences, tabHasName } from 'payload/types'
import { toKebabCase } from 'payload/utilities' import { toKebabCase } from 'payload/utilities'
import { Tab } from 'payload/types' import { useTranslation } from '../../../providers/Translation'
import { withCondition } from '../../withCondition' import { FieldPathProvider, useFieldPath } from '../../FieldPathProvider'
import './index.scss' import './index.scss'
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments'
const baseClass = 'tabs-field' const baseClass = 'tabs-field'
const getTabFieldSchema = ({ tabConfig, path }: { tabConfig: Tab; path }) => { const TabsField: React.FC<Props> = (props) => {
return tabConfig.fields.map((field) => {
const pathSegments = []
if (path) pathSegments.push(path)
if ('name' in tabConfig) pathSegments.push(tabConfig.name)
return {
...field,
path: createNestedFieldPath(pathSegments.join('.'), field),
}
})
}
const TabsField: React.FC<Props> = async (props) => {
const { const {
admin: { className, readOnly }, className,
fieldTypes, readOnly,
forceRender = false, forceRender = false,
indexPath, indexPath,
path,
permissions, permissions,
tabs, Description,
formState, fieldMap,
user, path: pathFromProps,
i18n, name,
payload,
docPreferences,
config,
} = props } = props
const pathFromContext = useFieldPath()
const path = pathFromContext || pathFromProps || name
const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo()
const { i18n } = useTranslation()
const isWithinCollapsible = useCollapsible()
const [activeTabIndex, setActiveTabIndex] = useState<number>(0)
const tabsPrefKey = `tabs-${indexPath}` const tabsPrefKey = `tabs-${indexPath}`
const activeTabIndex = docPreferences?.fields?.[path || tabsPrefKey]?.tabIndex || 0 const tabs = 'tabs' in props ? props.tabs : []
useEffect(() => {
const getInitialPref = async () => {
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
const initialIndex = path
? existingPreferences?.fields?.[path]?.tabIndex
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
setActiveTabIndex(initialIndex || 0)
}
void getInitialPref()
}, [path, getPreference, preferencesKey, tabsPrefKey])
const handleTabChange = useCallback(
async (incomingTabIndex: number) => {
setActiveTabIndex(incomingTabIndex)
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
setPreference(preferencesKey, {
...existingPreferences,
...(path
? {
fields: {
...(existingPreferences?.fields || {}),
[path]: {
...existingPreferences?.fields?.[path],
tabIndex: incomingTabIndex,
},
},
}
: {
fields: {
...existingPreferences?.fields,
[tabsPrefKey]: {
...existingPreferences?.fields?.[tabsPrefKey],
tabIndex: incomingTabIndex,
},
},
}),
})
},
[preferencesKey, getPreference, setPreference, path, tabsPrefKey],
)
const activeTabConfig = tabs[activeTabIndex] const activeTabConfig = tabs[activeTabIndex]
const isNamedTab = activeTabConfig && 'name' in activeTabConfig
// TODO: make this a server action
// const handleTabChange = useCallback(
// async (incomingTabIndex: number) => {
// setActiveTabIndex(incomingTabIndex)
// const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
// setPreference(preferencesKey, {
// ...existingPreferences,
// ...(path
// ? {
// fields: {
// ...(existingPreferences?.fields || {}),
// [path]: {
// ...existingPreferences?.fields?.[path],
// tabIndex: incomingTabIndex,
// },
// },
// }
// : {
// fields: {
// ...existingPreferences?.fields,
// [tabsPrefKey]: {
// ...existingPreferences?.fields?.[tabsPrefKey],
// tabIndex: incomingTabIndex,
// },
// },
// }),
// })
// },
// [preferencesKey, getPreference, setPreference, path, tabsPrefKey],
// )
return ( return (
<Wrapper className={className}> <div
className={[
fieldBaseClass,
className,
baseClass,
isWithinCollapsible && `${baseClass}--within-collapsible`,
]
.filter(Boolean)
.join(' ')}
>
<TabsProvider> <TabsProvider>
<div className={`${baseClass}__tabs-wrap`}> <div className={`${baseClass}__tabs-wrap`}>
<div className={`${baseClass}__tabs`}> <div className={`${baseClass}__tabs`}>
{tabs.map((tab, tabIndex) => { {tabs.map((tab, tabIndex) => {
const tabPath = [path, 'name' in tab && tab.name].filter(Boolean)?.join('.')
const pathSegments = buildPathSegments(tabPath, tab.fields)
return ( return (
<TabComponent <TabComponent
path={tabPath}
isActive={activeTabIndex === tabIndex} isActive={activeTabIndex === tabIndex}
key={tabIndex} key={tabIndex}
setIsActive={undefined} parentPath={path}
// setIsActive={() => handleTabChange(tabIndex)} setIsActive={() => handleTabChange(tabIndex)}
pathSegments={pathSegments} tab={tab}
name={'name' in tab && tab.name}
label={tab.label}
/> />
) )
})} })}
@@ -128,16 +131,10 @@ const TabsField: React.FC<Props> = async (props) => {
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
> >
<FieldDescription {Description}
className={`${baseClass}__description`} <FieldPathProvider path={'name' in activeTabConfig ? activeTabConfig.name : ''}>
description={activeTabConfig.description}
marginPlacement="bottom"
path={path}
i18n={i18n}
/>
<RenderFields <RenderFields
fieldSchema={getTabFieldSchema({ tabConfig: activeTabConfig, path })} fieldMap={activeTabConfig.subfields}
fieldTypes={fieldTypes}
forceRender={forceRender} forceRender={forceRender}
indexPath={indexPath} indexPath={indexPath}
key={ key={
@@ -147,23 +144,19 @@ const TabsField: React.FC<Props> = async (props) => {
} }
margins="small" margins="small"
permissions={ permissions={
isNamedTab && permissions?.[activeTabConfig.name] 'name' in activeTabConfig && permissions?.[activeTabConfig.name]
? permissions[activeTabConfig.name].fields ? permissions[activeTabConfig.name].fields
: permissions : permissions
} }
readOnly={readOnly} readOnly={readOnly}
user={user}
formState={formState}
i18n={i18n}
payload={payload}
config={config}
/> />
</FieldPathProvider>
</div> </div>
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
</TabsProvider> </TabsProvider>
</Wrapper> </div>
) )
} }

View File

@@ -1,13 +1,10 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth' import type { FieldPermissions } from 'payload/auth'
import type { TabsField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<TabsField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean forceRender?: boolean
indexPath: string indexPath: string
path?: string path?: string
permissions: FieldPermissions permissions: FieldPermissions
name?: string
} }

View File

@@ -1,82 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { SanitizedConfig, Validate } from 'payload/types'
import { useTranslation } from '../../../../providers/Translation'
import { isFieldRTL } from '../../shared'
import useField from '../../../useField'
import { useLocale } from '../../../../providers/Locale'
export const TextInput: React.FC<{
name: string
autoComplete?: string
readOnly?: boolean
path: string
required?: boolean
placeholder?: Record<string, string> | string
localized?: boolean
localizationConfig?: SanitizedConfig['localization']
rtl?: boolean
maxLength?: number
minLength?: number
validate?: Validate
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
inputRef?: React.MutableRefObject<HTMLInputElement>
}> = (props) => {
const {
path,
placeholder,
readOnly,
localized,
localizationConfig,
rtl,
maxLength,
minLength,
validate,
required,
onKeyDown,
inputRef,
} = props
const { i18n } = useTranslation()
const locale = useLocale()
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, minLength, maxLength, required],
)
const { setValue, value } = useField({
path,
validate: memoizedValidate,
})
const renderRTL = isFieldRTL({
fieldLocalized: localized,
fieldRTL: rtl,
locale,
localizationConfig: localizationConfig || undefined,
})
return (
<input
data-rtl={renderRTL}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={(e) => {
setValue(e.target.value)
}}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={(value as string) || ''}
/>
)
}

View File

@@ -1,36 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
import './index.scss'
export const TextInputWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
}> = (props) => {
const { children, className, style, width, path, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'text', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,4 +1,4 @@
@import '../../../../scss/styles.scss'; @import '../../../scss/styles.scss';
.field-type.text { .field-type.text {
position: relative; position: relative;

View File

@@ -1,78 +1,99 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { TextInput } from './Input'
import FieldDescription from '../../FieldDescription'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import { TextInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { fieldBaseClass, isFieldRTL } from '../shared'
import useField from '../../useField'
import { useTranslation } from '../../../providers/Translation'
import { useConfig, useLocale } from '../../..'
import { Validate } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import './index.scss'
const Text: React.FC<Props> = (props) => { const Text: React.FC<Props> = (props) => {
const { const {
name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder,
readOnly,
rtl,
style,
width,
} = {},
label,
localized, localized,
maxLength, maxLength,
minLength, minLength,
path: pathFromProps,
required, required,
value, Error,
i18n, Label,
Description,
BeforeInput,
AfterInput,
validate,
inputRef,
readOnly,
width,
style,
onKeyDown,
placeholder,
rtl,
name,
path: pathFromProps,
} = props } = props
const path = pathFromProps || name const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError const locale = useLocale()
const LabelComp = Label || DefaultLabel
const { localization: localizationConfig } = useConfig()
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, minLength, maxLength, required],
)
const { setValue, value, path, showError } = useField({
validate: memoizedValidate,
path: pathFromProps || name,
})
const renderRTL = isFieldRTL({
fieldLocalized: localized,
fieldRTL: rtl,
locale,
localizationConfig: localizationConfig || undefined,
})
return ( return (
<TextInputWrapper <div
className={className} className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
style={style} .filter(Boolean)
width={width} .join(' ')}
path={path} style={{
readOnly={readOnly} ...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp {Label}
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<div> <div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<TextInput <input
path={path} data-rtl={renderRTL}
name={name} disabled={readOnly}
localized={localized} id={`field-${path?.replace(/\./g, '__')}`}
rtl={rtl} name={path}
placeholder={placeholder} onChange={(e) => {
readOnly={readOnly} setValue(e.target.value)
maxLength={maxLength} }}
minLength={minLength} onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={(value as string) || ''}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div>
{Description}
</div> </div>
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={value}
i18n={i18n}
/>
</TextInputWrapper>
) )
} }

View File

@@ -1,9 +1,8 @@
import type { TextField } from 'payload/types'
import type { FormFieldBase } from '../shared' import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<TextField, 'type'> & { name?: string
path?: string
inputRef?: React.MutableRefObject<HTMLInputElement> inputRef?: React.MutableRefObject<HTMLInputElement>
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement> onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
value: string
} }

View File

@@ -1,53 +0,0 @@
'use client'
import React, { useCallback } from 'react'
import { useTranslation } from '../../../../providers/Translation'
import type { TextareaField, Validate } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../../useField'
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
className?: string
path: string
placeholder?: Record<string, string> | string
readOnly?: boolean
required?: boolean
rows?: number
rtl?: boolean
}
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
const { path, placeholder, readOnly, required, rows, rtl, validate, maxLength, minLength } = props
const { i18n } = useTranslation()
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, required],
)
const { setValue, value } = useField<string>({
path,
validate: memoizedValidate,
})
return (
<textarea
className="textarea-element"
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
value={value || ''}
/>
)
}
export default TextareaInput

View File

@@ -1,34 +0,0 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const TextareaInputWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
}> = (props) => {
const { children, className, style, width, path, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'textarea', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,42 +1,44 @@
import React from 'react' 'use client'
import React, { useCallback } from 'react'
import type { Props } from './types' import type { Props } from './types'
import { isFieldRTL } from '../shared' import { fieldBaseClass, isFieldRTL } from '../shared'
import TextareaInput from './Input'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import { TextareaInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition' import { withCondition } from '../../withCondition'
import { getTranslation } from '@payloadcms/translations'
import { useTranslation } from '../../../providers/Translation'
import useField from '../../useField'
import { Validate } from 'payload/types'
import { useConfig } from '../../../providers/Config'
import './index.scss' import './index.scss'
const Textarea: React.FC<Props> = (props) => { const Textarea: React.FC<Props> = (props) => {
const { const {
name, name,
admin: {
className, className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder, placeholder,
readOnly, readOnly,
rows,
rtl, rtl,
style, style,
width, width,
} = {},
label,
localized, localized,
maxLength, maxLength,
minLength, minLength,
path: pathFromProps, path: pathFromProps,
required, required,
value,
locale, locale,
config: { localization }, Error,
i18n, Label,
BeforeInput,
AfterInput,
validate,
Description,
} = props } = props
const path = pathFromProps || name const rows = 'rows' in props ? props.rows : undefined
const { i18n } = useTranslation()
const { localization } = useConfig()
const isRTL = isFieldRTL({ const isRTL = isFieldRTL({
fieldLocalized: localized, fieldLocalized: localized,
@@ -45,44 +47,57 @@ const Textarea: React.FC<Props> = (props) => {
localizationConfig: localization || undefined, localizationConfig: localization || undefined,
}) })
const ErrorComp = Error || DefaultError const memoizedValidate: Validate = useCallback(
const LabelComp = Label || DefaultLabel (value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, required],
)
const { setValue, value, path, showError } = useField<string>({
path: pathFromProps || name,
validate: memoizedValidate,
})
return ( return (
<TextareaInputWrapper <div
className={className} className={[
readOnly={readOnly} fieldBaseClass,
style={style} 'textarea',
width={width} className,
path={path} showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
> >
<ErrorComp path={path} /> {Error}
<LabelComp {Label}
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}> <label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner"> <div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} /> <div className="textarea-clone" data-value={value || placeholder || ''} />
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)} {BeforeInput}
<TextareaInput <textarea
name={name} className="textarea-element"
path={path} data-rtl={isRTL}
placeholder={placeholder} disabled={readOnly}
readOnly={readOnly} id={`field-${path.replace(/\./g, '__')}`}
required={required} name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
rows={rows} rows={rows}
rtl={isRTL} value={value || ''}
maxLength={maxLength}
minLength={minLength}
/> />
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)} {AfterInput}
</div> </div>
</label> </label>
<FieldDescription description={description} path={path} value={value} i18n={i18n} /> {Description}
</TextareaInputWrapper> </div>
) )
} }

View File

@@ -1,8 +1,6 @@
import type { TextareaField } from 'payload/types'
import { FormFieldBase } from '../shared' import { FormFieldBase } from '../shared'
export type Props = FormFieldBase & export type Props = FormFieldBase & {
Omit<TextareaField, 'type'> & {
path?: string path?: string
value: string name?: string
} }

View File

@@ -1,18 +1,6 @@
import React from 'react' import React from 'react'
import type { UIField } from 'payload/types' const UI: React.FC = () => {
const UI: React.FC<UIField> = (props) => {
const {
admin: {
components: { Field },
},
} = props
if (Field) {
return <Field {...props} />
}
return null return null
} }

View File

@@ -1,24 +1,92 @@
import type { Locale, SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config' import type { Locale, SanitizedLocalizationConfig } from 'payload/config'
import { FormState } from '../Form/types'
import { User } from 'payload/auth' import { User } from 'payload/auth'
import { I18n } from '@payloadcms/translations' import {
import { Payload } from 'payload' ArrayField,
import { DocumentPreferences } from 'payload/types' CodeField,
DateField,
DocumentPreferences,
JSONField,
RowLabel,
Tab,
Validate,
} from 'payload/types'
import { ReducedTab, createFieldMap } from '../RenderFields/createFieldMap'
import { Option } from 'payload/types'
export const fieldBaseClass = 'field-type' export const fieldBaseClass = 'field-type'
export type FormFieldBase = { export type FormFieldBase = {
formState?: FormState
path?: string path?: string
valid?: boolean
errorMessage?: string
user?: User user?: User
i18n?: I18n
payload?: Payload
docPreferences?: DocumentPreferences docPreferences?: DocumentPreferences
locale?: Locale locale?: Locale
config?: SanitizedConfig BeforeInput?: React.ReactNode
AfterInput?: React.ReactNode
Label?: React.ReactNode
Description?: React.ReactNode
Error?: React.ReactNode
fieldMap?: ReturnType<typeof createFieldMap>
style?: React.CSSProperties
width?: string
className?: string
label?: RowLabel
readOnly?: boolean
rtl?: boolean
maxLength?: number
minLength?: number
required?: boolean
placeholder?: string
localized?: boolean
validate?: Validate
} & (
| {
// For `number` fields
step?: number
hasMany?: boolean
maxRows?: number
min?: number
max?: number
} }
| {
// For `radio` fields
layout?: 'horizontal' | 'vertical'
options?: Option[]
}
| {
// For `textarea` fields
rows?: number
}
| {
// For `select` fields
isClearable?: boolean
isSortable?: boolean
}
| {
tabs?: ReducedTab[]
}
| {
// For `code` fields
editorOptions?: CodeField['admin']['editorOptions']
language?: CodeField['admin']['language']
}
| {
// For `json` fields
editorOptions?: JSONField['admin']['editorOptions']
}
| {
// For `collapsible` fields
initCollapsed?: boolean
}
| {
// For `date` fields
date?: DateField['admin']['date']
}
| {
// For `array` fields
minRows?: ArrayField['minRows']
maxRows?: ArrayField['maxRows']
}
)
/** /**
* Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale. * Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale.
@@ -41,6 +109,7 @@ export function isFieldRTL({
localizationConfig && localizationConfig &&
localizationConfig.locales && localizationConfig.locales &&
localizationConfig.locales.length > 1 localizationConfig.locales.length > 1
const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale
return ( return (

Some files were not shown because too many files have changed in this diff Show More