Skip to content

Commit 563694d

Browse files
fix(ui): prevents unwanted data overrides when bulk editing (#9842)
### What? It became possible for fields to reset to a defined `defaultValue` when bulk editing from the `edit-many` drawer. ### Why? The form-state of all fields were being considered during a bulk edit - this also meant using their initial states - this meant any fields with default values or nested fields (`arrays`) would be overwritten with their initial states I.e. empty values or default values. ### How? Now - we only send through the form data of the fields specifically being edited in the edit-many drawer and ignore all other fields. Leaving all other fields stay their current values. Fixes #9590 --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
1 parent fee1744 commit 563694d

File tree

7 files changed

+236
-31
lines changed

7 files changed

+236
-31
lines changed

packages/ui/src/elements/EditMany/index.tsx

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { ClientCollectionConfig, FormState } from 'payload'
2+
import type { ClientCollectionConfig, FieldWithPathClient, FormState } from 'payload'
33

44
import { useModal } from '@faceless-ui/modal'
55
import { getTranslation } from '@payloadcms/translations'
@@ -36,20 +36,36 @@ export type EditManyProps = {
3636
readonly collection: ClientCollectionConfig
3737
}
3838

39+
const sanitizeUnselectedFields = (formState: FormState, selected: FieldWithPathClient[]) => {
40+
const filteredData = selected.reduce((acc, field) => {
41+
const foundState = formState?.[field.path]
42+
43+
if (foundState) {
44+
acc[field.path] = formState?.[field.path]?.value
45+
}
46+
47+
return acc
48+
}, {} as FormData)
49+
50+
return filteredData
51+
}
52+
3953
const Submit: React.FC<{
4054
readonly action: string
4155
readonly disabled: boolean
42-
}> = ({ action, disabled }) => {
56+
readonly selected?: FieldWithPathClient[]
57+
}> = ({ action, disabled, selected }) => {
4358
const { submit } = useForm()
4459
const { t } = useTranslation()
4560

4661
const save = useCallback(() => {
4762
void submit({
4863
action,
4964
method: 'PATCH',
65+
overrides: (formState) => sanitizeUnselectedFields(formState, selected),
5066
skipValidation: true,
5167
})
52-
}, [action, submit])
68+
}, [action, submit, selected])
5369

5470
return (
5571
<FormSubmit className={`${baseClass}__save`} disabled={disabled} onClick={save}>
@@ -58,20 +74,25 @@ const Submit: React.FC<{
5874
)
5975
}
6076

61-
const PublishButton: React.FC<{ action: string; disabled: boolean }> = ({ action, disabled }) => {
77+
const PublishButton: React.FC<{
78+
action: string
79+
disabled: boolean
80+
selected?: FieldWithPathClient[]
81+
}> = ({ action, disabled, selected }) => {
6282
const { submit } = useForm()
6383
const { t } = useTranslation()
6484

6585
const save = useCallback(() => {
6686
void submit({
6787
action,
6888
method: 'PATCH',
69-
overrides: {
89+
overrides: (formState) => ({
90+
...sanitizeUnselectedFields(formState, selected),
7091
_status: 'published',
71-
},
92+
}),
7293
skipValidation: true,
7394
})
74-
}, [action, submit])
95+
}, [action, submit, selected])
7596

7697
return (
7798
<FormSubmit className={`${baseClass}__publish`} disabled={disabled} onClick={save}>
@@ -80,20 +101,25 @@ const PublishButton: React.FC<{ action: string; disabled: boolean }> = ({ action
80101
)
81102
}
82103

83-
const SaveDraftButton: React.FC<{ action: string; disabled: boolean }> = ({ action, disabled }) => {
104+
const SaveDraftButton: React.FC<{
105+
action: string
106+
disabled: boolean
107+
selected?: FieldWithPathClient[]
108+
}> = ({ action, disabled, selected }) => {
84109
const { submit } = useForm()
85110
const { t } = useTranslation()
86111

87112
const save = useCallback(() => {
88113
void submit({
89114
action,
90115
method: 'PATCH',
91-
overrides: {
116+
overrides: (formState) => ({
117+
...sanitizeUnselectedFields(formState, selected),
92118
_status: 'draft',
93-
},
119+
}),
94120
skipValidation: true,
95121
})
96-
}, [action, submit])
122+
}, [action, submit, selected])
97123

98124
return (
99125
<FormSubmit
@@ -125,7 +151,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
125151

126152
const { count, getQueryParams, selectAll } = useSelection()
127153
const { i18n, t } = useTranslation()
128-
const [selected, setSelected] = useState([])
154+
const [selected, setSelected] = useState<FieldWithPathClient[]>([])
129155
const searchParams = useSearchParams()
130156
const router = useRouter()
131157
const [initialState, setInitialState] = useState<FormState>()
@@ -184,7 +210,7 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
184210

185211
return state
186212
},
187-
[slug, getFormState, collectionPermissions],
213+
[getFormState, slug, collectionPermissions],
188214
)
189215

190216
useEffect(() => {
@@ -289,16 +315,19 @@ export const EditMany: React.FC<EditManyProps> = (props) => {
289315
<SaveDraftButton
290316
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
291317
disabled={selected.length === 0}
318+
selected={selected}
292319
/>
293320
<PublishButton
294321
action={`${serverURL}${apiRoute}/${slug}${queryString}&draft=true`}
295322
disabled={selected.length === 0}
323+
selected={selected}
296324
/>
297325
</React.Fragment>
298326
) : (
299327
<Submit
300328
action={`${serverURL}${apiRoute}/${slug}${queryString}`}
301329
disabled={selected.length === 0}
330+
selected={selected}
302331
/>
303332
)}
304333
</div>

packages/ui/src/elements/FieldSelect/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { ClientField, FieldWithPath, FormState } from 'payload'
2+
import type { ClientField, FieldWithPathClient, FormState } from 'payload'
33

44
import { fieldAffectsData, fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
55
import React, { Fragment, useState } from 'react'
@@ -16,7 +16,7 @@ const baseClass = 'field-select'
1616

1717
export type FieldSelectProps = {
1818
readonly fields: ClientField[]
19-
readonly setSelected: (fields: FieldWithPath[]) => void
19+
readonly setSelected: (fields: FieldWithPathClient[]) => void
2020
}
2121

2222
export const combineLabel = ({
@@ -56,7 +56,7 @@ const reduceFields = ({
5656
formState?: FormState
5757
labelPrefix?: React.ReactNode
5858
path?: string
59-
}): { Label: React.ReactNode; value: FieldWithPath }[] => {
59+
}): { Label: React.ReactNode; value: FieldWithPathClient }[] => {
6060
if (!fields) {
6161
return []
6262
}

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

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import React, { useCallback, useEffect, useReducer, useRef, useState } from 'rea
1414
import { toast } from 'sonner'
1515

1616
import type {
17+
CreateFormData,
1718
Context as FormContextType,
1819
FormProps,
1920
GetDataByPath,
@@ -174,7 +175,7 @@ export const Form: React.FC<FormProps> = (props) => {
174175
const {
175176
action: actionArg = action,
176177
method: methodToUse = method,
177-
overrides = {},
178+
overrides: overridesFromArgs = {},
178179
skipValidation,
179180
} = options
180181

@@ -263,17 +264,23 @@ export const Form: React.FC<FormProps> = (props) => {
263264
return
264265
}
265266

267+
let overrides = {}
268+
269+
if (typeof overridesFromArgs === 'function') {
270+
overrides = overridesFromArgs(contextRef.current.fields)
271+
} else if (typeof overridesFromArgs === 'object') {
272+
overrides = overridesFromArgs
273+
}
274+
266275
// If submit handler comes through via props, run that
267276
if (onSubmit) {
268277
const serializableFields = deepCopyObjectSimpleWithoutReactComponents(
269278
contextRef.current.fields,
270279
)
271280
const data = reduceFieldsToValues(serializableFields, true)
272281

273-
if (overrides) {
274-
for (const [key, value] of Object.entries(overrides)) {
275-
data[key] = value
276-
}
282+
for (const [key, value] of Object.entries(overrides)) {
283+
data[key] = value
277284
}
278285

279286
onSubmit(serializableFields, data)
@@ -288,7 +295,9 @@ export const Form: React.FC<FormProps> = (props) => {
288295
return
289296
}
290297

291-
const formData = contextRef.current.createFormData(overrides)
298+
const formData = contextRef.current.createFormData(overrides, {
299+
mergeOverrideData: Boolean(typeof overridesFromArgs !== 'function'),
300+
})
292301

293302
try {
294303
let res
@@ -443,23 +452,26 @@ export const Form: React.FC<FormProps> = (props) => {
443452
[],
444453
)
445454

446-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
447-
const createFormData = useCallback((overrides: any = {}) => {
448-
const data = reduceFieldsToValues(contextRef.current.fields, true)
455+
const createFormData = useCallback<CreateFormData>((overrides, { mergeOverrideData = true }) => {
456+
let data = reduceFieldsToValues(contextRef.current.fields, true)
449457

450458
const file = data?.file
451459

452460
if (file) {
453461
delete data.file
454462
}
455463

456-
const dataWithOverrides = {
457-
...data,
458-
...overrides,
464+
if (mergeOverrideData) {
465+
data = {
466+
...data,
467+
...overrides,
468+
}
469+
} else {
470+
data = overrides
459471
}
460472

461473
const dataToSerialize = {
462-
_payload: JSON.stringify(dataWithOverrides),
474+
_payload: JSON.stringify(data),
463475
file,
464476
}
465477

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export type FormProps = {
6060
export type SubmitOptions = {
6161
action?: string
6262
method?: string
63-
overrides?: Record<string, unknown>
63+
overrides?: ((formState) => FormData) | Record<string, unknown>
6464
skipValidation?: boolean
6565
}
6666

@@ -70,7 +70,14 @@ export type Submit = (
7070
e?: React.FormEvent<HTMLFormElement>,
7171
) => Promise<void>
7272
export type ValidateForm = () => Promise<boolean>
73-
export type CreateFormData = (overrides?: any) => FormData
73+
export type CreateFormData = (
74+
overrides?: Record<string, unknown>,
75+
/**
76+
* If mergeOverrideData true, the data will be merged with the existing data in the form state.
77+
* @default true
78+
*/
79+
options?: { mergeOverrideData?: boolean },
80+
) => FormData
7481
export type GetFields = () => FormState
7582
export type GetField = (path: string) => FormField
7683
export type GetData = () => Data

test/admin/collections/Posts.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,64 @@ export const Posts: CollectionConfig = {
103103
},
104104
],
105105
},
106+
{
107+
name: 'arrayOfFields',
108+
type: 'array',
109+
admin: {
110+
initCollapsed: true,
111+
},
112+
fields: [
113+
{
114+
name: 'optional',
115+
type: 'text',
116+
},
117+
{
118+
name: 'innerArrayOfFields',
119+
type: 'array',
120+
fields: [
121+
{
122+
name: 'innerOptional',
123+
type: 'text',
124+
},
125+
],
126+
},
127+
],
128+
},
106129
{
107130
name: 'group',
108131
type: 'group',
109132
fields: [
133+
{
134+
name: 'defaultValueField',
135+
type: 'text',
136+
defaultValue: 'testing',
137+
},
110138
{
111139
name: 'title',
112140
type: 'text',
113141
},
114142
],
115143
},
144+
{
145+
name: 'someBlock',
146+
type: 'blocks',
147+
blocks: [
148+
{
149+
slug: 'textBlock',
150+
fields: [
151+
{
152+
name: 'textFieldForBlock',
153+
type: 'text',
154+
},
155+
],
156+
},
157+
],
158+
},
159+
{
160+
name: 'defaultValueField',
161+
type: 'text',
162+
defaultValue: 'testing',
163+
},
116164
{
117165
name: 'relationship',
118166
type: 'relationship',

0 commit comments

Comments
 (0)