Files
payloadcms/packages/ui/src/fields/Blocks/BlockRow.tsx
Jacob Fletcher 796df37461 fix(ui): awaits form state before rendering conditional fields (#9933)
When a condition exists on a field and it resolves to `false`, it
currently "blinks" in and out when rendered within an array or block
row. This is because when add rows to form state, we iterate over the
_fields_ of that row and render their respective components. Then when
conditions are checked for that field, we're expecting `passesCondition`
to be explicitly `false`, ultimately _rendering_ the field for a brief
moment before form state returns with evaluated conditions. The fix is
to set these fields into local form state with a new `isLoading: true`
prop, then display a loader within the row until form state returns with
its proper conditions.
2024-12-13 16:42:52 +00:00

181 lines
5.1 KiB
TypeScript

'use client'
import type { ClientBlock, ClientField, Labels, Row, SanitizedFieldPermissions } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import type { UseDraggableSortableReturn } from '../../elements/DraggableSortable/useDraggableSortable/types.js'
import type { RenderFieldsProps } from '../../forms/RenderFields/types.js'
import { Collapsible } from '../../elements/Collapsible/index.js'
import { ErrorPill } from '../../elements/ErrorPill/index.js'
import { Pill } from '../../elements/Pill/index.js'
import { ShimmerEffect } from '../../elements/ShimmerEffect/index.js'
import { useFormSubmitted } from '../../forms/Form/context.js'
import { RenderFields } from '../../forms/RenderFields/index.js'
import { useThrottledValue } from '../../hooks/useThrottledValue.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { RowActions } from './RowActions.js'
import { SectionTitle } from './SectionTitle/index.js'
const baseClass = 'blocks-field'
type BlocksFieldProps = {
addRow: (rowIndex: number, blockType: string) => Promise<void> | void
block: ClientBlock
blocks: ClientBlock[]
duplicateRow: (rowIndex: number) => void
errorCount: number
fields: ClientField[]
hasMaxRows?: boolean
isLoading?: boolean
isSortable?: boolean
Label?: React.ReactNode
labels: Labels
moveRow: (fromIndex: number, toIndex: number) => void
parentPath: string
path: string
permissions: SanitizedFieldPermissions
readOnly: boolean
removeRow: (rowIndex: number) => void
row: Row
rowCount: number
rowIndex: number
schemaPath: string
setCollapse: (id: string, collapsed: boolean) => void
} & UseDraggableSortableReturn
export const BlockRow: React.FC<BlocksFieldProps> = ({
addRow,
attributes,
block,
blocks,
duplicateRow,
errorCount,
fields,
hasMaxRows,
isLoading: isLoadingFromProps,
isSortable,
Label,
labels,
listeners,
moveRow,
parentPath,
path,
permissions,
readOnly,
removeRow,
row,
rowCount,
rowIndex,
schemaPath,
setCollapse,
setNodeRef,
transform,
}) => {
const isLoading = useThrottledValue(isLoadingFromProps, 500)
const { i18n } = useTranslation()
const hasSubmitted = useFormSubmitted()
const fieldHasErrors = hasSubmitted && errorCount > 0
const classNames = [
`${baseClass}__row`,
fieldHasErrors ? `${baseClass}__row--has-errors` : `${baseClass}__row--no-errors`,
]
.filter(Boolean)
.join(' ')
let blockPermissions: RenderFieldsProps['permissions'] = undefined
if (permissions === true) {
blockPermissions = true
} else {
const permissionsBlockSpecific = permissions?.blocks?.[block.slug]
if (permissionsBlockSpecific === true) {
blockPermissions = true
} else {
blockPermissions = permissionsBlockSpecific?.fields
}
}
return (
<div
id={`${parentPath?.split('.').join('-')}-row-${rowIndex}`}
key={`${parentPath}-row-${rowIndex}`}
ref={setNodeRef}
style={{
transform,
}}
>
<Collapsible
actions={
!readOnly ? (
<RowActions
addRow={addRow}
blocks={blocks}
blockType={row.blockType}
duplicateRow={duplicateRow}
fields={block.fields}
hasMaxRows={hasMaxRows}
isSortable={isSortable}
labels={labels}
moveRow={moveRow}
removeRow={removeRow}
rowCount={rowCount}
rowIndex={rowIndex}
/>
) : undefined
}
className={classNames}
collapsibleStyle={fieldHasErrors ? 'error' : 'default'}
dragHandleProps={
isSortable
? {
id: row.id,
attributes,
listeners,
}
: undefined
}
header={
Label || (
<div className={`${baseClass}__block-header`}>
<span className={`${baseClass}__block-number`}>
{String(rowIndex + 1).padStart(2, '0')}
</span>
<Pill
className={`${baseClass}__block-pill ${baseClass}__block-pill-${row.blockType}`}
pillStyle="white"
>
{getTranslation(block.labels.singular, i18n)}
</Pill>
<SectionTitle path={`${path}.blockName`} readOnly={readOnly} />
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div>
)
}
isCollapsed={row.collapsed}
key={row.id}
onToggle={(collapsed) => setCollapse(row.id, collapsed)}
>
{isLoading ? (
<ShimmerEffect />
) : (
<RenderFields
className={`${baseClass}__fields`}
fields={fields}
margins="small"
parentIndexPath=""
parentPath={path}
parentSchemaPath={schemaPath}
permissions={blockPermissions}
readOnly={readOnly}
/>
)}
</Collapsible>
</div>
)
}