'use client' import type { ArrayFieldClientComponent, ArrayFieldClientProps, ArrayField as ArrayFieldType, } from 'payload' import { getTranslation } from '@payloadcms/translations' import React, { useCallback } from 'react' import { Banner } from '../../elements/Banner/index.js' import { Button } from '../../elements/Button/index.js' import { DraggableSortableItem } from '../../elements/DraggableSortable/DraggableSortableItem/index.js' import { DraggableSortable } from '../../elements/DraggableSortable/index.js' import { ErrorPill } from '../../elements/ErrorPill/index.js' import { useFieldProps } from '../../forms/FieldPropsProvider/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 { FieldDescription } from '../FieldDescription/index.js' import { FieldError } from '../FieldError/index.js' import { FieldLabel } from '../FieldLabel/index.js' import { fieldBaseClass } from '../shared/index.js' import { ArrayRow } from './ArrayRow.js' import './index.scss' const baseClass = 'array-field' export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { const { descriptionProps, errorProps, field, field: { name, _path: pathFromProps, admin: { className, components: { RowLabel }, description, isSortable = true, readOnly: readOnlyFromAdmin, } = {}, fields, label, localized, maxRows, minRows: minRowsProp, required, }, forceRender = false, labelProps, readOnly: readOnlyFromTopLevelProps, validate, } = props const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin const { indexPath, path: pathFromContext, permissions, readOnly: readOnlyFromContext, } = useFieldProps() const minRows = (minRowsProp ?? required) ? 1 : 0 const { setDocFieldPreferences } = useDocumentInfo() const { addFieldRow, dispatchFields, setModified } = useForm() const submitted = useFormSubmitted() const { code: locale } = useLocale() const { i18n, t } = useTranslation() const { config: { localization }, } = useConfig() const editingDefaultLocale = (() => { if (localization && localization.fallback) { const defaultLocale = localization.defaultLocale || 'en' return locale === defaultLocale } return true })() // Handle labeling for Arrays, Global Arrays, and Blocks const getLabels = (p: ArrayFieldClientProps): Partial => { if ('labels' in p && p?.labels) { return p.labels } if ('labels' in p.field && p.field.labels) { return { plural: p.field.labels?.plural, singular: p.field.labels?.singular } } if ('label' in p.field && p.field.label) { return { plural: undefined, singular: p.field.label } } return { plural: t('general:rows'), singular: t('general:row') } } const labels = getLabels(props) 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 { errorPaths, formInitializing, formProcessing, path, rows = [], schemaPath, showError, valid, value, } = useField({ hasRows: true, path: pathFromContext ?? pathFromProps ?? name, validate: memoizedValidate, }) const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing const addRow = useCallback( async (rowIndex: number): Promise => { await addFieldRow({ path, rowIndex, schemaPath }) setModified(true) setTimeout(() => { scrollToID(`${path}-row-${rowIndex + 1}`) }, 0) }, [addFieldRow, path, setModified, schemaPath], ) const duplicateRow = useCallback( (rowIndex: number) => { dispatchFields({ type: 'DUPLICATE_ROW', path, rowIndex }) setModified(true) setTimeout(() => { scrollToID(`${path}-row-${rowIndex}`) }, 0) }, [dispatchFields, path, setModified], ) const removeRow = useCallback( (rowIndex: number) => { dispatchFields({ type: 'REMOVE_ROW', path, rowIndex }) setModified(true) }, [dispatchFields, path, setModified], ) const moveRow = useCallback( (moveFromIndex: number, moveToIndex: number) => { dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path }) setModified(true) }, [dispatchFields, path, setModified], ) 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 hasMaxRows = maxRows && rows.length >= maxRows const fieldErrorCount = errorPaths.length const fieldHasErrors = submitted && errorPaths.length > 0 const showRequired = disabled && rows.length === 0 const showMinRows = rows.length < minRows || (required && rows.length === 0) return (
{showError && ( )}

{fieldHasErrors && fieldErrorCount > 0 && ( )}
{rows.length > 0 && (
)}
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && ( row.id)} onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)} > {rows.map((row, i) => { const rowErrorCount = errorPaths?.filter((errorPath) => errorPath.startsWith(`${path}.${i}.`), ).length return ( {(draggableSortableItemProps) => ( )} ) })} {!valid && ( {showRequired && ( {t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })} )} {showMinRows && ( {t('validation:requiresAtLeast', { count: minRows, label: getTranslation(minRows > 1 ? labels.plural : labels.singular, i18n) || t(minRows > 1 ? 'general:rows' : 'general:row'), })} )} )} )} {!disabled && !hasMaxRows && ( )}
) } export const ArrayField = withCondition(ArrayFieldComponent)