🚧 Under development — suggestions for new items are always welcome! ✨

Array Row Field

A simple and flexible helper component designed to enhance the readability of Array fields in Payload CMS admin UI by generating meaningful preview labels.

array row label demo

What it does

  • Generates preview labels for Array field rows.
  • Supports fallback logic between multiple fields.
  • Tries to resolve relationship fields and their values.

Install

pnpm dlx shadcn@latest add https://p.livog.com/r/array-row-label.json

Code

'use client'

import { useConfig, useFormFields, useRowLabel } from '@payloadcms/ui'
import { useMemo } from 'react'
import type { ClientComponentProps } from 'payload'
import { useArrayRowDocumentCache } from './use-array-row-document-cache'

const getParentPath = (path: string): string => path.replace(/\.[^.]*$/, '')

type ArrayRowLabelComponentProps = {
  fieldToUse: string | string[]
  fallbackLabel?: string
  prefix?: string
  suffix?: string
  count?: boolean
  prefixCount?: boolean
} & ClientComponentProps

export const ArrayRowLabel = ({ fieldToUse, prefix, suffix, count, prefixCount = false, ...props }: ArrayRowLabelComponentProps) => {
  // @ts-ignore
  const fallbackLabel = props?.field?.labels?.singular || 'Item'
  const { path: rowPath, rowNumber } = useRowLabel<Record<string, unknown>>()
  const formFields = useFormFields(([f]) => f)
  const { config: { routes: { api: apiRoute } } } = useConfig()
  const relativeFieldPaths = useMemo(() => (Array.isArray(fieldToUse) ? fieldToUse : [fieldToUse]), [fieldToUse])
  const apiRouteToUse = apiRoute

  const relationship = useMemo(() => {
    for (const relPath of relativeFieldPaths) {
      const parentPath = getParentPath(relPath)
      if (parentPath.split('.').length < 2) continue
      const maybeRelationship = formFields[`${rowPath}.${parentPath}`]?.value
      if (maybeRelationship && typeof maybeRelationship === 'object' && 'relationTo' in maybeRelationship && 'value' in maybeRelationship) {
        return {
          collection: maybeRelationship.relationTo as string,
          id: typeof maybeRelationship.value === 'string' ? maybeRelationship.value : String(maybeRelationship.value),
          leafKey: relPath.split('.').pop()!
        }
      }
    }
  }, [relativeFieldPaths, formFields, rowPath])

  const relatedDoc = useArrayRowDocumentCache(
    apiRouteToUse,
    relationship?.collection,
    relationship?.id,
    relationship ? [relationship.leafKey] : []
  )

  const label = useMemo(() => {
    for (const relPath of relativeFieldPaths) {
      const direct = formFields[`${rowPath}.${relPath}`]?.value
      if (typeof direct === 'string' && direct.trim()) return direct
    }

    if (relationship && relatedDoc) {
      const resolved = (relatedDoc as any)[relationship.leafKey]
      if (typeof resolved === 'string' && resolved.trim()) return resolved
    }
  }, [relativeFieldPaths, formFields, rowPath, relationship, relatedDoc])

  const displayLabel = (() => {
    const rowIndexLabel = String((rowNumber ?? 0) + 1).padStart(2, '0')
    const baseLabel = label ?? fallbackLabel

    const shouldShowCount = count === true || (count === undefined && !label)
    const counted = !shouldShowCount ? baseLabel : prefixCount ? `${rowIndexLabel} ${baseLabel}` : `${baseLabel} ${rowIndexLabel}`

    const withPrefix = prefix ? `${prefix}${counted}` : counted
    return suffix ? `${withPrefix}${suffix}` : withPrefix
  })()

  return (
    <span className="row-label" style={{ pointerEvents: 'none' }}>
      {displayLabel}
    </span>
  )
}

Usage

Import the component:

import { customRowLabel } from '@/payload/components/array-row-label';

Basic Usage

Use a single field for labeling:

fields: [
  {
    name: 'items',
    type: 'array',
    admin: {
      components: {
        RowLabel: customRowLabel({ fieldToUse: ['heading'] })
      }
    },
    fields: [ /* your fields here */ ]
  }
]

Advanced Usage with Fallback Logic

If the primary field isn't set, fallback to another field or resolve related documents:

fields: [
  {
    name: 'links',
    type: 'array',
    admin: {
      components: {
        RowLabel: customRowLabel({ fieldToUse: ['link.text', 'link.doc.title'] })
      }
    },
    fields: [ /* your fields here */ ]
  }
]

In this example, the label logic:

  • First attempts to use link.text.
  • Falls back to link.doc.title if link.text isn't available.
  • If link.doc.title is not set, it will attempt to resolve link.doc as a relationship field, and if found then use the .title from that document (one-level deep only).