Skip to content

Commit b74f4fb

Browse files
fix(ui): fallback to default locale checkbox passes wrong value (#12396)
### What? Allows document to successfully be saved when `fallback to default locale` checked without throwing an error. ### Why? The `fallback to default locale` checkbox allows users to successfully save a document in the admin panel while using fallback data for required fields, this has been broken since the release of `v3`. Without the checkbox override, the user would be prevented from saving the document in the UI because the field is required and will throw an error. The logic of using fallback data is not affected by this checkbox - it is purely to allow saving the document in the UI. ### How? The `fallback` checkbox used to have an `onChange` function that replaces the field value with null, allowing it to get processed through the standard localization logic and get replaced by fallback data. However, this `onChange` was removed at some point and the field was passing the actual checkbox value `true`/`false` which then breaks the form and prevent it from saving. This fallback checkbox is only displayed when `fallback: true` is set in the localization config. This PR also updated the checkbox to only be displayed when `required: true` - when it's the field is not `required` this checkbox serves no purpose. Also adds tests to `localization/e2e`. Fixes #11245 --------- Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
1 parent 8401b21 commit b74f4fb

File tree

12 files changed

+317
-34
lines changed

12 files changed

+317
-34
lines changed

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

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ export const promise = async ({
111111
parentSchemaPath,
112112
})
113113

114+
const fieldAffectsDataResult = fieldAffectsData(field)
114115
const pathSegments = path ? path.split('.') : []
115116
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
116117
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
117118
let removedFieldValue = false
118119

119120
if (
120-
fieldAffectsData(field) &&
121+
fieldAffectsDataResult &&
121122
field.hidden &&
122123
typeof siblingDoc[field.name!] !== 'undefined' &&
123124
!showHiddenFields
@@ -139,16 +140,17 @@ export const promise = async ({
139140
}
140141
}
141142

142-
const shouldHoistLocalizedValue =
143+
const shouldHoistLocalizedValue: boolean = Boolean(
143144
flattenLocales &&
144-
fieldAffectsData(field) &&
145-
typeof siblingDoc[field.name!] === 'object' &&
146-
siblingDoc[field.name!] !== null &&
147-
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
148-
locale !== 'all' &&
149-
req.payload.config.localization
150-
151-
if (shouldHoistLocalizedValue) {
145+
fieldAffectsDataResult &&
146+
typeof siblingDoc[field.name!] === 'object' &&
147+
siblingDoc[field.name!] !== null &&
148+
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
149+
locale !== 'all' &&
150+
req.payload.config.localization,
151+
)
152+
153+
if (fieldAffectsDataResult && shouldHoistLocalizedValue) {
152154
// replace actual value with localized value before sanitizing
153155
// { [locale]: fields } -> fields
154156
const value = siblingDoc[field.name!][locale!]
@@ -187,7 +189,7 @@ export const promise = async ({
187189
case 'group': {
188190
// Fill groups with empty objects so fields with hooks within groups can populate
189191
// themselves virtually as necessary
190-
if (fieldAffectsData(field) && typeof siblingDoc[field.name] === 'undefined') {
192+
if (fieldAffectsDataResult && typeof siblingDoc[field.name] === 'undefined') {
191193
siblingDoc[field.name] = {}
192194
}
193195

@@ -234,7 +236,7 @@ export const promise = async ({
234236
}
235237
}
236238

237-
if (fieldAffectsData(field)) {
239+
if (fieldAffectsDataResult) {
238240
// Execute hooks
239241
if (triggerHooks && field.hooks?.afterRead) {
240242
for (const hook of field.hooks.afterRead) {
@@ -400,7 +402,7 @@ export const promise = async ({
400402
}
401403
}
402404

403-
if (Array.isArray(rows)) {
405+
if (Array.isArray(rows) && rows.length > 0) {
404406
rows.forEach((row, rowIndex) => {
405407
traverseFields({
406408
blockData,
@@ -468,6 +470,8 @@ export const promise = async ({
468470
})
469471
}
470472
})
473+
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
474+
siblingDoc[field.name] = null
471475
} else if (field.hidden !== true || showHiddenFields === true) {
472476
siblingDoc[field.name] = []
473477
}
@@ -477,7 +481,7 @@ export const promise = async ({
477481
case 'blocks': {
478482
const rows = siblingDoc[field.name]
479483

480-
if (Array.isArray(rows)) {
484+
if (Array.isArray(rows) && rows.length > 0) {
481485
rows.forEach((row, rowIndex) => {
482486
const blockTypeToMatch = (row as JsonObject).blockType
483487

@@ -573,6 +577,8 @@ export const promise = async ({
573577
})
574578
}
575579
})
580+
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
581+
siblingDoc[field.name] = null
576582
} else if (field.hidden !== true || showHiddenFields === true) {
577583
siblingDoc[field.name] = []
578584
}
@@ -617,7 +623,7 @@ export const promise = async ({
617623
}
618624

619625
case 'group': {
620-
if (fieldAffectsData(field)) {
626+
if (fieldAffectsDataResult) {
621627
let groupDoc = siblingDoc[field.name] as JsonObject
622628

623629
if (typeof siblingDoc[field.name] !== 'object') {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,12 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => {
381381
Fallback={<FieldDescription description={description} path={path} />}
382382
/>
383383
</header>
384-
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
384+
<NullifyLocaleField
385+
fieldValue={value}
386+
localized={localized}
387+
path={path}
388+
readOnly={readOnly}
389+
/>
385390
{BeforeInput}
386391
{(rows?.length > 0 || (!valid && (showRequired || showMinRows))) && (
387392
<DraggableSortable

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,12 @@ const BlocksFieldComponent: BlocksFieldClientComponent = (props) => {
382382
/>
383383
</header>
384384
{BeforeInput}
385-
<NullifyLocaleField fieldValue={value} localized={localized} path={path} />
385+
<NullifyLocaleField
386+
fieldValue={value}
387+
localized={localized}
388+
path={path}
389+
readOnly={readOnly}
390+
/>
386391
{(rows.length > 0 || (!valid && (showRequired || showMinRows))) && (
387392
<DraggableSortable
388393
className={`${baseClass}__rows`}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@layer payload-default {
2+
.nullify-locale-field {
3+
margin-bottom: 0;
4+
.field-type.checkbox {
5+
display: flex;
6+
flex-direction: column;
7+
margin: 0;
8+
}
9+
10+
+ .array-field__add-row {
11+
margin-top: calc(var(--base) / 2);
12+
}
13+
}
14+
}

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

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,31 @@ import { CheckboxField } from '../../fields/Checkbox/index.js'
77
import { useConfig } from '../../providers/Config/index.js'
88
import { useLocale } from '../../providers/Locale/index.js'
99
import { useTranslation } from '../../providers/Translation/index.js'
10+
import { useForm } from '../Form/context.js'
11+
import './index.scss'
12+
13+
const baseClass = 'nullify-locale-field'
1014

1115
type NullifyLocaleFieldProps = {
1216
readonly fieldValue?: [] | null | number
1317
readonly localized: boolean
1418
readonly path: string
19+
readonly readOnly?: boolean
1520
}
1621

1722
export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
1823
fieldValue,
1924
localized,
2025
path,
26+
readOnly = false,
2127
}) => {
2228
const { code: currentLocale } = useLocale()
2329
const {
2430
config: { localization },
2531
} = useConfig()
2632
const [checked, setChecked] = React.useState<boolean>(typeof fieldValue !== 'number')
2733
const { t } = useTranslation()
34+
const { dispatchFields, setModified } = useForm()
2835

2936
if (!localized || !localization) {
3037
// hide when field is not localized or localization is not enabled
@@ -36,6 +43,18 @@ export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
3643
return null
3744
}
3845

46+
const onChange = () => {
47+
const useFallback = !checked
48+
49+
dispatchFields({
50+
type: 'UPDATE',
51+
path,
52+
value: useFallback ? null : fieldValue || 0,
53+
})
54+
setModified(true)
55+
setChecked(useFallback)
56+
}
57+
3958
if (fieldValue) {
4059
let hideCheckbox = false
4160
if (typeof fieldValue === 'number' && fieldValue > 0) {
@@ -54,18 +73,22 @@ export const NullifyLocaleField: React.FC<NullifyLocaleFieldProps> = ({
5473
}
5574

5675
return (
57-
<Banner>
58-
<CheckboxField
59-
checked={checked}
60-
field={{
61-
name: '',
62-
label: t('general:fallbackToDefaultLocale'),
63-
}}
64-
id={`field-${path.replace(/\./g, '__')}`}
65-
path={path}
66-
schemaPath=""
67-
// onToggle={onChange}
68-
/>
76+
<Banner className={baseClass}>
77+
{!fieldValue && readOnly ? (
78+
t('general:fallbackToDefaultLocale')
79+
) : (
80+
<CheckboxField
81+
checked={checked}
82+
field={{
83+
name: '',
84+
label: t('general:fallbackToDefaultLocale'),
85+
}}
86+
id={`field-${path.replace(/\./g, '__')}`}
87+
onChange={onChange}
88+
path={path}
89+
schemaPath=""
90+
/>
91+
)}
6992
</Banner>
7093
)
7194
}

test/localization/collections/Array/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const ArrayCollection: CollectionConfig = {
1313
{
1414
name: 'text',
1515
type: 'text',
16-
required: true,
1716
},
1817
],
1918
},
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { arrayWithFallbackCollectionSlug } from '../../shared.js'
4+
5+
export const ArrayWithFallbackCollection: CollectionConfig = {
6+
slug: arrayWithFallbackCollectionSlug,
7+
fields: [
8+
{
9+
name: 'items',
10+
type: 'array',
11+
localized: true,
12+
required: true,
13+
fields: [
14+
{
15+
name: 'text',
16+
type: 'text',
17+
},
18+
],
19+
},
20+
{
21+
name: 'itemsReadOnly',
22+
type: 'array',
23+
localized: true,
24+
admin: {
25+
readOnly: true,
26+
},
27+
fields: [
28+
{
29+
name: 'text',
30+
type: 'text',
31+
required: true,
32+
},
33+
],
34+
},
35+
],
36+
}

test/localization/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { LocalizedPost } from './payload-types.js'
99
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
1010
import { devUser } from '../credentials.js'
1111
import { ArrayCollection } from './collections/Array/index.js'
12+
import { ArrayWithFallbackCollection } from './collections/ArrayWithFallback/index.js'
1213
import { BlocksCollection } from './collections/Blocks/index.js'
1314
import { Group } from './collections/Group/index.js'
1415
import { LocalizedDateFields } from './collections/LocalizedDateFields/index.js'
@@ -397,6 +398,7 @@ export default buildConfigWithDefaults({
397398
],
398399
},
399400
LocalizedWithinLocalized,
401+
ArrayWithFallbackCollection,
400402
],
401403
globals: [
402404
{

0 commit comments

Comments
 (0)