Problem
Follow-up to #78 (stale share-link fix). After #78, a failed permission refetch safely shows "No link available" — but that leaves the user stuck: the select keeps showing the failed mode, there's no link at all, and the only signal is a small error line. The user must manually figure out that switching back restores things.
Proposed behavior
When a permission-switch fetch fails:
- Revert the selection to the last confirmed permission (e.g. Edit fails → select snaps back to View).
- Restore the previous link instantly from cache so the dialog always shows a valid, copyable URL matching the select.
- Signal failure clearly: shake animation on the select trigger + red accent (~1.5s), keeping the existing error text line.
Design notes (from review discussion)
- Model the state explicitly as two concepts in
ShareButton:
confirmedPermission — the permission whose link is actually loaded (source of truth for what the Select displays at rest)
pendingPermission — the optimistic value shown while the fetch is in flight
- Success:
confirmed = pending. Failure: discard pending, snap back, fire the failure cue.
- The revert path must apply the same
requestId !== requestRef.current guard as the success path: if a failed request has been superseded (view→edit→view quickly), the late failure does nothing at all — no UI flicker, no stomping the newer confirmed choice.
- Cache restore = keep a
lastValid {permission, url} pair in the hook; on guarded failure, restore both and notify the component via callback so it can sync its select state.
- Shake keyframe needs a small custom
@keyframes / --animate-shake in client/src/index.css (tw-animate-css has no shake).
Out of scope (noted during review)
AbortController for superseded requests (wasted-work optimization, not correctness)
- Debounce on rapid permission toggling
Relates to #48, #78.
Problem
Follow-up to #78 (stale share-link fix). After #78, a failed permission refetch safely shows "No link available" — but that leaves the user stuck: the select keeps showing the failed mode, there's no link at all, and the only signal is a small error line. The user must manually figure out that switching back restores things.
Proposed behavior
When a permission-switch fetch fails:
Design notes (from review discussion)
ShareButton:confirmedPermission— the permission whose link is actually loaded (source of truth for what the Select displays at rest)pendingPermission— the optimistic value shown while the fetch is in flightconfirmed = pending. Failure: discard pending, snap back, fire the failure cue.requestId !== requestRef.currentguard as the success path: if a failed request has been superseded (view→edit→view quickly), the late failure does nothing at all — no UI flicker, no stomping the newer confirmed choice.lastValid {permission, url}pair in the hook; on guarded failure, restore both and notify the component via callback so it can sync its select state.@keyframes/--animate-shakeinclient/src/index.css(tw-animate-csshas no shake).Out of scope (noted during review)
AbortControllerfor superseded requests (wasted-work optimization, not correctness)Relates to #48, #78.