Unsaved-work prompt: after answering Stay, the form that asked stays dead until a reload #518

Closed
opened 2026-10-01 10:47:20 +00:00 by tiagoagueda · 1 comment
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading how the double-submit guard and the unsaved-work guard in static/js/app.js interact.

Related to #258.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at static/js/app.js:534-559 (the submit listener, with the timed release for data-download forms at :554-558), static/js/app.js:561-563 (the release on pageshow) and static/js/app.js:664-672 (beforeunload); the comment on asking beside a dirty review is at app.js:584-588 and Sign out at templates/base.html:248-253. templates/jobs/capture_review.html has not moved.

The double-submit guard marks a submitted form and releases the mark only on pageshow. The unsaved-work guard can cancel that very submission: when another form on the page is dirty, beforeunload asks, and a person who answers Stay stays on the page with no pageshow to come. The submitted form keeps data-submitted, its buttons keep aria-disabled and pointer-events-none, and every later submit of it is cancelled. The button the person just pressed is dead for the rest of the visit.

  • Where: static/js/app.js:495-520 (the submit listener sets form.dataset.submitted and greys the buttons; a second submit hits if (form.dataset.submitted) { event.preventDefault(); … }); static/js/app.js:522-524 (the only release, on pageshow; the timed release at :515-519 applies to data-download forms, and no template carries that attribute now); static/js/app.js:625-633 (beforeunload calls preventDefault() whenever anythingDirty()). Nothing lifts the mark when the unload is cancelled.
  • What happens:
    1. On a capture review (templates/jobs/capture_review.html:185-199) someone corrects a field, so the review form is dirty, then presses d or clicks Discard this capture. Asking here is intended (app.js:545-549, and tests/e2e/test_unsaved_work.py checks that the question is asked). They choose Stay to keep the correction. Discard this capture and Discard and next now do nothing, whether clicked, pressed with Enter or reached through d, whose target.click() submits a form the guard refuses.
    2. Halfway through a cover letter someone opens the account menu, presses Sign out (templates/base.html:249-254) and chooses Stay. Sign out is inert until the page is reloaded, which asks the question again, or until the letter is saved.
  • Why it matters: the person did what the prompt offered, and the page then silently ignores a control that still looks like it works (dimmed, but present). The only way out is a reload, which is what the prompt was trying to spare them.
  • Fix: release the mark when the unload does not happen. In the beforeunload handler, when it asks, set a flag; on the next pointerdown or keydown on the page, release every form[data-submitted] and clear the flag. If the person leaves, no further input reaches the page; if they stay, their next action finds the form usable again. (A setTimeout(…, 0) scheduled from the handler also works, but can release the mark in the moment between Leave and the new page arriving.)

Done when

  • A browser test in tests/e2e/test_unsaved_work.py types into the review form of a capture, presses Discard this capture, dismisses the beforeunload dialog, presses Discard this capture again and accepts, and asserts that the capture is discarded. It fails today and passes after the fix.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading how the double-submit guard and the unsaved-work guard in `static/js/app.js` interact. Related to #258. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `static/js/app.js:534-559` (the submit listener, with the timed release for `data-download` forms at `:554-558`), `static/js/app.js:561-563` (the release on `pageshow`) and `static/js/app.js:664-672` (`beforeunload`); the comment on asking beside a dirty review is at `app.js:584-588` and *Sign out* at `templates/base.html:248-253`. `templates/jobs/capture_review.html` has not moved. The double-submit guard marks a submitted form and releases the mark only on `pageshow`. The unsaved-work guard can cancel that very submission: when another form on the page is dirty, `beforeunload` asks, and a person who answers *Stay* stays on the page with no `pageshow` to come. The submitted form keeps `data-submitted`, its buttons keep `aria-disabled` and `pointer-events-none`, and every later submit of it is cancelled. The button the person just pressed is dead for the rest of the visit. - **Where:** `static/js/app.js:495-520` (the submit listener sets `form.dataset.submitted` and greys the buttons; a second submit hits `if (form.dataset.submitted) { event.preventDefault(); … }`); `static/js/app.js:522-524` (the only release, on `pageshow`; the timed release at `:515-519` applies to `data-download` forms, and no template carries that attribute now); `static/js/app.js:625-633` (`beforeunload` calls `preventDefault()` whenever `anythingDirty()`). Nothing lifts the mark when the unload is cancelled. - **What happens:** 1. On a capture review (`templates/jobs/capture_review.html:185-199`) someone corrects a field, so the review form is dirty, then presses `d` or clicks *Discard this capture*. Asking here is intended (`app.js:545-549`, and `tests/e2e/test_unsaved_work.py` checks that the question is asked). They choose *Stay* to keep the correction. *Discard this capture* and *Discard and next* now do nothing, whether clicked, pressed with Enter or reached through `d`, whose `target.click()` submits a form the guard refuses. 2. Halfway through a cover letter someone opens the account menu, presses *Sign out* (`templates/base.html:249-254`) and chooses *Stay*. *Sign out* is inert until the page is reloaded, which asks the question again, or until the letter is saved. - **Why it matters:** the person did what the prompt offered, and the page then silently ignores a control that still looks like it works (dimmed, but present). The only way out is a reload, which is what the prompt was trying to spare them. - **Fix:** release the mark when the unload does not happen. In the `beforeunload` handler, when it asks, set a flag; on the next `pointerdown` or `keydown` on the page, release every `form[data-submitted]` and clear the flag. If the person leaves, no further input reaches the page; if they stay, their next action finds the form usable again. (A `setTimeout(…, 0)` scheduled from the handler also works, but can release the mark in the moment between *Leave* and the new page arriving.) ## Done when - A browser test in `tests/e2e/test_unsaved_work.py` types into the review form of a capture, presses *Discard this capture*, dismisses the `beforeunload` dialog, presses *Discard this capture* again and accepts, and asserts that the capture is discarded. It fails today and passes after the fix.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:20 +00:00
Author
Owner

The maintainer's list of 2026-10-02 asks for a save guard that prompts a dialog when a page is left unsaved. #657 builds on the guard this issue concerns: it adds Postulo's own dialog for links and buttons on the page, and keeps the browser's prompt for closing the tab, reloading and the address bar.

The dialog removes the in-app route to the dead form described here, because its listener runs before the double-submit mark is set. The browser's prompt still takes the other routes, and the mark is still released only on pageshow (src/postulo/static/js/app.js:776-778 at f09b6010b), so this fix is still needed. #657 asks for it first.

From the list of improvements of 2026-10-02.

The maintainer's list of 2026-10-02 asks for a save guard that prompts a dialog when a page is left unsaved. #657 builds on the guard this issue concerns: it adds Postulo's own dialog for links and buttons on the page, and keeps the browser's prompt for closing the tab, reloading and the address bar. The dialog removes the in-app route to the dead form described here, because its listener runs before the double-submit mark is set. The browser's prompt still takes the other routes, and the mark is still released only on `pageshow` (`src/postulo/static/js/app.js:776-778` at `f09b6010b`), so this fix is still needed. #657 asks for it first. From the list of improvements of 2026-10-02.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#518
No description provided.