Skip to content

Commit 4811531

Browse files
fix(ui): incorrect error states (#11574)
Fixes #11568 ### What? Out of sync errors states - Collaspibles & Tabs were not reporting accurate child error counts - Arrays could get into a state where they would not update their error states - Slight issue with toasts ### Tabs & Collapsibles The logic for determining matching field paths was not functioning as intended. Fields were attempting to match with paths such as `_index-0` which will not work. ### Arrays The form state was not updating when the server sent back errorPaths. This PR adds `errorPaths` to `serverPropsToAccept`. ### Toasts Some toasts could report errors in the form of `my > > error`. This ensures they will be `my > error` ### Misc Removes 2 files that were not in use: - `getFieldStateFromPaths.ts` - `getNestedFieldState.ts`
1 parent 7cef890 commit 4811531

File tree

19 files changed

+141
-165
lines changed

19 files changed

+141
-165
lines changed

packages/payload/src/fields/hooks/beforeChange/promise.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import { traverseFields } from './traverseFields.js'
1717

1818
function buildFieldLabel(parentLabel: string, label: string): string {
1919
const capitalizedLabel = label.charAt(0).toUpperCase() + label.slice(1)
20-
return parentLabel ? `${parentLabel} > ${capitalizedLabel}` : capitalizedLabel
20+
return parentLabel && capitalizedLabel
21+
? `${parentLabel} > ${capitalizedLabel}`
22+
: capitalizedLabel || parentLabel
2123
}
2224

2325
type Args = {

packages/ui/src/elements/Toasts/fieldErrors.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ function createErrorsFromMessage(message: string): {
4646

4747
if (errors.length === 1) {
4848
return {
49-
message: `${intro}: ${errors[0]}`,
49+
errors,
50+
message: `${intro}:`,
5051
}
5152
}
5253

packages/ui/src/fields/Collapsible/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ const CollapsibleFieldComponent: CollapsibleFieldClientComponent = (props) => {
107107

108108
return (
109109
<Fragment>
110-
<WatchChildErrors fields={fields} path={path.split('.')} setErrorCount={setErrorCount} />
110+
<WatchChildErrors
111+
fields={fields}
112+
// removes the 'collapsible' path segment, i.e. `_index-0`
113+
path={path.split('.').slice(0, -1)}
114+
setErrorCount={setErrorCount}
115+
/>
111116
<div
112117
className={[
113118
fieldBaseClass,

packages/ui/src/fields/Tabs/Tab/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ export const TabComponent: React.FC<TabProps> = ({ isActive, parentPath, setIsAc
2525
const [errorCount, setErrorCount] = useState(undefined)
2626

2727
const path = [
28-
...(parentPath ? parentPath.split('.') : []),
28+
// removes parent 'tabs' path segment, i.e. `_index-0`
29+
...(parentPath ? parentPath.split('.').slice(0, -1) : []),
2930
...(tabHasName(tab) ? [tab.name] : []),
30-
].join('.')
31+
]
3132

3233
const fieldHasErrors = errorCount > 0
3334

3435
return (
3536
<React.Fragment>
36-
<WatchChildErrors fields={tab.fields} path={path.split('.')} setErrorCount={setErrorCount} />
37+
<WatchChildErrors fields={tab.fields} path={path} setErrorCount={setErrorCount} />
3738
<button
3839
className={[
3940
baseClass,

packages/ui/src/forms/Form/mergeServerFormState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const mergeServerFormState = ({
3131
'passesCondition',
3232
'valid',
3333
'errorMessage',
34+
'errorPaths',
3435
'rows',
3536
'customComponents',
3637
'requiresRender',

packages/ui/src/forms/WatchChildErrors/buildPathSegments.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,34 @@ import type { ClientField } from 'payload'
33

44
import { fieldAffectsData } from 'payload/shared'
55

6-
export const buildPathSegments = (
7-
parentPath: (number | string)[],
8-
fields: ClientField[],
9-
): string[] => {
10-
const pathNames = fields.reduce((acc, field) => {
6+
export const buildPathSegments = (fields: ClientField[]): (`${string}.` | string)[] => {
7+
return fields.reduce((acc: (`${string}.` | string)[], field) => {
118
const fields: ClientField[] = 'fields' in field ? field.fields : undefined
129

1310
if (fields) {
1411
if (fieldAffectsData(field)) {
1512
// group, block, array
16-
const name = 'name' in field ? field.name : 'unnamed'
17-
acc.push(...[...parentPath, name])
13+
acc.push(`${field.name}.`)
1814
} else {
1915
// rows, collapsibles, unnamed-tab
20-
acc.push(...buildPathSegments(parentPath, fields))
16+
acc.push(...buildPathSegments(fields))
2117
}
2218
} else if (field.type === 'tabs') {
2319
// tabs
2420
if ('tabs' in field) {
2521
field.tabs?.forEach((tab) => {
26-
let tabPath = parentPath
2722
if ('name' in tab) {
28-
tabPath = [...parentPath, tab.name]
23+
acc.push(`${tab.name}.`)
24+
} else {
25+
acc.push(...buildPathSegments(tab.fields))
2926
}
30-
acc.push(...buildPathSegments(tabPath, tab.fields))
3127
})
3228
}
3329
} else if (fieldAffectsData(field)) {
3430
// text, number, date, etc.
35-
const name = 'name' in field ? field.name : 'unnamed'
36-
acc.push(...[...parentPath, name])
31+
acc.push(field.name)
3732
}
3833

3934
return acc
4035
}, [])
41-
42-
return pathNames
4336
}

packages/ui/src/forms/WatchChildErrors/getFieldStateFromPaths.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

packages/ui/src/forms/WatchChildErrors/getNestedFieldState.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

packages/ui/src/forms/WatchChildErrors/index.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,50 @@ import type React from 'react'
55
import { useThrottledEffect } from '../../hooks/useThrottledEffect.js'
66
import { useAllFormFields, useFormSubmitted } from '../Form/context.js'
77
import { buildPathSegments } from './buildPathSegments.js'
8-
import { getFieldStateFromPaths } from './getFieldStateFromPaths.js'
98

109
type TrackSubSchemaErrorCountProps = {
1110
fields?: ClientField[]
11+
/**
12+
* This path should only include path segments that affect data
13+
* i.e. it should not include _index-0 type segments
14+
*
15+
* For collapsibles and tabs you can simply pass their parent path
16+
*/
1217
path: (number | string)[]
1318
setErrorCount: (count: number) => void
1419
}
15-
1620
export const WatchChildErrors: React.FC<TrackSubSchemaErrorCountProps> = ({
1721
fields,
18-
path,
22+
path: parentPath,
1923
setErrorCount,
2024
}) => {
2125
const [formState] = useAllFormFields()
2226
const hasSubmitted = useFormSubmitted()
2327

24-
const pathSegments = buildPathSegments(path, fields)
28+
const segmentsToMatch = buildPathSegments(fields)
2529

2630
useThrottledEffect(
2731
() => {
2832
if (hasSubmitted) {
29-
const { errorCount } = getFieldStateFromPaths({ formState, pathSegments })
33+
let errorCount = 0
34+
Object.entries(formState).forEach(([key]) => {
35+
const matchingSegment = segmentsToMatch?.some((segment) => {
36+
const segmentToMatch = [...parentPath, segment].join('.')
37+
// match fields with same parent path
38+
if (segmentToMatch.endsWith('.')) {
39+
return key.startsWith(segmentToMatch)
40+
}
41+
// match fields with same path
42+
return key === segmentToMatch
43+
})
44+
45+
if (matchingSegment) {
46+
const pathState = formState[key]
47+
if ('valid' in pathState && !pathState.valid) {
48+
errorCount += 1
49+
}
50+
}
51+
})
3052
setErrorCount(errorCount)
3153
}
3254
},

test/eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export const testEslintConfig = [
6262
'payload/no-wait-function': 'warn',
6363
// Enable the no-non-retryable-assertions rule ONLY for hunting for flakes
6464
// 'payload/no-non-retryable-assertions': 'error',
65+
'playwright/expect-expect': [
66+
'error',
67+
{
68+
assertFunctionNames: ['assertToastErrors', 'saveDocAndAssert', 'runFilterOptionsTest'],
69+
},
70+
],
6571
},
6672
},
6773
{

0 commit comments

Comments
 (0)