chore(next): ssr field validations (#4700)

This commit is contained in:
Jacob Fletcher
2024-01-05 12:15:14 -05:00
committed by GitHub
parent bd6a3a633d
commit e4e5cab60f
57 changed files with 472 additions and 453 deletions

View File

@@ -4,7 +4,7 @@ import { DocumentLayout } from '@payloadcms/next/layouts/Document'
import configPromise from 'payload-config'
export default async ({ children, params }: { children: React.ReactNode; params }) => (
<DocumentLayout config={configPromise} collectionSlug={params.collection} id={params.id}>
<DocumentLayout config={configPromise} collectionSlug={params.collection}>
{children}
</DocumentLayout>
)

View File

@@ -15,13 +15,11 @@ export const DocumentLayout = async ({
config: configPromise,
collectionSlug,
globalSlug,
id,
}: {
children: React.ReactNode
config: Promise<SanitizedConfig>
collectionSlug?: string
globalSlug?: string
id?: string
}) => {
const { user, permissions, config } = await initPage(configPromise)
@@ -36,14 +34,9 @@ export const DocumentLayout = async ({
return (
<Fragment>
<DocumentHeader
// apiURL={apiURL}
config={config}
collectionConfig={collectionConfig}
globalConfig={globalConfig}
// customHeader={customHeader}
// data={data}
id={id}
// isEditing={isEditing}
/>
{children}
</Fragment>

View File

@@ -107,7 +107,7 @@ export const Account = async ({
return (
<Fragment>
<HydrateClientUser user={user} />
<HydrateClientUser user={user} permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined

View File

@@ -13,9 +13,11 @@ import {
FormQueryParamsProvider,
QueryParamTypes,
HydrateClientUser,
DocumentInfoProvider,
} from '@payloadcms/ui'
import queryString from 'qs'
import { notFound } from 'next/navigation'
import { TFunction } from 'i18next'
export const CollectionEdit = async ({
collectionSlug,
@@ -99,7 +101,7 @@ export const CollectionEdit = async ({
locale,
operation: isEditing ? 'update' : 'create',
preferences,
// t,
t: ((key: string) => key) as TFunction, // TODO: i18n
user,
})
@@ -136,7 +138,14 @@ export const CollectionEdit = async ({
return (
<Fragment>
<HydrateClientUser user={user} />
<HydrateClientUser user={user} permissions={permissions} />
<DocumentInfoProvider
collectionSlug={collectionConfig.slug}
id={id}
key={`${collectionSlug}-${locale}`}
versionsEnabled={Boolean(collectionConfig.versions)}
draftsEnabled={Boolean(collectionConfig.versions?.drafts)}
>
<EditDepthProvider depth={1}>
<FormQueryParamsProvider formQueryParams={formQueryParams}>
<RenderCustomComponent
@@ -146,6 +155,7 @@ export const CollectionEdit = async ({
/>
</FormQueryParamsProvider>
</EditDepthProvider>
</DocumentInfoProvider>
</Fragment>
)
}

View File

@@ -67,7 +67,7 @@ export const CollectionList = async ({
return (
<Fragment>
<HydrateClientUser user={user} />
<HydrateClientUser user={user} permissions={permissions} />
<RenderCustomComponent
CustomComponent={ListToRender}
DefaultComponent={DefaultList}

View File

@@ -11,13 +11,13 @@ export const Dashboard = async ({
config: Promise<SanitizedConfig>
searchParams: { [key: string]: string | string[] | undefined }
}) => {
const { config, user } = await initPage(configPromise, true)
const { config, user, permissions } = await initPage(configPromise, true)
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
return (
<Fragment>
<HydrateClientUser user={user} />
<HydrateClientUser user={user} permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined

View File

@@ -13,6 +13,7 @@ import {
FormQueryParamsProvider,
QueryParamTypes,
HydrateClientUser,
DocumentInfoProvider,
} from '@payloadcms/ui'
import { notFound } from 'next/navigation'
import { Metadata } from 'next'
@@ -126,7 +127,7 @@ export const Global = async ({
globalConfig,
data,
fieldTypes,
initialState: state,
state,
permissions: globalPermission,
updatedAt: data?.updatedAt?.toString(),
user,
@@ -135,7 +136,13 @@ export const Global = async ({
return (
<Fragment>
<HydrateClientUser user={user} />
<HydrateClientUser user={user} permissions={permissions} />
<DocumentInfoProvider
collectionSlug={globalConfig.slug}
key={`${globalSlug}-${locale}`}
versionsEnabled={Boolean(globalConfig.versions)}
draftsEnabled={Boolean(globalConfig.versions?.drafts)}
>
<EditDepthProvider depth={1}>
<FormQueryParamsProvider formQueryParams={formQueryParams}>
<RenderCustomComponent
@@ -145,6 +152,7 @@ export const Global = async ({
/>
</FormQueryParamsProvider>
</EditDepthProvider>
</DocumentInfoProvider>
</Fragment>
)
}

View File

@@ -5,6 +5,7 @@ import { DefaultVersionsView } from './Default'
import { SanitizedConfig } from 'payload/types'
import { initPage } from '../../utilities/initPage'
import { DefaultVersionsViewProps } from './Default/types'
import { notFound } from 'next/navigation'
export const VersionsView = async ({
collectionSlug,
@@ -46,6 +47,7 @@ export const VersionsView = async ({
let versionsData
if (collectionSlug) {
try {
data = await payload.findByID({
collection: collectionSlug,
id,
@@ -53,7 +55,15 @@ export const VersionsView = async ({
user,
// draft: true,
})
} catch (error) {
console.error(error)
}
if (!data) {
return notFound()
}
try {
versionsData = await payload.findVersions({
collection: collectionSlug,
depth: 0,
@@ -68,6 +78,9 @@ export const VersionsView = async ({
// },
// },
})
} catch (error) {
console.error(error)
}
docURL = `${serverURL}${api}/${slug}/${id}`
// entityLabel = getTranslation(collectionConfig.labels.singular, i18n)
@@ -97,13 +110,22 @@ export const VersionsView = async ({
}
if (globalSlug) {
try {
data = await payload.findGlobal({
slug: globalSlug,
depth: 0,
user,
// draft: true,
})
} catch (error) {
console.error(error)
}
if (!data) {
return notFound()
}
try {
versionsData = await payload.findGlobalVersions({
slug: globalSlug,
depth: 0,
@@ -116,6 +138,13 @@ export const VersionsView = async ({
},
},
})
} catch (error) {
console.error(error)
}
if (!versionsData) {
return notFound()
}
docURL = `${serverURL}${api}/globals/${globalSlug}`
// entityLabel = getTranslation(globalConfig.label, i18n)

View File

@@ -2,7 +2,6 @@
import { Modal, useModal } from '@faceless-ui/modal'
import React, { useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
import { toast } from 'react-toastify'
import type { Props } from './types'
@@ -11,22 +10,17 @@ import { getTranslation } from 'payload/utilities'
// import { requests } from '../../../api'
import useTitle from '../../hooks/useTitle'
import { useForm } from '../../forms/Form/context'
import { Minimal as MinimalTemplate } from '../../templates/Minimal'
import { MinimalTemplate } from '../../templates/Minimal'
import { useConfig } from '../../providers/Config'
import { Button } from '../Button'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss'
import { useRouter } from 'next/navigation'
const baseClass = 'delete-document'
const DeleteDocument: React.FC<Props> = (props) => {
const {
id,
buttonId,
collection: { labels: { singular } = {}, slug } = {},
collection,
title: titleFromProps,
} = props
const { id, buttonId, useAsTitle, collectionSlug, singularLabel, title: titleFromProps } = props
const {
routes: { admin, api },
@@ -36,9 +30,12 @@ const DeleteDocument: React.FC<Props> = (props) => {
const { setModified } = useForm()
const [deleting, setDeleting] = useState(false)
const { toggleModal } = useModal()
const history = useHistory()
const history = useRouter()
const { i18n, t } = useTranslation('general')
const title = useTitle({ collection })
const title = useTitle({
useAsTitle,
})
const titleToRender = titleFromProps || title || id
const modalSlug = `delete-${id}`
@@ -86,12 +83,12 @@ const DeleteDocument: React.FC<Props> = (props) => {
setModified,
serverURL,
api,
slug,
collectionSlug,
id,
toggleModal,
modalSlug,
t,
singular,
singularLabel,
i18n,
title,
history,
@@ -118,7 +115,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
<Trans
i18nKey="aboutToDelete"
t={t}
values={{ label: getTranslation(singular, i18n), title: titleToRender }}
values={{ label: getTranslation(singularLabel, i18n), title: titleToRender }}
>
aboutToDelete
<strong>{titleToRender}</strong>

View File

@@ -2,7 +2,9 @@ import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = {
buttonId?: string
collection?: SanitizedCollectionConfig
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
collectionSlug: SanitizedCollectionConfig['slug']
singularLabel: SanitizedCollectionConfig['labels']['singular']
id?: string
title?: string
}

View File

@@ -70,6 +70,10 @@ export const DocumentControls: React.FC<{
{collectionConfig && !isEditing && !isAccountView && (
<li className={`${baseClass}__list-item`}>
<p className={`${baseClass}__value`}>
Creating new{' '}
{typeof collectionConfig?.labels?.singular === 'string'
? collectionConfig.labels.singular
: 'Doc'}
{/* {t('creatingNewLabel', {
label:
typeof collectionConfig?.labels?.singular === 'string'
@@ -221,25 +225,31 @@ export const DocumentControls: React.FC<{
<PopupList.ButtonGroup>
{hasCreatePermission && (
<React.Fragment>
{/* <PopupList.Button
<PopupList.Button
id="action-create"
to={`${adminRoute}/collections/${collectionConfig?.slug}/create`}
>
{t('createNew')}
</PopupList.Button> */}
{/* {!collectionConfig?.admin?.disableDuplicate && isEditing && (
Create New
{/* {t('createNew')} */}
</PopupList.Button>
{!collectionConfig?.admin?.disableDuplicate && isEditing && (
<DuplicateDocument
collection={collectionConfig}
singularLabel={collectionConfig?.labels?.singular}
id={id}
slug={collectionConfig?.slug}
/>
)} */}
)}
</React.Fragment>
)}
{/* {hasDeletePermission && (
<DeleteDocument buttonId="action-delete" collection={collectionConfig} id={id} />
)} */}
{hasDeletePermission && (
<DeleteDocument
buttonId="action-delete"
collectionSlug={collectionConfig?.slug}
useAsTitle={collectionConfig?.admin?.useAsTitle}
singularLabel={collectionConfig?.labels?.singular}
id={id}
/>
)}
</PopupList.ButtonGroup>
</Popup>
)}

View File

@@ -43,7 +43,7 @@ export const DocumentHeader: React.FC<{
data={data}
isDate={titleFieldConfig?.type === 'date'}
dateFormat={
'date' in titleFieldConfig?.admin
titleFieldConfig && 'date' in titleFieldConfig?.admin
? titleFieldConfig?.admin?.date?.displayFormat
: undefined
}

View File

@@ -2,7 +2,6 @@
import { Modal, useModal } from '@faceless-ui/modal'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'
import { toast } from 'react-toastify'
import type { Props } from './types'
@@ -10,16 +9,17 @@ import type { Props } from './types'
import { getTranslation } from 'payload/utilities'
// import { requests } from '../../../api'
import { useForm, useFormModified } from '../../forms/Form/context'
import { Minimal as MinimalTemplate } from '../../templates/Minimal'
import { MinimalTemplate } from '../../templates/Minimal'
import { useConfig } from '../../providers/Config'
import { Button } from '../Button'
import * as PopupList from '../Popup/PopupButtonList'
import './index.scss'
import { useRouter } from 'next/navigation'
const baseClass = 'duplicate'
const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
const { push } = useHistory()
const Duplicate: React.FC<Props> = ({ id, slug, singularLabel }) => {
const { push } = useRouter()
const modified = useFormModified()
const { toggleModal } = useModal()
const { setModified } = useForm()
@@ -66,13 +66,14 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
// let data = await response.json()
if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
data = await collection.admin.hooks.beforeDuplicate({
collection,
data,
locale,
})
}
// TODO: convert this into a server action
// if (typeof collection.admin.hooks?.beforeDuplicate === 'function') {
// data = await collection.admin.hooks.beforeDuplicate({
// collection,
// data,
// locale,
// })
// }
if (!duplicateID) {
if ('createdAt' in data) delete data.createdAt
@@ -133,10 +134,9 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
return
}
toast.success(
t('successfullyDuplicated', { label: getTranslation(collection.labels.singular, i18n) }),
{ autoClose: 3000 },
)
toast.success(t('successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }), {
autoClose: 3000,
})
if (localeErrors.length > 0) {
toast.error(
@@ -151,9 +151,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
setModified(false)
setTimeout(() => {
push({
pathname: `${admin}/collections/${slug}/${duplicateID}`,
})
push(`${admin}/collections/${slug}/${duplicateID}`)
}, 10)
},
[
@@ -161,7 +159,6 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
localization,
t,
i18n,
collection,
setModified,
toggleModal,
modalSlug,

View File

@@ -1,7 +1,7 @@
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = {
collection: SanitizedCollectionConfig
singularLabel: SanitizedCollectionConfig['labels']['singular']
id: string
slug: string
}

View File

@@ -2,13 +2,18 @@
import { useEffect } from 'react'
import { useAuth } from '../../providers/Auth'
import { Permissions, User } from 'payload/auth'
export const HydrateClientUser: React.FC<{ user: any }> = ({ user }) => {
const { setUser } = useAuth()
export const HydrateClientUser: React.FC<{ user: User; permissions: Permissions }> = ({
user,
permissions,
}) => {
const { setUser, setPermissions } = useAuth()
useEffect(() => {
setUser(user)
}, [user])
setPermissions(permissions)
}, [user, permissions, setUser, setPermissions])
return null
}

View File

@@ -28,14 +28,18 @@ export const LoginForm: React.FC<{
action={`${api}/${userSlug}/login`}
className={`${baseClass}__form`}
disableSuccessStatus
initialData={
prefillForm
? {
email: autoLogin.email,
password: autoLogin.password,
}
: undefined
}
initialState={{
email: {
initialValue: prefillForm ? autoLogin.email : undefined,
value: prefillForm ? autoLogin.email : undefined,
valid: true,
},
password: {
initialValue: prefillForm ? autoLogin.password : undefined,
value: prefillForm ? autoLogin.password : undefined,
valid: true,
},
}}
method="POST"
redirect={`${admin}${searchParams?.redirect || ''}`}
waitForAutocomplete

View File

@@ -1,3 +1,4 @@
'use client'
import type { LinkProps } from 'react-router-dom'
import * as React from 'react'
@@ -6,6 +7,7 @@ import Link from 'next/link' // TODO: abstract this out to support all routers
import './index.scss'
const baseClass = 'popup-button-list'
export const ButtonGroup: React.FC<{
buttonSize?: 'default' | 'small'
children: React.ReactNode
@@ -31,6 +33,7 @@ type MenuButtonProps = {
onClick?: () => void
to?: LinkProps['to']
}
export const Button: React.FC<MenuButtonProps> = ({
id,
active,

View File

@@ -8,3 +8,4 @@ export { useLocale } from '../providers/Locale'
export { useActions } from '../providers/ActionsProvider'
export { useAuth } from '../providers/Auth'
export { useDocumentInfo } from '../providers/DocumentInfo'
export { DocumentInfoProvider } from '../providers/DocumentInfo'

View File

@@ -45,12 +45,12 @@ export const addFieldStatePromise = async ({
user,
}: Args): Promise<void> => {
if (fieldAffectsData(field)) {
const validate = operation === 'update' ? field.validate : undefined
const fieldState: FormField = {
// condition: field.admin?.condition,
initialValue: undefined,
passesCondition,
valid: true,
// validate: field.validate,
value: undefined,
}
@@ -67,18 +67,18 @@ export const addFieldStatePromise = async ({
let validationResult: boolean | string = true
// if (typeof fieldState.validate === 'function') {
// validationResult = await fieldState.validate(data?.[field.name], {
// ...field,
// id,
// config,
// data: fullData,
// operation,
// siblingData: data,
// t,
// user,
// })
// }
if (typeof validate === 'function') {
validationResult = await validate(data?.[field.name], {
...field,
id,
config,
data: fullData,
operation,
siblingData: data,
t,
user,
})
}
if (typeof validationResult === 'string') {
fieldState.errorMessage = validationResult

View File

@@ -4,8 +4,6 @@ import equal from 'deep-equal'
import type { FieldAction, Fields, FormField } from './types'
import { deepCopyObject } from 'payload/utilities'
import getSiblingData from './getSiblingData'
import reduceFieldsToValues from './reduceFieldsToValues'
import { flattenRows, separateRows } from './rows'
/**
@@ -42,53 +40,14 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
return newState
}
case 'MODIFY_CONDITION': {
const { path, result, user } = action
return Object.entries(state).reduce((newState, [fieldPath, field]) => {
if (fieldPath === path || fieldPath.indexOf(`${path}.`) === 0) {
let passesCondition = result
// If a condition is being set to true,
// Set all conditions to true
// Besides those who still fail their own conditions
if (passesCondition && field.condition) {
passesCondition = field.condition(
reduceFieldsToValues(state, true),
getSiblingData(state, path),
{ user },
)
}
return {
...newState,
[fieldPath]: {
...field,
passesCondition,
},
}
}
return {
...newState,
[fieldPath]: {
...field,
},
}
}, {})
}
case 'UPDATE': {
const newField = Object.entries(action).reduce(
(field, [key, value]) => {
if (
[
'condition',
'disableFormData',
'errorMessage',
'initialValue',
'passesCondition',
'rows',
'valid',
'validate',

View File

@@ -31,7 +31,6 @@ import { useLocale } from '../../providers/Locale'
import { useOperation } from '../../providers/OperationProvider'
import { WatchFormErrors } from './WatchFormErrors'
import { buildFieldSchemaMap } from './buildFieldSchemaMap'
import buildInitialState from './buildInitialState'
import buildStateFromSchema from './buildStateFromSchema'
import {
FormContext,
@@ -51,7 +50,7 @@ import reduceFieldsToValues from './reduceFieldsToValues'
const baseClass = 'form'
const Form: React.FC<Props> = (props) => {
const { id, collection, getDocPreferences, global } = useDocumentInfo()
const { id, collectionSlug, getDocPreferences, globalSlug } = useDocumentInfo()
const {
action,
@@ -59,9 +58,9 @@ const Form: React.FC<Props> = (props) => {
className,
disableSuccessStatus,
disabled,
fields: fieldsFromProps = collection?.fields || global?.fields,
fields: fieldsFromProps,
// fields: fieldsFromProps = collection?.fields || global?.fields,
handleResponse,
initialData, // values only, paths are required as key - form should build initial state as convenience
initialState, // fully formed initial field state
method,
onSubmit,
@@ -83,17 +82,10 @@ const Form: React.FC<Props> = (props) => {
const [modified, setModified] = useState(false)
const [processing, setProcessing] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [formattedInitialData, setFormattedInitialData] = useState(buildInitialState(initialData))
const formRef = useRef<HTMLFormElement>(null)
const contextRef = useRef({} as FormContextType)
let initialFieldState = {}
if (formattedInitialData) initialFieldState = formattedInitialData
if (initialState) initialFieldState = initialState
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState)
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
/**
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
* which calls the fieldReducer, which then updates the state.
@@ -642,15 +634,6 @@ const Form: React.FC<Props> = (props) => {
}
}, [initialState, dispatchFields])
useEffect(() => {
if (initialData) {
contextRef.current = { ...initContextState } as FormContextType
const builtState = buildInitialState(initialData)
setFormattedInitialData(builtState)
dispatchFields({ state: builtState, type: 'REPLACE_STATE' })
}
}, [initialData, dispatchFields])
useThrottledEffect(
() => {
refreshCookie()

View File

@@ -2,7 +2,7 @@ import type React from 'react'
import type { Dispatch } from 'react'
import type { User } from 'payload/auth'
import type { Condition, Field, Field as FieldConfig, Validate } from 'payload/types'
import type { Field, Field as FieldConfig, Validate } from 'payload/types'
export type Row = {
blockType?: string
@@ -12,15 +12,14 @@ export type Row = {
}
export type FormField = {
// condition?: Condition
disableFormData?: boolean
errorMessage?: string
initialValue: unknown
passesCondition?: boolean
rows?: Row[]
valid: boolean
// validate?: Validate
value: unknown
validate?: Validate
}
export type Fields = {
@@ -48,7 +47,6 @@ export type Props = {
*/
fields?: Field[]
handleResponse?: (res: Response) => void
initialData?: Data
initialState?: Fields
log?: boolean
method?: 'DELETE' | 'GET' | 'PATCH' | 'POST'

View File

@@ -56,6 +56,8 @@ const RenderFields: React.FC<Props> = (props) => {
permissions: fieldPermissions,
data,
user,
valid: fieldState?.valid,
errorMessage: fieldState?.errorMessage,
}
if (field) {

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { array } from 'payload/fields/validations'
import { getTranslation } from 'payload/utilities'
import { scrollToID } from '../../../utilities/scrollToID'
import Banner from '../../../elements/Banner'
@@ -40,7 +39,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
path: pathFromProps,
permissions,
required,
validate = array,
validate,
} = props
const path = pathFromProps || name
@@ -93,7 +92,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
valid,
value,
} = useField<number>({
condition,
hasRows: true,
path,
validate: memoizedValidate,

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { blocks as blocksValidator } from 'payload/fields/validations'
import { getTranslation } from 'payload/utilities'
import { scrollToID } from '../../../utilities/scrollToID'
import Banner from '../../../elements/Banner'
@@ -34,7 +33,7 @@ const BlocksField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly },
admin: { className, description, readOnly },
blocks,
fieldTypes,
forceRender = false,
@@ -47,7 +46,7 @@ const BlocksField: React.FC<Props> = (props) => {
path: pathFromProps,
permissions,
required,
validate = blocksValidator,
validate,
} = props
const path = pathFromProps || name
@@ -92,7 +91,6 @@ const BlocksField: React.FC<Props> = (props) => {
valid,
value,
} = useField<number>({
condition,
hasRows: true,
path,
validate: memoizedValidate,

View File

@@ -1,10 +1,10 @@
'use client'
import React, { useCallback } from 'react'
import React, { Fragment, useCallback } from 'react'
import './index.scss'
import useField from '../../useField'
const baseClass = 'checkbox-input'
import { Validate } from 'payload/types'
import { Check } from '../../../icons/Check'
import { Line } from '../../../icons/Line'
type CheckboxInputProps = {
'aria-label'?: string
@@ -15,10 +15,12 @@ type CheckboxInputProps = {
label?: string
name?: string
onChange?: (value: boolean) => void
partialChecked?: boolean
readOnly?: boolean
required?: boolean
path: string
validate?: Validate
partialChecked?: boolean
iconClassName?: string
}
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
@@ -28,24 +30,27 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
'aria-label': ariaLabel,
checked: checkedFromProps,
className,
iconClassName,
inputRef,
onChange: onChangeFromProps,
readOnly,
required,
path,
validate,
partialChecked,
} = props
// const memoizedValidate = useCallback(
// (value, options) => {
// return validate(value, { ...options, required })
// },
// [validate, required],
// )
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField({
const { setValue, value } = useField({
// disableFormData,
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
const onToggle = useCallback(() => {
@@ -58,10 +63,11 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const checked = checkedFromProps || Boolean(value)
return (
<Fragment>
<input
className={className}
aria-label={ariaLabel}
defaultChecked={checked}
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={id}
name={name}
@@ -70,5 +76,14 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
type="checkbox"
required={required}
/>
<span
className={[iconClassName, !value && partialChecked ? 'check' : 'partial']
.filter(Boolean)
.join(' ')}
>
{value && <Check />}
{!value && partialChecked && <Line />}
</span>
</Fragment>
)
}

View File

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

View File

@@ -2,15 +2,16 @@ import React from 'react'
import type { Props } from './types'
import { checkbox } from 'payload/fields/validations'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import { fieldBaseClass } from '../shared'
import { CheckboxInput } from './Input'
import DefaultLabel from '../../Label'
import './index.scss'
import { CheckboxWrapper } from './Wrapper'
const baseClass = 'checkbox'
const inputBaseClass = 'checkbox-input'
const Checkbox: React.FC<Props> = (props) => {
const {
@@ -25,10 +26,11 @@ const Checkbox: React.FC<Props> = (props) => {
} = {},
disableFormData,
label,
onChange,
path: pathFromProps,
required,
validate = checkbox,
valid,
errorMessage,
value,
} = props
const path = pathFromProps || name
@@ -43,9 +45,9 @@ const Checkbox: React.FC<Props> = (props) => {
className={[
fieldBaseClass,
baseClass,
// showError && 'error',
!valid && 'error',
className,
// value && `${baseClass}--checked`,
value && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
@@ -56,23 +58,10 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<ErrorComp
alignCaret="left"
// message={errorMessage}
// showError={showError}
/>
<ErrorComp alignCaret="left" message={errorMessage} showError={!valid} />
</div>
<div
className={[
baseClass,
className,
// (checked || partialChecked) && `${baseClass}--checked`,
readOnly && `${baseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__input`}>
<CheckboxWrapper path={path} readOnly={readOnly} baseClass={inputBaseClass}>
<div className={`${inputBaseClass}__input`}>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<CheckboxInput
id={fieldID}
@@ -82,20 +71,13 @@ const Checkbox: React.FC<Props> = (props) => {
readOnly={readOnly}
required={required}
path={path}
iconClassName={`${inputBaseClass}__icon`}
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
{/* <span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
{!partialChecked && <Check />}
{partialChecked && <Line />}
</span> */}
</div>
{label && <LabelComp htmlFor={fieldID} label={label} required={required} />}
</div>
<FieldDescription
description={description}
path={path}
// value={value}
/>
</CheckboxWrapper>
<FieldDescription description={description} path={path} value={value} />
</div>
)
}

View File

@@ -1,7 +1,8 @@
import type { CheckboxField } from 'payload/types'
import { FormFieldBase } from '../Text/types'
export type Props = Omit<CheckboxField, 'type'> & {
export type Props = FormFieldBase &
Omit<CheckboxField, 'type'> & {
disableFormData?: boolean
onChange?: (val: boolean) => void
path?: string
}

View File

@@ -3,7 +3,6 @@ import React, { useCallback } from 'react'
import type { Props } from './types'
import { code } from 'payload/fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
@@ -35,7 +34,7 @@ const Code: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
validate = code,
validate,
} = props
const ErrorComp = Error || DefaultError

View File

@@ -1,8 +1,8 @@
'use client'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { DateField } from 'payload/types'
import type { DateField, Validate } from 'payload/types'
import { getTranslation } from 'payload/utilities'
import DatePicker from '../../../elements/DatePicker'
@@ -18,21 +18,20 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
}
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
const { path, readOnly, placeholder, datePickerProps, style, width } = props
const { path, readOnly, placeholder, datePickerProps, style, width, validate, required } = props
const { i18n } = useTranslation()
// const memoizedValidate = useCallback(
// (value, options) => {
// return validate(value, { ...options, required })
// },
// [validate, required],
// )
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField<Date>({
// condition,
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
return (

View File

@@ -2,7 +2,6 @@ import React from 'react'
import type { Props } from './types'
import { date as dateValidation } from 'payload/fields/validations'
import { DateTimeInput } from './Input'
import './index.scss'
import FieldDescription from '../../FieldDescription'
@@ -28,7 +27,6 @@ const DateTime: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
validate = dateValidation,
} = props
const path = pathFromProps || name

View File

@@ -1,10 +1,11 @@
'use client'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import useField from '../../useField'
import './index.scss'
import { getTranslation } from 'payload/utilities'
import { Validate } from 'payload/types'
export const EmailInput: React.FC<{
name: string
@@ -13,6 +14,7 @@ export const EmailInput: React.FC<{
path: string
required?: boolean
placeholder?: Record<string, string> | string
validate?: Validate
}> = (props) => {
const {
name,
@@ -20,7 +22,7 @@ export const EmailInput: React.FC<{
readOnly,
path: pathFromProps,
required,
// validate = email,
validate,
placeholder,
} = props
@@ -28,12 +30,12 @@ export const EmailInput: React.FC<{
const path = pathFromProps || name
// const memoizedValidate = useCallback(
// (value, options) => {
// return validate(value, { ...options, required })
// },
// [validate, required],
// )
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
const {
// errorMessage,
@@ -42,7 +44,7 @@ export const EmailInput: React.FC<{
value,
} = useField({
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
return (

View File

@@ -2,7 +2,6 @@ import React from 'react'
import type { Props } from './types'
import { email } from 'payload/fields/validations'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
@@ -25,7 +24,6 @@ export const Email: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
validate = email,
} = props
const path = pathFromProps || name
@@ -59,7 +57,6 @@ export const Email: React.FC<Props> = (props) => {
<EmailInput
name={name}
autoComplete={autoComplete}
// condition={condition}
readOnly={readOnly}
path={path}
required={required}

View File

@@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState } from 'react'
import type { Props } from './types'
import { json } from 'payload/fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
@@ -11,6 +10,7 @@ import DefaultLabel from '../../Label'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { Validate } from 'payload/types'
const baseClass = 'json-field'
@@ -20,7 +20,6 @@ const JSONField: React.FC<Props> = (props) => {
admin: {
className,
components: { Error, Label } = {},
condition,
description,
editorOptions,
readOnly,
@@ -30,7 +29,7 @@ const JSONField: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
validate = json,
validate,
} = props
const ErrorComp = Error || DefaultError
@@ -41,15 +40,15 @@ const JSONField: React.FC<Props> = (props) => {
const [jsonError, setJsonError] = useState<string>()
const [hasLoadedValue, setHasLoadedValue] = useState(false)
const memoizedValidate = useCallback(
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, jsonError, required })
},
[validate, required, jsonError],
[validate, required],
)
const { errorMessage, initialValue, setValue, showError, value } = useField<string>({
condition,
path,
validate: memoizedValidate,
})

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { getTranslation } from 'payload/utilities'
import useField from '../../useField'
import './index.scss'
import { Validate } from 'payload/types'
export const NumberInput: React.FC<{
path: string
@@ -16,6 +17,7 @@ export const NumberInput: React.FC<{
step?: number
hasMany?: boolean
name?: string
validate?: Validate
}> = (props) => {
const {
name,
@@ -27,23 +29,23 @@ export const NumberInput: React.FC<{
min,
path: pathFromProps,
required,
validate,
} = props
const { i18n, t } = useTranslation()
const path = pathFromProps || name
// const memoizedValidate = useCallback(
// (value, options) => {
// return validate(value, { ...options, max, min, required })
// },
// [validate, min, max, required],
// )
const memoizedValidate = useCallback(
(value, options) => {
return validate(value, { ...options, max, min, required })
},
[validate, min, max, required],
)
const { errorMessage, setValue, showError, value } = useField<number | number[]>({
// condition,
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
const handleChange = useCallback(

View File

@@ -1,9 +1,7 @@
import React from 'react'
import type { Option } from '../../../elements/ReactSelect/types'
import type { Props } from './types'
import { number } from 'payload/fields/validations'
import { isNumber } from 'payload/utilities'
import ReactSelect from '../../../elements/ReactSelect'
import DefaultError from '../../Error'
@@ -36,7 +34,7 @@ const NumberField: React.FC<Props> = (props) => {
minRows,
path: pathFromProps,
required,
validate = number,
validate,
} = props
const ErrorComp = Error || DefaultError
@@ -44,10 +42,16 @@ const NumberField: React.FC<Props> = (props) => {
const path = pathFromProps || name
const memoizedValidate = React.useCallback(
(value, options) => {
return validate(value, { ...options, required })
},
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField<number | number[]>({
condition,
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
return (

View File

@@ -1,11 +1,9 @@
'use client'
import React, { useCallback } from 'react'
import type { Props } from './types'
import { password } from 'payload/fields/validations'
import useField from '../../useField'
import './index.scss'
import { Validate } from 'payload/types'
export const PasswordInput: React.FC<{
name: string
@@ -13,28 +11,22 @@ export const PasswordInput: React.FC<{
disabled?: boolean
path: string
required?: boolean
validate?: Validate
}> = (props) => {
const {
name,
autoComplete,
disabled,
path: pathFromProps,
// required,
} = props
const { name, autoComplete, disabled, path: pathFromProps, required, validate } = props
const path = pathFromProps || name
// const memoizedValidate = useCallback(
// (value, options) => {
// const validationResult = validate(value, { ...options, required })
// return validationResult
// },
// [validate, required],
// )
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,
validate: memoizedValidate,
})
return (

View File

@@ -2,7 +2,6 @@ import React from 'react'
import type { Props } from './types'
import { password } from 'payload/fields/validations'
import Error from '../../Error'
import Label from '../../Label'
import './index.scss'
@@ -19,7 +18,6 @@ export const Password: React.FC<Props> = (props) => {
path: pathFromProps,
required,
style,
validate = password,
width,
} = props

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { point } from 'payload/fields/validations'
import { getTranslation } from 'payload/utilities'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
@@ -12,6 +11,7 @@ import DefaultLabel from '../../Label'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { Validate } from 'payload/types'
const baseClass = 'point'
@@ -32,7 +32,7 @@ const PointField: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
validate = point,
validate,
} = props
const ErrorComp = Error || DefaultError
@@ -42,9 +42,9 @@ const PointField: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation('fields')
const memoizedValidate = useCallback(
const memoizedValidate: Validate = useCallback(
(value, options) => {
return validate(value, { ...options, required })
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)
@@ -55,7 +55,6 @@ const PointField: React.FC<Props> = (props) => {
showError,
value = [null, null],
} = useField<[number, number]>({
condition,
path,
validate: memoizedValidate,
})

View File

@@ -3,7 +3,6 @@ import React, { useCallback } from 'react'
import type { Props } from './types'
import { radio } from 'payload/fields/validations'
import useField from '../../useField'
import RadioGroupInput from './Input'
@@ -24,20 +23,20 @@ const RadioGroup: React.FC<Props> = (props) => {
options,
path: pathFromProps,
required,
validate = radio,
validate,
} = props
const path = pathFromProps || name
const memoizedValidate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, options, required })
},
[validate, options, required],
)
const { errorMessage, setValue, showError, value } = useField<string>({
condition,
path,
validate: memoizedValidate,
})

View File

@@ -8,7 +8,6 @@ import type { Where } from 'payload/types'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types'
import type { FilterOptionsResult, GetResults, Option, Props, Value } from './types'
import { relationship } from 'payload/fields/validations'
import { wordBoundariesRegex } from 'payload/utilities'
import { useDebouncedCallback } from '../../../hooks/useDebouncedCallback'
import ReactSelect from '../../../elements/ReactSelect'
@@ -55,7 +54,7 @@ const Relationship: React.FC<Props> = (props) => {
path,
relationTo,
required,
validate = relationship,
validate,
} = props
const ErrorComp = Error || DefaultError
@@ -94,7 +93,6 @@ const Relationship: React.FC<Props> = (props) => {
)
const { errorMessage, initialValue, setValue, showError, value } = useField<Value | Value[]>({
condition,
path: pathOrName,
validate: memoizedValidate,
})

View File

@@ -2,7 +2,7 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { OptionObject } from 'payload/types'
import type { OptionObject, Validate } from 'payload/types'
import type { Option } from '../../../elements/ReactSelect/types'
import { getTranslation } from 'payload/utilities'
@@ -17,19 +17,22 @@ const SelectInput: React.FC<{
isSortable: boolean
options: OptionObject[]
path: string
}> = ({ readOnly, isClearable, hasMany, isSortable, options, path }) => {
validate?: Validate
required?: boolean
}> = ({ readOnly, isClearable, hasMany, isSortable, options, path, validate, required }) => {
const { i18n } = useTranslation()
// const memoizedValidate = useCallback(
// (value, validationOptions) => {
// return validate(value, { ...validationOptions, hasMany, options, required })
// },
// [validate, required, hasMany, options],
// )
const memoizedValidate: Validate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function')
return validate(value, { ...validationOptions, hasMany, options, required })
},
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField({
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
let valueToRender

View File

@@ -39,7 +39,6 @@ export const Select: React.FC<Props> = (props) => {
options,
path: pathFromProps,
required,
// validate = select,
} = props
const path = pathFromProps || name

View File

@@ -1,8 +1,8 @@
'use client'
import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { SanitizedConfig } from 'payload/types'
import type { SanitizedConfig, Validate } from 'payload/types'
import { getTranslation } from 'payload/utilities'
import { isFieldRTL } from '../shared'
@@ -22,6 +22,8 @@ export const TextInput: React.FC<{
rtl?: boolean
maxLength?: number
minLength?: number
validate?: Validate
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
}> = (props) => {
const {
path,
@@ -30,29 +32,30 @@ export const TextInput: React.FC<{
localized,
localizationConfig,
rtl,
// maxLength,
// minLength,
maxLength,
minLength,
validate,
required,
onKeyDown,
} = props
const { i18n } = useTranslation()
const locale = useLocale()
const {
// errorMessage,
setValue,
// showError,
value,
} = useField({
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, minLength, maxLength, required],
)
const field = useField({
path,
// validate: memoizedValidate,
validate: memoizedValidate,
})
// const memoizedValidate = useCallback(
// (value, options) => {
// return validate(value, { ...options, maxLength, minLength, required })
// },
// [validate, minLength, maxLength, required],
// )
const { setValue, value } = field
const renderRTL = isFieldRTL({
fieldLocalized: localized,
@@ -70,7 +73,7 @@ export const TextInput: React.FC<{
onChange={(e) => {
setValue(e.target.value)
}}
// onKeyDown={onKeyDown}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
// ref={inputRef}
type="text"

View File

@@ -2,7 +2,6 @@ import React from 'react'
import type { Props } from './types'
import { text } from 'payload/fields/validations'
import { fieldBaseClass } from '../shared'
import { TextInput } from './Input'
import FieldDescription from '../../FieldDescription'
@@ -28,7 +27,8 @@ const Text: React.FC<Props> = (props) => {
minLength,
path: pathFromProps,
required,
validate = text,
valid,
errorMessage,
} = props
const path = pathFromProps || name
@@ -38,12 +38,7 @@ const Text: React.FC<Props> = (props) => {
return (
<div
className={[
fieldBaseClass,
'text',
className,
// showError && 'error', readOnly && 'read-only'
]
className={[fieldBaseClass, 'text', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
@@ -51,10 +46,7 @@ const Text: React.FC<Props> = (props) => {
width,
}}
>
<ErrorComp
// message={errorMessage}
// showError={showError}
/>
<ErrorComp message={errorMessage} showError={!valid} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}

View File

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

View File

@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'
import type { Props } from './types'
import { textarea } from 'payload/fields/validations'
import { getTranslation } from 'payload/utilities'
import { useConfig } from '../../../providers/Config'
import { useLocale } from '../../../providers/Locale'
@@ -12,6 +11,7 @@ import useField from '../../useField'
import { isFieldRTL } from '../shared'
import TextareaInput from './Input'
import './index.scss'
import { Validate } from 'payload/types'
const Textarea: React.FC<Props> = (props) => {
const {
@@ -33,7 +33,7 @@ const Textarea: React.FC<Props> = (props) => {
minLength,
path: pathFromProps,
required,
validate = textarea,
validate,
} = props
const { i18n } = useTranslation()
@@ -49,11 +49,13 @@ const Textarea: React.FC<Props> = (props) => {
locale,
localizationConfig: localization || undefined,
})
const memoizedValidate = useCallback(
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function')
return validate(value, { ...options, maxLength, minLength, required })
},
[validate, required, maxLength, minLength],
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField({

View File

@@ -3,7 +3,6 @@ import React, { useCallback } from 'react'
import type { Props } from './types'
import { upload } from 'payload/fields/validations'
import { useConfig } from '../../../providers/Config'
import useField from '../../useField'
import UploadInput from './Input'
@@ -20,7 +19,6 @@ const Upload: React.FC<Props> = (props) => {
name,
admin: {
className,
condition,
description,
readOnly,
style,
@@ -33,14 +31,14 @@ const Upload: React.FC<Props> = (props) => {
path,
relationTo,
required,
validate = upload,
validate,
} = props
const collection = collections.find((coll) => coll.slug === relationTo)
const memoizedValidate = useCallback(
(value, options) => {
return validate(value, { ...options, required })
if (typeof validate === 'function') return validate(value, { ...options, required })
},
[validate, required],
)

View File

@@ -102,9 +102,9 @@ const useField = <T,>(options: Options): FieldType<T> => {
}
let errorMessage: string | undefined
let valid: boolean | string = false
let valid: boolean | string = prevValid.current
const validationResult =
const isValid =
typeof validate === 'function'
? await validate(valueToValidate, {
id,
@@ -117,11 +117,11 @@ const useField = <T,>(options: Options): FieldType<T> => {
})
: true
if (typeof validationResult === 'string') {
errorMessage = validationResult
if (typeof isValid === 'string') {
valid = false
} else {
valid = validationResult
errorMessage = isValid
} else if (typeof isValid === 'boolean') {
valid = isValid
errorMessage = undefined
}

View File

@@ -298,6 +298,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setUser,
token: tokenInMemory,
user,
setPermissions,
}}
>
{children}

View File

@@ -10,4 +10,5 @@ export type AuthContext<T = User> = {
setUser: (user: T) => void
token?: string
user?: T | null
setPermissions: (permissions: Permissions) => void
}

View File

@@ -2,7 +2,6 @@
import qs from 'qs'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import type { TypeWithTimestamps } from 'payload/dist/collections/config/types'
import type { PaginatedDocs } from 'payload/database'
@@ -13,6 +12,7 @@ import { useAuth } from '../Auth'
import { useConfig } from '../Config'
import { useLocale } from '../Locale'
import { usePreferences } from '../Preferences'
import { useParams } from 'next/navigation'
const Context = createContext({} as ContextType)
@@ -21,12 +21,14 @@ export const useDocumentInfo = (): ContextType => useContext(Context)
export const DocumentInfoProvider: React.FC<Props> = ({
id: idFromProps,
children,
collection,
global,
collectionSlug,
globalSlug,
idFromParams: getIDFromParams,
draftsEnabled,
versionsEnabled,
}) => {
const { id: idFromParams } = useParams<{ id: string }>()
const id = idFromProps || (getIDFromParams ? idFromParams : null)
const { id: idFromParams } = useParams()
const id = idFromProps || (getIDFromParams ? (idFromParams as string) : null)
const {
routes: { api },
@@ -46,14 +48,14 @@ export const DocumentInfoProvider: React.FC<Props> = ({
let pluralType: 'collections' | 'globals'
let preferencesKey: string
if (global) {
slug = global.slug
if (globalSlug) {
slug = globalSlug
pluralType = 'globals'
preferencesKey = `global-${slug}`
}
if (collection) {
slug = collection.slug
if (collectionSlug) {
slug = collectionSlug
pluralType = 'collections'
if (id) {
@@ -64,8 +66,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const getVersions = useCallback(async () => {
let versionFetchURL
let publishedFetchURL
let draftsEnabled = false
let shouldFetchVersions = false
let shouldFetchVersions = versionsEnabled
let unpublishedVersionJSON = null
let versionJSON = null
let shouldFetch = true
@@ -100,19 +101,13 @@ export const DocumentInfoProvider: React.FC<Props> = ({
},
}
if (global) {
draftsEnabled = Boolean(global?.versions?.drafts)
shouldFetchVersions = Boolean(global?.versions)
versionFetchURL = `${baseURL}/globals/${global.slug}/versions`
publishedFetchURL = `${baseURL}/globals/${global.slug}?${qs.stringify(
publishedVersionParams,
)}`
if (globalSlug) {
versionFetchURL = `${baseURL}/globals/${globalSlug}/versions`
publishedFetchURL = `${baseURL}/globals/${globalSlug}?${qs.stringify(publishedVersionParams)}`
}
if (collection) {
draftsEnabled = Boolean(collection?.versions?.drafts)
shouldFetchVersions = Boolean(collection?.versions)
versionFetchURL = `${baseURL}/${collection.slug}/versions`
if (collectionSlug) {
versionFetchURL = `${baseURL}/${collectionSlug}/versions`
publishedVersionParams.where.and.push({
id: {
@@ -120,7 +115,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
},
})
publishedFetchURL = `${baseURL}/${collection.slug}?${qs.stringify(publishedVersionParams)}`
publishedFetchURL = `${baseURL}/${collectionSlug}?${qs.stringify(publishedVersionParams)}`
if (!id) {
shouldFetch = false
@@ -144,7 +139,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
},
}).then((res) => res.json())
if (collection) {
if (collectionSlug) {
publishedJSON = publishedJSON?.docs?.[0]
}
}
@@ -194,7 +189,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
setVersions(versionJSON)
setUnpublishedVersions(unpublishedVersionJSON)
}
}, [i18n, global, collection, id, baseURL, code])
}, [i18n, globalSlug, collectionSlug, id, baseURL, code, versionsEnabled, draftsEnabled])
const getDocPermissions = React.useCallback(async () => {
let docAccessURL: string
@@ -219,7 +214,7 @@ export const DocumentInfoProvider: React.FC<Props> = ({
} else {
// fallback to permissions from the entity type
// (i.e. create has no id)
setDocPermissions(permissions[pluralType][slug])
setDocPermissions(permissions?.[pluralType]?.[slug])
}
}, [serverURL, api, pluralType, slug, id, permissions, i18n.language, code])
@@ -261,18 +256,19 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const value: ContextType = {
id,
collection,
collectionSlug,
docPermissions,
getDocPermissions,
getDocPreferences,
getVersions,
global,
globalSlug,
preferencesKey,
publishedDoc,
setDocFieldPreferences,
slug,
unpublishedVersions,
versions,
versionsEnabled,
draftsEnabled,
}
return <Context.Provider value={value}>{children}</Context.Provider>

View File

@@ -11,25 +11,29 @@ export type Version = TypeWithVersion<any>
export type DocumentPermissions = CollectionPermission | GlobalPermission | null
export type ContextType = {
collection?: SanitizedCollectionConfig
collectionSlug?: SanitizedCollectionConfig['slug']
docPermissions: DocumentPermissions
getDocPermissions: () => Promise<void>
getDocPreferences: () => Promise<{ [key: string]: unknown }>
getVersions: () => Promise<void>
global?: SanitizedGlobalConfig
globalSlug?: SanitizedGlobalConfig['slug']
id?: number | string
preferencesKey?: string
publishedDoc?: TypeWithID & TypeWithTimestamps & { _status?: string }
setDocFieldPreferences: (field: string, fieldPreferences: { [key: string]: unknown }) => void
slug?: string
unpublishedVersions?: PaginatedDocs<Version>
versions?: PaginatedDocs<Version>
versionsCount?: PaginatedDocs<Version>
draftsEnabled?: boolean
versionsEnabled?: boolean
}
export type Props = {
children?: React.ReactNode
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedGlobalConfig['slug']
id?: number | string
idFromParams?: boolean
draftsEnabled?: boolean
versionsEnabled?: boolean
}

View File

@@ -13,7 +13,7 @@ export const DefaultGlobalEdit: React.FC<
fieldTypes: FieldTypes
}
> = (props) => {
const { apiURL, data, fieldTypes, globalConfig, permissions, config } = props
const { apiURL, data, fieldTypes, globalConfig, permissions, config, user, state } = props
const { admin: { description } = {}, fields, label } = globalConfig
@@ -40,6 +40,9 @@ export const DefaultGlobalEdit: React.FC<
fields={fields}
hasSavePermission={hasSavePermission}
permissions={permissions}
user={user}
state={state}
data={data}
/>
</React.Fragment>
)

View File

@@ -20,7 +20,7 @@ export const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
// disableRoutes,
// fieldTypes,
globalConfig,
initialState,
state,
// onSave,
permissions,
} = props
@@ -93,7 +93,7 @@ export const DefaultGlobalView: React.FC<DefaultGlobalViewProps> = (props) => {
action={action}
className={`${baseClass}__form`}
disabled={!hasSavePermission}
initialState={initialState}
initialState={state}
method="POST"
// onSuccess={onSave}
>

View File

@@ -22,7 +22,7 @@ export type CollectionEditViewProps = BaseEditViewProps & {
export type GlobalEditViewProps = BaseEditViewProps & {
config: SanitizedConfig
globalConfig: SanitizedGlobalConfig
initialState?: Fields
state?: Fields
permissions: GlobalPermission | null
}