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:
@@ -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 field’s 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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user