Previously, if you were querying a collection that has a join field with
`draft: true`, and the join field's collection also has
`versions.drafts: true` our db adapter would still query the original
SQL table / mongodb collection instead of the versions one which isn't
quite right since we respect `draft: true` when populating relationships
Passes the `i18n` arg through field label and description functions.
This is to avoid using custom components when simply needing to
translate a `StaticLabel` object, such as collection labels.
Here's an example:
```ts
{
labels: {
singular: {
en: 'My Collection'
}
},
fields: [
// ...
{
type: 'collapsible',
label: ({ i18n }) => `Translate this: ${getTranslation(collectionConfig.labels.singular, i18n)}`
// ...
}
]
}
```
### What?
When a user lands on an edit page that has a relationship to an `Upload`
field (which is `HasMany`). The UI will make a request with `limit=0` to
the backend providing there is no IDs populated already.
When a media collection is large, it will try and load all media items
into memory which causes OOM crashes.
### Why?
Fixes: https://github.com/payloadcms/payload/issues/11655 causing OOM
issues.
### How?
Adding guard check on the `populate` to ensure that it doesn't make a
request if not needed.
https://github.com/user-attachments/assets/f195025f-3e31-423e-b13e-6faf8db40129
Fixes#10019. When bulk editing subfields, such as a field within a
group, changes are not persisted to the database. Not only this, but
nested fields with the same name as another selected field are
controlled by the same input. E.g. typing into one fields changes the
value of both.
The root problem is that field paths are incorrect.
When opening the bulk edit drawer, fields are flattened into options for
the field selector. This is so that fields in a tab, for example, aren't
hidden behind their tab when bulk editing. The problem is that
`RenderFields` is not set up to receive pre-determined field paths. It
attempts to build up its own field paths, but are never correct because
`getFieldPaths` receives the wrong arguments.
The fix is to just render the top-level fields directly, bypassing
`RenderFields` altogether.
Fields with subfields will still recurse through this function, but at
the top-level, fields can be sent directly to `RenderField` (singular)
since their paths have already been already formatted in the flattening
step.
If the `useAsTitle` property is defined referencing a richtext field,
the admin panel throws errors in several places.
I noticed this in the email builder plugin, where we're making the
subject field (which is the title) a single-paragraph richtext field
instead of a text field for technical reasons.
In this PR, for the lexical richtext case, I'm converting the first
child of the RootNode (usually a paragraph or heading) to plain text.
Additionally, I am verifying that if the resulting title is not of type
string, fallback to "untitled" so that this does not happen again in the
future (perhaps with slate, or with other fields).
The form component's `initializing` and `processing` states do not
disable fields that are rendered outside of `DocumentFields`. Fields
currently rely on the `readOnly` prop provided by `DocumentFields` and
do not subscribe to these states for themselves. This means that fields
that are rendered outright, such as within the bulk edit drawer, they do
not receive a `readOnly` prop and are therefore never disabled.
The fix is add a `disabled` property to the `useField` hook. This
subscribes to the `initializing` and `processing` states in the same way
as `DocumentFields`, however, now each field can determine its own
disabled state instead of relying solely on the `readOnly` prop. Adding
this new prop has no overhead as `processing` and `initializing` is
already being subscribed to within `useField`.
This PR updates the field `validate` function property to include a new
`path` argument.
The `path` arg provides the schema path of the field, including array
indices where applicable.
#### Changes:
- Added `path: (number | string)[]` in the ValidateOptions type.
### What?
We have the option to set `displayPreview: true || false` on upload
collections / upload fields - with the **field** option taking
precedence.
Currently, `displayPreview` is only affecting the list view for the
**_related_** collection.
i.e. if you go to a collection that has an upload field - the preview
will be hidden/shown correctly according to the `displayPreview` option.
<img width="620" alt="Screenshot 2025-03-03 at 12 38 18 PM"
src="https://github.com/user-attachments/assets/c11c2a84-0f64-4a08-940e-8c3f9096484b"
/>
However, when you go directly to the upload collection and look at the
list view - the preview is always shown, not affected by the
`displayPreview` option.
<img width="446" alt="Screenshot 2025-03-03 at 12 39 24 PM"
src="https://github.com/user-attachments/assets/f5e1267a-d98a-4c8c-8d54-93dea6cd2e31"
/>
Also, we have previews within the file field itself - also not being
affected by the `displayPreview` option.
<img width="528" alt="Screenshot 2025-03-03 at 12 40 06 PM"
src="https://github.com/user-attachments/assets/3dd04c9a-3d9f-4823-90f8-b538f3d420f9"
/>
All the upload related previews (excluding preview sizes and upload
editing options) should be affected by the `displayPreview` option.
### How?
Checks for `collection.displayPreview` and `field.displayPreview` in all
places where previews are displayed.
Closes#11404
Two small separate issues here (1) and (2):
### What?
1. Excess margin is displayed when a row is hidden due to
`admin.condition`
2. The `admin.style` props is never passed to the `row` field
### Why?
1. Unlike other fields, the `row` field still gets rendered when
`admin.condition` returns false - this is because the logic gets passed
down to the fields within the row
2. `style` was never being threaded to the `row` field wrapper
### How?
1. Hides the row using css to `display: none` when no children are
present
2. Passes `admin.styles` to the `row` wrapper
Fixes#11477
Fixes https://github.com/payloadcms/payload/issues/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`
Adds a new `admin.disableBlockName` property that allows you to disable
the blockName field entirely in the admin view. It defaults to false for
backwards compatibility.
### What?
Unable to update json fields externally. For example, calling `setValue`
on a json field would not be reflected in the admin panel UI.
### Why?
JSON fields use the monaco editor to manage state internally, so
programmatically updating the value in state does not change the
internal value.
### How?
Set a ref when the user updates the value and then unset the ref after
the change is complete.
Inside the hook that watches `value`, if the value changed and the
change came from the system (i.e. a programmatic change) refresh the
editor by adjusting its key prop. If the change was made by the user,
there is no need to refresh the editor.
Fixes https://github.com/payloadcms/payload/issues/10819
When rendering a list drawer, you can pass a custom `onSelect` callback
to execute when the user clicks on the linked cell within the table. The
underlying handler, however, only passes the `docID` and
`collectionSlug` args through the callback, rather than the document
itself. This makes it impossible to perform side-effects that require
the data of the row that was selected.
Instances of this callback were also largely untyped.
Needed for #11330.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR introduces support for the `admin.group` property in block
configs. This property enables blocks to be grouped under a common,
potentially localized, label in the block drawer component. This makes
it easier to sort through large collections of blocks. Previously, all
blocks would be in one common layout.
This PR also encompasses documentation changes and e2e tests to check
for the rendering of group labels.
### Why?
To make it easier to organize many blocks in block fields.
### How?
By introducing a new `admin.group` property in block configs and
assembling them in the blocks drawer component.
Before:

After:

Demo:
[Editing---Block-Field---Payload-groups-demo.webm](https://github.com/user-attachments/assets/2b351dc1-0d14-4a5b-ae71-bcd31fbb23df)
Addresses #5609
Fixes https://github.com/payloadcms/payload/issues/10962
This fix addresses fields with timezones enabled specifically for not
time pickers. If all you want to do is pick a date such as 14th Feb, it
would store the incorrect version and display a date in the future for
people in the Pacific.
This is because Auckland is +12 offset, but +13 with Daylight Savings
Time. In our date picker we try to normalise date pickers with no time
to 12pm and so half the year we ended up pushing dates visually to the
next day for people in the pacific only. Other regions were not affected
by this because their offset would be less than 12.
This PR fixes this by ensuring that our dates are always normalised to
selected timezone's 12pm date to UTC.
There's also additional tests for these two fields from 3 main locations
to cover a wider range of possible timezones.
This feature allows you to specify `collection` for the join field as
array.
This can be useful for example to describe relationship linking like
this:
```ts
{
slug: 'folders',
fields: [
{
type: 'join',
on: 'folder',
collection: ['files', 'documents', 'folders'],
name: 'children',
},
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
{
slug: 'files',
upload: true,
fields: [
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
{
slug: 'documents',
fields: [
{
type: 'relationship',
relationTo: 'folders',
name: 'folder',
},
],
},
```
Documents and files can be placed to folders and folders themselves can
be nested to other folders (root folders just have `folder` as `null`).
Output type of `Folder`:
```ts
export interface Folder {
id: string;
children?: {
docs?:
| (
| {
relationTo?: 'files';
value: string | File;
}
| {
relationTo?: 'documents';
value: string | Document;
}
| {
relationTo?: 'folders';
value: string | Folder;
}
)[]
| null;
hasNextPage?: boolean | null;
} | null;
folder?: (string | null) | Folder;
updatedAt: string;
createdAt: string;
}
```
While you could instead have many join fields (for example
`childrenFolders`, `childrenFiles`) etc - this doesn't allow you to
sort/filter and paginate things across many collections, which isn't
trivial. With SQL we use `UNION ALL` query to achieve that.
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
When reusing the SelectInput component from the UI package, if you set
value to `''` it will continue to display the previously selected value
instead of clearing out the field as expected.
The ReactSelect component doesn't behave in this way and instead will
clear out the field.
This fix addresses this difference by resetting `valueToRender` inside
the SelectInput to null.
### What?
Two new configuration properties added for upload enabled collections.
- *hideFileInputOnCreate* - Set to `true` to prevent the admin UI from
showing file inputs during document creation, useful for programmatic
file generation.
- *hideRemoveFile* - Set to `true` to prevent the admin UI having a way
to remove an existing file while editing.
### Why?
When using file uploads that get created programmatically in
`beforeOperation` hooks or files created using `jobs`, or when
`filesRequiredOnCreate` is false, you may want to use these new flags to
prevent users from interacting with these controls.
### How?
The new properties only impact the admin UI components to dial in the UX
for various use cases.
Screenshot showing that the upload controls are not available on create:

Screenshot showing hideRemoveFile has removed the ability to remove the
existing file:

Prerequisite for https://github.com/payloadcms/payload/pull/10795
### What?
This PR fixes an issue where a deleted relationship entry would lead to
a runtime error if the user clicked on the edit button in ui due to not
having a `doc` available in `handleServerFunction`.
### Why?
To prevent runtime errors during expected usage.
### How?
By hiding the edit button in entries that have been deleted. This is
done for entries where the user does not have read access already.
Fixes#11004
Before:
[Editing---Post-userdelete--before--Payload.webm](https://github.com/user-attachments/assets/33180eba-9be3-418f-92d2-3bad93e3dfae)
After:
[Editing---Post-userdelete--after--Payload.webm](https://github.com/user-attachments/assets/ba1a736b-3422-4fe0-93ae-7e8e6496d1bd)
### What?
Assuming you have a hook in your collection that is looking for certain
conditions to be met related to the join field. The way you would
prevent it is to throw a `new ValidationError()` with errors containing
the path of the field. Previously, the error message for the field would
not show in the admin UI at all.
### Why?
Users need to be able to see any custom error messages for joins field
in the UI so they can address the issue.
### How?
Adds an error class and display the FieldError in the Join field in the
UI component.
If you have multiple blocks that are used in multiple places, this can quickly blow up the size of your Payload Config. This will incur a performance hit, as more data is
1. sent to the client (=> bloated `ClientConfig` and large initial html) and
2. processed on the server (permissions are calculated every single time you navigate to a page - this iterates through all blocks you have defined, even if they're duplicative)
This can be optimized by defining your block **once** in your Payload Config, and just referencing the block slug whenever it's used, instead of passing the entire block config. To do this, the block can be defined in the `blocks` array of the Payload Config. The slug can then be passed to the `blockReferences` array in the Blocks Field - the `blocks` array has to be empty for compatibility reasons.
```ts
import { buildConfig } from 'payload'
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'
// Payload Config
const config = buildConfig({
// Define the block once
blocks: [
{
slug: 'TextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
collections: [
{
slug: 'collection1',
fields: [
{
name: 'content',
type: 'blocks',
// Reference the block by slug
blockReferences: ['TextBlock'],
blocks: [], // Required to be empty, for compatibility reasons
},
],
},
{
slug: 'collection2',
fields: [
{
name: 'editor',
type: 'richText',
editor: lexicalEditor({
BlocksFeature({
// Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
blocks: ['TextBlock'],
})
})
},
],
},
],
})
```
## v4.0 Plans
In 4.0, we will remove the `blockReferences` property, and allow string block references to be passed directly to the blocks `property`. Essentially, we'd remove the `blocks` property and rename `blockReferences` to `blocks`.
The reason we opted to a new property in this PR is to avoid breaking changes. Allowing strings to be passed to the `blocks` property will prevent plugins that iterate through fields / blocks from compiling.
## PR Changes
- Testing: This PR introduces a plugin that automatically converts blocks to block references. This is done in the fields__blocks test suite, to run our existing test suite using block references.
- Block References support: Most changes are similar. Everywhere we iterate through blocks, we have to now do the following:
1. Check if `field.blockReferences` is provided. If so, only iterate through that.
2. Check if the block is an object (= actual block), or string
3. If it's a string, pull the actual block from the Payload Config or from `payload.blocks`.
The exception is config sanitization and block type generations. This PR optimizes them so that each block is only handled once, instead of every time the block is referenced.
## Benchmarks
60 Block fields, each block field having the same 600 Blocks.
### Before:
**Initial HTML:** 195 kB
**Generated types:** takes 11 minutes, 461,209 lines
https://github.com/user-attachments/assets/11d49a4e-5414-4579-8050-e6346e552f56
### After:
**Initial HTML:** 73.6 kB
**Generated types:** takes 2 seconds, 35,810 lines
https://github.com/user-attachments/assets/3eab1a99-6c29-489d-add5-698df67780a3
### After Permissions Optimization (follow-up PR)
Initial HTML: 73.6 kB
https://github.com/user-attachments/assets/a909202e-45a8-4bf6-9a38-8c85813f1312
## Future Plans
1. This PR does not yet deduplicate block references during permissions calculation. We'll optimize that in a separate PR, as this one is already large enough
2. The same optimization can be done to deduplicate fields. One common use-case would be link field groups that may be referenced in multiple entities, outside of blocks. We might explore adding a new `fieldReferences` property, that allows you to reference those same `config.blocks`.
### What?
This PR displays file size in upload cards for all upload mimetypes. The
current behavior hides this metric from the user if the file mimetype
does not start with `image`.
### Why?
Showing end-users and editors a file size is universally useful - not
only for images, but for all types of files that can be uploaded via the
upload field.
### How?
By making the predicate that adds this metric less restrictive. Instead
of checking if the mimetype is image-like, it checks if the file size is
truthy.
Before:

After:

Adds support for timezone selection on date fields.
### Summary
New `admin.timezones` config:
```ts
{
// ...
admin: {
// ...
timezones: {
supportedTimezones: ({ defaultTimezones }) => [
...defaultTimezones,
{ label: '(GMT-6) Monterrey, Nuevo Leon', value: 'America/Monterrey' },
],
defaultTimezone: 'America/Monterrey',
},
}
}
```
New `timezone` property on date fields:
```ts
{
type: 'date',
name: 'date',
timezone: true,
}
```
### Configuration
All date fields now accept `timezone: true` to enable this feature,
which will inject a new field into the configuration using the date
field's name to construct the name for the timezone column. So
`publishingDate` will have `publishingDate_tz` as an accompanying
column. This new field is inserted during config sanitisation.
Dates continue to be stored in UTC, this will help maintain dates
without needing a migration and it makes it easier for data to be
manipulated as needed. Mongodb also has a restriction around storing
dates only as UTC.
All timezones are stored by their IANA names so it's compatible with
browser APIs. There is a newly generated type for `SupportedTimezones`
which is reused across fields.
We handle timezone calculations via a new package `@date-fns/tz` which
we will be using in the future for handling timezone aware scheduled
publishing/unpublishing and more.
### UI
Dark mode

Light mode

Fixes https://github.com/payloadcms/payload/issues/10940
This PR does the following:
- adds a `useDocumentForm` hook to access the document Form. Useful if
you are within a sub-Form
- ensure the `data` property passed to field conditions, read access
control, validation and filterOptions is always the top-level document
data. Previously, for fields within lexical blocks/links/upload, this
incorrectly was the lexical block-level data.
- adds a `blockData` property to hooks, field conditions,
read/update/create field access control, validation and filterOptions
for all fields. This allows you to access the data of the nearest parent
block, which is especially useful for lexical sub-fields. Users that
were previously depending on the incorrect behavior of the `data`
property in order to access the data of the lexical block can now switch
to the new `blockData` property
The `useIgnoredEffect` hook is useful in firing an effect only when a _subset_ of dependencies change, despite subscribing to many dependencies. But the previous implementation of `useIgnoredEffect` had a few problems:
- The effect did not receive the updated values of `ignoredDeps` - thus, `useIgnoredEffect` pretty much worked the same way as using `useEffect` and omitting said dependencies from the dependency array. This caused the `ignoredDeps` values to be stale.
- It compared objects by value instead of reference, which is slower and behaves differently than `useEffect` itself.
- Edge cases where the effect does not run even though the dependencies have changed. E.g. if an `ignoredDep` has value `null` and a `dep` changes its value from _something_ to `null`, the effect incorrectly does **not** run, as the current logic detects that said value is part of `ignoredDeps` => no `dep` actually changed.
This PR replaces the `useIgnoredEffect` hook with a new pattern which to combine `useEffect` with a new `useEffectEvent` hook as described here: https://react.dev/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects. While this is not available in React 19 stable, there is a polyfill available that's already used in several big projects (e.g. react-spectrum and bluesky).
### What?
This updates the UX of `TextFields` with `hasMany: true` by:
- Removing the dropdown menu and its indicator
- Removing the ClearIndicator
- Making text items directly editable
### Why?
- The dropdown didn’t enhance usability.
- The ClearIndicator removed all values at once with no way to undo,
risking accidental data loss. Backspace still allows quick and
intentional clearing.
- Previously, text items could only be removed and re-added, but not
edited inline. Allowing inline editing improves the editing experience.
### How?
https://github.com/user-attachments/assets/02e8cc26-7faf-4444-baa1-39ce2b4547fa
### What?
This PR fixes an issue for tab subfield permissions
### Why?
Permissions were not being correctly extracted when passing them down.
### How?
Properly extracts permissions when rendering fields inside the active tab.
Fixes#10720
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Field validations currently run very often, such as within form state on
type. This can lead to serious performance implications within the admin
panel if those validation functions are async, especially if they
perform expensive database queries. One glaring example of this is how
all relationship and upload fields perform a database lookup in order to
evaluate that the given value(s) satisfy the defined filter options. If
the field is polymorphic, this can happen multiple times over, one for
each collection. Similarly, custom validation functions might also
perform expensive tasks, something that Payload has no control over.
The fix here is two-fold. First, we now provide a new `event` arg to all
`validate` functions that allow you to opt-in to performing expensive
operations _only when documents are submitted_, and fallback to
significantly more performant validations as form state is generated.
This new pattern will be the new default for relationship and upload
fields, however, any custom validation functions will need to be
implemented in this way in order to take advantage of it. Here's what
that might look like:
```
[
// ...
{
name: 'text'
type: 'text',
validate: async (value, { event }) => {
if (event === 'onChange') {
// Do something highly performant here
return true
}
// Do something more expensive here
return true
}
}
]
```
The second part of this is to only run validations _after the form as
been submitted_, and then every change event thereafter. This work is
being done in #10580.
- Fixes collection and tab descriptions not having a bottom padding:
- Deprecates `description` on tabs config in favour of
`admin.description` but supports both
- Adds test to make sure `admin.description` renders with static string
and function too for tabs
Custom block row labels defined on `admin.components.Label` were not
rendering despite existing in the config. Instead, if a custom label
component was defined on the _top-level_ blocks field itself, it was
incorrectly replacing each blocks label _in addition_ to the field's
label. Now, custom labels defined at the field-level now only replace
the field's label as expected, and custom labels defined at the
block-level are now supported as the types suggest.
Fixes#9882 and #9691
In 2.0, we would accept data coming back from an update operation and
then reflect those changes in UI.
However, in 3.0, we did not do that anymore - meaning you could change a
document with hooks in `beforeChange` or `afterChange`, but then not see
the changes made on the server.
This PR updates the way that `mergeServerFormState` works, and adds a
property to optionally allow values from server form state - which can
then be used in the `onSuccess` form handler which may need to define
new field values.
### What?
This PR aims to prevent a runtime error by allowing deletions in media
collections and preventing them from impacting workflows while using
`hasMany` upload fields.
### Why?
To prevent runtime issues during common tasks.
### How?
By treating deleted media documents as empty placeholders instead of
undefined objects.
Fixes#9328
Before:
[Dashboard-before--Payload.webm](https://github.com/user-attachments/assets/32cbfe1d-2569-43bc-be05-abd2d9913b9b)
After:
[Dashboard-after--Payload.webm](https://github.com/user-attachments/assets/b5cb67b8-21de-403c-879c-680e49fd380c)
Notes:
- There is room for improvement here: allow selection of existing media
in upload collection instead of forcing user to upload new. Workaround
is to remove the row and choose existing as usual.
- New uploads chosen will always replace the first instance of undefined
placeholder due to the simple check. If you have 3 deleted entries and
open the drawer on the second one, then it will populate the first one
anyway. This can be improved but will require more laborious code
changes.
- Noticed an issue where deleted non-hasMany upload fields do not
re-render after a file is uploaded. It only shows the previous deleted
doc, ie id: "untitled - ID: ...". Re-renders correctly when choosing
from existing. This is unrelated to this PR. I will investigate further.
- I'm realizing this actually diverges from what `db-postgres` does, as
deleted entries in a `hasMany` upload gets removed from the field in the
initial deletion probably due to cascading the deletion...
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR fixes an issue where multiple `upload` fields would sequentially
overwrite the `BulkUpload` internal `onSuccess` function causing new
uploads to populate the incorrect field from which the interaction
started.
### Why?
Sequential `upload` fields use a `useEffect` to set the success function
of the `BulkUpload` provider component, however this did not take into
account many `upload` fields in a single document. This PR prevents many
`upload` fields from overriding their sibling's `onSuccess` function in
order to populate those fields correctly.
### How?
By changing the way the bulk upload component handles success functions
from a singular function to a map of functions based on a string path of
the field, or if necessary, using a collection slug in the case of a
bulk upload on an `upload` collection list view.
Fixes#10177
Before (One hasMany, one single):
[Editing-hasmany-single--Post-before--Payload.webm](https://github.com/user-attachments/assets/01aeaa64-a065-4e66-8ab4-6bb9d4fa8556)
Before (Many hasMany):
[Editing-hasmany-two--Post-before--Payload.webm](https://github.com/user-attachments/assets/a65c58aa-9a15-4cca-b2c4-17484c020ddc)
After (One hasMany, one single):
[Editing-hasmany-single--Post-after--Payload.webm](https://github.com/user-attachments/assets/7206f94e-4ce2-41b3-8b45-625f4974d28d)
After (Many hasMany):
[Editing-hasmany-two--Post-after--Payload.webm](https://github.com/user-attachments/assets/72dbbdee-d4a5-4488-8ef0-3dd3918115a9)
Fixes#10070. Adding new blocks or array rows can randomly get stuck
within an infinite loading state. This was because the abort controllers
responsible for disregarding duplicate `onChange` and `onSave` events
was not properly resetting its refs across invocations. This caused
subsequent event handlers to incorrectly abort themselves, leading to
unresolved requests and a `null` form state. Similarly, the cleanup
effects responsible for aborting these requests on component unmount
were also referencing its `current` property directly off the refs,
which can possible be stale if not first set as a variable outside the
return function.
This PR also carries over some missing `onSave` logic from the default
edit view into the live preview view. In the future the logic between
these two views should be standardized, as they're nearly identical but
often become out of sync. This can likely be done through the use of
reusable hooks, such as `useOnSave`, `useOnChange`, etc. Same with the
document locking functionality which is complex and deeply integrated
into each of these views.