chore(next): overhauls field rendering strategy (#4924)
This commit is contained in:
5
packages/dev/src/collections/Pages/AfterInput.tsx
Normal file
5
packages/dev/src/collections/Pages/AfterInput.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const AfterInput: React.FC = () => {
|
||||||
|
return <div>This is a custom `AfterInput` component</div>
|
||||||
|
}
|
||||||
5
packages/dev/src/collections/Pages/BeforeInput.tsx
Normal file
5
packages/dev/src/collections/Pages/BeforeInput.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const BeforeInput: React.FC = () => {
|
||||||
|
return <div>This is a custom `BeforeInput` component</div>
|
||||||
|
}
|
||||||
5
packages/dev/src/collections/Pages/CustomDescription.tsx
Normal file
5
packages/dev/src/collections/Pages/CustomDescription.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const CustomDescription: React.FC = () => {
|
||||||
|
return <div>This is a custom `Description` component</div>
|
||||||
|
}
|
||||||
5
packages/dev/src/collections/Pages/CustomField.tsx
Normal file
5
packages/dev/src/collections/Pages/CustomField.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const CustomField: React.FC = () => {
|
||||||
|
return <div>This is a custom `Field` component</div>
|
||||||
|
}
|
||||||
5
packages/dev/src/collections/Pages/CustomLabel.tsx
Normal file
5
packages/dev/src/collections/Pages/CustomLabel.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const CustomLabel: React.FC = () => {
|
||||||
|
return <div>This is a custom `Label` component</div>
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { CollectionConfig } from 'payload/types'
|
import { CollectionConfig } from 'payload/types'
|
||||||
import { CustomView } from './CustomView'
|
import { CustomView } from './CustomView'
|
||||||
|
import { BeforeInput } from './BeforeInput'
|
||||||
|
import { AfterInput } from './AfterInput'
|
||||||
|
import { CustomField } from './CustomField'
|
||||||
|
import { CustomDescription } from './CustomDescription'
|
||||||
|
import { CustomLabel } from './CustomLabel'
|
||||||
|
|
||||||
export const Pages: CollectionConfig = {
|
export const Pages: CollectionConfig = {
|
||||||
slug: 'pages',
|
slug: 'pages',
|
||||||
@@ -24,11 +29,47 @@ export const Pages: CollectionConfig = {
|
|||||||
drafts: true,
|
drafts: true,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'titleWithCustomComponents',
|
||||||
|
label: 'Title With Custom Components',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: CustomDescription,
|
||||||
|
components: {
|
||||||
|
beforeInput: [BeforeInput],
|
||||||
|
afterInput: [AfterInput],
|
||||||
|
Label: CustomLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: 'Title',
|
label: 'Title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: 'This is a description',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'titleWithCustomField',
|
||||||
|
label: 'Title With Custom Field',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: CustomField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sidebarTitle',
|
||||||
|
label: 'Sidebar Title',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableConditionalField',
|
name: 'enableConditionalField',
|
||||||
@@ -118,7 +159,9 @@ export const Pages: CollectionConfig = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
|
// TODO: fix this
|
||||||
|
// label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
|
||||||
|
label: 'Hello',
|
||||||
type: 'collapsible',
|
type: 'collapsible',
|
||||||
admin: {
|
admin: {
|
||||||
initCollapsed: true,
|
initCollapsed: true,
|
||||||
@@ -145,6 +188,20 @@ export const Pages: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// name: 'array',
|
||||||
|
// label: 'Array',
|
||||||
|
// type: 'array',
|
||||||
|
// required: true,
|
||||||
|
// fields: [
|
||||||
|
// {
|
||||||
|
// name: 'arrayText',
|
||||||
|
// label: 'Array Text',
|
||||||
|
// type: 'text',
|
||||||
|
// required: true,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
label: 'Tabs',
|
label: 'Tabs',
|
||||||
type: 'tabs',
|
type: 'tabs',
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const LoginForm: React.FC<{
|
|||||||
>
|
>
|
||||||
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
||||||
<div className={`${baseClass}__inputWrap`}>
|
<div className={`${baseClass}__inputWrap`}>
|
||||||
<Email admin={{ autoComplete: 'email' }} label={t('general:email')} name="email" required />
|
<Email autoComplete="email" label={t('general:email')} name="email" required />
|
||||||
<Password autoComplete="off" label={t('general:password')} name="password" required />
|
<Password autoComplete="off" label={t('general:password')} name="password" required />
|
||||||
</div>
|
</div>
|
||||||
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>
|
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export type ErrorProps = {
|
export type ErrorProps = {
|
||||||
alignCaret?: 'center' | 'left' | 'right'
|
alignCaret?: 'center' | 'left' | 'right'
|
||||||
message?: string
|
message?: string
|
||||||
path: string
|
|
||||||
showError?: boolean
|
showError?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
type Args<T = unknown> = {
|
type Args<T = unknown> = {
|
||||||
path: string
|
|
||||||
value?: T
|
value?: T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { I18n } from '@payloadcms/translations'
|
|
||||||
|
|
||||||
export type LabelProps = {
|
export type LabelProps = {
|
||||||
htmlFor?: string
|
htmlFor?: string
|
||||||
i18n: I18n
|
|
||||||
label?: JSX.Element | Record<string, string> | false | string
|
label?: JSX.Element | Record<string, string> | false | string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { CollectionPermission, GlobalPermission, User } from 'payload/auth'
|
import type { CollectionPermission, GlobalPermission, User } from 'payload/auth'
|
||||||
import type { Description, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types'
|
import type { Description, DocumentPreferences, Payload, SanitizedConfig } from 'payload/types'
|
||||||
import type { FieldTypes, Locale } from 'payload/config'
|
import type { Locale } from 'payload/config'
|
||||||
|
|
||||||
import RenderFields from '../../forms/RenderFields'
|
import RenderFields from '../../forms/RenderFields'
|
||||||
import { filterFields } from '../../forms/RenderFields/filterFields'
|
|
||||||
import { Gutter } from '../Gutter'
|
import { Gutter } from '../Gutter'
|
||||||
import './index.scss'
|
import { Document } from 'payload/types'
|
||||||
import { Document, FieldWithPath } from 'payload/types'
|
|
||||||
import { FormState } from '../../forms/Form/types'
|
import { FormState } from '../../forms/Form/types'
|
||||||
import { I18n } from '@payloadcms/translations'
|
import { useTranslation } from '../../providers/Translation'
|
||||||
|
import { createFieldMap } from '../../forms/RenderFields/createFieldMap'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'document-fields'
|
const baseClass = 'document-fields'
|
||||||
|
|
||||||
@@ -18,8 +20,6 @@ export const DocumentFields: React.FC<{
|
|||||||
AfterFields?: React.ReactNode
|
AfterFields?: React.ReactNode
|
||||||
BeforeFields?: React.ReactNode
|
BeforeFields?: React.ReactNode
|
||||||
description?: Description
|
description?: Description
|
||||||
fieldTypes: FieldTypes
|
|
||||||
fields: FieldWithPath[]
|
|
||||||
forceSidebarWrap?: boolean
|
forceSidebarWrap?: boolean
|
||||||
hasSavePermission: boolean
|
hasSavePermission: boolean
|
||||||
docPermissions: CollectionPermission | GlobalPermission
|
docPermissions: CollectionPermission | GlobalPermission
|
||||||
@@ -27,17 +27,13 @@ export const DocumentFields: React.FC<{
|
|||||||
data: Document
|
data: Document
|
||||||
formState: FormState
|
formState: FormState
|
||||||
user: User
|
user: User
|
||||||
i18n: I18n
|
|
||||||
payload: Payload
|
|
||||||
locale?: Locale
|
locale?: Locale
|
||||||
config: SanitizedConfig
|
fieldMap?: ReturnType<typeof createFieldMap>
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const {
|
const {
|
||||||
AfterFields,
|
AfterFields,
|
||||||
BeforeFields,
|
BeforeFields,
|
||||||
description,
|
description,
|
||||||
fieldTypes,
|
|
||||||
fields,
|
|
||||||
forceSidebarWrap,
|
forceSidebarWrap,
|
||||||
hasSavePermission,
|
hasSavePermission,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
@@ -45,27 +41,15 @@ export const DocumentFields: React.FC<{
|
|||||||
data,
|
data,
|
||||||
formState,
|
formState,
|
||||||
user,
|
user,
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
locale,
|
locale,
|
||||||
config,
|
fieldMap,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const mainFields = filterFields({
|
const { i18n } = useTranslation()
|
||||||
fieldSchema: fields,
|
|
||||||
fieldTypes,
|
|
||||||
filter: (field) => !field?.admin?.position || field?.admin?.position !== 'sidebar',
|
|
||||||
permissions: docPermissions.fields,
|
|
||||||
readOnly: !hasSavePermission,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sidebarFields = filterFields({
|
const mainFields = fieldMap.filter(({ isSidebar }) => !isSidebar)
|
||||||
fieldSchema: fields,
|
|
||||||
fieldTypes,
|
const sidebarFields = fieldMap.filter(({ isSidebar }) => isSidebar)
|
||||||
filter: (field) => field?.admin?.position === 'sidebar',
|
|
||||||
permissions: docPermissions.fields,
|
|
||||||
readOnly: !hasSavePermission,
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasSidebarFields = sidebarFields && sidebarFields.length > 0
|
const hasSidebarFields = sidebarFields && sidebarFields.length > 0
|
||||||
|
|
||||||
@@ -89,23 +73,17 @@ export const DocumentFields: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
{BeforeFields || null}
|
{BeforeFields}
|
||||||
<RenderFields
|
<RenderFields
|
||||||
className={`${baseClass}__fields`}
|
className={`${baseClass}__fields`}
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
fields={mainFields}
|
|
||||||
// permissions={permissions.fields}
|
// permissions={permissions.fields}
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
data={data}
|
data={data}
|
||||||
formState={formState}
|
|
||||||
user={user}
|
|
||||||
i18n={i18n}
|
|
||||||
payload={payload}
|
|
||||||
docPreferences={docPreferences}
|
docPreferences={docPreferences}
|
||||||
config={config}
|
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
fieldMap={mainFields}
|
||||||
/>
|
/>
|
||||||
{AfterFields || null}
|
{AfterFields}
|
||||||
</Gutter>
|
</Gutter>
|
||||||
</div>
|
</div>
|
||||||
{hasSidebarFields && (
|
{hasSidebarFields && (
|
||||||
@@ -113,17 +91,11 @@ export const DocumentFields: React.FC<{
|
|||||||
<div className={`${baseClass}__sidebar`}>
|
<div className={`${baseClass}__sidebar`}>
|
||||||
<div className={`${baseClass}__sidebar-fields`}>
|
<div className={`${baseClass}__sidebar-fields`}>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
fields={sidebarFields}
|
|
||||||
// permissions={permissions.fields}
|
// permissions={permissions.fields}
|
||||||
readOnly={!hasSavePermission}
|
readOnly={!hasSavePermission}
|
||||||
data={data}
|
data={data}
|
||||||
formState={formState}
|
|
||||||
user={user}
|
|
||||||
i18n={i18n}
|
|
||||||
payload={payload}
|
|
||||||
locale={locale}
|
locale={locale}
|
||||||
config={config}
|
fieldMap={sidebarFields}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export { default as RadioGroupInput } from '../forms/field-types/RadioGroup'
|
|||||||
export { default as Label } from '../forms/Label'
|
export { default as Label } from '../forms/Label'
|
||||||
export { default as Submit } from '../forms/Submit'
|
export { default as Submit } from '../forms/Submit'
|
||||||
export { default as Checkbox } from '../forms/field-types/Checkbox'
|
export { default as Checkbox } from '../forms/field-types/Checkbox'
|
||||||
export { CheckboxInput } from '../forms/field-types/Checkbox/Input'
|
export { default as CheckboxInput } from '../forms/field-types/Checkbox'
|
||||||
export { default as Select } from '../forms/field-types/Select'
|
export { default as Select } from '../forms/field-types/Select'
|
||||||
export { default as SelectInput } from '../forms/field-types/Select/Input'
|
export { default as SelectInput } from '../forms/field-types/Select'
|
||||||
export { default as Number } from '../forms/field-types/Number'
|
export { default as Number } from '../forms/field-types/Number'
|
||||||
export { useAllFormFields } from '../forms/Form/context'
|
export { useAllFormFields } from '../forms/Form/context'
|
||||||
export { default as reduceFieldsToValues } from '../forms/Form/reduceFieldsToValues'
|
export { default as reduceFieldsToValues } from '../forms/Form/reduceFieldsToValues'
|
||||||
|
|||||||
@@ -3,26 +3,25 @@ import React from 'react'
|
|||||||
|
|
||||||
import { Tooltip } from '../../elements/Tooltip'
|
import { Tooltip } from '../../elements/Tooltip'
|
||||||
import type { ErrorProps } from 'payload/types'
|
import type { ErrorProps } from 'payload/types'
|
||||||
import { useFormFields } from '../Form/context'
|
import { useFormFields, useFormSubmitted } from '../Form/context'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'field-error'
|
const baseClass = 'field-error'
|
||||||
|
|
||||||
const Error: React.FC<ErrorProps> = (props) => {
|
const Error: React.FC<ErrorProps> = (props) => {
|
||||||
const {
|
const { alignCaret = 'right', message: messageFromProps, showError: showErrorFromProps } = props
|
||||||
alignCaret = 'right',
|
|
||||||
message: messageFromProps,
|
|
||||||
path,
|
|
||||||
showError: showErrorFromProps,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
|
// TODO: get path from context
|
||||||
|
const path = ''
|
||||||
|
|
||||||
|
const hasSubmitted = useFormSubmitted()
|
||||||
const field = useFormFields(([fields]) => fields[path])
|
const field = useFormFields(([fields]) => fields[path])
|
||||||
|
|
||||||
const { valid, errorMessage } = field || {}
|
const { valid, errorMessage } = field || {}
|
||||||
|
|
||||||
const message = messageFromProps || errorMessage
|
const message = messageFromProps || errorMessage
|
||||||
const showMessage = showErrorFromProps || !valid
|
const showMessage = showErrorFromProps || (hasSubmitted && !valid)
|
||||||
|
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { isComponent } from './types'
|
import { useTranslation } from '../../providers/Translation'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'field-description'
|
const baseClass = 'field-description'
|
||||||
|
|
||||||
const FieldDescription: React.FC<Props> = (props) => {
|
const FieldDescription: React.FC<Props> = (props) => {
|
||||||
const { className, description, marginPlacement, path, value, i18n } = props
|
const { className, description, marginPlacement } = props
|
||||||
|
|
||||||
if (isComponent(description)) {
|
const { i18n } = useTranslation()
|
||||||
const Description = description
|
|
||||||
return <Description path={path} value={value} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description) {
|
if (description) {
|
||||||
return (
|
return (
|
||||||
@@ -27,12 +26,7 @@ const FieldDescription: React.FC<Props> = (props) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
{typeof description === 'function'
|
{getTranslation(description, i18n)}
|
||||||
? description({
|
|
||||||
path,
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
: getTranslation(description, i18n)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { I18n } from '@payloadcms/translations'
|
|
||||||
import { Description, DescriptionComponent } from 'payload/types'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
description?: Description
|
description?: string
|
||||||
marginPlacement?: 'bottom' | 'top'
|
marginPlacement?: 'bottom' | 'top'
|
||||||
path?: string
|
|
||||||
value?: unknown
|
value?: unknown
|
||||||
i18n: I18n
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isComponent(description: Description): description is DescriptionComponent {
|
|
||||||
return React.isValidElement(description)
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/ui/src/forms/FieldPathProvider/index.scss
Normal file
12
packages/ui/src/forms/FieldPathProvider/index.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/ui/src/forms/FieldPathProvider/index.tsx
Normal file
18
packages/ui/src/forms/FieldPathProvider/index.tsx
Normal 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
|
||||||
|
}
|
||||||
6
packages/ui/src/forms/FieldPathProvider/types.ts
Normal file
6
packages/ui/src/forms/FieldPathProvider/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type Props = {
|
||||||
|
className?: string
|
||||||
|
description?: string
|
||||||
|
marginPlacement?: 'bottom' | 'top'
|
||||||
|
value?: unknown
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { LabelProps } from 'payload/types'
|
import { LabelProps } from 'payload/types'
|
||||||
|
import { useTranslation } from '../..'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const Label: React.FC<LabelProps> = (props) => {
|
const Label: React.FC<LabelProps> = (props) => {
|
||||||
const { htmlFor, label, required = false, i18n } = props
|
const { htmlFor, label, required = false } = props
|
||||||
|
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
if (label) {
|
if (label) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Banner } from '../../elements/Banner'
|
|||||||
import { useConfig } from '../../providers/Config'
|
import { useConfig } from '../../providers/Config'
|
||||||
import { useLocale } from '../../providers/Locale'
|
import { useLocale } from '../../providers/Locale'
|
||||||
import { useForm } from '../Form/context'
|
import { useForm } from '../Form/context'
|
||||||
import { CheckboxInput } from '../field-types/Checkbox/Input'
|
import CheckboxInput from '../field-types/Checkbox'
|
||||||
|
|
||||||
type NullifyLocaleFieldProps = {
|
type NullifyLocaleFieldProps = {
|
||||||
fieldValue?: [] | null | number
|
fieldValue?: [] | null | number
|
||||||
|
|||||||
13
packages/ui/src/forms/RenderFields/RenderField.tsx
Normal file
13
packages/ui/src/forms/RenderFields/RenderField.tsx
Normal 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>
|
||||||
|
}
|
||||||
231
packages/ui/src/forms/RenderFields/createFieldMap.tsx
Normal file
231
packages/ui/src/forms/RenderFields/createFieldMap.tsx
Normal 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
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}, [])
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,23 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { fieldAffectsData } from 'payload/types'
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent'
|
import { useTranslation } from '../../providers/Translation'
|
||||||
import { FormFieldBase } from '../field-types/shared'
|
import { RenderField } from './RenderField'
|
||||||
import { filterFields } from './filterFields'
|
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'render-fields'
|
const baseClass = 'render-fields'
|
||||||
|
|
||||||
const RenderFields: React.FC<Props> = (props) => {
|
const RenderFields: React.FC<Props> = (props) => {
|
||||||
const {
|
const { className, margins, fieldMap } = props
|
||||||
className,
|
|
||||||
fieldTypes,
|
const { i18n } = useTranslation()
|
||||||
forceRender,
|
|
||||||
margins,
|
|
||||||
data,
|
|
||||||
user,
|
|
||||||
formState,
|
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
docPreferences,
|
|
||||||
locale,
|
|
||||||
config,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
if (!i18n) {
|
if (!i18n) {
|
||||||
console.error('Need to implement i18n when calling RenderFields')
|
console.error('Need to implement i18n when calling RenderFields')
|
||||||
}
|
}
|
||||||
|
|
||||||
let fieldsToRender = 'fields' in props ? props?.fields : null
|
if (fieldMap) {
|
||||||
|
|
||||||
if (!fieldsToRender && 'fieldSchema' in props) {
|
|
||||||
const { fieldSchema, fieldTypes, filter, permissions, readOnly: readOnlyOverride } = props
|
|
||||||
|
|
||||||
fieldsToRender = filterFields({
|
|
||||||
fieldSchema,
|
|
||||||
fieldTypes,
|
|
||||||
filter,
|
|
||||||
operation: props?.operation,
|
|
||||||
permissions,
|
|
||||||
readOnly: readOnlyOverride,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldsToRender) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
@@ -59,71 +29,9 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
{fieldsToRender?.map((reducedField, fieldIndex) => {
|
{fieldMap?.map(({ Field, name }, fieldIndex) => (
|
||||||
const {
|
<RenderField key={fieldIndex} name={name} Field={Field} />
|
||||||
FieldComponent,
|
))}
|
||||||
field,
|
|
||||||
fieldIsPresentational,
|
|
||||||
fieldPermissions,
|
|
||||||
isFieldAffectingData,
|
|
||||||
readOnly,
|
|
||||||
} = reducedField
|
|
||||||
|
|
||||||
const path = field.path || (isFieldAffectingData && 'name' in field ? field.name : '')
|
|
||||||
|
|
||||||
const fieldState = formState?.[path]
|
|
||||||
|
|
||||||
if (fieldIsPresentational) {
|
|
||||||
return <FieldComponent key={fieldIndex} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: type this, i.e. `componentProps: FieldComponentProps`
|
|
||||||
const componentProps: FormFieldBase & Record<string, any> = {
|
|
||||||
...field,
|
|
||||||
admin: {
|
|
||||||
...(field.admin || {}),
|
|
||||||
readOnly,
|
|
||||||
},
|
|
||||||
fieldTypes,
|
|
||||||
forceRender,
|
|
||||||
indexPath: 'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
|
|
||||||
path,
|
|
||||||
permissions: fieldPermissions,
|
|
||||||
data,
|
|
||||||
user,
|
|
||||||
formState,
|
|
||||||
valid: fieldState?.valid,
|
|
||||||
errorMessage: fieldState?.errorMessage,
|
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
docPreferences,
|
|
||||||
locale,
|
|
||||||
config,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
return (
|
|
||||||
<RenderCustomComponent
|
|
||||||
CustomComponent={field?.admin?.components?.Field}
|
|
||||||
DefaultComponent={FieldComponent}
|
|
||||||
componentProps={componentProps}
|
|
||||||
key={fieldIndex}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="missing-field" key={fieldIndex}>
|
|
||||||
{i18n
|
|
||||||
? i18n.t('error:noMatchedField', {
|
|
||||||
label: fieldAffectsData(field)
|
|
||||||
? getTranslation(field.label || field.name, i18n)
|
|
||||||
: field.path,
|
|
||||||
})
|
|
||||||
: 'Need to implement i18n when calling RenderFields'}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import type { FieldPermissions, User } from 'payload/auth'
|
import type { FieldPermissions, User } from 'payload/auth'
|
||||||
import type {
|
import type { Document, DocumentPreferences, Field } from 'payload/types'
|
||||||
Document,
|
|
||||||
DocumentPreferences,
|
|
||||||
Field,
|
|
||||||
FieldWithPath,
|
|
||||||
Payload,
|
|
||||||
SanitizedConfig,
|
|
||||||
} from 'payload/types'
|
|
||||||
import type { ReducedField } from './filterFields'
|
|
||||||
import { FormState } from '../Form/types'
|
import { FormState } from '../Form/types'
|
||||||
import { FieldTypes, Locale } from 'payload/config'
|
import { Locale } from 'payload/config'
|
||||||
import { I18n } from '@payloadcms/translations'
|
import { createFieldMap } from './createFieldMap'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
fieldTypes: FieldTypes
|
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
margins?: 'small' | false
|
margins?: 'small' | false
|
||||||
data?: Document
|
data?: Document
|
||||||
formState: FormState
|
fieldMap: ReturnType<typeof createFieldMap>
|
||||||
user: User
|
|
||||||
docPreferences?: DocumentPreferences
|
docPreferences?: DocumentPreferences
|
||||||
permissions?:
|
permissions?:
|
||||||
| {
|
| {
|
||||||
@@ -27,20 +17,9 @@ export type Props = {
|
|||||||
}
|
}
|
||||||
| FieldPermissions
|
| FieldPermissions
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
i18n: I18n
|
|
||||||
payload: Payload
|
|
||||||
locale?: Locale
|
locale?: Locale
|
||||||
config: SanitizedConfig
|
} & {
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
// FormState to be filtered by the component
|
|
||||||
fieldSchema: FieldWithPath[]
|
|
||||||
filter?: (field: Field) => boolean
|
filter?: (field: Field) => boolean
|
||||||
indexPath?: string
|
indexPath?: string
|
||||||
operation?: 'create' | 'update'
|
operation?: 'create' | 'update'
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
// Pre-filtered fields to be simply rendered
|
|
||||||
fields: ReducedField[]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import type { Field, TabAsField } from 'payload/types'
|
import type { TabAsField } from 'payload/types'
|
||||||
|
|
||||||
import { fieldAffectsData, fieldHasSubFields, tabHasName } from 'payload/types'
|
import { tabHasName } from 'payload/types'
|
||||||
|
import { createFieldMap } from '../RenderFields/createFieldMap'
|
||||||
|
|
||||||
export const buildPathSegments = (parentPath: string, fieldSchema: Field[]): string[] => {
|
export const buildPathSegments = (
|
||||||
const pathNames = fieldSchema.reduce((acc, subField) => {
|
parentPath: string,
|
||||||
if (fieldHasSubFields(subField) && fieldAffectsData(subField)) {
|
fieldMap: ReturnType<typeof createFieldMap>,
|
||||||
|
): string[] => {
|
||||||
|
const pathNames = fieldMap.reduce((acc, subField) => {
|
||||||
|
if (subField.subfields && subField.isFieldAffectingData) {
|
||||||
// group, block, array
|
// group, block, array
|
||||||
acc.push(parentPath ? `${parentPath}.${subField.name}.` : `${subField.name}.`)
|
acc.push(parentPath ? `${parentPath}.${subField.name}.` : `${subField.name}.`)
|
||||||
} else if (fieldHasSubFields(subField)) {
|
} else if (subField.subfields) {
|
||||||
// rows, collapsibles, unnamed-tab
|
// rows, collapsibles, unnamed-tab
|
||||||
acc.push(...buildPathSegments(parentPath, subField.fields))
|
acc.push(...buildPathSegments(parentPath, subField.subfields))
|
||||||
} else if (subField.type === 'tabs') {
|
} else if (subField.type === 'tabs') {
|
||||||
// tabs
|
// tabs
|
||||||
subField.tabs.forEach((tab: TabAsField) => {
|
subField.tabs.forEach((tab) => {
|
||||||
let tabPath = parentPath
|
let tabPath = parentPath
|
||||||
if (tabHasName(tab)) {
|
if ('name' in tab) {
|
||||||
tabPath = parentPath ? `${parentPath}.${tab.name}` : tab.name
|
tabPath = parentPath ? `${parentPath}.${tab.name}` : tab.name
|
||||||
}
|
}
|
||||||
acc.push(...buildPathSegments(tabPath, tab.fields))
|
acc.push(...buildPathSegments(tabPath, tab.subfields))
|
||||||
})
|
})
|
||||||
} else if (fieldAffectsData(subField)) {
|
} else if (subField.isFieldAffectingData) {
|
||||||
// text, number, date, etc.
|
// text, number, date, etc.
|
||||||
acc.push(parentPath ? `${parentPath}.${subField.name}` : subField.name)
|
acc.push(parentPath ? `${parentPath}.${subField.name}` : subField.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@ import React from 'react'
|
|||||||
import useThrottledEffect from '../../hooks/useThrottledEffect'
|
import useThrottledEffect from '../../hooks/useThrottledEffect'
|
||||||
import { useAllFormFields, useFormSubmitted } from '../Form/context'
|
import { useAllFormFields, useFormSubmitted } from '../Form/context'
|
||||||
import { getFieldStateFromPaths } from './getFieldStateFromPaths'
|
import { getFieldStateFromPaths } from './getFieldStateFromPaths'
|
||||||
|
import { buildPathSegments } from './buildPathSegments'
|
||||||
|
import { createFieldMap } from '../RenderFields/createFieldMap'
|
||||||
|
|
||||||
type TrackSubSchemaErrorCountProps = {
|
type TrackSubSchemaErrorCountProps = {
|
||||||
pathSegments?: string[]
|
path: string
|
||||||
|
fieldMap?: ReturnType<typeof createFieldMap>
|
||||||
setErrorCount: (count: number) => void
|
setErrorCount: (count: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
|
export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
|
||||||
pathSegments,
|
path,
|
||||||
|
fieldMap,
|
||||||
setErrorCount,
|
setErrorCount,
|
||||||
}) => {
|
}) => {
|
||||||
const [formState] = useAllFormFields()
|
const [formState] = useAllFormFields()
|
||||||
const hasSubmitted = useFormSubmitted()
|
const hasSubmitted = useFormSubmitted()
|
||||||
|
|
||||||
|
const pathSegments = buildPathSegments(path, fieldMap)
|
||||||
|
|
||||||
useThrottledEffect(
|
useThrottledEffect(
|
||||||
() => {
|
() => {
|
||||||
if (hasSubmitted) {
|
if (hasSubmitted) {
|
||||||
@@ -25,7 +31,7 @@ export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
250,
|
250,
|
||||||
[formState, hasSubmitted, pathSegments],
|
[formState, hasSubmitted, fieldMap],
|
||||||
)
|
)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import './index.scss'
|
|||||||
const baseClass = 'array-field'
|
const baseClass = 'array-field'
|
||||||
|
|
||||||
type ArrayRowProps = UseDraggableSortableReturn &
|
type ArrayRowProps = UseDraggableSortableReturn &
|
||||||
Pick<Props, 'fieldTypes' | 'fields' | 'indexPath' | 'labels' | 'path' | 'permissions'> & {
|
Pick<Props, 'indexPath' | 'labels' | 'path' | 'permissions'> & {
|
||||||
CustomRowLabel?: RowLabelType
|
CustomRowLabel?: RowLabelType
|
||||||
addRow: (rowIndex: number) => void
|
addRow: (rowIndex: number) => void
|
||||||
duplicateRow: (rowIndex: number) => void
|
duplicateRow: (rowIndex: number) => void
|
||||||
@@ -34,13 +34,12 @@ type ArrayRowProps = UseDraggableSortableReturn &
|
|||||||
rowIndex: number
|
rowIndex: number
|
||||||
setCollapse: (rowID: string, collapsed: boolean) => void
|
setCollapse: (rowID: string, collapsed: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArrayRow: React.FC<ArrayRowProps> = ({
|
export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||||
CustomRowLabel,
|
CustomRowLabel,
|
||||||
addRow,
|
addRow,
|
||||||
attributes,
|
attributes,
|
||||||
duplicateRow,
|
duplicateRow,
|
||||||
fieldTypes,
|
|
||||||
fields,
|
|
||||||
forceRender = false,
|
forceRender = false,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
indexPath,
|
indexPath,
|
||||||
@@ -57,6 +56,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
setCollapse,
|
setCollapse,
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
transform,
|
transform,
|
||||||
|
fieldMap,
|
||||||
}) => {
|
}) => {
|
||||||
const path = `${parentPath}.${rowIndex}`
|
const path = `${parentPath}.${rowIndex}`
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -115,26 +115,21 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
path={path}
|
path={path}
|
||||||
rowNumber={rowIndex + 1}
|
rowNumber={rowIndex + 1}
|
||||||
/>
|
/>
|
||||||
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage />}
|
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage i18n={i18n} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
|
||||||
>
|
>
|
||||||
<HiddenInput name={`${path}.id`} value={row.id} />
|
<HiddenInput name={`${path}.id`} value={row.id} />
|
||||||
[RenderFields]
|
<RenderFields
|
||||||
{/* <RenderFields
|
|
||||||
className={`${baseClass}__fields`}
|
className={`${baseClass}__fields`}
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldMap={fieldMap}
|
||||||
...field,
|
|
||||||
path: createNestedFieldPath(path, field),
|
|
||||||
}))}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={permissions?.fields}
|
permissions={permissions?.fields}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/> */}
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import { ErrorPill } from '../../../elements/ErrorPill'
|
|||||||
import { useConfig } from '../../../providers/Config'
|
import { useConfig } from '../../../providers/Config'
|
||||||
import { useDocumentInfo } from '../../../providers/DocumentInfo'
|
import { useDocumentInfo } from '../../../providers/DocumentInfo'
|
||||||
import { useLocale } from '../../../providers/Locale'
|
import { useLocale } from '../../../providers/Locale'
|
||||||
import Error from '../../Error'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import { useForm, useFormSubmitted } from '../../Form/context'
|
import { useForm, useFormSubmitted } from '../../Form/context'
|
||||||
import { NullifyLocaleField } from '../../NullifyField'
|
import { NullifyLocaleField } from '../../NullifyField'
|
||||||
import useField from '../../useField'
|
import useField from '../../useField'
|
||||||
@@ -28,26 +26,23 @@ const baseClass = 'array-field'
|
|||||||
const ArrayFieldType: React.FC<Props> = (props) => {
|
const ArrayFieldType: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: { className, components, condition, description, readOnly },
|
className,
|
||||||
fieldTypes,
|
readOnly,
|
||||||
fields,
|
|
||||||
forceRender = false,
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
localized,
|
localized,
|
||||||
maxRows,
|
|
||||||
minRows,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
permissions,
|
permissions,
|
||||||
required,
|
required,
|
||||||
validate,
|
validate,
|
||||||
|
Error,
|
||||||
|
Label,
|
||||||
|
Description,
|
||||||
|
fieldMap,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const minRows = 'minRows' in props ? props.minRows : 0
|
||||||
|
const maxRows = 'maxRows' in props ? props.maxRows : undefined
|
||||||
// eslint-disable-next-line react/destructuring-assignment
|
|
||||||
const label = props?.label ?? props?.labels?.singular
|
|
||||||
|
|
||||||
const CustomRowLabel = components?.RowLabel || undefined
|
|
||||||
|
|
||||||
const { setDocFieldPreferences } = useDocumentInfo()
|
const { setDocFieldPreferences } = useDocumentInfo()
|
||||||
const { addFieldRow, dispatchFields, removeFieldRow, setModified } = useForm()
|
const { addFieldRow, dispatchFields, removeFieldRow, setModified } = useForm()
|
||||||
@@ -86,20 +81,20 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorMessage,
|
|
||||||
rows = [],
|
rows = [],
|
||||||
showError,
|
showError,
|
||||||
valid,
|
valid,
|
||||||
value,
|
value,
|
||||||
|
path,
|
||||||
} = useField<number>({
|
} = useField<number>({
|
||||||
hasRows: true,
|
hasRows: true,
|
||||||
path,
|
path: pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowIndex: number) => {
|
async (rowIndex: number) => {
|
||||||
await addFieldRow({ path, rowIndex })
|
await addFieldRow({ path, rowIndex, fieldMap })
|
||||||
setModified(true)
|
setModified(true)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -173,17 +168,13 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
.join(' ')}
|
.join(' ')}
|
||||||
id={`field-${path.replace(/\./g, '__')}`}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
>
|
>
|
||||||
{showError && (
|
{showError && <div className={`${baseClass}__error-wrap`}>{Error}</div>}
|
||||||
<div className={`${baseClass}__error-wrap`}>
|
|
||||||
<Error message={errorMessage} showError={showError} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<header className={`${baseClass}__header`}>
|
<header className={`${baseClass}__header`}>
|
||||||
<div className={`${baseClass}__header-wrap`}>
|
<div className={`${baseClass}__header-wrap`}>
|
||||||
<div className={`${baseClass}__header-content`}>
|
<div className={`${baseClass}__header-content`}>
|
||||||
<h3 className={`${baseClass}__title`}>{getTranslation(label || name, i18n)}</h3>
|
<h3 className={`${baseClass}__title`}>{Label}</h3>
|
||||||
{fieldHasErrors && fieldErrorCount > 0 && (
|
{fieldHasErrors && fieldErrorCount > 0 && (
|
||||||
<ErrorPill count={fieldErrorCount} withMessage />
|
<ErrorPill count={fieldErrorCount} withMessage i18n={i18n} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{rows.length > 0 && (
|
{rows.length > 0 && (
|
||||||
@@ -209,14 +200,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FieldDescription
|
{Description}
|
||||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
|
||||||
description={description}
|
|
||||||
path={path}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
|
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
|
||||||
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
|
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
|
||||||
<DraggableSortable
|
<DraggableSortable
|
||||||
@@ -230,10 +215,9 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
<ArrayRow
|
<ArrayRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
CustomRowLabel={CustomRowLabel}
|
CustomRowLabel={CustomRowLabel}
|
||||||
|
fieldMap={fieldMap}
|
||||||
addRow={addRow}
|
addRow={addRow}
|
||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
fields={fields}
|
|
||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { FieldTypes } from 'payload/config'
|
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { ArrayField } from 'payload/types'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = Omit<ArrayField, 'type'> & {
|
export type Props = FormFieldBase & {
|
||||||
fieldTypes: FieldTypes
|
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
label: false | string
|
label: false | string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import { fieldBaseClass } from '../shared'
|
import { fieldBaseClass } from '../shared'
|
||||||
import { CheckboxInput } from './Input'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import { CheckboxWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { Check } from '../../../icons/Check'
|
||||||
|
import { Line } from '../../../icons/Line'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -19,38 +17,56 @@ export const inputBaseClass = 'checkbox-input'
|
|||||||
|
|
||||||
const Checkbox: React.FC<Props> = (props) => {
|
const Checkbox: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
|
||||||
description,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
disableFormData,
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
|
||||||
required,
|
required,
|
||||||
valid = true,
|
validate,
|
||||||
errorMessage,
|
BeforeInput,
|
||||||
value,
|
AfterInput,
|
||||||
i18n,
|
Label,
|
||||||
|
Error,
|
||||||
|
Description,
|
||||||
|
onChange: onChangeFromProps,
|
||||||
|
partialChecked,
|
||||||
|
checked: checkedFromProps,
|
||||||
|
disableFormData,
|
||||||
|
id,
|
||||||
|
path: pathFromProps,
|
||||||
|
name,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
const fieldID = `field-${path.replace(/\./g, '__')}`
|
const { setValue, value, showError, path } = useField({
|
||||||
|
disableFormData,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
path: pathFromProps || name,
|
||||||
|
})
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const onToggle = useCallback(() => {
|
||||||
const LabelComp = Label || DefaultLabel
|
if (!readOnly) {
|
||||||
|
setValue(!value)
|
||||||
|
if (typeof onChangeFromProps === 'function') onChangeFromProps(!value)
|
||||||
|
}
|
||||||
|
}, [onChangeFromProps, readOnly, setValue, value])
|
||||||
|
|
||||||
|
const checked = checkedFromProps || Boolean(value)
|
||||||
|
|
||||||
|
const fieldID = id || `field-${path?.replace(/\./g, '__')}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
fieldBaseClass,
|
fieldBaseClass,
|
||||||
baseClass,
|
baseClass,
|
||||||
!valid && 'error',
|
showError && 'error',
|
||||||
className,
|
className,
|
||||||
value && `${baseClass}--checked`,
|
value && `${baseClass}--checked`,
|
||||||
readOnly && `${baseClass}--read-only`,
|
readOnly && `${baseClass}--read-only`,
|
||||||
@@ -62,26 +78,42 @@ const Checkbox: React.FC<Props> = (props) => {
|
|||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__error-wrap`}>
|
<div className={`${baseClass}__error-wrap`}>{Error}</div>
|
||||||
<ErrorComp alignCaret="left" path={path} />
|
<div
|
||||||
</div>
|
className={[
|
||||||
<CheckboxWrapper path={path} readOnly={readOnly} baseClass={inputBaseClass}>
|
inputBaseClass,
|
||||||
|
checked && `${inputBaseClass}--checked`,
|
||||||
|
readOnly && `${inputBaseClass}--read-only`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
<div className={`${inputBaseClass}__input`}>
|
<div className={`${inputBaseClass}__input`}>
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<CheckboxInput
|
<input
|
||||||
|
aria-label=""
|
||||||
|
defaultChecked={Boolean(checked)}
|
||||||
|
disabled={readOnly}
|
||||||
id={fieldID}
|
id={fieldID}
|
||||||
label={getTranslation(label || name, i18n)}
|
|
||||||
name={path}
|
name={path}
|
||||||
readOnly={readOnly}
|
onInput={onToggle}
|
||||||
|
// ref={inputRef}
|
||||||
|
type="checkbox"
|
||||||
required={required}
|
required={required}
|
||||||
path={path}
|
|
||||||
iconClassName={`${inputBaseClass}__icon`}
|
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
<span
|
||||||
|
className={[`${inputBaseClass}__icon`, !value && partialChecked ? 'check' : 'partial']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{value && <Check />}
|
||||||
|
{!value && partialChecked && <Line />}
|
||||||
|
</span>
|
||||||
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
{label && <LabelComp htmlFor={fieldID} label={label} required={required} i18n={i18n} />}
|
{Label}
|
||||||
</CheckboxWrapper>
|
</div>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{Description}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { CheckboxField } from 'payload/types'
|
|
||||||
import type { I18n } from '@payloadcms/translations'
|
|
||||||
import type { FormFieldBase } from '../shared'
|
import type { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<CheckboxField, 'type'> & {
|
|
||||||
disableFormData?: boolean
|
disableFormData?: boolean
|
||||||
onChange?: (val: boolean) => void
|
onChange?: (val: boolean) => void
|
||||||
i18n: I18n
|
partialChecked?: boolean
|
||||||
value?: boolean
|
checked?: boolean
|
||||||
|
id?: string
|
||||||
|
path?: string
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) || ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.code-field {
|
.code-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1,59 +1,87 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
import { CodeEditor } from '../../../elements/CodeEditor'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import { withCondition } from '../../withCondition'
|
||||||
|
|
||||||
import DefaultError from '../../Error'
|
import './index.scss'
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import DefaultLabel from '../../Label'
|
const prismToMonacoLanguageMap = {
|
||||||
import { CodeInputWrapper } from './Wrapper'
|
js: 'javascript',
|
||||||
import { CodeInput } from './Input'
|
ts: 'typescript',
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClass = 'code-field'
|
||||||
|
|
||||||
const Code: React.FC<Props> = (props) => {
|
const Code: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label } = {},
|
|
||||||
description,
|
|
||||||
editorOptions,
|
|
||||||
language,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
i18n,
|
Error,
|
||||||
value,
|
Label,
|
||||||
|
Description,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
|
||||||
const LabelComp = Label || DefaultLabel
|
const language = 'language' in props ? props.language : 'javascript'
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const memoizedValidate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') {
|
||||||
|
return validate(value, { ...options, required })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, value, path, showError } = useField({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeInputWrapper
|
<div
|
||||||
className={className}
|
className={[
|
||||||
path={path}
|
fieldBaseClass,
|
||||||
readOnly={readOnly}
|
baseClass,
|
||||||
style={style}
|
className,
|
||||||
width={width}
|
showError && 'error',
|
||||||
|
readOnly && 'read-only',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
|
{Label}
|
||||||
<CodeInput
|
<div>
|
||||||
path={path}
|
{BeforeInput}
|
||||||
required={required}
|
<CodeEditor
|
||||||
|
defaultLanguage={prismToMonacoLanguageMap[language] || language}
|
||||||
|
onChange={readOnly ? () => null : (val) => setValue(val)}
|
||||||
|
options={editorOptions}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
name={name}
|
value={(value as string) || ''}
|
||||||
language={language}
|
|
||||||
editorOptions={editorOptions}
|
|
||||||
/>
|
/>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{AfterInput}
|
||||||
</CodeInputWrapper>
|
</div>
|
||||||
|
{Description}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Code
|
export default withCondition(Code)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { CodeField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<CodeField, 'type'> & {
|
|
||||||
path?: string
|
path?: string
|
||||||
value?: string
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
import { Collapsible } from '../../../elements/Collapsible'
|
||||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
import { useDocumentInfo } from '../../../providers/DocumentInfo'
|
||||||
|
import { usePreferences } from '../../../providers/Preferences'
|
||||||
|
import { useFormSubmitted } from '../../Form/context'
|
||||||
import RenderFields from '../../RenderFields'
|
import RenderFields from '../../RenderFields'
|
||||||
import { CollapsibleFieldWrapper } from './Wrapper'
|
import { withCondition } from '../../withCondition'
|
||||||
import { CollapsibleInput } from './Input'
|
import { fieldBaseClass } from '../shared'
|
||||||
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState'
|
import { DocumentPreferences } from 'payload/types'
|
||||||
import { RowLabel } from '../../RowLabel'
|
import { useFieldPath } from '../../FieldPathProvider'
|
||||||
|
import { WatchChildErrors } from '../../WatchChildErrors'
|
||||||
|
import { ErrorPill } from '../../../elements/ErrorPill'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -16,70 +22,116 @@ const baseClass = 'collapsible-field'
|
|||||||
|
|
||||||
const CollapsibleField: React.FC<Props> = (props) => {
|
const CollapsibleField: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
admin: { className, description, initCollapsed: initCollapsedFromProps, readOnly },
|
className,
|
||||||
fieldTypes,
|
readOnly,
|
||||||
fields,
|
path: pathFromProps,
|
||||||
indexPath,
|
|
||||||
label,
|
|
||||||
path,
|
|
||||||
permissions,
|
permissions,
|
||||||
i18n,
|
Description,
|
||||||
config,
|
Error,
|
||||||
payload,
|
fieldMap,
|
||||||
user,
|
Label,
|
||||||
formState,
|
|
||||||
docPreferences,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { fieldState: nestedFieldState, pathSegments } = getNestedFieldState({
|
const pathFromContext = useFieldPath()
|
||||||
formState,
|
const path = pathFromProps || pathFromContext
|
||||||
path,
|
|
||||||
fieldSchema: fields,
|
const { i18n } = useTranslation()
|
||||||
|
const initCollapsed = 'initCollapsed' in props ? props.initCollapsed : false
|
||||||
|
const { getPreference, setPreference } = usePreferences()
|
||||||
|
const { preferencesKey } = useDocumentInfo()
|
||||||
|
const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>()
|
||||||
|
const fieldPreferencesKey = `collapsible-${path.replace(/\./g, '__')}`
|
||||||
|
const [errorCount, setErrorCount] = useState(0)
|
||||||
|
const submitted = useFormSubmitted()
|
||||||
|
const fieldHasErrors = errorCount > 0 && submitted
|
||||||
|
|
||||||
|
const onToggle = useCallback(
|
||||||
|
async (newCollapsedState: boolean) => {
|
||||||
|
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
||||||
|
|
||||||
|
setPreference(preferencesKey, {
|
||||||
|
...existingPreferences,
|
||||||
|
...(path
|
||||||
|
? {
|
||||||
|
fields: {
|
||||||
|
...(existingPreferences?.fields || {}),
|
||||||
|
[path]: {
|
||||||
|
...existingPreferences?.fields?.[path],
|
||||||
|
collapsed: newCollapsedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fields: {
|
||||||
|
...(existingPreferences?.fields || {}),
|
||||||
|
[fieldPreferencesKey]: {
|
||||||
|
...existingPreferences?.fields?.[fieldPreferencesKey],
|
||||||
|
collapsed: newCollapsedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
},
|
||||||
const fieldPreferencesKey = `collapsible-${indexPath.replace(/\./g, '__')}`
|
[preferencesKey, fieldPreferencesKey, getPreference, setPreference, path],
|
||||||
|
|
||||||
const initCollapsed = Boolean(
|
|
||||||
docPreferences
|
|
||||||
? docPreferences?.fields?.[path || fieldPreferencesKey]?.collapsed
|
|
||||||
: initCollapsedFromProps,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchInitialState = async () => {
|
||||||
|
const preferences = await getPreference(preferencesKey)
|
||||||
|
if (preferences) {
|
||||||
|
const initCollapsedFromPref = path
|
||||||
|
? preferences?.fields?.[path]?.collapsed
|
||||||
|
: preferences?.fields?.[fieldPreferencesKey]?.collapsed
|
||||||
|
setCollapsedOnMount(Boolean(initCollapsedFromPref))
|
||||||
|
} else {
|
||||||
|
setCollapsedOnMount(typeof initCollapsed === 'boolean' ? initCollapsed : false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInitialState()
|
||||||
|
}, [getPreference, preferencesKey, fieldPreferencesKey, initCollapsed, path])
|
||||||
|
|
||||||
|
if (typeof collapsedOnMount !== 'boolean') return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleFieldWrapper
|
<Fragment>
|
||||||
className={className}
|
<WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
|
||||||
path={path}
|
<div
|
||||||
|
className={[
|
||||||
|
fieldBaseClass,
|
||||||
|
baseClass,
|
||||||
|
className,
|
||||||
|
fieldHasErrors ? `${baseClass}--has-error` : `${baseClass}--has-no-error`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
|
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
|
||||||
>
|
>
|
||||||
<CollapsibleInput
|
<Collapsible
|
||||||
initCollapsed={initCollapsed}
|
className={`${baseClass}__collapsible`}
|
||||||
baseClass={baseClass}
|
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
|
||||||
RowLabel={<RowLabel data={formState} label={label} path={path} i18n={i18n} />}
|
header={
|
||||||
path={path}
|
<div className={`${baseClass}__row-label-wrap`}>
|
||||||
fieldPreferencesKey={fieldPreferencesKey}
|
{Label}
|
||||||
pathSegments={pathSegments}
|
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
initCollapsed={collapsedOnMount}
|
||||||
|
onToggle={onToggle}
|
||||||
>
|
>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldMap={fieldMap}
|
||||||
...field,
|
|
||||||
path: createNestedFieldPath(path, field),
|
|
||||||
}))}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender
|
forceRender
|
||||||
indexPath={indexPath}
|
indexPath={path}
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
i18n={i18n}
|
|
||||||
config={config}
|
|
||||||
payload={payload}
|
|
||||||
formState={nestedFieldState}
|
|
||||||
user={user}
|
|
||||||
/>
|
/>
|
||||||
</CollapsibleInput>
|
</Collapsible>
|
||||||
<FieldDescription description={description} path={path} i18n={i18n} />
|
{Description}
|
||||||
</CollapsibleFieldWrapper>
|
</div>
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CollapsibleField
|
export default withCondition(CollapsibleField)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { FieldTypes } from 'payload/config'
|
import type { FieldTypes } from 'payload/config'
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { CollapsibleField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<CollapsibleField, 'type'> & {
|
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
indexPath: string
|
indexPath: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,52 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import { DateTimeInput } from './Input'
|
|
||||||
import './index.scss'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import { fieldBaseClass } from '../shared'
|
import { fieldBaseClass } from '../shared'
|
||||||
import DefaultLabel from '../../Label'
|
import DatePickerField from '../../../elements/DatePicker'
|
||||||
import DefaultError from '../../Error'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'date-time-field'
|
const baseClass = 'date-time-field'
|
||||||
|
|
||||||
const DateTime: React.FC<Props> = (props) => {
|
const DateTime: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { beforeInput, afterInput, Label, Error },
|
|
||||||
date,
|
|
||||||
description,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
|
Error,
|
||||||
|
Label,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
Description,
|
||||||
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const datePickerProps = 'date' in props ? props.date : {}
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const { i18n } = useTranslation()
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, showError, value, path } = useField<Date>({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -40,7 +54,7 @@ const DateTime: React.FC<Props> = (props) => {
|
|||||||
fieldBaseClass,
|
fieldBaseClass,
|
||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
// showError && `${baseClass}--has-error`,
|
showError && `${baseClass}--has-error`,
|
||||||
readOnly && 'read-only',
|
readOnly && 'read-only',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -50,26 +64,22 @@ const DateTime: React.FC<Props> = (props) => {
|
|||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__error-wrap`}>
|
<div className={`${baseClass}__error-wrap`}>{Error}</div>
|
||||||
{/* <ErrorComp
|
{Label}
|
||||||
message={errorMessage}
|
|
||||||
showError={showError}
|
|
||||||
/> */}
|
|
||||||
</div>
|
|
||||||
<LabelComp htmlFor={path} label={label} required={required} />
|
|
||||||
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<DateTimeInput
|
<DatePickerField
|
||||||
datePickerProps={date}
|
{...datePickerProps}
|
||||||
placeholder={placeholder}
|
onChange={(incomingDate) => {
|
||||||
|
if (!readOnly) setValue(incomingDate?.toISOString() || null)
|
||||||
|
}}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
path={path}
|
value={value}
|
||||||
style={style}
|
|
||||||
width={width}
|
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
<FieldDescription description={description} path={path} />
|
{Description}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { DateField } from 'payload/types'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = Omit<DateField, 'type'> & {
|
export type Props = FormFieldBase & {
|
||||||
path: string
|
path: string
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) || ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,67 +1,77 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import { EmailInput } from './Input'
|
|
||||||
import { EmailInputWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
export const Email: React.FC<Props> = (props) => {
|
export const Email: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
path: pathFromProps,
|
||||||
description,
|
|
||||||
autoComplete,
|
autoComplete,
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
Error,
|
||||||
label,
|
Label,
|
||||||
path: pathFromProps,
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
Description,
|
||||||
required,
|
required,
|
||||||
i18n,
|
validate,
|
||||||
value,
|
placeholder,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const memoizedValidate: Validate = useCallback(
|
||||||
const LabelComp = Label || DefaultLabel
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, showError, value, path } = useField({
|
||||||
|
validate: memoizedValidate,
|
||||||
|
path: pathFromProps || name,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EmailInputWrapper
|
<div
|
||||||
className={className}
|
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
|
||||||
readOnly={readOnly}
|
.filter(Boolean)
|
||||||
style={style}
|
.join(' ')}
|
||||||
width={width}
|
style={{
|
||||||
path={path}
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
|
||||||
label={label}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<EmailInput
|
<input
|
||||||
name={name}
|
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
readOnly={readOnly}
|
disabled={Boolean(readOnly)}
|
||||||
path={path}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
required={required}
|
name={path}
|
||||||
|
onChange={setValue}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
|
type="email"
|
||||||
|
value={(value as string) || ''}
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
|
</div>
|
||||||
|
{Description}
|
||||||
</div>
|
</div>
|
||||||
<FieldDescription description={description} path={path} i18n={i18n} value={value} />
|
|
||||||
</EmailInputWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import type { EmailField } from 'payload/types'
|
|
||||||
import type { FormFieldBase } from '../shared'
|
import type { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<EmailField, 'type'> & {
|
|
||||||
path?: string
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type InputProps = Omit<EmailField, 'type' | 'admin'> & {
|
|
||||||
autoComplete?: EmailField['admin']['autoComplete']
|
|
||||||
readOnly?: EmailField['admin']['readOnly']
|
|
||||||
path?: string
|
path?: string
|
||||||
|
name?: string
|
||||||
|
autoComplete?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,102 +1,80 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
|
||||||
import RenderFields from '../../RenderFields'
|
import RenderFields from '../../RenderFields'
|
||||||
import { GroupProvider } from './provider'
|
import { GroupProvider, useGroup } from './provider'
|
||||||
import { GroupWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState'
|
import { useCollapsible } from '../../../elements/Collapsible/provider'
|
||||||
import { GroupFieldErrors } from './Errors'
|
import { useRow } from '../Row/provider'
|
||||||
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments'
|
import { useTabs } from '../Tabs/provider'
|
||||||
|
import { useFormSubmitted } from '../../../forms/Form/context'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import { useFieldPath } from '../../FieldPathProvider'
|
||||||
|
import { WatchChildErrors } from '../../WatchChildErrors'
|
||||||
|
import { ErrorPill } from '../../../elements/ErrorPill'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'group-field'
|
const baseClass = 'group-field'
|
||||||
|
|
||||||
const Group: React.FC<Props> = (props) => {
|
const Group: React.FC<Props> = (props) => {
|
||||||
const {
|
const { className, style, width, fieldMap, Description, hideGutter, Label } = props
|
||||||
name,
|
|
||||||
admin: { description, className, hideGutter = false, readOnly, style, width },
|
|
||||||
fieldTypes,
|
|
||||||
fields,
|
|
||||||
forceRender = false,
|
|
||||||
indexPath,
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
|
||||||
permissions,
|
|
||||||
formState,
|
|
||||||
user,
|
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
config,
|
|
||||||
value,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const path = useFieldPath()
|
||||||
|
|
||||||
const { fieldState: nestedFieldState } = getNestedFieldState({
|
const { i18n } = useTranslation()
|
||||||
formState,
|
const hasSubmitted = useFormSubmitted()
|
||||||
path,
|
const isWithinCollapsible = useCollapsible()
|
||||||
fieldSchema: fields,
|
const isWithinGroup = useGroup()
|
||||||
})
|
const isWithinRow = useRow()
|
||||||
|
const isWithinTab = useTabs()
|
||||||
|
const [errorCount, setErrorCount] = React.useState(undefined)
|
||||||
|
const fieldHasErrors = errorCount > 0 && hasSubmitted
|
||||||
|
|
||||||
const fieldSchema = fields.map((subField) => ({
|
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
|
||||||
...subField,
|
|
||||||
path: createNestedFieldPath(path, subField),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const pathSegments = buildPathSegments(path, fieldSchema)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupWrapper
|
<Fragment>
|
||||||
name={name}
|
<WatchChildErrors fieldMap={fieldMap} path={path} setErrorCount={setErrorCount} />
|
||||||
path={path}
|
<div
|
||||||
className={className}
|
className={[
|
||||||
hideGutter={hideGutter}
|
fieldBaseClass,
|
||||||
style={style}
|
baseClass,
|
||||||
width={width}
|
isTopLevel && `${baseClass}--top-level`,
|
||||||
|
isWithinCollapsible && `${baseClass}--within-collapsible`,
|
||||||
|
isWithinGroup && `${baseClass}--within-group`,
|
||||||
|
isWithinRow && `${baseClass}--within-row`,
|
||||||
|
isWithinTab && `${baseClass}--within-tab`,
|
||||||
|
!hideGutter && isWithinGroup && `${baseClass}--gutter`,
|
||||||
|
fieldHasErrors && `${baseClass}--has-error`,
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
id={`field-${path?.replace(/\./g, '__')}`}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<GroupProvider>
|
<GroupProvider>
|
||||||
<div className={`${baseClass}__wrap`}>
|
<div className={`${baseClass}__wrap`}>
|
||||||
<div className={`${baseClass}__header`}>
|
<div className={`${baseClass}__header`}>
|
||||||
{(label || description) && (
|
{(Label || Description) && (
|
||||||
<header>
|
<header>
|
||||||
{label && (
|
{Label}
|
||||||
<h3 className={`${baseClass}__title`}>
|
{Description}
|
||||||
{typeof label === 'string' ? label : 'Group Title'}
|
|
||||||
{/* {getTranslation(label, i18n)} */}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
<FieldDescription
|
|
||||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
|
||||||
description={description}
|
|
||||||
path={path}
|
|
||||||
value={value}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
<GroupFieldErrors pathSegments={pathSegments} />
|
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
|
||||||
</div>
|
</div>
|
||||||
<RenderFields
|
<RenderFields fieldMap={fieldMap} />
|
||||||
fieldSchema={fieldSchema}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender={forceRender}
|
|
||||||
indexPath={indexPath}
|
|
||||||
margins="small"
|
|
||||||
permissions={permissions?.fields}
|
|
||||||
readOnly={readOnly}
|
|
||||||
user={user}
|
|
||||||
formState={nestedFieldState}
|
|
||||||
i18n={i18n}
|
|
||||||
payload={payload}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</GroupProvider>
|
</GroupProvider>
|
||||||
</GroupWrapper>
|
</div>
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import type { FieldTypes } from 'payload/config'
|
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { GroupField } from 'payload/types'
|
|
||||||
import type { FormFieldBase } from '../shared'
|
import type { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<GroupField, 'type'> & {
|
|
||||||
fieldTypes: FieldTypes
|
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
value: Record<string, unknown>
|
hideGutter?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) || ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,39 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import { HiddenInput } from './Input'
|
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { withCondition } from '../../withCondition'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is mainly used to save a value on the form that is not visible to the user.
|
* This is mainly used to save a value on the form that is not visible to the user.
|
||||||
* For example, this sets the `ìd` property of a block in the Blocks field.
|
* For example, this sets the `ìd` property of a block in the Blocks field.
|
||||||
*/
|
*/
|
||||||
const HiddenField: React.FC<Props> = (props) => {
|
const HiddenInput: React.FC<Props> = (props) => {
|
||||||
const { name, disableModifyingForm = true, path: pathFromProps, value } = props
|
const { name, disableModifyingForm = true, path: pathFromProps, value: valueFromProps } = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const path = pathFromProps || name
|
||||||
|
|
||||||
return <HiddenInput path={path} value={value} disableModifyingForm={disableModifyingForm} />
|
const { setValue, value } = useField({
|
||||||
|
path,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (valueFromProps !== undefined) {
|
||||||
|
setValue(valueFromProps, disableModifyingForm)
|
||||||
|
}
|
||||||
|
}, [valueFromProps, setValue, disableModifyingForm])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={`field-${path?.replace(/\./g, '__')}`}
|
||||||
|
name={path}
|
||||||
|
onChange={setValue}
|
||||||
|
type="hidden"
|
||||||
|
value={(value as string) || ''}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HiddenField
|
export default withCondition(HiddenInput)
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.json-field {
|
.json-field {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1,56 +1,107 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import DefaultError from '../../Error'
|
import { fieldBaseClass } from '../shared'
|
||||||
import FieldDescription from '../../FieldDescription'
|
import { CodeEditor } from '../../../elements/CodeEditor'
|
||||||
import DefaultLabel from '../../Label'
|
import { Validate } from 'payload/types'
|
||||||
import { JSONInputWrapper } from './Wrapper'
|
import useField from '../../useField'
|
||||||
import { JSONInput } from './Input'
|
import { withCondition } from '../../withCondition'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const baseClass = 'json-field'
|
||||||
|
|
||||||
const JSONField: React.FC<Props> = (props) => {
|
const JSONField: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label } = {},
|
|
||||||
description,
|
|
||||||
editorOptions,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
|
Error,
|
||||||
|
Label,
|
||||||
|
Description,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
required,
|
required,
|
||||||
i18n,
|
|
||||||
value,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const editorOptions = 'editorOptions' in props ? props.editorOptions : {}
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const [stringValue, setStringValue] = useState<string>()
|
||||||
|
const [jsonError, setJsonError] = useState<string>()
|
||||||
|
const [hasLoadedValue, setHasLoadedValue] = useState(false)
|
||||||
|
|
||||||
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function')
|
||||||
|
return validate(value, { ...options, jsonError, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { initialValue, setValue, value, path, showError } = useField<string>({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(val) => {
|
||||||
|
if (readOnly) return
|
||||||
|
setStringValue(val)
|
||||||
|
|
||||||
|
try {
|
||||||
|
setValue(JSON.parse(val))
|
||||||
|
setJsonError(undefined)
|
||||||
|
} catch (e) {
|
||||||
|
setJsonError(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[readOnly, setValue, setStringValue],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasLoadedValue) return
|
||||||
|
setStringValue(JSON.stringify(value ? value : initialValue, null, 2))
|
||||||
|
setHasLoadedValue(true)
|
||||||
|
}, [initialValue, value])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JSONInputWrapper
|
<div
|
||||||
className={className}
|
className={[
|
||||||
path={path}
|
fieldBaseClass,
|
||||||
readOnly={readOnly}
|
baseClass,
|
||||||
width={width}
|
className,
|
||||||
style={style}
|
showError && 'error',
|
||||||
|
readOnly && 'read-only',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
|
{Label}
|
||||||
<JSONInput
|
<div>
|
||||||
path={path}
|
{BeforeInput}
|
||||||
required={required}
|
<CodeEditor
|
||||||
|
defaultLanguage="json"
|
||||||
|
onChange={handleChange}
|
||||||
|
options={editorOptions}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
editorOptions={editorOptions}
|
value={stringValue}
|
||||||
/>
|
/>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{AfterInput}
|
||||||
</JSONInputWrapper>
|
</div>
|
||||||
|
{Description}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JSONField
|
export default withCondition(JSONField)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { JSONField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<JSONField, 'type'> & {
|
|
||||||
path?: string
|
path?: string
|
||||||
value?: string
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 : ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +1,128 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import { isNumber } from 'payload/utilities'
|
import { isNumber } from 'payload/utilities'
|
||||||
import ReactSelect from '../../../elements/ReactSelect'
|
import ReactSelect from '../../../elements/ReactSelect'
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import { NumberInput } from './Input'
|
|
||||||
import { NumberInputWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { Option } from '../../../elements/ReactSelect/types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const NumberField: React.FC<Props> = (props) => {
|
const NumberField: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
|
||||||
description,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
step,
|
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
hasMany,
|
|
||||||
label,
|
|
||||||
maxRows,
|
|
||||||
min,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
valid = true,
|
Error,
|
||||||
i18n,
|
Label,
|
||||||
value,
|
Description,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const max = 'max' in props ? props.max : Infinity
|
||||||
const LabelComp = Label || DefaultLabel
|
const min = 'min' in props ? props.min : -Infinity
|
||||||
|
const step = 'step' in props ? props.step : 1
|
||||||
|
const hasMany = 'hasMany' in props ? props.hasMany : false
|
||||||
|
const maxRows = 'maxRows' in props ? props.maxRows : Infinity
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const memoizedValidate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, max, min, required })
|
||||||
|
},
|
||||||
|
[validate, min, max, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, showError, value, path } = useField<number | number[]>({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const val = parseFloat(e.target.value)
|
||||||
|
|
||||||
|
if (Number.isNaN(val)) {
|
||||||
|
setValue('')
|
||||||
|
} else {
|
||||||
|
setValue(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setValue],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [valueToRender, setValueToRender] = useState<
|
||||||
|
{ id: string; label: string; value: { value: number } }[]
|
||||||
|
>([]) // Only for hasMany
|
||||||
|
|
||||||
|
const handleHasManyChange = useCallback(
|
||||||
|
(selectedOption) => {
|
||||||
|
if (!readOnly) {
|
||||||
|
let newValue
|
||||||
|
if (!selectedOption) {
|
||||||
|
newValue = []
|
||||||
|
} else if (Array.isArray(selectedOption)) {
|
||||||
|
newValue = selectedOption.map((option) => Number(option.value?.value || option.value))
|
||||||
|
} else {
|
||||||
|
newValue = [Number(selectedOption.value?.value || selectedOption.value)]
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[readOnly, setValue],
|
||||||
|
)
|
||||||
|
|
||||||
|
// useEffect update valueToRender:
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMany && Array.isArray(value)) {
|
||||||
|
setValueToRender(
|
||||||
|
value.map((val, index) => {
|
||||||
|
return {
|
||||||
|
id: `${val}${index}`, // append index to avoid duplicate keys but allow duplicate numbers
|
||||||
|
label: `${val}`,
|
||||||
|
value: {
|
||||||
|
toString: () => `${val}${index}`,
|
||||||
|
value: (val as any)?.value || val,
|
||||||
|
}, // You're probably wondering, why the hell is this done that way? Well, React-select automatically uses "label-value" as a key, so we will get that react duplicate key warning if we just pass in the value as multiple values can be the same. So we need to append the index to the toString() of the value to avoid that warning, as it uses that as the key.
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [value, hasMany])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberInputWrapper
|
<div
|
||||||
className={className}
|
className={[
|
||||||
readOnly={readOnly}
|
fieldBaseClass,
|
||||||
hasMany={hasMany}
|
'number',
|
||||||
style={style}
|
className,
|
||||||
width={width}
|
showError && 'error',
|
||||||
path={path}
|
readOnly && 'read-only',
|
||||||
|
hasMany && 'has-many',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
|
||||||
label={label}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
{hasMany ? (
|
{hasMany ? (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
className={`field-${path.replace(/\./g, '__')}`}
|
className={`field-${path.replace(/\./g, '__')}`}
|
||||||
@@ -81,27 +147,32 @@ const NumberField: React.FC<Props> = (props) => {
|
|||||||
// onChange={handleHasManyChange}
|
// onChange={handleHasManyChange}
|
||||||
options={[]}
|
options={[]}
|
||||||
// placeholder={t('general:enterAValue')}
|
// placeholder={t('general:enterAValue')}
|
||||||
showError={!valid}
|
showError={showError}
|
||||||
// value={valueToRender as Option[]}
|
value={valueToRender as Option[]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<NumberInput
|
<input
|
||||||
path={path}
|
disabled={readOnly}
|
||||||
required={required}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
min={min}
|
name={path}
|
||||||
placeholder={placeholder}
|
onChange={handleChange}
|
||||||
readOnly={readOnly}
|
onWheel={(e) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
e.target.blur()
|
||||||
|
}}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
step={step}
|
step={step}
|
||||||
hasMany={hasMany}
|
type="number"
|
||||||
name={name}
|
value={typeof value === 'number' ? value : ''}
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FieldDescription description={description} value={value} i18n={i18n} />
|
{Description}
|
||||||
</NumberInputWrapper>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NumberField } from 'payload/types'
|
import type { NumberField } from 'payload/types'
|
||||||
import type { FormFieldBase } from '../shared'
|
import type { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<NumberField, 'type'> & {
|
path?: string
|
||||||
value?: number
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) || ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,41 +1,63 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import Error from '../../Error'
|
|
||||||
import Label from '../../Label'
|
|
||||||
import { PasswordInput } from './Input'
|
|
||||||
import { PasswordInputWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import useField from '../../useField'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
export const Password: React.FC<Props> = (props) => {
|
export const Password: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
autoComplete,
|
autoComplete,
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
|
||||||
required,
|
required,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
i18n,
|
validate,
|
||||||
|
path: pathFromProps,
|
||||||
|
name,
|
||||||
|
Error,
|
||||||
|
Label,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { formProcessing, setValue, showError, value, path } = useField({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PasswordInputWrapper className={className} style={style} width={width} path={path}>
|
<div
|
||||||
<Error path={path} />
|
className={[fieldBaseClass, 'password', className, showError && 'error']
|
||||||
<Label
|
.filter(Boolean)
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
.join(' ')}
|
||||||
label={label}
|
style={{
|
||||||
required={required}
|
...style,
|
||||||
i18n={i18n}
|
width,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Error}
|
||||||
|
{Label}
|
||||||
|
<input
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
disabled={formProcessing || disabled}
|
||||||
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
|
name={path}
|
||||||
|
onChange={setValue}
|
||||||
|
type="password"
|
||||||
|
value={(value as string) || ''}
|
||||||
/>
|
/>
|
||||||
<PasswordInput name={name} autoComplete={autoComplete} disabled={disabled} path={path} />
|
</div>
|
||||||
</PasswordInputWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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] : ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import DefaultError from '../../Error'
|
import useField from '../../useField'
|
||||||
import FieldDescription from '../../FieldDescription'
|
import { fieldBaseClass } from '../shared'
|
||||||
import DefaultLabel from '../../Label'
|
import { Validate } from 'payload/types'
|
||||||
import { NumberInputWrapper } from '../Number/Wrapper'
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
import { PointInput } from './Input'
|
import { withCondition } from '../../withCondition'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -16,82 +17,111 @@ const baseClass = 'point'
|
|||||||
const PointField: React.FC<Props> = (props) => {
|
const PointField: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
|
||||||
description,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
step,
|
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
path: pathFromProps,
|
|
||||||
required,
|
required,
|
||||||
i18n,
|
validate,
|
||||||
i18n: { t },
|
path: pathFromProps,
|
||||||
value,
|
Error,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
Label,
|
||||||
|
Description,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const { i18n } = useTranslation()
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const step = 'step' in props ? props.step : 1
|
||||||
|
|
||||||
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function') return validate(value, { ...options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
setValue,
|
||||||
|
value = [null, null],
|
||||||
|
showError,
|
||||||
|
path,
|
||||||
|
} = useField<[number, number]>({
|
||||||
|
validate: memoizedValidate,
|
||||||
|
path: pathFromProps || name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e, index: 0 | 1) => {
|
||||||
|
let val = parseFloat(e.target.value)
|
||||||
|
if (Number.isNaN(val)) {
|
||||||
|
val = e.target.value
|
||||||
|
}
|
||||||
|
const coordinates = [...value]
|
||||||
|
coordinates[index] = val
|
||||||
|
setValue(coordinates)
|
||||||
|
},
|
||||||
|
[setValue, value],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NumberInputWrapper
|
<div
|
||||||
className={[className, baseClass].filter(Boolean).join(' ')}
|
className={[
|
||||||
readOnly={readOnly}
|
fieldBaseClass,
|
||||||
style={style}
|
baseClass,
|
||||||
width={width}
|
className,
|
||||||
path={path}
|
showError && 'error',
|
||||||
|
readOnly && 'read-only',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<ul className={`${baseClass}__wrap`}>
|
<ul className={`${baseClass}__wrap`}>
|
||||||
<li>
|
<li>
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-longitude-${path.replace(/\./g, '__')}`}
|
|
||||||
label={`${getTranslation(label || name, i18n)} - ${t('fields:longitude')}`}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<div className="input-wrapper">
|
<div className="input-wrapper">
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<PointInput
|
<input
|
||||||
path={path}
|
disabled={readOnly}
|
||||||
placeholder={placeholder}
|
id={`field-longitude-${path.replace(/\./g, '__')}`}
|
||||||
readOnly={readOnly}
|
name={`${path}.longitude`}
|
||||||
|
onChange={(e) => handleChange(e, 0)}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
step={step}
|
step={step}
|
||||||
required={required}
|
type="number"
|
||||||
isLatitude={false}
|
value={value && typeof value[0] === 'number' ? value[0] : ''}
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-latitude-${path.replace(/\./g, '__')}`}
|
|
||||||
label={`${getTranslation(label || name, i18n)} - ${t('fields:latitude')}`}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<div className="input-wrapper">
|
<div className="input-wrapper">
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<PointInput
|
<input
|
||||||
path={path}
|
disabled={readOnly}
|
||||||
placeholder={placeholder}
|
id={`field-latitude-${path.replace(/\./g, '__')}`}
|
||||||
readOnly={readOnly}
|
name={`${path}.latitude`}
|
||||||
|
onChange={(e) => handleChange(e, 1)}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
step={step}
|
step={step}
|
||||||
required={required}
|
type="number"
|
||||||
|
value={value && typeof value[1] === 'number' ? value[1] : ''}
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{Description}
|
||||||
</NumberInputWrapper>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PointField
|
export default withCondition(PointField)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { NumberField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<NumberField, 'type'> & {
|
|
||||||
path?: string
|
path?: string
|
||||||
value?: number
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import React from 'react'
|
|||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { OptionObject } from 'payload/types'
|
import { OptionObject } from 'payload/types'
|
||||||
import { OnChange } from '../types'
|
import { OnChange } from '../types'
|
||||||
import { useTranslation } from '../../../..'
|
import { useTranslation } from '../../../../providers/Translation'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,4 +4,47 @@
|
|||||||
&__error-wrap {
|
&__error-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--layout-horizontal {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
flex-shrink: 0;
|
||||||
|
[dir='ltr'] & {
|
||||||
|
padding-right: $baseline;
|
||||||
|
}
|
||||||
|
[dir='rtl'] & {
|
||||||
|
padding-left: $baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='light'] {
|
||||||
|
.radio-group {
|
||||||
|
&.error {
|
||||||
|
.radio-input__styled-radio {
|
||||||
|
@include lightInputError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
.radio-group {
|
||||||
|
&.error {
|
||||||
|
.radio-input__styled-radio {
|
||||||
|
@include darkInputError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import { RadioGroupWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
import FieldDescription from '../../FieldDescription'
|
import { Radio } from './Radio'
|
||||||
import DefaultError from '../../Error'
|
import { optionIsObject } from 'payload/types'
|
||||||
import DefaultLabel from '../../Label'
|
import useField from '../../useField'
|
||||||
import { RadioGroupInput } from './Input'
|
import { fieldBaseClass } from '../shared'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -16,45 +16,86 @@ const baseClass = 'radio-group'
|
|||||||
const RadioGroup: React.FC<Props> = (props) => {
|
const RadioGroup: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label } = {},
|
|
||||||
description,
|
|
||||||
layout = 'horizontal',
|
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
options,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
|
Error,
|
||||||
|
Label,
|
||||||
|
Description,
|
||||||
|
validate,
|
||||||
required,
|
required,
|
||||||
i18n,
|
|
||||||
value,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const options = 'options' in props ? props.options : []
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const layout = 'layout' in props ? props.layout : 'horizontal'
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
const memoizedValidate = useCallback(
|
||||||
|
(value, validationOptions) => {
|
||||||
|
if (typeof validate === 'function')
|
||||||
|
return validate(value, { ...validationOptions, options, required })
|
||||||
|
},
|
||||||
|
[validate, options, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, value, path, showError } = useField<string>({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadioGroupWrapper
|
<div
|
||||||
width={width}
|
className={[
|
||||||
className={className}
|
fieldBaseClass,
|
||||||
style={style}
|
baseClass,
|
||||||
layout={layout}
|
className,
|
||||||
path={path}
|
`${baseClass}--layout-${layout}`,
|
||||||
readOnly={readOnly}
|
showError && 'error',
|
||||||
baseClass={baseClass}
|
readOnly && `${baseClass}--read-only`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__error-wrap`}>
|
<div className={`${baseClass}__error-wrap`}>{Error}</div>
|
||||||
<ErrorComp path={path} />
|
{Label}
|
||||||
|
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
|
||||||
|
{options.map((option) => {
|
||||||
|
let optionValue = ''
|
||||||
|
let optionLabel: string | Record<string, string> = ''
|
||||||
|
|
||||||
|
if (optionIsObject(option)) {
|
||||||
|
optionValue = option.value
|
||||||
|
optionLabel = option.label
|
||||||
|
} else {
|
||||||
|
optionValue = option
|
||||||
|
optionLabel = option
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = String(optionValue) === String(value)
|
||||||
|
|
||||||
|
const id = `field-${path}-${optionValue}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={`${path} - ${optionValue}`}>
|
||||||
|
<Radio
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onChange={readOnly ? undefined : setValue}
|
||||||
|
option={optionIsObject(option) ? option : { label: option, value: option }}
|
||||||
|
path={path}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
{Description}
|
||||||
</div>
|
</div>
|
||||||
<LabelComp htmlFor={`field-${path}`} label={label} required={required} i18n={i18n} />
|
|
||||||
<RadioGroupInput path={path} options={options} readOnly={readOnly} baseClass={baseClass} />
|
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
|
||||||
</RadioGroupWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { RadioField } from 'payload/types'
|
import type { RadioField } from 'payload/types'
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<RadioField, 'type'> & {
|
name?: string
|
||||||
path?: string
|
path?: string
|
||||||
value?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OnChange<T = string> = (value: T) => void
|
export type OnChange<T = string> = (value: T) => void
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
|
||||||
import RenderFields from '../../RenderFields'
|
import RenderFields from '../../RenderFields'
|
||||||
import { fieldBaseClass } from '../shared'
|
import { fieldBaseClass } from '../shared'
|
||||||
import { RowProvider } from './provider'
|
import { RowProvider } from './provider'
|
||||||
@@ -13,41 +13,19 @@ import './index.scss'
|
|||||||
const baseClass = 'row'
|
const baseClass = 'row'
|
||||||
|
|
||||||
const Row: React.FC<Props> = (props) => {
|
const Row: React.FC<Props> = (props) => {
|
||||||
const {
|
const { className, readOnly, forceRender = false, indexPath, permissions, fieldMap } = props
|
||||||
admin: { className, readOnly },
|
|
||||||
fieldTypes,
|
|
||||||
fields,
|
|
||||||
forceRender = false,
|
|
||||||
indexPath,
|
|
||||||
path,
|
|
||||||
permissions,
|
|
||||||
formState,
|
|
||||||
user,
|
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
config,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowProvider>
|
<RowProvider>
|
||||||
<div className={[fieldBaseClass, baseClass, className].filter(Boolean).join(' ')}>
|
<div className={[fieldBaseClass, baseClass, className].filter(Boolean).join(' ')}>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
className={`${baseClass}__fields`}
|
className={`${baseClass}__fields`}
|
||||||
fieldSchema={fields.map((field) => ({
|
fieldMap={fieldMap}
|
||||||
...field,
|
|
||||||
path: createNestedFieldPath(path, field),
|
|
||||||
}))}
|
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
margins={false}
|
margins={false}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
formState={formState}
|
|
||||||
user={user}
|
|
||||||
i18n={i18n}
|
|
||||||
payload={payload}
|
|
||||||
config={config}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</RowProvider>
|
</RowProvider>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { FieldTypes } from 'payload/config'
|
import type { FieldTypes } from 'payload/config'
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { RowField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<RowField, 'type'> & {
|
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Option, OptionObject } from 'payload/types'
|
import type { Option, OptionObject, Validate } from 'payload/types'
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import SelectInput from './Input'
|
|
||||||
import { SelectFieldWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import ReactSelect from '../../../elements/ReactSelect'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -26,55 +27,120 @@ const formatOptions = (options: Option[]): OptionObject[] =>
|
|||||||
export const Select: React.FC<Props> = (props) => {
|
export const Select: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
description,
|
|
||||||
isClearable,
|
|
||||||
isSortable = true,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
components: { Error, Label } = {},
|
|
||||||
} = {},
|
|
||||||
hasMany,
|
|
||||||
label,
|
|
||||||
options,
|
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
i18n,
|
Description,
|
||||||
value,
|
Error,
|
||||||
|
Label,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const optionsFromProps = 'options' in props ? props.options : []
|
||||||
|
const hasMany = 'hasMany' in props ? props.hasMany : false
|
||||||
|
const isClearable = 'isClearable' in props ? props.isClearable : true
|
||||||
|
const isSortable = 'isSortable' in props ? props.isSortable : true
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const { i18n } = useTranslation()
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
const [options] = useState(formatOptions(optionsFromProps))
|
||||||
|
|
||||||
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, validationOptions) => {
|
||||||
|
if (typeof validate === 'function')
|
||||||
|
return validate(value, { ...validationOptions, hasMany, options, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, value, showError, path } = useField({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
|
let valueToRender
|
||||||
|
|
||||||
|
if (hasMany && Array.isArray(value)) {
|
||||||
|
valueToRender = value.map((val) => {
|
||||||
|
const matchingOption = options.find((option) => option.value === val)
|
||||||
|
return {
|
||||||
|
label: matchingOption ? getTranslation(matchingOption.label, i18n) : val,
|
||||||
|
value: matchingOption?.value ?? val,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (value) {
|
||||||
|
const matchingOption = options.find((option) => option.value === value)
|
||||||
|
valueToRender = {
|
||||||
|
label: matchingOption ? getTranslation(matchingOption.label, i18n) : value,
|
||||||
|
value: matchingOption?.value ?? value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(selectedOption) => {
|
||||||
|
if (!readOnly) {
|
||||||
|
let newValue
|
||||||
|
if (!selectedOption) {
|
||||||
|
newValue = null
|
||||||
|
} else if (hasMany) {
|
||||||
|
if (Array.isArray(selectedOption)) {
|
||||||
|
newValue = selectedOption.map((option) => option.value)
|
||||||
|
} else {
|
||||||
|
newValue = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValue = selectedOption.value
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[readOnly, hasMany, setValue],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectFieldWrapper
|
<div
|
||||||
className={className}
|
className={[
|
||||||
style={style}
|
fieldBaseClass,
|
||||||
width={width}
|
'select',
|
||||||
path={path}
|
className,
|
||||||
readOnly={readOnly}
|
showError && 'error',
|
||||||
|
readOnly && 'read-only',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
<div>
|
||||||
label={label}
|
{BeforeInput}
|
||||||
required={required}
|
<ReactSelect
|
||||||
i18n={i18n}
|
disabled={readOnly}
|
||||||
/>
|
|
||||||
<SelectInput
|
|
||||||
readOnly={readOnly}
|
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
hasMany={hasMany}
|
isMulti={hasMany}
|
||||||
isSortable={isSortable}
|
isSortable={isSortable}
|
||||||
options={formatOptions(options)}
|
onChange={onChange}
|
||||||
path={path}
|
options={options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
label: getTranslation(option.label, i18n),
|
||||||
|
}))}
|
||||||
|
showError={showError}
|
||||||
|
value={valueToRender as OptionObject}
|
||||||
/>
|
/>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{AfterInput}
|
||||||
</SelectFieldWrapper>
|
</div>
|
||||||
|
{Description}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { SelectField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<SelectField, 'type'> & {
|
|
||||||
path?: string
|
path?: string
|
||||||
value?: string
|
value?: string
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { NamedTab, Tab } from 'payload/types'
|
import React, { useState } from 'react'
|
||||||
import React from 'react'
|
|
||||||
import { ErrorPill } from '../../../../elements/ErrorPill'
|
import { ErrorPill } from '../../../../elements/ErrorPill'
|
||||||
import { WatchChildErrors } from '../../../WatchChildErrors'
|
import { WatchChildErrors } from '../../../WatchChildErrors'
|
||||||
import { useTranslation } from '../../../..'
|
import { useFormSubmitted, useTranslation } from '../../../..'
|
||||||
|
import { ReducedTab } from '../../../RenderFields/createFieldMap'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
type TabProps = {
|
|
||||||
isActive?: boolean
|
|
||||||
setIsActive: () => void
|
|
||||||
pathSegments: string[]
|
|
||||||
path: string
|
|
||||||
label: Tab['label']
|
|
||||||
name: NamedTab['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseClass = 'tabs-field__tab-button'
|
const baseClass = 'tabs-field__tab-button'
|
||||||
|
|
||||||
export const TabComponent: React.FC<TabProps> = (props) => {
|
type TabProps = {
|
||||||
const { isActive, setIsActive, pathSegments, name, label } = props
|
isActive?: boolean
|
||||||
|
parentPath: string
|
||||||
|
setIsActive: () => void
|
||||||
|
tab: ReducedTab
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsActive, tab }) => {
|
||||||
|
const { label, name } = tab
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
const [errorCount, setErrorCount] = useState(undefined)
|
||||||
|
const hasName = 'name' in tab
|
||||||
|
const hasSubmitted = useFormSubmitted()
|
||||||
|
|
||||||
const [errorCount, setErrorCount] = React.useState(0)
|
const path = `${parentPath ? `${parentPath}.` : ''}${'name' in tab ? name : ''}`
|
||||||
|
const fieldHasErrors = errorCount > 0 && hasSubmitted
|
||||||
const tabHasErrors = errorCount > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<WatchChildErrors fieldMap={tab.subfields} path={path} setErrorCount={setErrorCount} />
|
||||||
<button
|
<button
|
||||||
className={[
|
className={[
|
||||||
baseClass,
|
baseClass,
|
||||||
tabHasErrors && `${baseClass}--has-error`,
|
fieldHasErrors && `${baseClass}--has-error`,
|
||||||
isActive && `${baseClass}--active`,
|
isActive && `${baseClass}--active`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -40,9 +42,9 @@ export const TabComponent: React.FC<TabProps> = (props) => {
|
|||||||
onClick={setIsActive}
|
onClick={setIsActive}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<WatchChildErrors pathSegments={pathSegments} setErrorCount={setErrorCount} />
|
{label ? getTranslation(label, i18n) : hasName && name}
|
||||||
{label ? getTranslation(label, i18n) : name}
|
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} />}
|
||||||
{tabHasErrors && <ErrorPill i18n={i18n} count={errorCount} />}
|
|
||||||
</button>
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,114 +1,117 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
import { useCollapsible } from '../../../elements/Collapsible/provider'
|
||||||
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
import { useDocumentInfo } from '../../../providers/DocumentInfo'
|
||||||
|
import { usePreferences } from '../../../providers/Preferences'
|
||||||
import RenderFields from '../../RenderFields'
|
import RenderFields from '../../RenderFields'
|
||||||
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
import { TabsProvider } from './provider'
|
import { TabsProvider } from './provider'
|
||||||
import { TabComponent } from './Tab'
|
import { TabComponent } from './Tab'
|
||||||
import { Wrapper } from './Wrapper'
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { DocumentPreferences, tabHasName } from 'payload/types'
|
||||||
import { toKebabCase } from 'payload/utilities'
|
import { toKebabCase } from 'payload/utilities'
|
||||||
import { Tab } from 'payload/types'
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
import { withCondition } from '../../withCondition'
|
import { FieldPathProvider, useFieldPath } from '../../FieldPathProvider'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { buildPathSegments } from '../../WatchChildErrors/buildPathSegments'
|
|
||||||
|
|
||||||
const baseClass = 'tabs-field'
|
const baseClass = 'tabs-field'
|
||||||
|
|
||||||
const getTabFieldSchema = ({ tabConfig, path }: { tabConfig: Tab; path }) => {
|
const TabsField: React.FC<Props> = (props) => {
|
||||||
return tabConfig.fields.map((field) => {
|
|
||||||
const pathSegments = []
|
|
||||||
|
|
||||||
if (path) pathSegments.push(path)
|
|
||||||
if ('name' in tabConfig) pathSegments.push(tabConfig.name)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
path: createNestedFieldPath(pathSegments.join('.'), field),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const TabsField: React.FC<Props> = async (props) => {
|
|
||||||
const {
|
const {
|
||||||
admin: { className, readOnly },
|
className,
|
||||||
fieldTypes,
|
readOnly,
|
||||||
forceRender = false,
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
path,
|
|
||||||
permissions,
|
permissions,
|
||||||
tabs,
|
Description,
|
||||||
formState,
|
fieldMap,
|
||||||
user,
|
path: pathFromProps,
|
||||||
i18n,
|
name,
|
||||||
payload,
|
|
||||||
docPreferences,
|
|
||||||
config,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const pathFromContext = useFieldPath()
|
||||||
|
const path = pathFromContext || pathFromProps || name
|
||||||
|
const { getPreference, setPreference } = usePreferences()
|
||||||
|
const { preferencesKey } = useDocumentInfo()
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
const isWithinCollapsible = useCollapsible()
|
||||||
|
const [activeTabIndex, setActiveTabIndex] = useState<number>(0)
|
||||||
const tabsPrefKey = `tabs-${indexPath}`
|
const tabsPrefKey = `tabs-${indexPath}`
|
||||||
|
|
||||||
const activeTabIndex = docPreferences?.fields?.[path || tabsPrefKey]?.tabIndex || 0
|
const tabs = 'tabs' in props ? props.tabs : []
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getInitialPref = async () => {
|
||||||
|
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
||||||
|
const initialIndex = path
|
||||||
|
? existingPreferences?.fields?.[path]?.tabIndex
|
||||||
|
: existingPreferences?.fields?.[tabsPrefKey]?.tabIndex
|
||||||
|
setActiveTabIndex(initialIndex || 0)
|
||||||
|
}
|
||||||
|
void getInitialPref()
|
||||||
|
}, [path, getPreference, preferencesKey, tabsPrefKey])
|
||||||
|
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
async (incomingTabIndex: number) => {
|
||||||
|
setActiveTabIndex(incomingTabIndex)
|
||||||
|
|
||||||
|
const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
||||||
|
|
||||||
|
setPreference(preferencesKey, {
|
||||||
|
...existingPreferences,
|
||||||
|
...(path
|
||||||
|
? {
|
||||||
|
fields: {
|
||||||
|
...(existingPreferences?.fields || {}),
|
||||||
|
[path]: {
|
||||||
|
...existingPreferences?.fields?.[path],
|
||||||
|
tabIndex: incomingTabIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
fields: {
|
||||||
|
...existingPreferences?.fields,
|
||||||
|
[tabsPrefKey]: {
|
||||||
|
...existingPreferences?.fields?.[tabsPrefKey],
|
||||||
|
tabIndex: incomingTabIndex,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[preferencesKey, getPreference, setPreference, path, tabsPrefKey],
|
||||||
|
)
|
||||||
|
|
||||||
const activeTabConfig = tabs[activeTabIndex]
|
const activeTabConfig = tabs[activeTabIndex]
|
||||||
|
|
||||||
const isNamedTab = activeTabConfig && 'name' in activeTabConfig
|
|
||||||
|
|
||||||
// TODO: make this a server action
|
|
||||||
// const handleTabChange = useCallback(
|
|
||||||
// async (incomingTabIndex: number) => {
|
|
||||||
// setActiveTabIndex(incomingTabIndex)
|
|
||||||
|
|
||||||
// const existingPreferences: DocumentPreferences = await getPreference(preferencesKey)
|
|
||||||
|
|
||||||
// setPreference(preferencesKey, {
|
|
||||||
// ...existingPreferences,
|
|
||||||
// ...(path
|
|
||||||
// ? {
|
|
||||||
// fields: {
|
|
||||||
// ...(existingPreferences?.fields || {}),
|
|
||||||
// [path]: {
|
|
||||||
// ...existingPreferences?.fields?.[path],
|
|
||||||
// tabIndex: incomingTabIndex,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// : {
|
|
||||||
// fields: {
|
|
||||||
// ...existingPreferences?.fields,
|
|
||||||
// [tabsPrefKey]: {
|
|
||||||
// ...existingPreferences?.fields?.[tabsPrefKey],
|
|
||||||
// tabIndex: incomingTabIndex,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// })
|
|
||||||
// },
|
|
||||||
// [preferencesKey, getPreference, setPreference, path, tabsPrefKey],
|
|
||||||
// )
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper className={className}>
|
<div
|
||||||
|
className={[
|
||||||
|
fieldBaseClass,
|
||||||
|
className,
|
||||||
|
baseClass,
|
||||||
|
isWithinCollapsible && `${baseClass}--within-collapsible`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
<TabsProvider>
|
<TabsProvider>
|
||||||
<div className={`${baseClass}__tabs-wrap`}>
|
<div className={`${baseClass}__tabs-wrap`}>
|
||||||
<div className={`${baseClass}__tabs`}>
|
<div className={`${baseClass}__tabs`}>
|
||||||
{tabs.map((tab, tabIndex) => {
|
{tabs.map((tab, tabIndex) => {
|
||||||
const tabPath = [path, 'name' in tab && tab.name].filter(Boolean)?.join('.')
|
|
||||||
const pathSegments = buildPathSegments(tabPath, tab.fields)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabComponent
|
<TabComponent
|
||||||
path={tabPath}
|
|
||||||
isActive={activeTabIndex === tabIndex}
|
isActive={activeTabIndex === tabIndex}
|
||||||
key={tabIndex}
|
key={tabIndex}
|
||||||
setIsActive={undefined}
|
parentPath={path}
|
||||||
// setIsActive={() => handleTabChange(tabIndex)}
|
setIsActive={() => handleTabChange(tabIndex)}
|
||||||
pathSegments={pathSegments}
|
tab={tab}
|
||||||
name={'name' in tab && tab.name}
|
|
||||||
label={tab.label}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -128,16 +131,10 @@ const TabsField: React.FC<Props> = async (props) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
<FieldDescription
|
{Description}
|
||||||
className={`${baseClass}__description`}
|
<FieldPathProvider path={'name' in activeTabConfig ? activeTabConfig.name : ''}>
|
||||||
description={activeTabConfig.description}
|
|
||||||
marginPlacement="bottom"
|
|
||||||
path={path}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<RenderFields
|
<RenderFields
|
||||||
fieldSchema={getTabFieldSchema({ tabConfig: activeTabConfig, path })}
|
fieldMap={activeTabConfig.subfields}
|
||||||
fieldTypes={fieldTypes}
|
|
||||||
forceRender={forceRender}
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
key={
|
key={
|
||||||
@@ -147,23 +144,19 @@ const TabsField: React.FC<Props> = async (props) => {
|
|||||||
}
|
}
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={
|
permissions={
|
||||||
isNamedTab && permissions?.[activeTabConfig.name]
|
'name' in activeTabConfig && permissions?.[activeTabConfig.name]
|
||||||
? permissions[activeTabConfig.name].fields
|
? permissions[activeTabConfig.name].fields
|
||||||
: permissions
|
: permissions
|
||||||
}
|
}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
user={user}
|
|
||||||
formState={formState}
|
|
||||||
i18n={i18n}
|
|
||||||
payload={payload}
|
|
||||||
config={config}
|
|
||||||
/>
|
/>
|
||||||
|
</FieldPathProvider>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsProvider>
|
</TabsProvider>
|
||||||
</Wrapper>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import type { FieldTypes } from 'payload/config'
|
|
||||||
import type { FieldPermissions } from 'payload/auth'
|
import type { FieldPermissions } from 'payload/auth'
|
||||||
import type { TabsField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<TabsField, 'type'> & {
|
|
||||||
fieldTypes: FieldTypes
|
|
||||||
forceRender?: boolean
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) || ''}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.field-type.text {
|
.field-type.text {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1,78 +1,99 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import { TextInput } from './Input'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import { TextInputWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { fieldBaseClass, isFieldRTL } from '../shared'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
import { useConfig, useLocale } from '../../..'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
const Text: React.FC<Props> = (props) => {
|
const Text: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
|
||||||
description,
|
|
||||||
placeholder,
|
|
||||||
readOnly,
|
|
||||||
rtl,
|
|
||||||
style,
|
|
||||||
width,
|
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
localized,
|
localized,
|
||||||
maxLength,
|
maxLength,
|
||||||
minLength,
|
minLength,
|
||||||
path: pathFromProps,
|
|
||||||
required,
|
required,
|
||||||
value,
|
Error,
|
||||||
i18n,
|
Label,
|
||||||
|
Description,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
|
inputRef,
|
||||||
|
readOnly,
|
||||||
|
width,
|
||||||
|
style,
|
||||||
|
onKeyDown,
|
||||||
|
placeholder,
|
||||||
|
rtl,
|
||||||
|
name,
|
||||||
|
path: pathFromProps,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const locale = useLocale()
|
||||||
const LabelComp = Label || DefaultLabel
|
|
||||||
|
const { localization: localizationConfig } = useConfig()
|
||||||
|
|
||||||
|
const memoizedValidate: Validate = useCallback(
|
||||||
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function')
|
||||||
|
return validate(value, { ...options, maxLength, minLength, required })
|
||||||
|
},
|
||||||
|
[validate, minLength, maxLength, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, value, path, showError } = useField({
|
||||||
|
validate: memoizedValidate,
|
||||||
|
path: pathFromProps || name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderRTL = isFieldRTL({
|
||||||
|
fieldLocalized: localized,
|
||||||
|
fieldRTL: rtl,
|
||||||
|
locale,
|
||||||
|
localizationConfig: localizationConfig || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInputWrapper
|
<div
|
||||||
className={className}
|
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
|
||||||
style={style}
|
.filter(Boolean)
|
||||||
width={width}
|
.join(' ')}
|
||||||
path={path}
|
style={{
|
||||||
readOnly={readOnly}
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
|
||||||
label={label}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<TextInput
|
<input
|
||||||
path={path}
|
data-rtl={renderRTL}
|
||||||
name={name}
|
disabled={readOnly}
|
||||||
localized={localized}
|
id={`field-${path?.replace(/\./g, '__')}`}
|
||||||
rtl={rtl}
|
name={path}
|
||||||
placeholder={placeholder}
|
onChange={(e) => {
|
||||||
readOnly={readOnly}
|
setValue(e.target.value)
|
||||||
maxLength={maxLength}
|
}}
|
||||||
minLength={minLength}
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={(value as string) || ''}
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
|
</div>
|
||||||
|
{Description}
|
||||||
</div>
|
</div>
|
||||||
<FieldDescription
|
|
||||||
className={`field-description-${path.replace(/\./g, '__')}`}
|
|
||||||
description={description}
|
|
||||||
path={path}
|
|
||||||
value={value}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
</TextInputWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { TextField } from 'payload/types'
|
|
||||||
import type { FormFieldBase } from '../shared'
|
import type { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<TextField, 'type'> & {
|
name?: string
|
||||||
|
path?: string
|
||||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||||
value: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,44 @@
|
|||||||
import React from 'react'
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
import { isFieldRTL } from '../shared'
|
import { fieldBaseClass, isFieldRTL } from '../shared'
|
||||||
import TextareaInput from './Input'
|
|
||||||
import DefaultError from '../../Error'
|
|
||||||
import DefaultLabel from '../../Label'
|
|
||||||
import FieldDescription from '../../FieldDescription'
|
|
||||||
import { TextareaInputWrapper } from './Wrapper'
|
|
||||||
import { withCondition } from '../../withCondition'
|
import { withCondition } from '../../withCondition'
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { useTranslation } from '../../../providers/Translation'
|
||||||
|
import useField from '../../useField'
|
||||||
|
import { Validate } from 'payload/types'
|
||||||
|
import { useConfig } from '../../../providers/Config'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const Textarea: React.FC<Props> = (props) => {
|
const Textarea: React.FC<Props> = (props) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
admin: {
|
|
||||||
className,
|
className,
|
||||||
components: { Error, Label, afterInput, beforeInput } = {},
|
|
||||||
description,
|
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
rows,
|
|
||||||
rtl,
|
rtl,
|
||||||
style,
|
style,
|
||||||
width,
|
width,
|
||||||
} = {},
|
|
||||||
label,
|
|
||||||
localized,
|
localized,
|
||||||
maxLength,
|
maxLength,
|
||||||
minLength,
|
minLength,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
required,
|
required,
|
||||||
value,
|
|
||||||
locale,
|
locale,
|
||||||
config: { localization },
|
Error,
|
||||||
i18n,
|
Label,
|
||||||
|
BeforeInput,
|
||||||
|
AfterInput,
|
||||||
|
validate,
|
||||||
|
Description,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const path = pathFromProps || name
|
const rows = 'rows' in props ? props.rows : undefined
|
||||||
|
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const { localization } = useConfig()
|
||||||
|
|
||||||
const isRTL = isFieldRTL({
|
const isRTL = isFieldRTL({
|
||||||
fieldLocalized: localized,
|
fieldLocalized: localized,
|
||||||
@@ -45,44 +47,57 @@ const Textarea: React.FC<Props> = (props) => {
|
|||||||
localizationConfig: localization || undefined,
|
localizationConfig: localization || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ErrorComp = Error || DefaultError
|
const memoizedValidate: Validate = useCallback(
|
||||||
const LabelComp = Label || DefaultLabel
|
(value, options) => {
|
||||||
|
if (typeof validate === 'function')
|
||||||
|
return validate(value, { ...options, maxLength, minLength, required })
|
||||||
|
},
|
||||||
|
[validate, required],
|
||||||
|
)
|
||||||
|
|
||||||
|
const { setValue, value, path, showError } = useField<string>({
|
||||||
|
path: pathFromProps || name,
|
||||||
|
validate: memoizedValidate,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextareaInputWrapper
|
<div
|
||||||
className={className}
|
className={[
|
||||||
readOnly={readOnly}
|
fieldBaseClass,
|
||||||
style={style}
|
'textarea',
|
||||||
width={width}
|
className,
|
||||||
path={path}
|
showError && 'error',
|
||||||
|
readOnly && 'read-only',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ErrorComp path={path} />
|
{Error}
|
||||||
<LabelComp
|
{Label}
|
||||||
htmlFor={`field-${path.replace(/\./g, '__')}`}
|
|
||||||
label={label}
|
|
||||||
required={required}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
|
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
|
||||||
<div className="textarea-inner">
|
<div className="textarea-inner">
|
||||||
<div className="textarea-clone" data-value={value || placeholder || ''} />
|
<div className="textarea-clone" data-value={value || placeholder || ''} />
|
||||||
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
|
{BeforeInput}
|
||||||
<TextareaInput
|
<textarea
|
||||||
name={name}
|
className="textarea-element"
|
||||||
path={path}
|
data-rtl={isRTL}
|
||||||
placeholder={placeholder}
|
disabled={readOnly}
|
||||||
readOnly={readOnly}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
required={required}
|
name={path}
|
||||||
|
onChange={setValue}
|
||||||
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
rtl={isRTL}
|
value={value || ''}
|
||||||
maxLength={maxLength}
|
|
||||||
minLength={minLength}
|
|
||||||
/>
|
/>
|
||||||
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
|
{AfterInput}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
|
{Description}
|
||||||
</TextareaInputWrapper>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { TextareaField } from 'payload/types'
|
|
||||||
import { FormFieldBase } from '../shared'
|
import { FormFieldBase } from '../shared'
|
||||||
|
|
||||||
export type Props = FormFieldBase &
|
export type Props = FormFieldBase & {
|
||||||
Omit<TextareaField, 'type'> & {
|
|
||||||
path?: string
|
path?: string
|
||||||
value: string
|
name?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { UIField } from 'payload/types'
|
const UI: React.FC = () => {
|
||||||
|
|
||||||
const UI: React.FC<UIField> = (props) => {
|
|
||||||
const {
|
|
||||||
admin: {
|
|
||||||
components: { Field },
|
|
||||||
},
|
|
||||||
} = props
|
|
||||||
|
|
||||||
if (Field) {
|
|
||||||
return <Field {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,92 @@
|
|||||||
import type { Locale, SanitizedConfig, SanitizedLocalizationConfig } from 'payload/config'
|
import type { Locale, SanitizedLocalizationConfig } from 'payload/config'
|
||||||
import { FormState } from '../Form/types'
|
|
||||||
import { User } from 'payload/auth'
|
import { User } from 'payload/auth'
|
||||||
import { I18n } from '@payloadcms/translations'
|
import {
|
||||||
import { Payload } from 'payload'
|
ArrayField,
|
||||||
import { DocumentPreferences } from 'payload/types'
|
CodeField,
|
||||||
|
DateField,
|
||||||
|
DocumentPreferences,
|
||||||
|
JSONField,
|
||||||
|
RowLabel,
|
||||||
|
Tab,
|
||||||
|
Validate,
|
||||||
|
} from 'payload/types'
|
||||||
|
import { ReducedTab, createFieldMap } from '../RenderFields/createFieldMap'
|
||||||
|
import { Option } from 'payload/types'
|
||||||
|
|
||||||
export const fieldBaseClass = 'field-type'
|
export const fieldBaseClass = 'field-type'
|
||||||
|
|
||||||
export type FormFieldBase = {
|
export type FormFieldBase = {
|
||||||
formState?: FormState
|
|
||||||
path?: string
|
path?: string
|
||||||
valid?: boolean
|
|
||||||
errorMessage?: string
|
|
||||||
user?: User
|
user?: User
|
||||||
i18n?: I18n
|
|
||||||
payload?: Payload
|
|
||||||
docPreferences?: DocumentPreferences
|
docPreferences?: DocumentPreferences
|
||||||
locale?: Locale
|
locale?: Locale
|
||||||
config?: SanitizedConfig
|
BeforeInput?: React.ReactNode
|
||||||
|
AfterInput?: React.ReactNode
|
||||||
|
Label?: React.ReactNode
|
||||||
|
Description?: React.ReactNode
|
||||||
|
Error?: React.ReactNode
|
||||||
|
fieldMap?: ReturnType<typeof createFieldMap>
|
||||||
|
style?: React.CSSProperties
|
||||||
|
width?: string
|
||||||
|
className?: string
|
||||||
|
label?: RowLabel
|
||||||
|
readOnly?: boolean
|
||||||
|
rtl?: boolean
|
||||||
|
maxLength?: number
|
||||||
|
minLength?: number
|
||||||
|
required?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
localized?: boolean
|
||||||
|
validate?: Validate
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
// For `number` fields
|
||||||
|
step?: number
|
||||||
|
hasMany?: boolean
|
||||||
|
maxRows?: number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
// For `radio` fields
|
||||||
|
layout?: 'horizontal' | 'vertical'
|
||||||
|
options?: Option[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `textarea` fields
|
||||||
|
rows?: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `select` fields
|
||||||
|
isClearable?: boolean
|
||||||
|
isSortable?: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
tabs?: ReducedTab[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `code` fields
|
||||||
|
editorOptions?: CodeField['admin']['editorOptions']
|
||||||
|
language?: CodeField['admin']['language']
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `json` fields
|
||||||
|
editorOptions?: JSONField['admin']['editorOptions']
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `collapsible` fields
|
||||||
|
initCollapsed?: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `date` fields
|
||||||
|
date?: DateField['admin']['date']
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// For `array` fields
|
||||||
|
minRows?: ArrayField['minRows']
|
||||||
|
maxRows?: ArrayField['maxRows']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale.
|
* Determines whether a field should be displayed as right-to-left (RTL) based on its configuration, payload's localization configuration and the adming user's currently enabled locale.
|
||||||
@@ -41,6 +109,7 @@ export function isFieldRTL({
|
|||||||
localizationConfig &&
|
localizationConfig &&
|
||||||
localizationConfig.locales &&
|
localizationConfig.locales &&
|
||||||
localizationConfig.locales.length > 1
|
localizationConfig.locales.length > 1
|
||||||
|
|
||||||
const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale
|
const isCurrentLocaleDefaultLocale = locale?.code === localizationConfig?.defaultLocale
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user