fix(ui): properly formats collapsible field IDs (#5435)

* test: passing collapsible fields test suite

* chore: passes indexPath into ArrayRow & updates path in collapsible field

* fix: collapsible paths and indexPath prop types

* chore: improves path and schemaPath syntax

* leftover

* chore: updates selectors in collapsibles tests

* chore: updates selector in live-preview test suite

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
This commit is contained in:
Patrik
2024-03-25 14:16:35 -04:00
committed by GitHub
parent 76e9bd8ad6
commit a9b46a4d63
14 changed files with 54 additions and 23 deletions

View File

@@ -46,6 +46,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
fieldMap, fieldMap,
forceRender = false, forceRender = false,
hasMaxRows, hasMaxRows,
indexPath,
labels, labels,
listeners, listeners,
moveRow, moveRow,
@@ -128,6 +129,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldMap={fieldMap} fieldMap={fieldMap}
forceRender={forceRender} forceRender={forceRender}
indexPath={indexPath}
margins="small" margins="small"
path={path} path={path}
permissions={permissions?.fields} permissions={permissions?.fields}

View File

@@ -36,7 +36,6 @@ export type ArrayFieldProps = FormFieldBase & {
CustomRowLabel?: React.ReactNode CustomRowLabel?: React.ReactNode
fieldMap: FieldMap fieldMap: FieldMap
forceRender?: boolean forceRender?: boolean
indexPath: string
label?: FieldBase['label'] label?: FieldBase['label']
labels?: ArrayFieldType['labels'] labels?: ArrayFieldType['labels']
maxRows?: ArrayFieldType['maxRows'] maxRows?: ArrayFieldType['maxRows']
@@ -58,7 +57,6 @@ export const ArrayField: React.FC<ArrayFieldProps> = (props) => {
errorProps, errorProps,
fieldMap, fieldMap,
forceRender = false, forceRender = false,
indexPath,
labelProps, labelProps,
localized, localized,
maxRows, maxRows,
@@ -70,6 +68,8 @@ export const ArrayField: React.FC<ArrayFieldProps> = (props) => {
validate, validate,
} = props } = props
const { indexPath } = useFieldProps()
const { setDocFieldPreferences } = useDocumentInfo() const { setDocFieldPreferences } = useDocumentInfo()
const { addFieldRow, dispatchFields, setModified } = useForm() const { addFieldRow, dispatchFields, setModified } = useForm()
const submitted = useFormSubmitted() const submitted = useFormSubmitted()

View File

@@ -38,7 +38,6 @@ import type { FormFieldBase } from '../shared/index.js'
export type BlocksFieldProps = FormFieldBase & { export type BlocksFieldProps = FormFieldBase & {
blocks?: ReducedBlock[] blocks?: ReducedBlock[]
forceRender?: boolean forceRender?: boolean
indexPath: string
label?: FieldBase['label'] label?: FieldBase['label']
labels?: BlockField['labels'] labels?: BlockField['labels']
maxRows?: number maxRows?: number
@@ -62,7 +61,6 @@ export const BlocksField: React.FC<BlocksFieldProps> = (props) => {
descriptionProps, descriptionProps,
errorProps, errorProps,
forceRender = false, forceRender = false,
indexPath,
labelProps, labelProps,
labels: labelsFromProps, labels: labelsFromProps,
localized, localized,
@@ -74,6 +72,8 @@ export const BlocksField: React.FC<BlocksFieldProps> = (props) => {
validate, validate,
} = props } = props
const { indexPath } = useFieldProps()
const { setDocFieldPreferences } = useDocumentInfo() const { setDocFieldPreferences } = useDocumentInfo()
const { addFieldRow, dispatchFields, setModified } = useForm() const { addFieldRow, dispatchFields, setModified } = useForm()
const { code: locale } = useLocale() const { code: locale } = useLocale()

View File

@@ -27,7 +27,6 @@ import type { FormFieldBase } from '../shared/index.js'
export type CollapsibleFieldProps = FormFieldBase & { export type CollapsibleFieldProps = FormFieldBase & {
fieldMap: FieldMap fieldMap: FieldMap
indexPath: string
initCollapsed?: boolean initCollapsed?: boolean
permissions: FieldPermissions permissions: FieldPermissions
width?: string width?: string
@@ -45,14 +44,20 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
path: pathFromProps, path: pathFromProps,
} = props } = props
const { path: pathFromContext, readOnly, schemaPath, siblingPermissions } = useFieldProps() const {
const path = pathFromProps || pathFromContext indexPath,
path: pathFromContext,
readOnly,
schemaPath,
siblingPermissions,
} = useFieldProps()
const path = pathFromContext || pathFromProps
const { i18n } = useTranslation() const { i18n } = useTranslation()
const { getPreference, setPreference } = usePreferences() const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo() const { preferencesKey } = useDocumentInfo()
const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>() const [collapsedOnMount, setCollapsedOnMount] = useState<boolean>()
const fieldPreferencesKey = `collapsible-${path.replace(/\./g, '__')}` const fieldPreferencesKey = `collapsible-${indexPath.replace(/\./g, '__')}`
const [errorCount, setErrorCount] = useState(0) const [errorCount, setErrorCount] = useState(0)
const fieldHasErrors = errorCount > 0 const fieldHasErrors = errorCount > 0
@@ -138,6 +143,7 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
<RenderFields <RenderFields
fieldMap={fieldMap} fieldMap={fieldMap}
forceRender forceRender
indexPath={indexPath}
margins="small" margins="small"
path={path} path={path}
permissions={siblingPermissions} permissions={siblingPermissions}

View File

@@ -28,7 +28,6 @@ export type GroupFieldProps = FormFieldBase & {
fieldMap: FieldMap fieldMap: FieldMap
forceRender?: boolean forceRender?: boolean
hideGutter?: boolean hideGutter?: boolean
indexPath: string
label?: FieldBase['label'] label?: FieldBase['label']
name?: string name?: string
permissions: FieldPermissions permissions: FieldPermissions

View File

@@ -17,7 +17,7 @@ const baseClass = 'row'
const RowField: React.FC<RowFieldProps> = (props) => { const RowField: React.FC<RowFieldProps> = (props) => {
const { className, fieldMap, forceRender = false } = props const { className, fieldMap, forceRender = false } = props
const { path, readOnly, schemaPath, siblingPermissions } = useFieldProps() const { indexPath, path, readOnly, schemaPath, siblingPermissions } = useFieldProps()
return ( return (
<RowProvider> <RowProvider>
@@ -27,6 +27,7 @@ const RowField: React.FC<RowFieldProps> = (props) => {
className={`${baseClass}__fields`} className={`${baseClass}__fields`}
fieldMap={fieldMap} fieldMap={fieldMap}
forceRender={forceRender} forceRender={forceRender}
indexPath={indexPath}
margins={false} margins={false}
permissions={siblingPermissions} permissions={siblingPermissions}
/> />

View File

@@ -28,7 +28,6 @@ export { TabsProvider }
export type TabsFieldProps = FormFieldBase & { export type TabsFieldProps = FormFieldBase & {
forceRender?: boolean forceRender?: boolean
indexPath: string
name?: string name?: string
path?: string path?: string
permissions: FieldPermissions permissions: FieldPermissions
@@ -43,13 +42,12 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
className, className,
descriptionProps, descriptionProps,
forceRender = false, forceRender = false,
indexPath,
path: pathFromProps, path: pathFromProps,
readOnly, readOnly,
tabs = [], tabs = [],
} = props } = props
const { path: pathFromContext, permissions, schemaPath } = useFieldProps() const { indexPath, path: pathFromContext, permissions, schemaPath } = useFieldProps()
const path = pathFromContext || pathFromProps || name const path = pathFromContext || pathFromProps || name
const { getPreference, setPreference } = usePreferences() const { getPreference, setPreference } = usePreferences()
const { preferencesKey } = useDocumentInfo() const { preferencesKey } = useDocumentInfo()

View File

@@ -4,6 +4,7 @@ import type { FieldPermissions } from 'payload/types'
import React from 'react' import React from 'react'
export type FieldPropsContextType = { export type FieldPropsContextType = {
indexPath?: string
path: string path: string
permissions?: FieldPermissions permissions?: FieldPermissions
readOnly: boolean readOnly: boolean
@@ -14,6 +15,7 @@ export type FieldPropsContextType = {
} }
const FieldPropsContext = React.createContext<FieldPropsContextType>({ const FieldPropsContext = React.createContext<FieldPropsContextType>({
indexPath: '',
path: '', path: '',
permissions: {} as FieldPermissions, permissions: {} as FieldPermissions,
readOnly: false, readOnly: false,
@@ -23,6 +25,7 @@ const FieldPropsContext = React.createContext<FieldPropsContextType>({
export type Props = { export type Props = {
children: React.ReactNode children: React.ReactNode
indexPath?: string
path: string path: string
permissions?: FieldPermissions permissions?: FieldPermissions
readOnly: boolean readOnly: boolean
@@ -34,6 +37,7 @@ export type Props = {
export const FieldPropsProvider: React.FC<Props> = ({ export const FieldPropsProvider: React.FC<Props> = ({
children, children,
indexPath,
path, path,
permissions, permissions,
readOnly, readOnly,
@@ -43,6 +47,7 @@ export const FieldPropsProvider: React.FC<Props> = ({
return ( return (
<FieldPropsContext.Provider <FieldPropsContext.Provider
value={{ value={{
indexPath,
path, path,
permissions, permissions,
readOnly, readOnly,

View File

@@ -19,6 +19,7 @@ type Props = {
CustomField: MappedField['CustomField'] CustomField: MappedField['CustomField']
disabled: boolean disabled: boolean
fieldComponentProps?: FieldComponentProps fieldComponentProps?: FieldComponentProps
indexPath?: string
isHidden?: boolean isHidden?: boolean
name?: string name?: string
path: string path: string
@@ -37,6 +38,7 @@ export const RenderField: React.FC<Props> = ({
CustomField, CustomField,
disabled, disabled,
fieldComponentProps, fieldComponentProps,
indexPath,
isHidden, isHidden,
path: pathFromProps, path: pathFromProps,
permissions, permissions,
@@ -48,8 +50,8 @@ export const RenderField: React.FC<Props> = ({
const { readOnly: readOnlyFromContext } = useFieldProps() const { readOnly: readOnlyFromContext } = useFieldProps()
const fieldComponents = useFieldComponents() const fieldComponents = useFieldComponents()
const path = `${pathFromProps ? `${pathFromProps}.` : ''}${name ? `${name}` : ''}` const path = [pathFromProps, name].filter(Boolean).join('.')
const schemaPath = `${schemaPathFromProps ? `${schemaPathFromProps}` : ''}${name ? `.${name}` : ''}` const schemaPath = [schemaPathFromProps, name].filter(Boolean).join('.')
// if the user cannot read the field, then filter it out // if the user cannot read the field, then filter it out
// this is different from `admin.readOnly` which is executed based on `operation` // this is different from `admin.readOnly` which is executed based on `operation`
@@ -76,6 +78,7 @@ export const RenderField: React.FC<Props> = ({
return ( return (
<FieldPropsProvider <FieldPropsProvider
indexPath={indexPath}
path={path} path={path}
permissions={permissions} permissions={permissions}
readOnly={readOnly} readOnly={readOnly}

View File

@@ -13,7 +13,8 @@ const baseClass = 'render-fields'
export { Props } export { Props }
export const RenderFields: React.FC<Props> = (props) => { export const RenderFields: React.FC<Props> = (props) => {
const { className, fieldMap, forceRender, margins, path, permissions, schemaPath } = props const { className, fieldMap, forceRender, indexPath, margins, path, permissions, schemaPath } =
props
const { i18n } = useTranslation() const { i18n } = useTranslation()
const [hasRendered, setHasRendered] = React.useState(Boolean(forceRender)) const [hasRendered, setHasRendered] = React.useState(Boolean(forceRender))
@@ -72,6 +73,7 @@ export const RenderFields: React.FC<Props> = (props) => {
CustomField={CustomField} CustomField={CustomField}
disabled={disabled} disabled={disabled}
fieldComponentProps={fieldComponentProps} fieldComponentProps={fieldComponentProps}
indexPath={indexPath !== undefined ? `${indexPath}.${fieldIndex}` : `${fieldIndex}`}
isHidden={isHidden} isHidden={isHidden}
key={fieldIndex} key={fieldIndex}
name={name} name={name}

View File

@@ -6,6 +6,7 @@ export type Props = {
className?: string className?: string
fieldMap: FieldMap fieldMap: FieldMap
forceRender?: boolean forceRender?: boolean
indexPath?: string
margins?: 'small' | false margins?: 'small' | false
operation?: Operation operation?: Operation
path: string path: string

View File

@@ -2,10 +2,9 @@
import type { RowLabelComponent } from 'payload/types' import type { RowLabelComponent } from 'payload/types'
import { useRowLabel } from '@payloadcms/ui/forms/RowLabel/Context'
import React from 'react' import React from 'react'
import { useRowLabel } from '../../../../packages/ui/src/forms/RowLabel/Context/index.js'
export const ArrayRowLabel: RowLabelComponent = () => { export const ArrayRowLabel: RowLabelComponent = () => {
const { data } = useRowLabel<{ title: string }>() const { data } = useRowLabel<{ title: string }>()
return ( return (

View File

@@ -469,28 +469,43 @@ describe('fields', () => {
url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug) url = new AdminUrlUtil(serverURL, collapsibleFieldsSlug)
}) })
test('should render collapsible as collapsed if initCollapsed is true', async () => {
await page.goto(url.create)
const collapsedCollapsible = page.locator(
'#field-collapsible-1 .collapsible__toggle--collapsed',
)
await expect(collapsedCollapsible).toBeVisible()
})
test('should render CollapsibleLabel using a function', async () => { test('should render CollapsibleLabel using a function', async () => {
const label = 'custom row label' const label = 'custom row label'
await page.goto(url.create) await page.goto(url.create)
await page.locator('#field-collapsible-3__1 >> #field-nestedTitle').fill(label) await page.locator('#field-collapsible-3__1 #field-nestedTitle').fill(label)
await wait(100) await wait(100)
const customCollapsibleLabel = page.locator('#field-collapsible-3__1 >> .row-label') const customCollapsibleLabel = page.locator(
`#field-collapsible-3__1 .collapsible-field__row-label-wrap :text("${label}")`,
)
await expect(customCollapsibleLabel).toContainText(label) await expect(customCollapsibleLabel).toContainText(label)
}) })
test('should render CollapsibleLabel using a component', async () => { test('should render CollapsibleLabel using a component', async () => {
const label = 'custom row label as component' const label = 'custom row label as component'
await page.goto(url.create) await page.goto(url.create)
await page.locator('#field-arrayWithCollapsibles').scrollIntoViewIfNeeded()
const arrayWithCollapsibles = page.locator('#field-arrayWithCollapsibles')
await expect(arrayWithCollapsibles).toBeVisible()
await page.locator('#field-arrayWithCollapsibles >> .array-field__add-row').click() await page.locator('#field-arrayWithCollapsibles >> .array-field__add-row').click()
await page await page
.locator( .locator(
'#field-collapsible-4__0-arrayWithCollapsibles__0 >> #field-arrayWithCollapsibles__0__innerCollapsible', '#arrayWithCollapsibles-row-0 #field-collapsible-4__0-arrayWithCollapsibles__0 #field-arrayWithCollapsibles__0__innerCollapsible',
) )
.fill(label) .fill(label)
await wait(100) await wait(100)
const customCollapsibleLabel = page.locator( const customCollapsibleLabel = page.locator(
`#field-collapsible-4__0-arrayWithCollapsibles__0 >> .row-label :text("${label}")`, `#field-arrayWithCollapsibles >> #arrayWithCollapsibles-row-0 >> .collapsible-field__row-label-wrap :text("${label}")`,
) )
await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase') await expect(customCollapsibleLabel).toHaveCSS('text-transform', 'uppercase')
}) })

View File

@@ -135,7 +135,7 @@ describe('Live Preview', () => {
test('global - can edit fields', async () => { test('global - can edit fields', async () => {
await goToGlobalPreview(page, 'header') await goToGlobalPreview(page, 'header')
const field = page.locator('input#field-navItems__0__link____newTab') const field = page.locator('input#field-navItems__0__link__newTab')
await expect(field).toBeVisible() await expect(field).toBeVisible()
await expect(field).toBeEnabled() await expect(field).toBeEnabled()
await field.check() await field.check()