Fix autosave form reset bug#302
Conversation
Autosave was invalidating the query cache after completion, which triggered a refetch of content data from the server. This refetch updated the item prop, causing a useEffect in ContentEditor to reset form state to the server values, overwriting unsaved user changes. The fix removes the queryClient.invalidateQueries() call from the autosaveMutation onSuccess handler. The local form state remains the source of truth during editing, and manual saves still properly invalidate and refresh the cache. Fixes emdash-cms#295
🦋 Changeset detectedLatest commit: 5cb7360 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
Pull request overview
Fixes the content editor autosave behavior so that successful autosaves no longer trigger a React Query refetch that resets in-progress form edits back to server state.
Changes:
- Removes
queryClient.invalidateQueries(["content", collection, id])from the autosave mutationonSuccesshandler. - Keeps autosave feedback via
lastAutosaveAtwhile leaving manual save/publish flows to continue invalidating queries as before. - Adds a changeset to release the admin package as a patch.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/admin/src/router.tsx | Stops autosave from invalidating the content query cache, preventing editor form state resets after autosave. |
| .changeset/fix-autosave-form-reset.md | Records the bugfix for a patch release of @emdash-cms/admin. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I have read the CLA Document and I hereby sign the CLA |
|
Can you fix the conflicts? |
|
I’m resolving the merge conflicts now and will push an updated branch shortly. |
|
Conflicts are fixed now. I merged the latest main into this branch and pushed the updated branch. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/admin/src/router.tsx
Outdated
| onSuccess: () => { | ||
| setLastAutosaveAt(new Date()); | ||
| // Invalidate content and draft revision so stale cached data | ||
| // doesn't overwrite the form via the sync effect | ||
| void queryClient.invalidateQueries({ queryKey: ["content", collection, id] }); | ||
| if (rawItem?.draftRevisionId) { | ||
| void queryClient.invalidateQueries({ | ||
| queryKey: ["revision", rawItem.draftRevisionId], | ||
| }); | ||
| } | ||
| // Keep the editor's local state as the source of truth during autosave. | ||
| // Invalidating here can refetch slightly older server data and reset the form | ||
| // while the user is still typing. |
There was a problem hiding this comment.
Removing the invalidate/refetch on autosave means the local react-query cache for ["content", collection, id] never gets updated with the mutation result. For revision-enabled collections, autosave can create a new draftRevisionId (see core runtime: packages/core/src/emdash-runtime.ts:1534-1549), so leaving the cache stale can cause the editor UI (e.g., Pending changes badge / Publish changes vs Unpublish actions) to remain out of sync until a manual save or reload.
Consider updating the query cache with the autosave mutation result (or at least the revision pointer metadata) in a way that does not trigger the form-reset effect (e.g., store draftRevisionId/updatedAt in separate local state, or adjust the ContentEditor sync effect to ignore autosave-driven item updates).
packages/admin/src/router.tsx
Outdated
| onSuccess: () => { | ||
| setLastAutosaveAt(new Date()); | ||
| // Invalidate content and draft revision so stale cached data | ||
| // doesn't overwrite the form via the sync effect | ||
| void queryClient.invalidateQueries({ queryKey: ["content", collection, id] }); | ||
| if (rawItem?.draftRevisionId) { | ||
| void queryClient.invalidateQueries({ | ||
| queryKey: ["revision", rawItem.draftRevisionId], | ||
| }); | ||
| } | ||
| // Keep the editor's local state as the source of truth during autosave. | ||
| // Invalidating here can refetch slightly older server data and reset the form | ||
| // while the user is still typing. |
There was a problem hiding this comment.
This change is meant to prevent form values from reverting after autosave, but the current tests only cover that a second autosave isn’t queued. Please add a regression assertion (unit or e2e) that edits in a field are still present after an autosave completes (i.e., the input value doesn’t reset to the previous server state).
packages/admin/src/router.tsx
Outdated
| // Keep the editor's local state as the source of truth during autosave. | ||
| // Invalidating here can refetch slightly older server data and reset the form | ||
| // while the user is still typing. |
There was a problem hiding this comment.
Instead of removing the invalidation, could you optimistically patch the cache insgtead. Soemthing liek this:
| // Keep the editor's local state as the source of truth during autosave. | |
| // Invalidating here can refetch slightly older server data and reset the form | |
| // while the user is still typing. | |
| // Optimistically update the cache so it stays fresh | |
| // without triggering a refetch | |
| queryClient.setQueryData( | |
| ["content", collection, id], | |
| (old) => old ? { ...old, data: variables.data ?? old.data, slug: variables.slug ?? old.slug, updatedAt: new Date().toISOString() } : old | |
| ); |
(not checked)
| await expect.element(savedBtn).toBeDisabled(); | ||
| }); | ||
|
|
||
| it("does not queue another autosave after a successful autosave", async () => { |
There was a problem hiding this comment.
This tests doesn't actually test the bug that this is fixing. Could you add a regressoin test?
What does this PR do?
Fixes a bug where autosave invalidates the query cache and resets form fields to server state, causing user edits to be lost.
When editing a content entry with many custom fields, autosave fires after 2 seconds of inactivity. Upon success, it was calling
queryClient.invalidateQueries(["content", collection, id]), which triggered a refetch from the server. This refetch updated theitemprop, which triggered auseEffectinContentEditorthat resets form state (formData,slug, etc.) to server values.The fix removes the
queryClient.invalidateQueries()call from theautosaveMutationonSuccesshandler. The local form state remains the source of truth during editing. Manual saves still properly invalidate and refresh the cache.Closes #295
Type of change
Checklist
pnpm typecheckpasses (pre-existing errors in admin package unrelated to this change)pnpm --silent lint:json | jq '.diagnostics | length'returns 0 (pre-existing errors unrelated to this change)pnpm formathas been run