fix(ui): array field state to return empty array instead of 0 (#11283)

### What?

This PR fixes an issue where empty array fields would return `0` instead
of an empty array `[]` in form state.

The issue was caused by `rows` being initialized as `undefined` within
the array field reducer.

As a result, `rows` did not exist on array field state when initial
state was empty.

This has been updated to initialize as an empty array (`rows: []`) to
ensure consistent behavior when using `getDataByPath`.

Fixes #10712 


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211439284995184
This commit is contained in:
Patrik
2025-09-24 13:17:30 -04:00
committed by GitHub
parent f980a86bd6
commit 3f5c989954
6 changed files with 184 additions and 2 deletions

View File

@@ -380,7 +380,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
},
{
promises: [],
rows: undefined,
rows: [],
},
)

View File

@@ -0,0 +1,25 @@
'use client'
import { useForm } from '@payloadcms/ui'
import React from 'react'
const GetDataByPathTest: React.FC = () => {
const { getDataByPath } = useForm()
// Test the empty array field
const emptyArrayResult = getDataByPath('potentiallyEmptyArray')
// Render the result directly so e2e test can easily read it
return (
<div id="getDataByPath-test">
<span id="empty-array-result">
{Array.isArray(emptyArrayResult) ? 'ARRAY' : String(emptyArrayResult)}
</span>
<span id="empty-array-length">
{Array.isArray(emptyArrayResult) ? emptyArrayResult.length : 'NOT_ARRAY'}
</span>
</div>
)
}
export default GetDataByPathTest

View File

@@ -637,4 +637,14 @@ describe('Array', () => {
await expect(subArrayContainer2).toHaveCount(1)
})
})
test('should return empty array from getDataByPath for array fields without rows', async () => {
await page.goto(url.create)
// Wait for the test component to render
await page.waitForSelector('#getDataByPath-test')
// Check that getDataByPath returned an empty array, not 0
await expect(page.locator('#empty-array-result')).toHaveText('ARRAY')
await expect(page.locator('#empty-array-length')).toHaveText('0')
})
})

View File

@@ -277,6 +277,15 @@ const ArrayFields: CollectionConfig = {
},
],
},
{
name: 'getDataByPathTest',
type: 'ui',
admin: {
components: {
Field: '/collections/Array/GetDataByPathTest.js',
},
},
},
],
slug: arrayFieldsSlug,
versions: true,

View File

@@ -557,6 +557,54 @@ export interface BlockField {
blockType: 'readOnlyBlock';
}[]
| null;
/**
* Change the value of this field to change the enabled blocks of the blocksWithDynamicFilterOptions field. If it's empty, all blocks are enabled.
*/
enabledBlocks?: string | null;
blocksWithDynamicFilterOptions?:
| (
| {
block1Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockOne';
}
| {
block2Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockTwo';
}
| {
block3Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockThree';
}
)[]
| null;
blocksWithFilterOptions?:
| (
| {
block1Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockFour';
}
| {
block2Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockFive';
}
| {
block3Text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockSix';
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
@@ -822,7 +870,7 @@ export interface ConditionalLogic {
root: {
type: string;
children: {
type: string;
type: any;
version: number;
[k: string]: unknown;
}[];
@@ -2263,6 +2311,57 @@ export interface BlockFieldsSelect<T extends boolean = true> {
blockName?: T;
};
};
enabledBlocks?: T;
blocksWithDynamicFilterOptions?:
| T
| {
blockOne?:
| T
| {
block1Text?: T;
id?: T;
blockName?: T;
};
blockTwo?:
| T
| {
block2Text?: T;
id?: T;
blockName?: T;
};
blockThree?:
| T
| {
block3Text?: T;
id?: T;
blockName?: T;
};
};
blocksWithFilterOptions?:
| T
| {
blockFour?:
| T
| {
block1Text?: T;
id?: T;
blockName?: T;
};
blockFive?:
| T
| {
block2Text?: T;
id?: T;
blockName?: T;
};
blockSix?:
| T
| {
block3Text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}

View File

@@ -730,4 +730,43 @@ describe('Form State', () => {
computedTitle: incomingStateFromServer.computedTitle, // This field was not modified locally, so should be updated from the server
})
})
it('should set rows to empty array for empty array fields', async () => {
const req = await createLocalReq({ user }, payload)
// Create a document with an empty array
const postData = await payload.create({
collection: postsSlug,
data: {
title: 'Test Post',
array: [], // Empty array - this should result in rows: [] in form state
},
})
const { state } = await buildFormState({
mockRSCs: true,
id: postData.id,
collectionSlug: postsSlug,
data: postData,
docPermissions: {
create: true,
delete: true,
fields: true,
read: true,
readVersions: true,
update: true,
},
docPreferences: {
fields: {},
},
documentFormState: undefined,
operation: 'update',
renderAllFields: false,
req,
schemaPath: postsSlug,
})
expect(state.array).toBeDefined()
expect(state?.array?.rows).toEqual([]) // should be [] not undefined
})
})