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.
181 lines
5.1 KiB
TypeScript
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>
|
|
)
|
|
}
|