fix(ui): autosave hooks are not reflected in form state (#13416)

Fixes #10515. Needed for #12956.

Hooks run within autosave are not reflected in form state.

Similar to #10268, but for autosave events.

For example, if you are using a computed value, like this:

```ts
[
  // ...
  {
    name: 'title',
    type: 'text',
  },
  {
    name: 'computedTitle',
    type: 'text',
    hooks: {
      beforeChange: [({ data }) => data?.title],
    },
  },
]
```

In the example above, when an autosave event is triggered after changing
the `title` field, we expect the `computedTitle` field to match. But
although this takes place on the database level, the UI does not reflect
this change unless you refresh the page or navigate back and forth.

Here's an example:

Before:


https://github.com/user-attachments/assets/c8c68a78-9957-45a8-a710-84d954d15bcc

After:


https://github.com/user-attachments/assets/16cb87a5-83ca-4891-b01f-f5c4b0a34362

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210561273449855
This commit is contained in:
Jacob Fletcher
2025-08-11 16:59:03 -04:00
committed by GitHub
parent 9c8f3202e4
commit 1d81b0c6dd
14 changed files with 214 additions and 201 deletions

View File

@@ -84,7 +84,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: number;
defaultIDType: string;
};
globals: {
menu: Menu;
@@ -124,7 +124,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: number;
id: string;
title?: string | null;
content?: {
root: {
@@ -149,7 +149,7 @@ export interface Post {
* via the `definition` "media".
*/
export interface Media {
id: number;
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -193,7 +193,7 @@ export interface Media {
* via the `definition` "users".
*/
export interface User {
id: number;
id: string;
updatedAt: string;
createdAt: string;
email: string;
@@ -217,24 +217,24 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: number;
id: string;
document?:
| ({
relationTo: 'posts';
value: number | Post;
value: string | Post;
} | null)
| ({
relationTo: 'media';
value: number | Media;
value: string | Media;
} | null)
| ({
relationTo: 'users';
value: number | User;
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
updatedAt: string;
createdAt: string;
@@ -244,10 +244,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
id: string;
user: {
relationTo: 'users';
value: number | User;
value: string | User;
};
key?: string | null;
value?:
@@ -267,7 +267,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: number;
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "menu".
*/
export interface Menu {
id: number;
id: string;
globalText?: string | null;
updatedAt?: string | null;
createdAt?: string | null;

View File

@@ -53,6 +53,14 @@ const AutosavePosts: CollectionConfig = {
unique: true,
localized: true,
},
{
name: 'computedTitle',
label: 'Computed Title',
type: 'text',
hooks: {
beforeChange: [({ data }) => data?.title],
},
},
{
name: 'description',
label: 'Description',

View File

@@ -1285,6 +1285,44 @@ describe('Versions', () => {
// Remove listener
page.removeListener('dialog', acceptAlert)
})
test('- with autosave - applies afterChange hooks to form state after autosave runs', async () => {
const url = new AdminUrlUtil(serverURL, autosaveCollectionSlug)
await page.goto(url.create)
const titleField = page.locator('#field-title')
await titleField.fill('Initial')
await waitForAutoSaveToRunAndComplete(page)
const computedTitleField = page.locator('#field-computedTitle')
await expect(computedTitleField).toHaveValue('Initial')
})
test('- with autosave - does not display success toast after autosave complete', async () => {
const url = new AdminUrlUtil(serverURL, autosaveCollectionSlug)
await page.goto(url.create)
const titleField = page.locator('#field-title')
await titleField.fill('Initial')
let hasDisplayedToast = false
const startTime = Date.now()
const timeout = 5000
const interval = 100
while (Date.now() - startTime < timeout) {
const isHidden = await page.locator('.payload-toast-item').isHidden()
console.log(`Toast is hidden: ${isHidden}`)
// eslint-disable-next-line playwright/no-conditional-in-test
if (!isHidden) {
hasDisplayedToast = true
break
}
await wait(interval)
}
expect(hasDisplayedToast).toBe(false)
})
})
describe('Globals - publish individual locale', () => {

View File

@@ -197,6 +197,7 @@ export interface Post {
export interface AutosavePost {
id: string;
title: string;
computedTitle?: string | null;
description: string;
updatedAt: string;
createdAt: string;
@@ -366,7 +367,6 @@ export interface Diff {
textInNamedTab1InBlock?: string | null;
};
textInUnnamedTab2InBlock?: string | null;
textInUnnamedTab2InBlockAccessFalse?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'TabsBlock';
@@ -469,7 +469,6 @@ export interface Diff {
};
textInUnnamedTab2?: string | null;
text?: string | null;
textCannotRead?: string | null;
textArea?: string | null;
upload?: (string | null) | Media;
uploadHasMany?: (string | Media)[] | null;
@@ -787,6 +786,7 @@ export interface PostsSelect<T extends boolean = true> {
*/
export interface AutosavePostsSelect<T extends boolean = true> {
title?: T;
computedTitle?: T;
description?: T;
updatedAt?: T;
createdAt?: T;
@@ -960,7 +960,6 @@ export interface DiffSelect<T extends boolean = true> {
textInNamedTab1InBlock?: T;
};
textInUnnamedTab2InBlock?: T;
textInUnnamedTab2InBlockAccessFalse?: T;
id?: T;
blockName?: T;
};
@@ -995,7 +994,6 @@ export interface DiffSelect<T extends boolean = true> {
};
textInUnnamedTab2?: T;
text?: T;
textCannotRead?: T;
textArea?: T;
upload?: T;
uploadHasMany?: T;