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.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, FieldWithPathClient } from 'payload'
|
||||
import type { ClientCollectionConfig, FieldWithPathClient, SelectType } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { unflatten } from 'payload/shared'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { FormProps } from '../../../forms/Form/index.js'
|
||||
import type { OnFieldSelect } from '../../FieldSelect/index.js'
|
||||
import type { State } from '../FormsManager/reducer.js'
|
||||
|
||||
import { Button } from '../../../elements/Button/index.js'
|
||||
@@ -14,8 +16,9 @@ import { Form } from '../../../forms/Form/index.js'
|
||||
import { RenderFields } from '../../../forms/RenderFields/index.js'
|
||||
import { XIcon } from '../../../icons/X/index.js'
|
||||
import { useAuth } from '../../../providers/Auth/index.js'
|
||||
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { filterOutUploadFields } from '../../../utilities/filterOutUploadFields.js'
|
||||
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
|
||||
import { FieldSelect } from '../../FieldSelect/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
|
||||
@@ -29,19 +32,62 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
|
||||
} & EditManyBulkUploadsProps
|
||||
> = (props) => {
|
||||
const {
|
||||
collection: { slug, fields, labels: { plural, singular } } = {},
|
||||
collection: { fields, labels: { plural, singular } } = {},
|
||||
collection,
|
||||
drawerSlug,
|
||||
forms,
|
||||
} = props
|
||||
|
||||
const [isInitializing, setIsInitializing] = useState(false)
|
||||
const { permissions } = useAuth()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { closeModal } = useModal()
|
||||
const { bulkUpdateForm } = useFormsManager()
|
||||
const { getFormState } = useServerFunctions()
|
||||
const abortFormStateRef = React.useRef<AbortController>(null)
|
||||
|
||||
const [selectedFields, setSelectedFields] = useState<FieldWithPathClient[]>([])
|
||||
const collectionPermissions = permissions?.collections?.[slug]
|
||||
const filteredFields = filterOutUploadFields(fields)
|
||||
const collectionPermissions = permissions?.collections?.[collection.slug]
|
||||
|
||||
const select = useMemo<SelectType>(() => {
|
||||
return unflatten(
|
||||
selectedFields.reduce((acc, field) => {
|
||||
acc[field.path] = true
|
||||
return acc
|
||||
}, {} as SelectType),
|
||||
)
|
||||
}, [selectedFields])
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState, submitted }) => {
|
||||
const controller = handleAbortRef(abortFormStateRef)
|
||||
|
||||
const { state } = await getFormState({
|
||||
collectionSlug: collection.slug,
|
||||
docPermissions: collectionPermissions,
|
||||
docPreferences: null,
|
||||
formState: prevFormState,
|
||||
operation: 'update',
|
||||
schemaPath: collection.slug,
|
||||
select,
|
||||
signal: controller.signal,
|
||||
skipValidation: !submitted,
|
||||
})
|
||||
|
||||
abortFormStateRef.current = null
|
||||
|
||||
return state
|
||||
},
|
||||
[getFormState, collection, collectionPermissions, select],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const abortFormState = abortFormStateRef.current
|
||||
|
||||
return () => {
|
||||
abortAndIgnore(abortFormState)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSubmit: FormProps['onSubmit'] = useCallback(
|
||||
(formState) => {
|
||||
@@ -58,6 +104,42 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
|
||||
[closeModal, drawerSlug, bulkUpdateForm, selectedFields],
|
||||
)
|
||||
|
||||
const onFieldSelect = useCallback<OnFieldSelect>(
|
||||
async ({ dispatchFields, formState, selected }) => {
|
||||
setIsInitializing(true)
|
||||
|
||||
if (selected === null) {
|
||||
setSelectedFields([])
|
||||
} else {
|
||||
setSelectedFields(selected.map(({ value }) => value))
|
||||
}
|
||||
|
||||
const { state } = await getFormState({
|
||||
collectionSlug: collection.slug,
|
||||
docPermissions: collectionPermissions,
|
||||
docPreferences: null,
|
||||
formState,
|
||||
operation: 'update',
|
||||
schemaPath: collection.slug,
|
||||
select: unflatten(
|
||||
selected.reduce((acc, option) => {
|
||||
acc[option.value.path] = true
|
||||
return acc
|
||||
}, {} as SelectType),
|
||||
),
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
dispatchFields({
|
||||
type: 'UPDATE_MANY',
|
||||
formState: state,
|
||||
})
|
||||
|
||||
setIsInitializing(false)
|
||||
},
|
||||
[getFormState, collection, collectionPermissions],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__main`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
@@ -77,14 +159,19 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<Form className={`${baseClass}__form`} initialState={{}} onSubmit={handleSubmit}>
|
||||
<FieldSelect fields={filteredFields} setSelected={setSelectedFields} />
|
||||
<Form
|
||||
className={`${baseClass}__form`}
|
||||
isInitializing={isInitializing}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<FieldSelect fields={fields} onChange={onFieldSelect} />
|
||||
{selectedFields.length === 0 ? null : (
|
||||
<RenderFields
|
||||
fields={selectedFields}
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={slug}
|
||||
parentSchemaPath={collection.slug}
|
||||
permissions={collectionPermissions?.fields}
|
||||
readOnly={false}
|
||||
/>
|
||||
|
||||
@@ -9,8 +9,8 @@ import { EditDepthProvider } from '../../../providers/EditDepth/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { Drawer, DrawerToggler } from '../../Drawer/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
import './index.scss'
|
||||
import { EditManyBulkUploadsDrawerContent } from './DrawerContent.js'
|
||||
import './index.scss'
|
||||
|
||||
export const baseClass = 'edit-many-bulk-uploads'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user