'use client' import type { BlocksFieldClientComponent, ClientBlock } from 'payload' import { getTranslation } from '@payloadcms/translations' import React, { Fragment, useCallback, useMemo } from 'react' import { toast } from 'sonner' import type { ClipboardPasteData } from '../../elements/ClipboardAction/types.js' import { Banner } from '../../elements/Banner/index.js' import { Button } from '../../elements/Button/index.js' import { clipboardCopy, clipboardPaste } from '../../elements/ClipboardAction/clipboardUtilities.js' import { ClipboardAction } from '../../elements/ClipboardAction/index.js' import { mergeFormStateFromClipboard, reduceFormStateByPath, } from '../../elements/ClipboardAction/mergeFormStateFromClipboard.js' import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js' import { DraggableSortable } from '../../elements/DraggableSortable/index.js' import { DrawerToggler } from '../../elements/Drawer/index.js' import { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js' import { ErrorPill } from '../../elements/ErrorPill/index.js' import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' import { useForm, useFormSubmitted } from '../../forms/Form/context.js' import { extractRowsAndCollapsedIDs, toggleAllRows } from '../../forms/Form/rowHelpers.js' import { NullifyLocaleField } from '../../forms/NullifyField/index.js' import { useField } from '../../forms/useField/index.js' import { withCondition } from '../../forms/withCondition/index.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { scrollToID } from '../../utilities/scrollToID.js' import './index.scss' import { FieldDescription } from '../FieldDescription/index.js' import { FieldError } from '../FieldError/index.js' import { FieldLabel } from '../FieldLabel/index.js' import { mergeFieldStyles } from '../mergeFieldStyles.js' import { fieldBaseClass } from '../shared/index.js' import { BlockRow } from './BlockRow.js' import { BlocksDrawer } from './BlocksDrawer/index.js' const baseClass = 'blocks-field' const BlocksFieldComponent: BlocksFieldClientComponent = (props) => { const { i18n, t } = useTranslation() const { field, field: { name, type, admin: { className, description, isSortable = true } = {}, blockReferences, blocks, label, labels: labelsFromProps, localized, maxRows, minRows: minRowsProp, required, }, path: pathFromProps, permissions, readOnly, schemaPath: schemaPathFromProps, validate, } = props const schemaPath = schemaPathFromProps ?? name const minRows = (minRowsProp ?? required) ? 1 : 0 const { setDocFieldPreferences } = useDocumentInfo() const { addFieldRow, dispatchFields, getFields, moveFieldRow, removeFieldRow, replaceState, setModified, } = useForm() const { code: locale } = useLocale() const { config: { localization }, config, } = useConfig() const drawerSlug = useDrawerSlug('blocks-drawer') const submitted = useFormSubmitted() const labels = { plural: t('fields:blocks'), singular: t('fields:block'), ...labelsFromProps, } const editingDefaultLocale = (() => { if (localization && localization.fallback) { const defaultLocale = localization.defaultLocale return locale === defaultLocale } return true })() const clientBlocks = useMemo(() => { if (!blockReferences) { return blocks } const resolvedBlocks: ClientBlock[] = [] for (const blockReference of blockReferences) { const block = typeof blockReference === 'string' ? config.blocksMap[blockReference] : blockReference if (block) { resolvedBlocks.push(block) } } return resolvedBlocks }, [blockReferences, blocks, config.blocksMap]) const memoizedValidate = useCallback( (value, options) => { // alternative locales can be null if (!editingDefaultLocale && value === null) { return true } if (typeof validate === 'function') { return validate(value, { ...options, maxRows, minRows, required }) } }, [maxRows, minRows, required, validate, editingDefaultLocale], ) const { customComponents: { AfterInput, BeforeInput, Description, Error, Label } = {}, disabled, errorPaths, path, rows = [], showError, valid, value, } = useField({ hasRows: true, potentiallyStalePath: pathFromProps, validate: memoizedValidate, }) const addRow = useCallback( (rowIndex: number, blockType: string) => { addFieldRow({ blockType, path, rowIndex, schemaPath, }) setTimeout(() => { scrollToID(`${path}-row-${rowIndex + 1}`) }, 0) }, [addFieldRow, path, schemaPath], ) const duplicateRow = useCallback( (rowIndex: number) => { dispatchFields({ type: 'DUPLICATE_ROW', path, rowIndex }) setModified(true) setTimeout(() => { scrollToID(`${path}-row-${rowIndex + 1}`) }, 0) }, [dispatchFields, path, setModified], ) const removeRow = useCallback( (rowIndex: number) => { removeFieldRow({ path, rowIndex, }) }, [path, removeFieldRow], ) const moveRow = useCallback( (moveFromIndex: number, moveToIndex: number) => { moveFieldRow({ moveFromIndex, moveToIndex, path }) }, [moveFieldRow, path], ) const toggleCollapseAll = useCallback( (collapsed: boolean) => { const { collapsedIDs, updatedRows } = toggleAllRows({ collapsed, rows, }) dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', path, updatedRows }) setDocFieldPreferences(path, { collapsed: collapsedIDs }) }, [dispatchFields, path, rows, setDocFieldPreferences], ) const setCollapse = useCallback( (rowID: string, collapsed: boolean) => { const { collapsedIDs, updatedRows } = extractRowsAndCollapsedIDs({ collapsed, rowID, rows, }) dispatchFields({ type: 'SET_ROW_COLLAPSED', path, updatedRows }) setDocFieldPreferences(path, { collapsed: collapsedIDs }) }, [dispatchFields, path, rows, setDocFieldPreferences], ) const copyRow = useCallback( (rowIndex: number) => { const clipboardResult = clipboardCopy({ type, blocks: clientBlocks, getDataToCopy: () => reduceFormStateByPath({ formState: { ...getFields() }, path, rowIndex, }), path, rowIndex, t, }) if (typeof clipboardResult === 'string') { toast.error(clipboardResult) } else { toast.success(t('general:copied')) } }, [clientBlocks, path, t, type, getFields], ) const pasteRow = useCallback( (rowIndex: number) => { const pasteArgs = { onPaste: (dataFromClipboard: ClipboardPasteData) => { const formState = { ...getFields() } const newState = mergeFormStateFromClipboard({ dataFromClipboard, formState, path, rowIndex, }) replaceState(newState) setModified(true) }, path, schemaBlocks: clientBlocks, t, } const clipboardResult = clipboardPaste(pasteArgs) if (typeof clipboardResult === 'string') { toast.error(clipboardResult) } }, [clientBlocks, getFields, path, replaceState, setModified, t], ) const pasteBlocks = useCallback( (dataFromClipboard: ClipboardPasteData) => { const formState = { ...getFields() } const newState = mergeFormStateFromClipboard({ dataFromClipboard, formState, path, }) replaceState(newState) setModified(true) }, [getFields, path, replaceState, setModified], ) const hasMaxRows = maxRows && rows.length >= maxRows const fieldErrorCount = errorPaths.length const fieldHasErrors = submitted && fieldErrorCount + (valid ? 0 : 1) > 0 const showMinRows = rows.length < minRows || (required && rows.length === 0) const showRequired = readOnly && rows.length === 0 const styles = useMemo(() => mergeFieldStyles(field), [field]) return (
{showError && ( } /> )}

} />

{fieldHasErrors && fieldErrorCount > 0 && ( )}
    {rows.length > 0 && (
  • )}
  • 0} allowPaste={!readOnly} blocks={clientBlocks} className={`${baseClass}__header-action`} disabled={disabled} getDataToCopy={() => reduceFormStateByPath({ formState: { ...getFields() }, path, }) } onPaste={pasteBlocks} path={path} type={type} />
} />
{BeforeInput} {(rows.length > 0 || (!valid && (showRequired || showMinRows))) && ( row.id)} onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)} > {rows.map((row, i) => { const { blockType, isLoading } = row const blockConfig: ClientBlock = config.blocksMap[blockType] ?? ((blockReferences ?? blocks).find( (block) => typeof block !== 'string' && block.slug === blockType, ) as ClientBlock) if (blockConfig) { const rowPath = `${path}.${i}` const rowErrorCount = errorPaths.filter((errorPath) => errorPath.startsWith(rowPath + '.'), ).length return ( {(draggableSortableItemProps) => ( )} ) } return null })} {!editingDefaultLocale && ( {showMinRows && ( {t('validation:requiresAtLeast', { count: minRows, label: getTranslation(minRows > 1 ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'), })} )} {showRequired && ( {t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })} )} )} )} {!hasMaxRows && ( )} {AfterInput}
) } export const BlocksField = withCondition(BlocksFieldComponent)