Skip to content

fix(admin): prevent autosave from overwriting in-progress edits#272

Open
tasdemir wants to merge 1 commit intoemdash-cms:mainfrom
tasdemir:fix/autosave-overwrites-edits
Open

fix(admin): prevent autosave from overwriting in-progress edits#272
tasdemir wants to merge 1 commit intoemdash-cms:mainfrom
tasdemir:fix/autosave-overwrites-edits

Conversation

@tasdemir
Copy link
Copy Markdown

@tasdemir tasdemir commented Apr 5, 2026

Summary

Fixes a bug where editing content fields (title, slug, excerpt, body) in the admin editor would silently revert user input to the previous server-stored values. Normal-speed typing was nearly impossible — only rapid copy-paste would survive the revert cycle.

Root Cause

A circular data refresh loop between autosave and the form state synchronization:

User types in field
  → formData updates locally, isDirty = true
  → 2s idle → autosave fires, sends formData to server
  → autosave onSuccess calls invalidateQueries(["content", collection, id])
  → React Query refetches content from server
  → Server returns item with new updatedAt timestamp
  → ContentEditor useEffect depends on item.updatedAt
  → Effect fires, calls setFormData(item.data) + setSlug(item.slug)
  → User's in-progress edits are overwritten with server values

The updatedAt timestamp changes on every server write, including autosave. So the form-sync effect fired after every autosave, unconditionally resetting all form state — even though the server data was identical to what the user already had (because autosave just sent it).

Additionally, React Query's default refetchOnWindowFocus: true combined with staleTime: 60s meant that even without autosave invalidation, any window focus event after 60 seconds would refetch and reset the form.

Fix

Two changes, both in packages/admin/:

1. src/router.tsx — Remove invalidateQueries from autosave onSuccess

Autosave already sent the current form data to the server. There is no benefit to immediately refetching it back — and doing so triggers the form reset. The query cache refreshes naturally via:

  • Manual save (updateMutation still invalidates)
  • Discard draft / restore revision (those mutations still invalidate)
  • Page navigation (React Query refetches on mount)
  • Window refocus (React Query default behavior, after staleTime)

2. src/components/ContentEditor.tsx — Remove item?.updatedAt from useEffect deps

The form-sync effect's dependency array was [item?.updatedAt, itemDataString, item?.slug, item?.status]. The updatedAt timestamp changes on every write but carries no meaningful content change. Removing it means the effect only fires when actual content changes:

Scenario itemDataString changes? item?.slug changes? Effect fires? Correct?
Initial page load
After autosave (same data)
After window-focus refetch
Discard draft (data reverts) possibly
Restore revision (data changes) possibly
External edit by another user possibly

Test Plan

  • Open a published post in the admin editor
  • Edit the title field — type at normal speed, wait 3+ seconds for autosave
  • Verify the title retains your edits (no revert)
  • Edit the slug field — same test
  • Edit the excerpt field — same test
  • Edit the body (Portable Text) — same test
  • Click "Save" manually — verify save works and form reflects saved state
  • Use "Discard Draft" — verify form resets to the published version
  • Use "Restore Revision" from revision history — verify form resets to the restored version
  • Switch browser tabs and return after 60+ seconds — verify no unexpected form reset

🤖 Generated with Claude Code

When editing content fields (title, slug, excerpt, etc.) in the admin
editor, user input was being silently reverted to server values. This
made normal-speed editing nearly impossible — only rapid copy-paste
would survive.

Root cause: a circular data refresh loop between autosave and the form
state synchronization effect:

1. User edits a field → local form state updates → isDirty becomes true
2. After 2s idle, autosave fires → sends current form data to server
3. Autosave onSuccess called invalidateQueries() for the content query
4. React Query refetched the content from the server
5. The form-sync useEffect depended on item.updatedAt, which changed
   on every server write (even autosave)
6. The effect unconditionally reset all form state (formData, slug,
   status, bylines) to the refetched server values
7. Any edits made between step 2 and step 6 were silently lost

This was particularly noticeable on fields like slug and excerpt where
users type at normal speed, but also affected the title and rich text
body fields.

Fix (two changes):

1. router.tsx — Remove invalidateQueries() from the autosave mutation's
   onSuccess callback. Autosave already sent the latest data to the
   server; there is no need to immediately refetch it back. The query
   cache refreshes naturally on manual save, page navigation, or
   window refocus (React Query default behavior).

2. ContentEditor.tsx — Remove item.updatedAt from the form-sync
   useEffect's dependency array. The timestamp changes on every server
   write but carries no meaningful data change. The effect now only
   fires when actual content (itemDataString), slug, or status change,
   which correctly covers explicit actions like "Discard Draft" and
   "Restore Revision" while ignoring timestamp-only updates from
   autosave or refetch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 5, 2026

⚠️ No Changeset found

Latest commit: 7582b06

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 5, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@272

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@272

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@272

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@272

emdash

npm i https://pkg.pr.new/emdash@272

create-emdash

npm i https://pkg.pr.new/create-emdash@272

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@272

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@272

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@272

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@272

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@272

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@272

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@272

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@272

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@272

commit: 7582b06

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant