payloadcms / payloadcms/payload
Failed publish leaves "Save draft" disabled — user believes the draft was saved, changes are lost
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
When a publish is rejected by the server (a missing required field, a validate function, a beforeChange/beforeValidate hook throwing an APIError, a unique-constraint violation, a 500, etc.), the edit view leaves Save draft disabled.
The user is stuck: their changes are still in the form, the document was not saved, but the way to park that work is greyed out. Because a greyed-out "Save draft" normally means "nothing to save", it strongly suggests the draft was saved. It wasn't — navigating away or reloading loses the work. This is a data-loss trap, and it hits exactly when the user most needs the draft escape hatch: their document isn't publishable yet.
The only way out today is to make an arbitrary extra edit to a field to flip the form back to modified.
Root cause
setModified(false) runs after every completed request regardless of status, and the error branch restores it only for _status: 'draft' submits. A failed publish therefore leaves modified === false — precisely what the save buttons key off, via operation === 'update' && !modified (SaveDraftButton, PublishButton, SaveButton).
That _status === 'draft' condition comes from #14584 (fixing #14227) — correct, but incomplete: its own rationale, "keep the form as modified so the save button remains enabled for retry", applies to any failed submit. Still broken today are a failed publish on a versioned collection (this report) and a failed save on a non-versioned collection, whose submit carries no _status at all.
Not an edge case: required is validated server-side, so an empty required field plus Publish changes is enough to trip it.
Suggested fix
Move setModified(true) out of the _status === 'draft' condition — a failed submit means the document was not persisted and the form still holds unsaved changes, whatever the status:
} else {
setProcessing(false)
setSubmitted(true)
// The submit failed, so the document was not persisted and the form still holds
// unsaved changes. Keep it marked as modified so the save/publish buttons remain
// enabled for a retry.
setModified(true)
// When there was an error submitting a draft, set the form state to unsubmitted,
// to not trigger visible form validation on changes after the failed submit.
if (overridesFromArgs['_status'] === 'draft' && !validateDrafts) {
setSubmitted(false)
}
I'm running this as a patch locally and have verified it in a real admin panel: with the change, a document whose publish is rejected (400 ValidationError on a missing required field) keeps Save draft enabled, so the work can be parked as a draft. Without it, Save draft is dead. Happy to open a PR with it plus an integration test if you'd like.
Link to the code that reproduces this issue
Reproduction Steps
No special hooks or custom validation are needed — a required field on a drafts-enabled collection is enough, because required-field validation runs on the server.
-
Start from a blank app (
pnpx create-payload-app@latest -t blank) and add a collection with drafts enabled and a required field:import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title' }, fields: [ { name: 'title', type: 'text', required: true }, { name: 'content', type: 'text' }, ], versions: { drafts: true }, } -
In the admin panel, create a post, fill in
title, and press Save draft. The document now exists as a draft (operation === 'update'from here on). -
Edit
contentand cleartitle. Both Save draft and Publish changes are enabled, as expected. -
Press Publish changes. The
PATCHreturns400with aValidationErrorfor the missingtitle, and the error is surfaced — correct so far. -
Look at the buttons: Save draft is now disabled, while your edit to
contentis still sitting in the form, unsaved. -
There is no way to save that work without editing some field again to re-flip
modified. Reloading the page or navigating away loses it.
Any other server-side rejection reproduces it identically — a unique-constraint violation, a beforeChange/beforeValidate hook throwing an APIError, or a 500.
Expected: after a failed submit the document was not persisted, so the form should stay modified and Save draft should remain clickable — letting the user park their work as a draft, exactly as it does when a draft save fails (#14227 / #14584).
Actual: Save draft is disabled, which reads as "everything is saved" and leads to silent data loss. (On a non-versioned collection the same root cause disables Save after any server-rejected save.)
Which area(s) are affected?
area: ui
Environment Info
Binaries:
Node: 22.19.0
npm: N/A
Yarn: N/A
pnpm: 11.1.2
Relevant Packages:
payload: 3.86.0
next: 16.2.6
@payloadcms/db-mongodb: 3.86.0
@payloadcms/ui: 3.86.0
react: 19.2.3
react-dom: 19.2.3
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin 24.6.0
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/ui/src/forms/Form/index.tsx around setModified(false) and the failed-submit error branch, then inspect the SaveDraftButton, PublishButton, and SaveButton conditions that depend on modified. Reproduce the rejected publish from the issue or add the suggested integration test; done means failed publishes and saves keep the form modified and the appropriate save button enabled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100