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:
@@ -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}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user