Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/payload/src/exports/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js'

export { getDataByPath } from '../utilities/getDataByPath.js'
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
export { getFormStateDataByPath } from '../utilities/getFormStateDataByPath.js'
export { getObjectDotNotation } from '../utilities/getObjectDotNotation.js'
export { getSafeRedirect } from '../utilities/getSafeRedirect.js'

Expand Down
79 changes: 79 additions & 0 deletions packages/payload/src/utilities/getDataByPath.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getDataByPath } from './getDataByPath'

const data = {
text: 'Sample text',
textLocalized: {
en: 'Sample text in English',
fr: 'Exemple de texte en français',
},
array: [
{
text: 'Array: row 1',
textLocalized: {
en: 'Array: row 1 in English',
fr: "Texte de l'élément 1 du tableau en français",
},
group: {
text: 'Group item text',
},
},
{
text: 'Array: row 2',
textLocalized: {
en: 'Array: row 2 in English',
fr: "Texte de l'élément 2 du tableau en français",
},
group: {
text: 'Group item text 2',
},
},
],
tabs: {
tab: {
array: [
{
text: 'Tab > Array: row 1',
},
{
text: 'Tab > Array: row 2',
},
],
},
},
}

describe('getDataByPath', () => {
it('gets top-level field', () => {
const value = getDataByPath({ data, path: 'text' })
expect(value).toEqual(data.text)
})

it('gets localized top-level field', () => {
const value = getDataByPath({ data, path: 'textLocalized' })
expect(value).toEqual(data.textLocalized)

const valueEn = getDataByPath({ data, path: 'textLocalized', locale: 'en' })
expect(valueEn).toEqual(data.textLocalized.en)
})

it('gets field nested in array', () => {
const row1Value = getDataByPath({ data, path: 'array.0.text' })
expect(row1Value).toEqual(data.array[0].text)

const row2Value = getDataByPath({ data, path: 'array.1.text' })
expect(row2Value).toEqual(data.array[1].text)
})

it('gets group field deeply nested in group', () => {
const value = getDataByPath({ data, path: 'array.1.group.text' })
expect(value).toEqual(data.array[1].group.text)
})

it('gets text field deeply nested in tabs', () => {
const row1Value = getDataByPath({ data, path: 'tabs.tab.array.0.text' })
expect(row1Value).toEqual(data.tabs.tab.array[0].text)

const row2Value = getDataByPath({ data, path: 'tabs.tab.array.1.text' })
expect(row2Value).toEqual(data.tabs.tab.array[1].text)
})
})
65 changes: 50 additions & 15 deletions packages/payload/src/utilities/getDataByPath.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
import type { FormState } from '../admin/types.js'
/**
* Gets a field's data by its path from a nested data object.
* To get data from flattened form state, use `getFormStateDataByPath` instead.
*
* @example
* ```ts
* // From document data
* const data = {
* group: {
* field: 'value',
* },
* }
* const value = getDataByPath({ data, path: 'group.field' })
* // value is 'value'
* ```
*/
export const getDataByPath = <T = unknown>(args: {
data: Record<string, any>
/**
* Optional locale for localized fields, e.g. "en", etc.
*/
locale?: string
/**
* The path to the desired field, e.g. "group.array.0.text", etc.
*/
path: string
}): T => {
const { data, path } = args

import { unflatten } from './unflatten.js'
const pathSegments = path.split('.')

export const getDataByPath = <T = unknown>(fields: FormState, path: string): T => {
const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1)
const name = path.split('.').pop()
let current: any = data

const data: Record<string, any> = {}
Object.keys(fields).forEach((key) => {
if (!fields[key]?.disableFormData && (key.indexOf(`${path}.`) === 0 || key === path)) {
data[key.replace(pathPrefixToRemove, '')] = fields[key]?.value
for (const pathSegment of pathSegments) {
if (current === undefined || current === null) {
break
}

const rowIndex = Number(pathSegment)

if (fields[key]?.rows && fields[key].rows.length === 0) {
data[key.replace(pathPrefixToRemove, '')] = []
if (!Number.isNaN(rowIndex) && Array.isArray(current)) {
current = current[rowIndex]
} else {
/**
* Effectively make "current" become "siblingData" for the next iteration
*/
const value = current[pathSegment]

if (args.locale && value && typeof value === 'object' && value[args.locale]) {
current = value[args.locale]
} else {
current = value
}
}
})

const unflattenedData = unflatten(data)
}

return unflattenedData?.[name!]
return current
}
51 changes: 51 additions & 0 deletions packages/payload/src/utilities/getFormStateDataByPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { FormState } from '../admin/types.js'

import { unflatten } from './unflatten.js'

/**
* Gets a field's data by its path from form state, which is a flattened data object keyed by field paths.
* To get data from nested document data, use `getDataByPath` instead.
*
* @example
* ```ts
* const formState = {
* 'group.field': { value: 'value' },
* }
*
* const value = getFormStateDataByPath({ formState, path: 'group.field' })
* // value is 'value'
* ```
*/
export const getFormStateDataByPath = <T = unknown>(args: {
/**
* The form state object to get the data from., e.g. `{ 'group.field': { value: 'value' } }`
*/
formState: FormState
/**
* The path to the desired field, e.g. "group.array.0.text", etc.
*/
path: string
}): T => {
const { formState, path } = args

const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1)
const pathSegments = path.split('.')

const fieldName = pathSegments[pathSegments.length - 1]

const siblingData: Record<string, any> = {}

Object.keys(formState).forEach((key) => {
if (!formState[key]?.disableFormData && (key.indexOf(`${path}.`) === 0 || key === path)) {
siblingData[key.replace(pathPrefixToRemove, '')] = formState[key]?.value

if (formState[key]?.rows && formState[key].rows.length === 0) {
siblingData[key.replace(pathPrefixToRemove, '')] = []
}
}
})

const unflattenedData = unflatten(siblingData)

return unflattenedData?.[fieldName!]
}
4 changes: 2 additions & 2 deletions packages/ui/src/forms/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { serialize } from 'object-to-formdata'
import { type FormState, type PayloadRequest } from 'payload'
import {
deepCopyObjectSimpleWithoutReactComponents,
getDataByPath as getDataByPathFunc,
getFormStateDataByPath,
getSiblingData as getSiblingDataFunc,
reduceFieldsToValues,
wait,
Expand Down Expand Up @@ -515,7 +515,7 @@ export const Form: React.FC<FormProps> = (props) => {
)

const getDataByPath = useCallback<GetDataByPath>(
(path: string) => getDataByPathFunc(contextRef.current.fields, path),
(path: string) => getFormStateDataByPath({ formState: contextRef.current.fields, path }),
[],
)

Expand Down
Loading