chore(next): ssr blocks field (#4942)

This commit is contained in:
Jacob Fletcher
2024-01-29 15:41:50 -05:00
committed by GitHub
parent a8aca3ad0f
commit e7c39cb53f
30 changed files with 300 additions and 243 deletions

View File

@@ -144,27 +144,6 @@ export const Pages: CollectionConfig = {
},
],
},
{
name: 'json',
label: 'JSON',
type: 'json',
required: true,
},
{
name: 'hidden',
label: 'Hidden',
type: 'text',
required: true,
admin: {
hidden: true,
},
},
{
name: 'code',
label: 'Code',
type: 'code',
required: true,
},
{
// TODO: fix this
// label: ({ data }) => `This is ${data?.title || 'Untitled'}`,
@@ -211,6 +190,51 @@ export const Pages: CollectionConfig = {
},
],
},
{
name: 'blocks',
label: 'Blocks',
type: 'blocks',
required: true,
minRows: 1,
maxRows: 2,
blocks: [
{
slug: 'text',
labels: {
singular: 'Text Block',
plural: 'Text Blocks',
},
fields: [
{
name: 'text',
label: 'Text',
type: 'text',
required: true,
admin: {
components: {
beforeInput: [BeforeInput],
},
},
},
],
},
{
slug: 'textarea',
labels: {
singular: 'Textarea Block',
plural: 'Textarea Blocks',
},
fields: [
{
name: 'textarea',
label: 'Textarea',
type: 'textarea',
required: true,
},
],
},
],
},
{
label: 'Tabs',
type: 'tabs',
@@ -262,5 +286,26 @@ export const Pages: CollectionConfig = {
},
],
},
{
name: 'json',
label: 'JSON',
type: 'json',
required: true,
},
{
name: 'code',
label: 'Code',
type: 'code',
required: true,
},
{
name: 'hidden',
label: 'Hidden',
type: 'text',
required: true,
admin: {
hidden: true,
},
},
],
}

View File

@@ -13,6 +13,14 @@ export const sanitizeField = (f) => {
field.fields = sanitizeFields(field.fields)
}
if ('blocks' in field) {
field.blocks = field.blocks.map((block) => {
const sanitized = { ...block }
sanitized.fields = sanitizeFields(sanitized.fields)
return sanitized
})
}
if ('tabs' in field) {
field.tabs = field.tabs.map((tab) => sanitizeField(tab))
}

View File

@@ -1,18 +0,0 @@
import type * as React from 'react'
import useThrottledEffect from '../../hooks/useThrottledEffect'
type Props = {
buildRowErrors: () => void
}
export const WatchFormErrors: React.FC<Props> = ({ buildRowErrors }) => {
useThrottledEffect(
() => {
buildRowErrors()
},
250,
[buildRowErrors],
)
return null
}

View File

@@ -26,11 +26,11 @@ type Args = {
state: FormState
t: TFunction
user: User
errorPaths: Set<string>
}
export const addFieldStatePromise = async ({
id,
data,
field,
fullData,
@@ -42,15 +42,16 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: parentErrorPaths,
}: Args): Promise<void> => {
if (fieldAffectsData(field)) {
const validate = operation === 'update' ? field.validate : undefined
const fieldState: FormField = {
initialValue: undefined,
passesCondition,
valid: true,
value: undefined,
errorPaths: new Set(),
}
const valueWithDefault = await getDefaultValue({
@@ -81,6 +82,7 @@ export const addFieldStatePromise = async ({
if (typeof validationResult === 'string') {
fieldState.errorMessage = validationResult
fieldState.valid = false
parentErrorPaths.add(`${path}${field.name}`)
} else {
fieldState.valid = true
}
@@ -88,6 +90,7 @@ export const addFieldStatePromise = async ({
switch (field.type) {
case 'array': {
const arrayValue = Array.isArray(valueWithDefault) ? valueWithDefault : []
const { promises, rowMetadata } = arrayValue.reduce(
(acc, row, i) => {
const rowPath = `${path}${field.name}.${i}.`
@@ -113,6 +116,7 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: fieldState.errorPaths,
}),
)
@@ -120,7 +124,7 @@ export const addFieldStatePromise = async ({
acc.rowMetadata.push({
id: row.id,
childErrorPaths: new Set(),
errorPaths: fieldState.errorPaths,
collapsed:
collapsedRowIDs === undefined
? field.admin.initCollapsed
@@ -201,6 +205,7 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: fieldState.errorPaths,
}),
)
@@ -209,7 +214,7 @@ export const addFieldStatePromise = async ({
acc.rowMetadata.push({
id: row.id,
blockType: row.blockType,
childErrorPaths: new Set(),
errorPaths: fieldState.errorPaths,
collapsed:
collapsedRowIDs === undefined
? field.admin.initCollapsed
@@ -262,6 +267,7 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: parentErrorPaths,
})
break
@@ -361,6 +367,7 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: parentErrorPaths,
})
} else if (field.type === 'tabs') {
const promises = field.tabs.map((tab) =>
@@ -377,6 +384,7 @@ export const addFieldStatePromise = async ({
state,
t,
user,
errorPaths: parentErrorPaths,
}),
)

View File

@@ -40,6 +40,7 @@ const buildStateFromSchema = async (args: Args): Promise<FormState> => {
state,
t,
user,
errorPaths: new Set(),
})
return state

View File

@@ -1,7 +1,7 @@
import type { TFunction } from '@payloadcms/translations'
import type { User } from 'payload/auth'
import type { Field as FieldSchema, SanitizedConfig, Data } from 'payload/types'
import type { Field as FieldSchema, Data } from 'payload/types'
import type { FormState } from '../types'
import { fieldIsPresentationalOnly } from 'payload/types'
import { addFieldStatePromise } from './addFieldStatePromise'
@@ -21,6 +21,7 @@ type Args = {
state: FormState
t: TFunction
user: User
errorPaths: Set<string>
}
export const iterateFields = async ({
@@ -36,6 +37,7 @@ export const iterateFields = async ({
state,
t,
user,
errorPaths,
}: Args): Promise<void> => {
const promises = []
@@ -63,9 +65,11 @@ export const iterateFields = async ({
state,
t,
user,
errorPaths,
}),
)
}
})
await Promise.all(promises)
}

View File

@@ -1,7 +1,7 @@
import ObjectID from 'bson-objectid'
import equal from 'deep-equal'
import type { FieldAction, FormState, FormField } from './types'
import type { FieldAction, FormState, FormField, Row } from './types'
import { deepCopyObject } from 'payload/utilities'
import { flattenRows, separateRows } from './rows'
@@ -102,17 +102,13 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
const withNewRow = [...(state[path]?.rows || [])]
withNewRow.splice(
rowIndex,
0,
// new row
{
id: new ObjectID().toHexString(),
blockType: blockType || undefined,
childErrorPaths: new Set(),
collapsed: false,
},
)
const newRow: Row = {
id: new ObjectID().toHexString(),
blockType: blockType || undefined,
collapsed: false,
}
withNewRow.splice(rowIndex, 0, newRow)
if (blockType) {
subFieldState.blockType = {
@@ -151,7 +147,6 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
rowsMetadata[rowIndex] = {
id: new ObjectID().toHexString(),
blockType: blockType || undefined,
childErrorPaths: new Set(),
collapsed: false,
}

View File

@@ -13,11 +13,10 @@ import type {
Context as FormContextType,
GetDataByPath,
Props,
Row,
SubmitOptions,
} from './types'
import { splitPathByArrayFields, setsAreEqual, wait, isNumber } from 'payload/utilities'
import { wait } from 'payload/utilities'
import { requests } from '../../utilities/api'
import useThrottledEffect from '../../hooks/useThrottledEffect'
import { useAuth } from '../../providers/Auth'
@@ -25,7 +24,6 @@ import { useConfig } from '../../providers/Config'
import { useDocumentInfo } from '../../providers/DocumentInfo'
import { useLocale } from '../../providers/Locale'
import { useOperation } from '../../providers/OperationProvider'
import { WatchFormErrors } from './WatchFormErrors'
import buildStateFromSchema from './buildStateFromSchema'
import {
FormContext,
@@ -94,62 +92,6 @@ const Form: React.FC<Props> = (props) => {
contextRef.current.fields = fields
// Build a current set of child errors for all rows in form state
const buildRowErrors = useCallback(() => {
const existingFieldRows: { [path: string]: Row[] } = {}
const newFieldRows: { [path: string]: Row[] } = {}
Object.entries(fields).forEach(([path, field]) => {
const pathSegments = splitPathByArrayFields(path)
for (let i = 0; i < pathSegments.length; i += 1) {
const fieldPath = pathSegments.slice(0, i + 1).join('.')
const formField = fields?.[fieldPath]
// Is this an array or blocks field?
if (Array.isArray(formField?.rows)) {
// Keep a reference to the existing row state
existingFieldRows[fieldPath] = formField.rows
// A new row state will be used to compare
// against the old state later,
// to see if we need to dispatch an update
if (!newFieldRows[fieldPath]) {
newFieldRows[fieldPath] = formField.rows.map((existingRow) => ({
...existingRow,
childErrorPaths: new Set(),
}))
}
const rowIndex = pathSegments[i + 1]
const childFieldPath = pathSegments.slice(i + 1).join('.')
if (field.valid === false && childFieldPath) {
newFieldRows[fieldPath][rowIndex].childErrorPaths.add(`${fieldPath}.${childFieldPath}`)
}
}
}
})
// Now loop over all fields with rows -
// if anything changed, dispatch an update for the field
// with the new row state
Object.entries(newFieldRows).forEach(([path, newRows]) => {
const stateMatches = newRows.every((newRow, i) => {
const existingRowErrorPaths = existingFieldRows[path][i]?.childErrorPaths
return setsAreEqual(newRow.childErrorPaths, existingRowErrorPaths)
})
if (!stateMatches) {
dispatchFields({
path,
rows: newRows,
type: 'UPDATE',
})
}
})
}, [fields, dispatchFields])
const validateForm = useCallback(async () => {
const validatedFieldState = {}
let isValid = true
@@ -229,7 +171,6 @@ const Form: React.FC<Props> = (props) => {
if (waitForAutocomplete) await wait(100)
const isValid = skipValidation ? true : await contextRef.current.validateForm()
contextRef.current.buildRowErrors()
if (!skipValidation) setSubmitted(true)
@@ -480,7 +421,6 @@ const Form: React.FC<Props> = (props) => {
contextRef.current.formRef = formRef
contextRef.current.reset = reset
contextRef.current.replaceState = replaceState
contextRef.current.buildRowErrors = buildRowErrors
contextRef.current.dispatchFields = dispatchFields
useEffect(() => {
@@ -553,7 +493,6 @@ const Form: React.FC<Props> = (props) => {
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
<FormFieldsContext.Provider value={fieldsReducer}>
<WatchFormErrors buildRowErrors={buildRowErrors} />
{children}
</FormFieldsContext.Provider>
</ModifiedContext.Provider>

View File

@@ -7,7 +7,7 @@ import type { Data } from 'payload/types'
export type Row = {
blockType?: string
childErrorPaths?: Set<string>
errorPaths?: Set<string>
collapsed?: boolean
id: string
}
@@ -21,6 +21,7 @@ export type FormField = {
valid: boolean
value: unknown
validate?: Validate
errorPaths?: Set<string>
}
export type FormState = {

View File

@@ -1,14 +1,13 @@
import React, { Fragment } from 'react'
import type { FieldPermissions } from 'payload/auth'
import type { Field, FieldWithPath, TabsField } from 'payload/types'
import type { BlockField, 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'
import { FormState } from '../../forms/Form/types'
export type ReducedField = {
type: keyof typeof fieldTypes
@@ -19,7 +18,13 @@ export type ReducedField = {
name: string
readOnly: boolean
isSidebar: boolean
/**
* On `array`, `blocks`, `group`, `collapsible`, and `tabs` fields only
*/
subfields?: ReducedField[]
/**
* On `tabs` fields only
*/
tabs?: ReducedTab[]
}
@@ -29,6 +34,14 @@ export type ReducedTab = {
subfields?: ReducedField[]
}
export type ReducedBlock = {
slug: string
subfields: ReducedField[]
labels: BlockField['labels']
imageAltText?: string
imageURL?: string
}
export const buildFieldMap = (args: {
fieldSchema: FieldWithPath[]
filter?: (field: Field) => boolean
@@ -176,11 +189,39 @@ export const buildFieldMap = (args: {
parentPath: path,
})
return {
const reducedTab: ReducedTab = {
name: 'name' in tab ? tab.name : undefined,
label: 'label' in tab ? tab.label : undefined,
label: tab.label,
subfields: tabFieldMap,
}
return reducedTab
})
// `blocks` fields require a field map of each of its block's nested fields
const blocks =
'blocks' in field &&
field.blocks &&
Array.isArray(field.blocks) &&
field.blocks.map((block) => {
const blockFieldMap = buildFieldMap({
fieldSchema: block.fields,
filter,
operation,
permissions,
readOnly: readOnlyOverride,
parentPath: path,
})
const reducedBlock: ReducedBlock = {
slug: block.slug,
subfields: blockFieldMap,
labels: block.labels,
imageAltText: block.imageAltText,
imageURL: block.imageURL,
}
return reducedBlock
})
// TODO: these types can get cleaned up
@@ -203,6 +244,7 @@ export const buildFieldMap = (args: {
max: 'max' in field ? field.max : undefined,
options: 'options' in field ? field.options : undefined,
tabs,
blocks,
}
const Field = <FieldComponent {...fieldComponentProps} />

View File

@@ -3,7 +3,7 @@ import { useTranslation } from '../../../providers/Translation'
import type { UseDraggableSortableReturn } from '../../../elements/DraggableSortable/useDraggableSortable/types'
import type { Row } from '../../Form/types'
import type { RowLabel as RowLabelType } from 'payload/types'
import type { ArrayField, RowLabel as RowLabelType } from 'payload/types'
import { ArrayAction } from '../../../elements/ArrayAction'
import { Collapsible } from '../../../elements/Collapsible'
@@ -16,6 +16,7 @@ import { buildFieldMap } from '../../RenderFields/buildFieldMap'
import { FieldPermissions } from 'payload/auth'
import './index.scss'
import { getTranslation } from '@payloadcms/translations'
const baseClass = 'array-field'
@@ -25,6 +26,7 @@ type ArrayRowProps = UseDraggableSortableReturn & {
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
labels: ArrayField['labels']
moveRow: (fromIndex: number, toIndex: number) => void
readOnly?: boolean
removeRow: (rowIndex: number) => void
@@ -46,6 +48,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
forceRender = false,
hasMaxRows,
indexPath,
labels,
listeners,
moveRow,
path: parentPath,
@@ -64,13 +67,13 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
const { i18n } = useTranslation()
const hasSubmitted = useFormSubmitted()
// const fallbackLabel = `${getTranslation(labels.singular, i18n)} ${String(rowIndex + 1).padStart(
// 2,
// '0',
// )}`
const fallbackLabel = `${getTranslation(labels.singular, i18n)} ${String(rowIndex + 1).padStart(
2,
'0',
)}`
const childErrorPathsCount = row.childErrorPaths?.size
const fieldHasErrors = hasSubmitted && childErrorPathsCount > 0
const errorCount = row.errorPaths?.size
const fieldHasErrors = errorCount > 0 && hasSubmitted
const classNames = [
`${baseClass}__row`,
@@ -117,7 +120,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
path={path}
rowNumber={rowIndex + 1}
/> */}
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage i18n={i18n} />}
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
}
onToggle={(collapsed) => setCollapse(row.id, collapsed)}

View File

@@ -62,9 +62,9 @@ const ArrayFieldType: React.FC<Props> = (props) => {
// Handle labeling for Arrays, Global Arrays, and Blocks
const getLabels = (p: Props) => {
if (p?.labels) return p.labels
if (p?.label) return { plural: undefined, singular: p.label }
return { plural: t('fields:rows'), singular: t('fields:row') }
if ('labels' in p && p?.labels) return p.labels
if ('label' in p && p?.label) return { plural: undefined, singular: p.label }
return { plural: t('general:rows'), singular: t('general:row') }
}
const labels = getLabels(props)
@@ -75,7 +75,9 @@ const ArrayFieldType: React.FC<Props> = (props) => {
if (!editingDefaultLocale && value === null) {
return true
}
return validate(value, { ...options, maxRows, minRows, required })
if (typeof validate === 'function') {
return validate(value, { ...options, maxRows, minRows, required })
}
},
[maxRows, minRows, required, validate, editingDefaultLocale],
)
@@ -154,7 +156,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const hasMaxRows = maxRows && rows.length >= maxRows
const fieldErrorCount =
rows.reduce((total, row) => total + (row?.childErrorPaths?.size || 0), 0) + (valid ? 0 : 1)
rows.reduce((total, row) => total + (row?.errorPaths?.size || 0), 0) + (valid ? 0 : 1)
const fieldHasErrors = submitted && fieldErrorCount > 0
@@ -227,6 +229,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
hasMaxRows={hasMaxRows}
indexPath={indexPath}
moveRow={moveRow}
labels={labels}
path={path}
permissions={permissions}
readOnly={readOnly}

View File

@@ -1,46 +1,52 @@
'use client'
import React from 'react'
import { useTranslation } from '../../../providers/Translation'
import type { Block } from 'payload/types'
import type { UseDraggableSortableReturn } from '../../../elements/DraggableSortable/useDraggableSortable/types'
import type { Row } from '../../Form/types'
import type { Props } from './types'
import { getTranslation } from '@payloadcms/translations'
import { Collapsible } from '../../../elements/Collapsible'
import { ErrorPill } from '../../../elements/ErrorPill'
import Pill from '../../../elements/Pill'
import { useFormSubmitted } from '../../Form/context'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields'
import HiddenInput from '../HiddenInput'
import { RowActions } from './RowActions'
import SectionTitle from './SectionTitle'
import { ReducedBlock } from '../../RenderFields/buildFieldMap'
import { FieldPathProvider } from '../../FieldPathProvider'
import { Labels } from 'payload/types'
import { FieldPermissions } from 'payload/auth'
const baseClass = 'blocks-field'
type BlockFieldProps = UseDraggableSortableReturn &
Pick<Props, 'blocks' | 'fieldTypes' | 'indexPath' | 'labels' | 'path' | 'permissions'> & {
addRow: (rowIndex: number, blockType: string) => void
blockToRender: Block
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly: boolean
removeRow: (rowIndex: number) => void
row: Row
rowCount: number
rowIndex: number
setCollapse: (id: string, collapsed: boolean) => void
}
type BlockFieldProps = UseDraggableSortableReturn & {
addRow: (rowIndex: number, blockType: string) => void
blocks: ReducedBlock[]
block: ReducedBlock
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly: boolean
removeRow: (rowIndex: number) => void
row: Row
rowCount: number
rowIndex: number
setCollapse: (id: string, collapsed: boolean) => void
indexPath: string
path: string
labels: Labels
permissions: FieldPermissions
}
export const BlockRow: React.FC<BlockFieldProps> = ({
addRow,
attributes,
blockToRender,
blocks,
block,
duplicateRow,
fieldTypes,
forceRender,
hasMaxRows,
indexPath,
@@ -62,8 +68,8 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
const { i18n } = useTranslation()
const hasSubmitted = useFormSubmitted()
const childErrorPathsCount = row.childErrorPaths?.size
const fieldHasErrors = hasSubmitted && childErrorPathsCount > 0
const errorCount = row.errorPaths?.size
const fieldHasErrors = errorCount > 0 && hasSubmitted
const classNames = [
`${baseClass}__row`,
@@ -87,7 +93,6 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
<RowActions
addRow={addRow}
blockType={row.blockType}
blocks={blocks}
duplicateRow={duplicateRow}
hasMaxRows={hasMaxRows}
labels={labels}
@@ -95,6 +100,8 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
removeRow={removeRow}
rowCount={rowCount}
rowIndex={rowIndex}
blocks={blocks}
fieldMap={block.subfields}
/>
) : undefined
}
@@ -115,30 +122,27 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
pillStyle="white"
>
{getTranslation(blockToRender.labels.singular, i18n)}
{getTranslation(block.labels.singular, i18n)}
</Pill>
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
{fieldHasErrors && <ErrorPill count={childErrorPathsCount} withMessage />}
{fieldHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
}
key={row.id}
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
>
<HiddenInput name={`${path}.id`} value={row.id} />
[RenderFields]
{/* <RenderFields
className={`${baseClass}__fields`}
fieldSchema={blockToRender.fields.map((field) => ({
...field,
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.blocks?.[row.blockType]?.fields}
readOnly={readOnly}
/> */}
<FieldPathProvider path={path}>
<RenderFields
className={`${baseClass}__fields`}
fieldMap={block.subfields}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.blocks?.[row.blockType]?.fields}
readOnly={readOnly}
/>
</FieldPathProvider>
</Collapsible>
</div>
)

View File

@@ -3,7 +3,6 @@ import { useModal } from '@faceless-ui/modal'
import React, { useEffect, useState } from 'react'
import { useTranslation } from '../../../../providers/Translation'
import type { Block } from 'payload/types'
import type { Props } from './types'
import { getTranslation } from '@payloadcms/translations'
@@ -12,11 +11,13 @@ import { Drawer } from '../../../../elements/Drawer'
import { ThumbnailCard } from '../../../../elements/ThumbnailCard'
import DefaultBlockImage from '../../../../graphics/DefaultBlockImage'
import BlockSearch from './BlockSearch'
import { ReducedBlock } from '../../../RenderFields/buildFieldMap'
import './index.scss'
const baseClass = 'blocks-drawer'
const getBlockLabel = (block: Block, i18n: I18n) => {
const getBlockLabel = (block: ReducedBlock, i18n: I18n) => {
if (typeof block.labels.singular === 'string') return block.labels.singular.toLowerCase()
if (typeof block.labels.singular === 'object') {
return getTranslation(block.labels.singular, i18n).toLowerCase()
@@ -41,7 +42,7 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
useEffect(() => {
const searchTermToUse = searchTerm.toLowerCase()
const matchingBlocks = blocks.reduce((matchedBlocks, block) => {
const matchingBlocks = blocks?.reduce((matchedBlocks, block) => {
const blockLabel = getBlockLabel(block, i18n)
if (blockLabel.includes(searchTermToUse)) matchedBlocks.push(block)
return matchedBlocks
@@ -65,7 +66,7 @@ export const BlocksDrawer: React.FC<Props> = (props) => {
<li className={`${baseClass}__block`} key={index}>
<ThumbnailCard
alignLabel="center"
label={getTranslation(blockLabels.singular, i18n)}
label={getTranslation(blockLabels?.singular, i18n)}
onClick={() => {
addRow(addRowIndex, slug)
closeModal(drawerSlug)

View File

@@ -1,9 +1,10 @@
import type { Block, Labels } from 'payload/types'
import type { Labels } from 'payload/types'
import { ReducedBlock } from '../../../RenderFields/buildFieldMap'
export type Props = {
addRow: (index: number, blockType?: string) => void
addRowIndex: number
blocks: Block[]
blocks: ReducedBlock[]
drawerSlug: string
labels: Labels
}

View File

@@ -1,16 +1,18 @@
'use client'
import { useModal } from '@faceless-ui/modal'
import React from 'react'
import type { Block, Labels } from 'payload/types'
import type { Labels, RowField } from 'payload/types'
import { ArrayAction } from '../../../elements/ArrayAction'
import { useDrawerSlug } from '../../../elements/Drawer/useDrawerSlug'
import { BlocksDrawer } from './BlocksDrawer'
import { ReducedBlock, buildFieldMap } from '../../RenderFields/buildFieldMap'
export const RowActions: React.FC<{
addRow: (rowIndex: number, blockType: string) => void
blockType: string
blocks: Block[]
fieldMap: ReturnType<typeof buildFieldMap>
duplicateRow: (rowIndex: number, blockType: string) => void
hasMaxRows?: boolean
labels: Labels
@@ -18,11 +20,11 @@ export const RowActions: React.FC<{
removeRow: (rowIndex: number) => void
rowCount: number
rowIndex: number
blocks: ReducedBlock[]
}> = (props) => {
const {
addRow,
blockType,
blocks,
duplicateRow,
hasMaxRows,
labels,
@@ -30,6 +32,7 @@ export const RowActions: React.FC<{
removeRow,
rowCount,
rowIndex,
blocks,
} = props
const { closeModal, openModal } = useModal()

View File

@@ -16,8 +16,6 @@ import { ErrorPill } from '../../../elements/ErrorPill'
import { useConfig } from '../../../providers/Config'
import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { useLocale } from '../../../providers/Locale'
import Error from '../../Error'
import FieldDescription from '../../FieldDescription'
import { useForm, useFormSubmitted } from '../../Form/context'
import { NullifyLocaleField } from '../../NullifyField'
import useField from '../../useField'
@@ -33,26 +31,27 @@ const BlocksField: React.FC<Props> = (props) => {
const {
name,
admin: { className, description, readOnly },
blocks,
fieldTypes,
className,
readOnly,
forceRender = false,
indexPath,
label,
labels: labelsFromProps,
localized,
maxRows,
minRows,
Description,
Error,
Label,
path: pathFromProps,
permissions,
required,
validate,
} = props
const path = pathFromProps || name
const minRows = 'minRows' in props ? props.minRows : 0
const maxRows = 'maxRows' in props ? props.maxRows : undefined
const blocks = 'blocks' in props ? props.blocks : undefined
const labelsFromProps = 'labels' in props ? props.labels : undefined
const { setDocFieldPreferences } = useDocumentInfo()
const { addFieldRow, dispatchFields, removeFieldRow, setModified } = useForm()
const { dispatchFields, setModified } = useForm()
const { code: locale } = useLocale()
const { localization } = useConfig()
const drawerSlug = useDrawerSlug('blocks-drawer')
@@ -79,39 +78,41 @@ const BlocksField: React.FC<Props> = (props) => {
if (!editingDefaultLocale && value === null) {
return true
}
return validate(value, { ...options, maxRows, minRows, required })
if (typeof validate === 'function') {
return validate(value, { ...options, maxRows, minRows, required })
}
},
[maxRows, minRows, required, validate, editingDefaultLocale],
)
const {
errorMessage,
rows = [],
showError,
valid,
value,
path,
} = useField<number>({
hasRows: true,
path,
path: pathFromProps || name,
validate: memoizedValidate,
})
const addRow = useCallback(
async (rowIndex: number, blockType: string) => {
await addFieldRow({
data: {
blockType,
},
dispatchFields({
blockType,
path,
rowIndex,
type: 'ADD_ROW',
})
setModified(true)
setTimeout(() => {
scrollToID(`${path}-row-${rowIndex + 1}`)
}, 0)
},
[addFieldRow, path, setModified],
[path, setModified],
)
const duplicateRow = useCallback(
@@ -128,10 +129,15 @@ const BlocksField: React.FC<Props> = (props) => {
const removeRow = useCallback(
(rowIndex: number) => {
removeFieldRow({ path, rowIndex })
dispatchFields({
path,
rowIndex,
type: 'REMOVE_ROW',
})
setModified(true)
},
[path, removeFieldRow, setModified],
[path, dispatchFields, setModified],
)
const moveRow = useCallback(
@@ -158,7 +164,7 @@ const BlocksField: React.FC<Props> = (props) => {
const hasMaxRows = maxRows && rows.length >= maxRows
const fieldErrorCount = rows.reduce((total, row) => total + (row?.childErrorPaths?.size || 0), 0)
const fieldErrorCount = rows.reduce((total, row) => total + (row?.errorPaths?.size || 0), 0)
const fieldHasErrors = submitted && fieldErrorCount + (valid ? 0 : 1) > 0
const showMinRows = rows.length < minRows || (required && rows.length === 0)
@@ -176,18 +182,13 @@ const BlocksField: React.FC<Props> = (props) => {
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
>
{showError && (
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
</div>
)}
{showError && <div className={`${baseClass}__error-wrap`}>{Error}</div>}
<header className={`${baseClass}__header`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__heading-with-error`}>
<h3>{getTranslation(label || name, i18n)}</h3>
<h3>{Label}</h3>
{fieldHasErrors && fieldErrorCount > 0 && (
<ErrorPill count={fieldErrorCount} withMessage />
<ErrorPill count={fieldErrorCount} withMessage i18n={i18n} />
)}
</div>
{rows.length > 0 && (
@@ -213,7 +214,7 @@ const BlocksField: React.FC<Props> = (props) => {
</ul>
)}
</div>
<FieldDescription description={description} path={path} value={value} />
{Description}
</header>
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
@@ -232,11 +233,10 @@ const BlocksField: React.FC<Props> = (props) => {
{(draggableSortableItemProps) => (
<BlockRow
{...draggableSortableItemProps}
addRow={addRow}
blockToRender={blockToRender}
blocks={blocks}
addRow={addRow}
block={blockToRender}
duplicateRow={duplicateRow}
fieldTypes={fieldTypes}
forceRender={forceRender}
hasMaxRows={hasMaxRows}
indexPath={indexPath}

View File

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

View File

@@ -39,7 +39,9 @@ const Checkbox: React.FC<Props> = (props) => {
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -6,7 +6,6 @@ import type { Props } from './types'
import { Collapsible } from '../../../elements/Collapsible'
import { useDocumentInfo } from '../../../providers/DocumentInfo'
import { usePreferences } from '../../../providers/Preferences'
import { useFormSubmitted } from '../../Form/context'
import RenderFields from '../../RenderFields'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
@@ -42,8 +41,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
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 fieldHasErrors = errorCount > 0
const onToggle = useCallback(
async (newCollapsedState: boolean) => {

View File

@@ -38,7 +38,9 @@ const DateTime: React.FC<Props> = (props) => {
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -34,7 +34,9 @@ export const Email: React.FC<Props> = (props) => {
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -8,7 +8,6 @@ import { withCondition } from '../../withCondition'
import { useCollapsible } from '../../../elements/Collapsible/provider'
import { useRow } from '../Row/provider'
import { useTabs } from '../Tabs/provider'
import { useFormSubmitted } from '../../../forms/Form/context'
import { fieldBaseClass } from '../shared'
import { useFieldPath } from '../../FieldPathProvider'
import { WatchChildErrors } from '../../WatchChildErrors'
@@ -25,13 +24,12 @@ const Group: React.FC<Props> = (props) => {
const path = useFieldPath()
const { i18n } = useTranslation()
const hasSubmitted = useFormSubmitted()
const isWithinCollapsible = useCollapsible()
const isWithinGroup = useGroup()
const isWithinRow = useRow()
const isWithinTab = useTabs()
const [errorCount, setErrorCount] = React.useState(undefined)
const fieldHasErrors = errorCount > 0 && hasSubmitted
const fieldHasErrors = errorCount > 0
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)

View File

@@ -41,7 +41,9 @@ const NumberField: React.FC<Props> = (props) => {
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, max, min, required })
if (typeof validate === 'function') {
return validate(value, { ...options, max, min, required })
}
},
[validate, min, max, required],
)

View File

@@ -26,7 +26,9 @@ export const Password: React.FC<Props> = (props) => {
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -38,7 +38,9 @@ const PointField: React.FC<Props> = (props) => {
const memoizedValidate: Validate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -3,7 +3,7 @@ import { getTranslation } from '@payloadcms/translations'
import React, { useState } from 'react'
import { ErrorPill } from '../../../../elements/ErrorPill'
import { WatchChildErrors } from '../../../WatchChildErrors'
import { useFormSubmitted, useTranslation } from '../../../..'
import { useTranslation } from '../../../..'
import { ReducedTab } from '../../../RenderFields/buildFieldMap'
import './index.scss'
@@ -23,10 +23,9 @@ export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsAc
const { i18n } = useTranslation()
const [errorCount, setErrorCount] = useState(undefined)
const hasName = 'name' in tab
const hasSubmitted = useFormSubmitted()
const path = `${parentPath ? `${parentPath}.` : ''}${'name' in tab ? name : ''}`
const fieldHasErrors = errorCount > 0 && hasSubmitted
const fieldHasErrors = errorCount > 0
return (
<React.Fragment>

View File

@@ -78,10 +78,10 @@ const Textarea: React.FC<Props> = (props) => {
>
{Error}
{Label}
{BeforeInput}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
{BeforeInput}
<textarea
className="textarea-element"
data-rtl={isRTL}
@@ -93,9 +93,9 @@ const Textarea: React.FC<Props> = (props) => {
rows={rows}
value={value || ''}
/>
{AfterInput}
</div>
</label>
{AfterInput}
{Description}
</div>
)

View File

@@ -38,7 +38,9 @@ const Upload: React.FC<Props> = (props) => {
const memoizedValidate = useCallback(
(value, options) => {
if (typeof validate === 'function') return validate(value, { ...options, required })
if (typeof validate === 'function') {
return validate(value, { ...options, required })
}
},
[validate, required],
)

View File

@@ -2,6 +2,7 @@ import type { Locale, SanitizedLocalizationConfig } from 'payload/config'
import { User } from 'payload/auth'
import {
ArrayField,
BlockField,
CodeField,
DateField,
DocumentPreferences,
@@ -9,7 +10,7 @@ import {
RowLabel,
Validate,
} from 'payload/types'
import { ReducedTab, buildFieldMap } from '../RenderFields/buildFieldMap'
import { ReducedBlock, ReducedTab, buildFieldMap } from '../RenderFields/buildFieldMap'
import { Option } from 'payload/types'
import { FormState } from '../..'
@@ -86,6 +87,15 @@ export type FormFieldBase = {
// For `array` fields
minRows?: ArrayField['minRows']
maxRows?: ArrayField['maxRows']
labels?: ArrayField['labels']
}
| {
// For `blocks` fields
slug?: string
minRows?: BlockField['minRows']
maxRows?: BlockField['maxRows']
labels?: BlockField['labels']
blocks?: ReducedBlock[]
}
)