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 { 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 = {
slug: 'pages',
@@ -24,11 +29,47 @@ export const Pages: CollectionConfig = {
drafts: true,
},
fields: [
{
name: 'titleWithCustomComponents',
label: 'Title With Custom Components',
type: 'text',
required: true,
admin: {
description: CustomDescription,
components: {
beforeInput: [BeforeInput],
afterInput: [AfterInput],
Label: CustomLabel,
},
},
},
{
name: 'title',
label: 'Title',
type: 'text',
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',
@@ -118,7 +159,9 @@ export const Pages: CollectionConfig = {
required: true,
},
{
label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
// TODO: fix this
// label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
label: 'Hello',
type: 'collapsible',
admin: {
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',
type: 'tabs',

View File

@@ -52,7 +52,7 @@ export const LoginForm: React.FC<{
>
<FormLoadingOverlayToggle action="loading" name="login-form" />
<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 />
</div>
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { Banner } from '../../elements/Banner'
import { useConfig } from '../../providers/Config'
import { useLocale } from '../../providers/Locale'
import { useForm } from '../Form/context'
import { CheckboxInput } from '../field-types/Checkbox/Input'
import CheckboxInput from '../field-types/Checkbox'
type NullifyLocaleFieldProps = {
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 { fieldAffectsData } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import type { Props } from './types'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent'
import { FormFieldBase } from '../field-types/shared'
import { filterFields } from './filterFields'
import { useTranslation } from '../../providers/Translation'
import { RenderField } from './RenderField'
import './index.scss'
const baseClass = 'render-fields'
const RenderFields: React.FC<Props> = (props) => {
const {
className,
fieldTypes,
forceRender,
margins,
data,
user,
formState,
i18n,
payload,
docPreferences,
locale,
config,
} = props
const { className, margins, fieldMap } = props
const { i18n } = useTranslation()
if (!i18n) {
console.error('Need to implement i18n when calling RenderFields')
}
let fieldsToRender = 'fields' in props ? props?.fields : null
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) {
if (fieldMap) {
return (
<div
className={[
@@ -59,71 +29,9 @@ const RenderFields: React.FC<Props> = (props) => {
.filter(Boolean)
.join(' ')}
>
{fieldsToRender?.map((reducedField, fieldIndex) => {
const {
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>
)
})}
{fieldMap?.map(({ Field, name }, fieldIndex) => (
<RenderField key={fieldIndex} name={name} Field={Field} />
))}
</div>
)
}

View File

@@ -1,25 +1,15 @@
import type { FieldPermissions, User } from 'payload/auth'
import type {
Document,
DocumentPreferences,
Field,
FieldWithPath,
Payload,
SanitizedConfig,
} from 'payload/types'
import type { ReducedField } from './filterFields'
import type { Document, DocumentPreferences, Field } from 'payload/types'
import { FormState } from '../Form/types'
import { FieldTypes, Locale } from 'payload/config'
import { I18n } from '@payloadcms/translations'
import { Locale } from 'payload/config'
import { createFieldMap } from './createFieldMap'
export type Props = {
className?: string
fieldTypes: FieldTypes
forceRender?: boolean
margins?: 'small' | false
data?: Document
formState: FormState
user: User
fieldMap: ReturnType<typeof createFieldMap>
docPreferences?: DocumentPreferences
permissions?:
| {
@@ -27,20 +17,9 @@ export type Props = {
}
| FieldPermissions
readOnly?: boolean
i18n: I18n
payload: Payload
locale?: Locale
config: SanitizedConfig
} & (
| {
// FormState to be filtered by the component
fieldSchema: FieldWithPath[]
} & {
filter?: (field: Field) => boolean
indexPath?: string
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[] => {
const pathNames = fieldSchema.reduce((acc, subField) => {
if (fieldHasSubFields(subField) && fieldAffectsData(subField)) {
export const buildPathSegments = (
parentPath: string,
fieldMap: ReturnType<typeof createFieldMap>,
): string[] => {
const pathNames = fieldMap.reduce((acc, subField) => {
if (subField.subfields && subField.isFieldAffectingData) {
// group, block, array
acc.push(parentPath ? `${parentPath}.${subField.name}.` : `${subField.name}.`)
} else if (fieldHasSubFields(subField)) {
} else if (subField.subfields) {
// rows, collapsibles, unnamed-tab
acc.push(...buildPathSegments(parentPath, subField.fields))
acc.push(...buildPathSegments(parentPath, subField.subfields))
} else if (subField.type === 'tabs') {
// tabs
subField.tabs.forEach((tab: TabAsField) => {
subField.tabs.forEach((tab) => {
let tabPath = parentPath
if (tabHasName(tab)) {
if ('name' in tab) {
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.
acc.push(parentPath ? `${parentPath}.${subField.name}` : subField.name)
}

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth'
import type { ArrayField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = Omit<ArrayField, 'type'> & {
fieldTypes: FieldTypes
export type Props = FormFieldBase & {
forceRender?: boolean
indexPath: string
label: false | string
path?: string
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'
import { getTranslation } from '@payloadcms/translations'
'use client'
import React, { useCallback } from 'react'
import type { Props } from './types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import { fieldBaseClass } from '../shared'
import { CheckboxInput } from './Input'
import DefaultLabel from '../../Label'
import { CheckboxWrapper } from './Wrapper'
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'
@@ -19,38 +17,56 @@ export const inputBaseClass = 'checkbox-input'
const Checkbox: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
readOnly,
style,
width,
} = {},
disableFormData,
label,
path: pathFromProps,
required,
valid = true,
errorMessage,
value,
i18n,
validate,
BeforeInput,
AfterInput,
Label,
Error,
Description,
onChange: onChangeFromProps,
partialChecked,
checked: checkedFromProps,
disableFormData,
id,
path: pathFromProps,
name,
} = 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 LabelComp = Label || DefaultLabel
const onToggle = useCallback(() => {
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 (
<div
className={[
fieldBaseClass,
baseClass,
!valid && 'error',
showError && 'error',
className,
value && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
@@ -62,26 +78,42 @@ const Checkbox: React.FC<Props> = (props) => {
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
<ErrorComp alignCaret="left" path={path} />
</div>
<CheckboxWrapper path={path} readOnly={readOnly} baseClass={inputBaseClass}>
<div className={`${baseClass}__error-wrap`}>{Error}</div>
<div
className={[
inputBaseClass,
checked && `${inputBaseClass}--checked`,
readOnly && `${inputBaseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${inputBaseClass}__input`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<CheckboxInput
{BeforeInput}
<input
aria-label=""
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={fieldID}
label={getTranslation(label || name, i18n)}
name={path}
readOnly={readOnly}
onInput={onToggle}
// ref={inputRef}
type="checkbox"
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>
{label && <LabelComp htmlFor={fieldID} label={label} required={required} i18n={i18n} />}
</CheckboxWrapper>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
{Label}
</div>
{Description}
</div>
)
}

View File

@@ -1,11 +1,11 @@
import type { CheckboxField } from 'payload/types'
import type { I18n } from '@payloadcms/translations'
import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<CheckboxField, 'type'> & {
export type Props = FormFieldBase & {
disableFormData?: boolean
onChange?: (val: boolean) => void
i18n: I18n
value?: boolean
partialChecked?: 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 {
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 { CodeEditor } from '../../../elements/CodeEditor'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import { withCondition } from '../../withCondition'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { CodeInputWrapper } from './Wrapper'
import { CodeInput } from './Input'
import './index.scss'
const prismToMonacoLanguageMap = {
js: 'javascript',
ts: 'typescript',
}
const baseClass = 'code-field'
const Code: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label } = {},
description,
editorOptions,
language,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
required,
i18n,
value,
Error,
Label,
Description,
BeforeInput,
AfterInput,
validate,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
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 (
<CodeInputWrapper
className={className}
path={path}
readOnly={readOnly}
style={style}
width={width}
<div
className={[
fieldBaseClass,
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
<CodeInput
path={path}
required={required}
{Error}
{Label}
<div>
{BeforeInput}
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}
options={editorOptions}
readOnly={readOnly}
name={name}
language={language}
editorOptions={editorOptions}
value={(value as string) || ''}
/>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</CodeInputWrapper>
{AfterInput}
</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'
export type Props = FormFieldBase &
Omit<CodeField, 'type'> & {
export type Props = FormFieldBase & {
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 FieldDescription from '../../FieldDescription'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import { Collapsible } from '../../../elements/Collapsible'
import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { usePreferences } from '../../../providers/Preferences'
import { useFormSubmitted } from '../../Form/context'
import RenderFields from '../../RenderFields'
import { CollapsibleFieldWrapper } from './Wrapper'
import { CollapsibleInput } from './Input'
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState'
import { RowLabel } from '../../RowLabel'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { DocumentPreferences } from 'payload/types'
import { useFieldPath } from '../../FieldPathProvider'
import { WatchChildErrors } from '../../WatchChildErrors'
import { ErrorPill } from '../../../elements/ErrorPill'
import { useTranslation } from '../../../providers/Translation'
import './index.scss'
@@ -16,70 +22,116 @@ const baseClass = 'collapsible-field'
const CollapsibleField: React.FC<Props> = (props) => {
const {
admin: { className, description, initCollapsed: initCollapsedFromProps, readOnly },
fieldTypes,
fields,
indexPath,
label,
path,
className,
readOnly,
path: pathFromProps,
permissions,
i18n,
config,
payload,
user,
formState,
docPreferences,
Description,
Error,
fieldMap,
Label,
} = props
const { fieldState: nestedFieldState, pathSegments } = getNestedFieldState({
formState,
path,
fieldSchema: fields,
const pathFromContext = useFieldPath()
const path = pathFromProps || pathFromContext
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, '__')}`
const initCollapsed = Boolean(
docPreferences
? docPreferences?.fields?.[path || fieldPreferencesKey]?.collapsed
: initCollapsedFromProps,
},
[preferencesKey, fieldPreferencesKey, getPreference, setPreference, path],
)
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 (
<CollapsibleFieldWrapper
className={className}
path={path}
<Fragment>
<WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
<div
className={[
fieldBaseClass,
baseClass,
className,
fieldHasErrors ? `${baseClass}--has-error` : `${baseClass}--has-no-error`,
]
.filter(Boolean)
.join(' ')}
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
>
<CollapsibleInput
initCollapsed={initCollapsed}
baseClass={baseClass}
RowLabel={<RowLabel data={formState} label={label} path={path} i18n={i18n} />}
path={path}
fieldPreferencesKey={fieldPreferencesKey}
pathSegments={pathSegments}
<Collapsible
className={`${baseClass}__collapsible`}
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
header={
<div className={`${baseClass}__row-label-wrap`}>
{Label}
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
}
initCollapsed={collapsedOnMount}
onToggle={onToggle}
>
<RenderFields
fieldSchema={fields.map((field) => ({
...field,
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
fieldMap={fieldMap}
forceRender
indexPath={indexPath}
indexPath={path}
margins="small"
permissions={permissions}
readOnly={readOnly}
i18n={i18n}
config={config}
payload={payload}
formState={nestedFieldState}
user={user}
/>
</CollapsibleInput>
<FieldDescription description={description} path={path} i18n={i18n} />
</CollapsibleFieldWrapper>
</Collapsible>
{Description}
</div>
</Fragment>
)
}
export default CollapsibleField
export default withCondition(CollapsibleField)

View File

@@ -1,10 +1,8 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth'
import type { CollapsibleField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<CollapsibleField, 'type'> & {
export type Props = FormFieldBase & {
fieldTypes: FieldTypes
indexPath: string
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 { DateTimeInput } from './Input'
import './index.scss'
import FieldDescription from '../../FieldDescription'
import { fieldBaseClass } from '../shared'
import DefaultLabel from '../../Label'
import DefaultError from '../../Error'
import DatePickerField from '../../../elements/DatePicker'
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 DateTime: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { beforeInput, afterInput, Label, Error },
date,
description,
placeholder,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
required,
Error,
Label,
BeforeInput,
AfterInput,
Description,
validate,
} = props
const path = pathFromProps || name
const datePickerProps = 'date' in props ? props.date : {}
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const { i18n } = useTranslation()
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 (
<div
@@ -40,7 +54,7 @@ const DateTime: React.FC<Props> = (props) => {
fieldBaseClass,
baseClass,
className,
// showError && `${baseClass}--has-error`,
showError && `${baseClass}--has-error`,
readOnly && 'read-only',
]
.filter(Boolean)
@@ -50,26 +64,22 @@ const DateTime: React.FC<Props> = (props) => {
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
{/* <ErrorComp
message={errorMessage}
showError={showError}
/> */}
</div>
<LabelComp htmlFor={path} label={label} required={required} />
<div className={`${baseClass}__error-wrap`}>{Error}</div>
{Label}
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<DateTimeInput
datePickerProps={date}
placeholder={placeholder}
{BeforeInput}
<DatePickerField
{...datePickerProps}
onChange={(incomingDate) => {
if (!readOnly) setValue(incomingDate?.toISOString() || null)
}}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly}
path={path}
style={style}
width={width}
value={value}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
{AfterInput}
</div>
<FieldDescription description={description} path={path} />
{Description}
</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
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 DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { EmailInput } from './Input'
import { EmailInputWrapper } from './Wrapper'
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'
export const Email: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
path: pathFromProps,
autoComplete,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
Error,
Label,
BeforeInput,
AfterInput,
Description,
required,
i18n,
value,
validate,
placeholder,
} = props
const path = pathFromProps || name
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
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({
validate: memoizedValidate,
path: pathFromProps || name,
})
return (
<EmailInputWrapper
className={className}
readOnly={readOnly}
style={style}
width={width}
path={path}
<div
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{Error}
{Label}
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<EmailInput
name={name}
{BeforeInput}
<input
autoComplete={autoComplete}
readOnly={readOnly}
path={path}
required={required}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
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>
<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'
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']
export type Props = FormFieldBase & {
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 FieldDescription from '../../FieldDescription'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields'
import { GroupProvider } from './provider'
import { GroupWrapper } from './Wrapper'
import { GroupProvider, useGroup } from './provider'
import { withCondition } from '../../withCondition'
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState'
import { GroupFieldErrors } from './Errors'
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments'
import { useCollapsible } from '../../../elements/Collapsible/provider'
import { useRow } from '../Row/provider'
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'
const baseClass = 'group-field'
const Group: React.FC<Props> = (props) => {
const {
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 { className, style, width, fieldMap, Description, hideGutter, Label } = props
const path = pathFromProps || name
const path = useFieldPath()
const { fieldState: nestedFieldState } = getNestedFieldState({
formState,
path,
fieldSchema: fields,
})
const { i18n } = useTranslation()
const hasSubmitted = useFormSubmitted()
const isWithinCollapsible = useCollapsible()
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) => ({
...subField,
path: createNestedFieldPath(path, subField),
}))
const pathSegments = buildPathSegments(path, fieldSchema)
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
return (
<GroupWrapper
name={name}
path={path}
className={className}
hideGutter={hideGutter}
style={style}
width={width}
<Fragment>
<WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
<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`,
fieldHasErrors && `${baseClass}--has-error`,
className,
]
.filter(Boolean)
.join(' ')}
id={`field-${path?.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
>
<GroupProvider>
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__header`}>
{(label || description) && (
{(Label || Description) && (
<header>
{label && (
<h3 className={`${baseClass}__title`}>
{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}
/>
{Label}
{Description}
</header>
)}
<GroupFieldErrors pathSegments={pathSegments} />
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
<RenderFields
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}
/>
<RenderFields fieldMap={fieldMap} />
</div>
</GroupProvider>
</GroupWrapper>
</div>
</Fragment>
)
}

View File

@@ -1,13 +1,9 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth'
import type { GroupField } from 'payload/types'
import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<GroupField, 'type'> & {
fieldTypes: FieldTypes
export type Props = FormFieldBase & {
forceRender?: boolean
indexPath: string
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 { 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.
* For example, this sets the `ìd` property of a block in the Blocks field.
*/
const HiddenField: React.FC<Props> = (props) => {
const { name, disableModifyingForm = true, path: pathFromProps, value } = props
const HiddenInput: React.FC<Props> = (props) => {
const { name, disableModifyingForm = true, path: pathFromProps, value: valueFromProps } = props
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 {
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 DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { JSONInputWrapper } from './Wrapper'
import { JSONInput } from './Input'
import { fieldBaseClass } from '../shared'
import { CodeEditor } from '../../../elements/CodeEditor'
import { Validate } from 'payload/types'
import useField from '../../useField'
import { withCondition } from '../../withCondition'
import './index.scss'
const baseClass = 'json-field'
const JSONField: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label } = {},
description,
editorOptions,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
Error,
Label,
Description,
BeforeInput,
AfterInput,
validate,
required,
i18n,
value,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
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 (
<JSONInputWrapper
className={className}
path={path}
readOnly={readOnly}
width={width}
style={style}
<div
className={[
fieldBaseClass,
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
<JSONInput
path={path}
required={required}
{Error}
{Label}
<div>
{BeforeInput}
<CodeEditor
defaultLanguage="json"
onChange={handleChange}
options={editorOptions}
readOnly={readOnly}
editorOptions={editorOptions}
value={stringValue}
/>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</JSONInputWrapper>
{AfterInput}
</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'
export type Props = FormFieldBase &
Omit<JSONField, 'type'> & {
export type Props = FormFieldBase & {
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 { isNumber } from 'payload/utilities'
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 { 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'
const NumberField: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder,
readOnly,
step,
style,
width,
} = {},
hasMany,
label,
maxRows,
min,
path: pathFromProps,
required,
valid = true,
i18n,
value,
Error,
Label,
Description,
BeforeInput,
AfterInput,
validate,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const max = 'max' in props ? props.max : Infinity
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 (
<NumberInputWrapper
className={className}
readOnly={readOnly}
hasMany={hasMany}
style={style}
width={width}
path={path}
<div
className={[
fieldBaseClass,
'number',
className,
showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{Error}
{Label}
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
@@ -81,27 +147,32 @@ const NumberField: React.FC<Props> = (props) => {
// onChange={handleHasManyChange}
options={[]}
// placeholder={t('general:enterAValue')}
showError={!valid}
// value={valueToRender as Option[]}
showError={showError}
value={valueToRender as Option[]}
/>
) : (
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<NumberInput
path={path}
required={required}
min={min}
placeholder={placeholder}
readOnly={readOnly}
{BeforeInput}
<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}
hasMany={hasMany}
name={name}
type="number"
value={typeof value === 'number' ? value : ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
{AfterInput}
</div>
)}
<FieldDescription description={description} value={value} i18n={i18n} />
</NumberInputWrapper>
{Description}
</div>
)
}

View File

@@ -1,7 +1,7 @@
import type { NumberField } from 'payload/types'
import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<NumberField, 'type'> & {
value?: number
export type Props = FormFieldBase & {
path?: string
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 Error from '../../Error'
import Label from '../../Label'
import { PasswordInput } from './Input'
import { PasswordInputWrapper } from './Wrapper'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { Validate } from 'payload/types'
import useField from '../../useField'
import './index.scss'
export const Password: React.FC<Props> = (props) => {
const {
name,
autoComplete,
className,
disabled,
label,
path: pathFromProps,
required,
style,
width,
i18n,
validate,
path: pathFromProps,
name,
Error,
Label,
} = 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 (
<PasswordInputWrapper className={className} style={style} width={width} path={path}>
<Error path={path} />
<Label
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
<div
className={[fieldBaseClass, 'password', className, showError && 'error']
.filter(Boolean)
.join(' ')}
style={{
...style,
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} />
</PasswordInputWrapper>
</div>
)
}

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

View File

@@ -1,8 +1,6 @@
import type { NumberField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<NumberField, 'type'> & {
export type Props = FormFieldBase & {
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 { OptionObject } from 'payload/types'
import { OnChange } from '../types'
import { useTranslation } from '../../../..'
import { useTranslation } from '../../../../providers/Translation'
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 {
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 { RadioGroupWrapper } from './Wrapper'
import { withCondition } from '../../withCondition'
import FieldDescription from '../../FieldDescription'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import { RadioGroupInput } from './Input'
import { Radio } from './Radio'
import { optionIsObject } from 'payload/types'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -16,45 +16,86 @@ const baseClass = 'radio-group'
const RadioGroup: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label } = {},
description,
layout = 'horizontal',
readOnly,
style,
width,
} = {},
label,
options,
path: pathFromProps,
Error,
Label,
Description,
validate,
required,
i18n,
value,
} = props
const path = pathFromProps || name
const options = 'options' in props ? props.options : []
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const layout = 'layout' in props ? props.layout : 'horizontal'
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 (
<RadioGroupWrapper
width={width}
className={className}
style={style}
layout={layout}
path={path}
readOnly={readOnly}
baseClass={baseClass}
<div
className={[
fieldBaseClass,
baseClass,
className,
`${baseClass}--layout-${layout}`,
showError && 'error',
readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<div className={`${baseClass}__error-wrap`}>
<ErrorComp path={path} />
<div className={`${baseClass}__error-wrap`}>{Error}</div>
{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>
<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 { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<RadioField, 'type'> & {
export type Props = FormFieldBase & {
name?: string
path?: string
value?: string
}
export type OnChange<T = string> = (value: T) => void

View File

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

View File

@@ -1,10 +1,8 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth'
import type { RowField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<RowField, 'type'> & {
export type Props = FormFieldBase & {
fieldTypes: FieldTypes
forceRender?: boolean
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 DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import SelectInput from './Input'
import { SelectFieldWrapper } from './Wrapper'
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'
@@ -26,55 +27,120 @@ const formatOptions = (options: Option[]): OptionObject[] =>
export const Select: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
description,
isClearable,
isSortable = true,
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
hasMany,
label,
options,
path: pathFromProps,
required,
i18n,
value,
Description,
Error,
Label,
BeforeInput,
AfterInput,
validate,
} = 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 LabelComp = Label || DefaultLabel
const { i18n } = useTranslation()
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 (
<SelectFieldWrapper
className={className}
style={style}
width={width}
path={path}
readOnly={readOnly}
<div
className={[
fieldBaseClass,
'select',
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<SelectInput
readOnly={readOnly}
{Error}
{Label}
<div>
{BeforeInput}
<ReactSelect
disabled={readOnly}
isClearable={isClearable}
hasMany={hasMany}
isMulti={hasMany}
isSortable={isSortable}
options={formatOptions(options)}
path={path}
onChange={onChange}
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} />
</SelectFieldWrapper>
{AfterInput}
</div>
{Description}
</div>
)
}

View File

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

View File

@@ -1,38 +1,40 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import { NamedTab, Tab } from 'payload/types'
import React from 'react'
import React, { useState } from 'react'
import { ErrorPill } from '../../../../elements/ErrorPill'
import { WatchChildErrors } from '../../../WatchChildErrors'
import { useTranslation } from '../../../..'
import { useFormSubmitted, useTranslation } from '../../../..'
import { ReducedTab } from '../../../RenderFields/createFieldMap'
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'
export const TabComponent: React.FC<TabProps> = (props) => {
const { isActive, setIsActive, pathSegments, name, label } = props
type TabProps = {
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 [errorCount, setErrorCount] = useState(undefined)
const hasName = 'name' in tab
const hasSubmitted = useFormSubmitted()
const [errorCount, setErrorCount] = React.useState(0)
const tabHasErrors = errorCount > 0
const path = `${parentPath ? `${parentPath}.` : ''}${'name' in tab ? name : ''}`
const fieldHasErrors = errorCount > 0 && hasSubmitted
return (
<React.Fragment>
<WatchChildErrors fieldMap={tab.subfields} path={path} setErrorCount={setErrorCount} />
<button
className={[
baseClass,
tabHasErrors && `${baseClass}--has-error`,
fieldHasErrors && `${baseClass}--has-error`,
isActive && `${baseClass}--active`,
]
.filter(Boolean)
@@ -40,9 +42,9 @@ export const TabComponent: React.FC<TabProps> = (props) => {
onClick={setIsActive}
type="button"
>
<WatchChildErrors pathSegments={pathSegments} setErrorCount={setErrorCount} />
{label ? getTranslation(label, i18n) : name}
{tabHasErrors && <ErrorPill i18n={i18n} count={errorCount} />}
{label ? getTranslation(label, i18n) : hasName && name}
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} />}
</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 FieldDescription from '../../FieldDescription'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import { useCollapsible } from '../../../elements/Collapsible/provider'
import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { usePreferences } from '../../../providers/Preferences'
import RenderFields from '../../RenderFields'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { TabsProvider } from './provider'
import { TabComponent } from './Tab'
import { Wrapper } from './Wrapper'
import { getTranslation } from '@payloadcms/translations'
import { DocumentPreferences, tabHasName } from 'payload/types'
import { toKebabCase } from 'payload/utilities'
import { Tab } from 'payload/types'
import { withCondition } from '../../withCondition'
import { useTranslation } from '../../../providers/Translation'
import { FieldPathProvider, useFieldPath } from '../../FieldPathProvider'
import './index.scss'
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments'
const baseClass = 'tabs-field'
const getTabFieldSchema = ({ tabConfig, path }: { tabConfig: Tab; path }) => {
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 TabsField: React.FC<Props> = (props) => {
const {
admin: { className, readOnly },
fieldTypes,
className,
readOnly,
forceRender = false,
indexPath,
path,
permissions,
tabs,
formState,
user,
i18n,
payload,
docPreferences,
config,
Description,
fieldMap,
path: pathFromProps,
name,
} = 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 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 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 (
<Wrapper className={className}>
<div
className={[
fieldBaseClass,
className,
baseClass,
isWithinCollapsible && `${baseClass}--within-collapsible`,
]
.filter(Boolean)
.join(' ')}
>
<TabsProvider>
<div className={`${baseClass}__tabs-wrap`}>
<div className={`${baseClass}__tabs`}>
{tabs.map((tab, tabIndex) => {
const tabPath = [path, 'name' in tab && tab.name].filter(Boolean)?.join('.')
const pathSegments = buildPathSegments(tabPath, tab.fields)
return (
<TabComponent
path={tabPath}
isActive={activeTabIndex === tabIndex}
key={tabIndex}
setIsActive={undefined}
// setIsActive={() => handleTabChange(tabIndex)}
pathSegments={pathSegments}
name={'name' in tab && tab.name}
label={tab.label}
parentPath={path}
setIsActive={() => handleTabChange(tabIndex)}
tab={tab}
/>
)
})}
@@ -128,16 +131,10 @@ const TabsField: React.FC<Props> = async (props) => {
.filter(Boolean)
.join(' ')}
>
<FieldDescription
className={`${baseClass}__description`}
description={activeTabConfig.description}
marginPlacement="bottom"
path={path}
i18n={i18n}
/>
{Description}
<FieldPathProvider path={'name' in activeTabConfig ? activeTabConfig.name : ''}>
<RenderFields
fieldSchema={getTabFieldSchema({ tabConfig: activeTabConfig, path })}
fieldTypes={fieldTypes}
fieldMap={activeTabConfig.subfields}
forceRender={forceRender}
indexPath={indexPath}
key={
@@ -147,23 +144,19 @@ const TabsField: React.FC<Props> = async (props) => {
}
margins="small"
permissions={
isNamedTab && permissions?.[activeTabConfig.name]
'name' in activeTabConfig && permissions?.[activeTabConfig.name]
? permissions[activeTabConfig.name].fields
: permissions
}
readOnly={readOnly}
user={user}
formState={formState}
i18n={i18n}
payload={payload}
config={config}
/>
</FieldPathProvider>
</div>
</React.Fragment>
)}
</div>
</TabsProvider>
</Wrapper>
</div>
)
}

View File

@@ -1,13 +1,10 @@
import type { FieldTypes } from 'payload/config'
import type { FieldPermissions } from 'payload/auth'
import type { TabsField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = FormFieldBase &
Omit<TabsField, 'type'> & {
fieldTypes: FieldTypes
export type Props = FormFieldBase & {
forceRender?: boolean
indexPath: string
path?: string
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 {
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 { TextInput } from './Input'
import FieldDescription from '../../FieldDescription'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import { TextInputWrapper } from './Wrapper'
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 {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder,
readOnly,
rtl,
style,
width,
} = {},
label,
localized,
maxLength,
minLength,
path: pathFromProps,
required,
value,
i18n,
Error,
Label,
Description,
BeforeInput,
AfterInput,
validate,
inputRef,
readOnly,
width,
style,
onKeyDown,
placeholder,
rtl,
name,
path: pathFromProps,
} = props
const path = pathFromProps || name
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const locale = useLocale()
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 (
<TextInputWrapper
className={className}
style={style}
width={width}
path={path}
readOnly={readOnly}
<div
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{Error}
{Label}
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<TextInput
path={path}
name={name}
localized={localized}
rtl={rtl}
placeholder={placeholder}
readOnly={readOnly}
maxLength={maxLength}
minLength={minLength}
{BeforeInput}
<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) || ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
{AfterInput}
</div>
{Description}
</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'
export type Props = FormFieldBase &
Omit<TextField, 'type'> & {
export type Props = FormFieldBase & {
name?: string
path?: string
inputRef?: React.MutableRefObject<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 { isFieldRTL } from '../shared'
import TextareaInput from './Input'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import { TextareaInputWrapper } from './Wrapper'
import { fieldBaseClass, isFieldRTL } from '../shared'
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'
const Textarea: React.FC<Props> = (props) => {
const {
name,
admin: {
className,
components: { Error, Label, afterInput, beforeInput } = {},
description,
placeholder,
readOnly,
rows,
rtl,
style,
width,
} = {},
label,
localized,
maxLength,
minLength,
path: pathFromProps,
required,
value,
locale,
config: { localization },
i18n,
Error,
Label,
BeforeInput,
AfterInput,
validate,
Description,
} = props
const path = pathFromProps || name
const rows = 'rows' in props ? props.rows : undefined
const { i18n } = useTranslation()
const { localization } = useConfig()
const isRTL = isFieldRTL({
fieldLocalized: localized,
@@ -45,44 +47,57 @@ const Textarea: React.FC<Props> = (props) => {
localizationConfig: localization || undefined,
})
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const memoizedValidate: Validate = useCallback(
(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 (
<TextareaInputWrapper
className={className}
readOnly={readOnly}
style={style}
width={width}
path={path}
<div
className={[
fieldBaseClass,
'textarea',
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{Error}
{Label}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<TextareaInput
name={name}
path={path}
placeholder={placeholder}
readOnly={readOnly}
required={required}
{BeforeInput}
<textarea
className="textarea-element"
data-rtl={isRTL}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
rtl={isRTL}
maxLength={maxLength}
minLength={minLength}
value={value || ''}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
{AfterInput}
</div>
</label>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</TextareaInputWrapper>
{Description}
</div>
)
}

View File

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

View File

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

View File

@@ -1,24 +1,92 @@
import type { Locale, SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config'
import { FormState } from '../Form/types'
import type { Locale, SanitizedLocalizationConfig } from 'payload/config'
import { User } from 'payload/auth'
import { I18n } from '@payloadcms/translations'
import { Payload } from 'payload'
import { DocumentPreferences } from 'payload/types'
import {
ArrayField,
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 type FormFieldBase = {
formState?: FormState
path?: string
valid?: boolean
errorMessage?: string
user?: User
i18n?: I18n
payload?: Payload
docPreferences?: DocumentPreferences
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.
@@ -41,6 +109,7 @@ export function isFieldRTL({
localizationConfig &&
localizationConfig.locales &&
localizationConfig.locales.length > 1
const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale
return (

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