Show unsaved-edit dots on manuscript issue tabs - #3427
Merged
Conversation
Sections only persist onBlur, so typing in issue 3 and tabbing to issue 7 left the pending text with no cue anywhere on screen. The saved-content baselines the beforeunload guard already tracked become state so the tab strip can subscribe to them; an issue whose content has drifted from its last server-confirmed save gets a dot plus an "unsaved edits" tooltip. The shared isSectionDirty predicate also gates on the baseline key being seeded, so an un-seeded section reads "unknown", not "dirty" -- the unload guard inherits that fix.
A format switch replaces `sections` and their baselines wholesale, so an edit that had not blurred yet -- or one whose save failed -- was silently discarded, and switching back refetched the server's older text. Found in review of the unsaved-edit tab dots, which made the loss visible without preventing it. changeView now flushes pending saves first and bails out (keeping the format, the text, and the dot) when one fails. Saves are serialized per section so the blur save and the flush -- both fired by one click on the format switcher -- can't PATCH identical text twice.
… one (#3399) Two callers that both awaited the SAME in-flight save woke up together, each still seeing the pre-save baseline, and each fired its own PATCH for identical text -- so editing during a slow save and then switching formats snapshotted a redundant version. Each call now extends the section's save chain and re-checks the baseline inside the link, so a link whose text the prior link already persisted no-ops.
…irty ones (#3399) A section can be mid-save while its live text matches the baseline -- type, blur, then type back -- which reads as clean, so the flush let the switch through with the save still in flight. Its result then landed on the incoming format's save badge (saveState is keyed by issue id, not format). The flush now drains every queued save first and re-derives the dirty set afterwards, so a queued save that failed is retried instead of silently passing the gate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Manuscript sections persist onBlur only, so editing issue 3 and tabbing to issue 7 left the pending text with no cue anywhere on screen — the tabs showed open-note counts but nothing about unsaved state.
The saved-content baselines the
beforeunloadguard already tracked move from a bare ref to state (with the ref kept as an eagerly-updated mirror for the handlers that read it outside the render cycle), soManuscriptIssueTabscan subscribe. An issue whose live content has drifted from its last server-confirmed save now renders a dot with an "unsaved edits" tooltip; it clears when the save lands and stays put when the save fails.Making the state visible surfaced that it was also being lost. Review found three related defects on the save path, all fixed here:
sectionsand their baselines wholesale, so an edit that hadn't blurred — or whose save had failed — was dropped, and switching back refetched the server's older text.changeViewnow flushes pending saves first and aborts (keeping the format, the text, and the dot) if one fails.saveStateis keyed by issue id, not format). The flush now drains the queue first and re-derives the dirty set afterwards, so a queued save that failed is retried rather than silently passing the gate.Two smaller things fell out of the shared
isSectionDirtypredicate: baseline-key construction is onebaselineKey()helper instead of four inline template literals, and the predicate gates on the key being seeded — an un-seeded key means "we don't know the saved text yet", not "unsaved". Thebeforeunloadguard inherits that.Closes #3399
Test plan
client: full suite green — 532 files / 6115 tests (NODE_ENV=test npx vitest run)ManuscriptIssueTabs.test.jsx(6 tests): deep-linkable tabs, open-note badge, dot on only the dirty issues, no dot when clean, absent-prop tolerance, empty-sections null renderPipelineManuscriptEditor.test.jsx(7): typing marks the tab → blur-save clears it; a failed save leaves it marked; an unblurred edit is flushed before a format switch; the blur/flush race PATCHes once; text edited during an in-flight save PATCHes once; a save in flight under baseline-matching text blocks the switch until it settles; a failed save aborts the switch