Skip to content

Commit d8bfb22

Browse files
authored
perf(ui): implements select in bulk edit (#11708)
Bulk edit can now request a partial form state thanks to #11689. This means that we only need to build form state (and send it through the network) for the currently selected fields, as opposed to the entire field schema. Not only this, but there is no longer a need to filter out unselected fields before submitting the form, as the form state will only ever include the currently selected fields. This is unnecessary processing and causes an excessive amount of rendering, especially since we were dispatching actions within a `for` loop to remove each field. React may have batched these updates, but is bad practice regardless. Related: stripping unselected fields was also error prone. This is because the `overrides` function we were using to do this receives `FormState` (shallow) as an argument, but was being treated as `Data` (not shallow, what the create and update operations expect). E.g. `{ myGroup.myTitle: { value: 'myValue' }}` → `{ myGroup: { myTitle: 'myValue' }}`. This led to the `sanitizeUnselectedFields` function improperly formatting data sent to the server and would throw an API error upon submission. This is only evident when sanitizing nested fields. Instead of converting this data _again_, the select API takes care of this by ensuring only selected fields exist in form state. Related: bulk upload was not hitting form state on change. This means that no field-level validation was occurring on type.
1 parent 11d7487 commit d8bfb22

File tree

8 files changed

+227
-158
lines changed

8 files changed

+227
-158
lines changed

packages/ui/src/elements/BulkUpload/EditMany/DrawerContent.tsx

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
'use client'
22

3-
import type { ClientCollectionConfig, FieldWithPathClient } from 'payload'
3+
import type { ClientCollectionConfig, FieldWithPathClient, SelectType } from 'payload'
44

55
import { useModal } from '@faceless-ui/modal'
66
import { getTranslation } from '@payloadcms/translations'
7-
import React, { useCallback, useState } from 'react'
7+
import { unflatten } from 'payload/shared'
8+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
89

910
import type { FormProps } from '../../../forms/Form/index.js'
11+
import type { OnFieldSelect } from '../../FieldSelect/index.js'
1012
import type { State } from '../FormsManager/reducer.js'
1113

1214
import { Button } from '../../../elements/Button/index.js'
1315
import { Form } from '../../../forms/Form/index.js'
1416
import { RenderFields } from '../../../forms/RenderFields/index.js'
1517
import { XIcon } from '../../../icons/X/index.js'
1618
import { useAuth } from '../../../providers/Auth/index.js'
19+
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
1720
import { useTranslation } from '../../../providers/Translation/index.js'
18-
import { filterOutUploadFields } from '../../../utilities/filterOutUploadFields.js'
21+
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
1922
import { FieldSelect } from '../../FieldSelect/index.js'
2023
import { useFormsManager } from '../FormsManager/index.js'
2124
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
@@ -29,19 +32,62 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
2932
} & EditManyBulkUploadsProps
3033
> = (props) => {
3134
const {
32-
collection: { slug, fields, labels: { plural, singular } } = {},
35+
collection: { fields, labels: { plural, singular } } = {},
36+
collection,
3337
drawerSlug,
3438
forms,
3539
} = props
3640

41+
const [isInitializing, setIsInitializing] = useState(false)
3742
const { permissions } = useAuth()
3843
const { i18n, t } = useTranslation()
3944
const { closeModal } = useModal()
4045
const { bulkUpdateForm } = useFormsManager()
46+
const { getFormState } = useServerFunctions()
47+
const abortFormStateRef = React.useRef<AbortController>(null)
4148

4249
const [selectedFields, setSelectedFields] = useState<FieldWithPathClient[]>([])
43-
const collectionPermissions = permissions?.collections?.[slug]
44-
const filteredFields = filterOutUploadFields(fields)
50+
const collectionPermissions = permissions?.collections?.[collection.slug]
51+
52+
const select = useMemo<SelectType>(() => {
53+
return unflatten(
54+
selectedFields.reduce((acc, field) => {
55+
acc[field.path] = true
56+
return acc
57+
}, {} as SelectType),
58+
)
59+
}, [selectedFields])
60+
61+
const onChange: FormProps['onChange'][0] = useCallback(
62+
async ({ formState: prevFormState, submitted }) => {
63+
const controller = handleAbortRef(abortFormStateRef)
64+
65+
const { state } = await getFormState({
66+
collectionSlug: collection.slug,
67+
docPermissions: collectionPermissions,
68+
docPreferences: null,
69+
formState: prevFormState,
70+
operation: 'update',
71+
schemaPath: collection.slug,
72+
select,
73+
signal: controller.signal,
74+
skipValidation: !submitted,
75+
})
76+
77+
abortFormStateRef.current = null
78+
79+
return state
80+
},
81+
[getFormState, collection, collectionPermissions, select],
82+
)
83+
84+
useEffect(() => {
85+
const abortFormState = abortFormStateRef.current
86+
87+
return () => {
88+
abortAndIgnore(abortFormState)
89+
}
90+
}, [])
4591

4692
const handleSubmit: FormProps['onSubmit'] = useCallback(
4793
(formState) => {
@@ -58,6 +104,42 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
58104
[closeModal, drawerSlug, bulkUpdateForm, selectedFields],
59105
)
60106

107+
const onFieldSelect = useCallback<OnFieldSelect>(
108+
async ({ dispatchFields, formState, selected }) => {
109+
setIsInitializing(true)
110+
111+
if (selected === null) {
112+
setSelectedFields([])
113+
} else {
114+
setSelectedFields(selected.map(({ value }) => value))
115+
}
116+
117+
const { state } = await getFormState({
118+
collectionSlug: collection.slug,
119+
docPermissions: collectionPermissions,
120+
docPreferences: null,
121+
formState,
122+
operation: 'update',
123+
schemaPath: collection.slug,
124+
select: unflatten(
125+
selected.reduce((acc, option) => {
126+
acc[option.value.path] = true
127+
return acc
128+
}, {} as SelectType),
129+
),
130+
skipValidation: true,
131+
})
132+
133+
dispatchFields({
134+
type: 'UPDATE_MANY',
135+
formState: state,
136+
})
137+
138+
setIsInitializing(false)
139+
},
140+
[getFormState, collection, collectionPermissions],
141+
)
142+
61143
return (
62144
<div className={`${baseClass}__main`}>
63145
<div className={`${baseClass}__header`}>
@@ -77,14 +159,19 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
77159
<XIcon />
78160
</button>
79161
</div>
80-
<Form className={`${baseClass}__form`} initialState={{}} onSubmit={handleSubmit}>
81-
<FieldSelect fields={filteredFields} setSelected={setSelectedFields} />
162+
<Form
163+
className={`${baseClass}__form`}
164+
isInitializing={isInitializing}
165+
onChange={[onChange]}
166+
onSubmit={handleSubmit}
167+
>
168+
<FieldSelect fields={fields} onChange={onFieldSelect} />
82169
{selectedFields.length === 0 ? null : (
83170
<RenderFields
84171
fields={selectedFields}
85172
parentIndexPath=""
86173
parentPath=""
87-
parentSchemaPath={slug}
174+
parentSchemaPath={collection.slug}
88175
permissions={collectionPermissions?.fields}
89176
readOnly={false}
90177
/>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { EditDepthProvider } from '../../../providers/EditDepth/index.js'
99
import { useTranslation } from '../../../providers/Translation/index.js'
1010
import { Drawer, DrawerToggler } from '../../Drawer/index.js'
1111
import { useFormsManager } from '../FormsManager/index.js'
12-
import './index.scss'
1312
import { EditManyBulkUploadsDrawerContent } from './DrawerContent.js'
13+
import './index.scss'
1414

1515
export const baseClass = 'edit-many-bulk-uploads'
1616

0 commit comments

Comments
 (0)