Context
Content Details saves dirty React Hook Form fields through GraphQL via useContentDetails.save(). On success, the form is reset so dirty markers clear. On failure, the previous behavior only toasted an error and left dirty markers in place.
That created a persistent failure loop: later saves kept resending the same failing payload. GraphQL mutations are all-or-nothing, so the correct rollback unit is every top-level key included in the failed request payload (dirty fields plus any pendingFields).
Many call sites invoke save() / saveAfterTick() without awaiting completion or checking loading state, allowing overlapping save attempts. A naive failure revert for an older attempt can undo values belonging to a newer attempt.
A decision was needed on how save failures should restore form state without wiping unrelated in-progress edits or racing newer saves.
Decision
On content save failure, useContentDetails.save() performs a transactional field-level revert of the attempted payload keys:
- Capture
attemptedKeys = Object.keys(payload)before the mutate call. - On failure, restore each attempted key with React Hook Form
resetField(key, { keepDirty: false, keepTouched: false }), which restores the last-known-good defaults (from the last successfulreset/ load), not the failed edit values. - Guard overlapping saves with a save-generation counter: increment at the start of each mutate attempt; in
catch, revert only when the failing attempt’s generation is still the current generation. - Never update
contentDataon failure (server state is unchanged). - Do not full-form
reseton failure (that would wipe unrelated dirty fields). - Do not parse GraphQL errors to infer per-field blame; the rollback set is the payload key set.

Consequences
- Failed saves no longer leave attempted fields dirty, so subsequent saves do not automatically resend the same failing payload.
- Users see last-known-good values for failed keys while unrelated dirty fields remain editable.
- Overlapping blur saves remain allowed; stale failure handlers cannot clobber a newer attempt.
- Call sites that already handle
save() === false(for example channels local list revert) remain complementary to hook-levelresetField. - No save queue/mutex is introduced; serializing saves would make “busy” look like failure for callers that treat
falseas a hard revert signal. updateContentState(empty field payload) is unchanged by this decision.
Notes (Optional)
- Related: ADR-008: Registry-Driven Content Details Page
- Implementation lives in
src/hooks/content/useContentDetails.ts; coverage for failure/revert/generation branches is required at 100% branch coverage for those paths.