feat: threads path through field condition functions (#11528)

This PR updates the field `condition` function property to include a new
`path` argument.

The `path` arg provides the schema path of the field, including array
indices where applicable.

#### Changes:

- Added `path: (number | string)[]` in the Condition type.
- Updated relevant condition checks to ensure correct parameter usage.
This commit is contained in:
Patrik
2025-03-05 12:45:08 -05:00
committed by GitHub
parent 04b046847b
commit ba30d7641f
7 changed files with 155 additions and 12 deletions

View File

@@ -512,11 +512,21 @@ All Description Functions receive the following arguments:
### Conditional Logic ### Conditional Logic
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments: You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes the following arguments:
- `data` - the entire document's data that is currently being edited | Argument | Description |
- `siblingData` - only the fields that are direct siblings to the field with the condition | --- | --- |
- `{ user }` - the final argument is an object containing the currently authenticated user | **`data`** | The entire document's data that is currently being edited. |
| **`siblingData`** | Only the fields that are direct siblings to the field with the condition. |
| **`ctx`** | An object containing additional information about the fields location and user. |
The `ctx` object:
| Property | Description |
| --- | --- |
| **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. |
| **`path`** | The full path to the field in the schema, including array indexes. Useful for dynamic lookups. |
| **`user`** | The currently authenticated user object. |
The `condition` function should return a boolean that will control if the field should be displayed or not. The `condition` function should return a boolean that will control if the field should be displayed or not.
@@ -535,7 +545,7 @@ The `condition` function should return a boolean that will control if the field
type: 'text', type: 'text',
admin: { admin: {
// highlight-start // highlight-start
condition: (data, siblingData, { user }) => { condition: (data, siblingData, { blockData, path, user }) => {
if (data.enableGreeting) { if (data.enableGreeting) {
return true return true
} else { } else {

View File

@@ -264,12 +264,17 @@ export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
siblingData: Partial<TSiblingData>, siblingData: Partial<TSiblingData>,
{ {
blockData, blockData,
path,
user, user,
}: { }: {
/** /**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`. * The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/ */
blockData: Partial<TData> blockData: Partial<TData>
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
path: (number | string)[]
user: PayloadRequest['user'] user: PayloadRequest['user']
}, },
) => boolean ) => boolean

View File

@@ -86,10 +86,6 @@ export const promise = async ({
parentSchemaPath, parentSchemaPath,
}) })
const passesCondition = field.admin?.condition
? Boolean(field.admin.condition(data, siblingData, { blockData, user: req.user }))
: true
let skipValidationFromHere = skipValidation || !passesCondition
const { localization } = req.payload.config const { localization } = req.payload.config
const defaultLocale = localization ? localization?.defaultLocale : 'en' const defaultLocale = localization ? localization?.defaultLocale : 'en'
const operationLocale = req.locale || defaultLocale const operationLocale = req.locale || defaultLocale
@@ -98,6 +94,13 @@ export const promise = async ({
const schemaPathSegments = schemaPath ? schemaPath.split('.') : [] const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : [] const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
const passesCondition = field.admin?.condition
? Boolean(
field.admin.condition(data, siblingData, { blockData, path: pathSegments, user: req.user }),
)
: true
let skipValidationFromHere = skipValidation || !passesCondition
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
// skip validation if the field is localized and the incoming data is null // skip validation if the field is localized and the incoming data is null
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && operationLocale !== defaultLocale) { if (fieldShouldBeLocalized({ field, parentIsLocalized }) && operationLocale !== defaultLocale) {

View File

@@ -118,12 +118,18 @@ export const iterateFields = async ({
parentSchemaPath, parentSchemaPath,
}) })
const pathSegments = path ? path.split('.') : []
if (!skipConditionChecks) { if (!skipConditionChecks) {
try { try {
passesCondition = Boolean( passesCondition = Boolean(
(field?.admin?.condition (field?.admin?.condition
? Boolean( ? Boolean(
field.admin.condition(fullData || {}, data || {}, { blockData, user: req.user }), field.admin.condition(fullData || {}, data || {}, {
blockData,
path: pathSegments,
user: req.user,
}),
) )
: true) && parentPassesCondition, : true) && parentPassesCondition,
) )

View File

@@ -161,7 +161,7 @@ describe('Conditional Logic', () => {
test('should not render fields when adding array or blocks rows until form state returns', async () => { test('should not render fields when adding array or blocks rows until form state returns', async () => {
await page.goto(url.create) await page.goto(url.create)
const addRowButton = page.locator('.array-field__add-row') const addRowButton = page.locator('#field-arrayWithConditionalField .array-field__add-row')
const fieldWithConditionSelector = 'input#field-arrayWithConditionalField__0__textWithCondition' const fieldWithConditionSelector = 'input#field-arrayWithConditionalField__0__textWithCondition'
await addRowButton.click() await addRowButton.click()
@@ -177,4 +177,30 @@ describe('Conditional Logic', () => {
await fieldToToggle.click() await fieldToToggle.click()
await expect(page.locator(fieldWithConditionSelector)).toBeVisible() await expect(page.locator(fieldWithConditionSelector)).toBeVisible()
}) })
test('should render field based on path argument', async () => {
await page.goto(url.create)
const arrayOneButton = page.locator('#field-arrayOne .array-field__add-row')
await arrayOneButton.click()
const arrayTwoButton = page.locator('#arrayOne-row-0 .array-field__add-row')
await arrayTwoButton.click()
const arrayThreeButton = page.locator('#arrayOne-0-arrayTwo-row-0 .array-field__add-row')
await arrayThreeButton.click()
const numberField = page.locator('#field-arrayOne__0__arrayTwo__0__arrayThree__0__numberField')
await expect(numberField).toBeHidden()
const selectField = page.locator('#field-arrayOne__0__arrayTwo__0__selectOptions')
await selectField.click({ delay: 100 })
const options = page.locator('.rs__option')
await options.locator('text=Option Two').click()
await expect(numberField).toBeVisible()
})
}) })

View File

@@ -182,6 +182,63 @@ const ConditionalLogic: CollectionConfig = {
}, },
], ],
}, },
{
name: 'arrayOne',
type: 'array',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'arrayTwo',
type: 'array',
fields: [
{
name: 'selectOptions',
type: 'select',
defaultValue: 'optionOne',
options: [
{
label: 'Option One',
value: 'optionOne',
},
{
label: 'Option Two',
value: 'optionTwo',
},
],
},
{
name: 'arrayThree',
type: 'array',
fields: [
{
name: 'numberField',
type: 'number',
admin: {
condition: (data, siblingData, { path, user }) => {
// Ensure path has enough depth
if (path.length < 5) {
return false
}
const arrayOneIndex = parseInt(String(path[1]), 10)
const arrayTwoIndex = parseInt(String(path[3]), 10)
const arrayOneItem = data.arrayOne?.[arrayOneIndex]
const arrayTwoItem = arrayOneItem?.arrayTwo?.[arrayTwoIndex]
return arrayTwoItem?.selectOptions === 'optionTwo'
},
},
},
],
},
],
},
],
},
], ],
} }

View File

@@ -1153,6 +1153,24 @@ export interface ConditionalLogic {
blockType: 'blockWithConditionalField'; blockType: 'blockWithConditionalField';
}[] }[]
| null; | null;
arrayOne?:
| {
title?: string | null;
arrayTwo?:
| {
selectOptions?: ('optionOne' | 'optionTwo') | null;
arrayThree?:
| {
numberField?: number | null;
id?: string | null;
}[]
| null;
id?: string | null;
}[]
| null;
id?: string | null;
}[]
| null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@@ -2874,6 +2892,24 @@ export interface ConditionalLogicSelect<T extends boolean = true> {
blockName?: T; blockName?: T;
}; };
}; };
arrayOne?:
| T
| {
title?: T;
arrayTwo?:
| T
| {
selectOptions?: T;
arrayThree?:
| T
| {
numberField?: T;
id?: T;
};
id?: T;
};
id?: T;
};
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }