Skip to content

One confirm dialog and one toast region replace every native dialog (#2643) - #2696

Merged
jh-RLI merged 4 commits into
developfrom
feature-2643-confirm-dialog
Oct 12, 2026
Merged

jh-RLI merged 4 commits into
developfrom
feature-2643-confirm-dialog

Conversation

@jh-RLI

@jh-RLI jh-RLI commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2643 (sub-issue of #2632).

Every confirmation on the platform now asks through one dialog, and every error a script reports goes to one toast region. Nothing calls the browser's confirm() or alert() any more.

What it builds

  • confirm_dialog: a house component with an include (base/templates/components/confirm_dialog.html), a partial (theming/scss/components/_confirm_dialog.scss) and a catalogue entry (plain, danger, typed, plus three "Try" buttons).
    • It is a Bootstrap modal on tokens: a title naming the action, a sentence saying what will happen, Cancel (btn-secondary) and a button labelled with the verb.
    • The verb button is btn-danger when the action cannot be undone, btn-primary otherwise.
    • A typed confirmation keeps the verb button disabled until the text matches exactly.
  • toast: a house component with an include, a partial and a catalogue entry. It has the modal's 8px radius and large shadow, and a 4px status bar like the alert's.
    • The shell region has two live regions: polite for news, assertive for errors.
    • Success and info toasts remove themselves; warnings and errors stay until closed.
  • One shell piece, base/templates/base/_feedback.html. All five shells include it once after the footer, outside every block, so no page can drop it. It holds the dialog, the toast region, the toast <template> and the module.
  • One ES module, base/static/base/dialogs.js:
    • confirmAction() returns a promise of true or false.
    • showToast() adds one toast to the region.
    • bindDialogs() installs one htmx:confirm listener, so every hx-confirm opens the dialog. It calls issueRequest(true) only on confirm, and true skips htmx's own confirm().
    • data-confirm on a plain button or link holds its click back until confirm. The click is caught in the capture phase and clicked again on confirm, so a form submits with its submitter and a jQuery handler runs only then.
    • Classic scripts and the vite bundles call window.oep.confirm(…) and window.oep.toast(…).
    • Templates add data-confirm-title, -verb, -danger and -typed.
  • Focus: Escape, the close button and the backdrop cancel. Focus starts on Cancel for a destructive action, in the input for a typed one, and on the verb otherwise. It returns to the opener afterwards.
  • Every Bootstrap modal takes the dialog's look, so the tables tab's action dialog gets it with its content untouched. The dashboard's two drawers (access, members) get the dialog's header.

What it replaces

Where Was Now
Model and framework factsheet Delete hx-confirm + native confirm(); <a> without href dialog, danger; a <button>, so it can take focus back
Organization delete, member remove hx-confirm + native dialog, danger; Remove user is now an outline-danger opener
Table permissions: Remove user / group onsubmit="return confirm(…)" data-confirm, danger
Table permissions: invalid user name alert() warning toast, focus back in the field
Tag editor Delete onclick="return confirm(…)", only for a tag in use data-confirm, danger, also for an unused tag
Table view Delete black-button Bootstrap modal data-confirm, danger; the modal is deleted
Wizard an identical modal nothing opened deleted
Peer review delete confirm("Are you sure?") window.oep.confirm, danger
Query builder, metadata editor, peer review save/submit/finish/delete errors alert() danger toasts
Factsheets, both peer review pages inline z-index: 11 toast containers the shell region; peer review's showToast now forwards to it

dataedit:table-permission joins RESTYLED_PAGES and passes the page lint. The lint's docstring now says why hx-confirm is not a native dialog.

For the reviewer: judgement calls

  1. The table view's delete is not typed. The dashboard asks for the name only for a published table or a batch, and this button exists only on drafts.
  2. The tag editor now confirms every delete, not only for a tag in use. A delete is platform-wide and irreversible either way.
  3. The dialog's typed input reads tokens itself Settled by One field look and the account forms on house components (#2642) #2695: its field look reads the same tokens, so the dialog's own rule is gone.
  4. The dashboard keeps its own toast region for now. Its toasts carry a link and a "Show tables" list, and their CSS reads --dash-*, which Dashboard lists on house tokens #2659 retires.
  5. The modal look applies to every modal (8px, large shadow, borders in the border colour, a 1.125rem title). That includes the peer review and metadata editor modals. The spec asks for exactly this; it does change pages this PR does not otherwise touch.
  6. Not on the page lint: login:organizations stays off for the reason already listed there (Settings becomes the account page; the dashboard gets one header #2653).

Tests

  • vitest + happy-dom (base/static/base/__tests__/dialogs.test.js, 28 tests; base/static/base/** added to the vitest include):
    • confirm resolves true or false for confirm, Cancel, the close button, Escape and the backdrop;
    • text is written as text;
    • danger vs primary, and where focus starts and returns;
    • typed stays disabled until an exact match;
    • an htmx:confirm issues its request only on confirm, and with true;
    • data-confirm acts only on confirm;
    • toasts go to the right region, and a success removes itself while an error stays;
    • three fade races: Bootstrap ignores show()/hide() mid-transition. Review found that an answer within ~300 ms left a dead dialog on screen; a real-browser check then found a third case. All three are fixed and pinned with a stand-in modal that fades.
  • Django (base/tests/test_confirm_dialog.py, 15 tests):
    • every shell renders one dialog, one toast region and the module;
    • no template and no platform script calls confirm( or alert( (comments are stripped; vendored htmx, query builder and jsi18n are named exceptions);
    • every hx-confirm names its title and verb;
    • no inline toast containers or old delete modals are left;
    • the include variants render correctly;
    • each former site renders through the test client.
  • Full suite on isolated databases: 2,155 tests. The only failure is LightImportTest, the known isolated-settings artefact, which passes under normal settings.

Checks

  • Homepage compare (benchmarks/homepage/compare.mjs, whole page, nothing masked): 8/8 byte-identical, before and after, including after the review fixes.
  • check_catalogue: complete, 15 entries, both token modes.
  • theming/literal_scan.mjs: 0 under ?tokens=bootstrap.
  • Theme: built with npm run build:theme's pinned sass@1.77.0 + bootstrap@5.2.0. Develop's sources reproduce develop's stylesheet byte for byte. stock_tokens.css is unchanged, since no token was added.
  • REUSE: compliant.
  • Headless Chrome against real Bootstrap and htmx 1.9.8:
    • focus lands on Cancel, the input or the verb, and goes back to the opener;
    • Escape cancels;
    • Enter in a matching typed input confirms;
    • a planted hx-confirm sends no request on Cancel and exactly one on confirm;
    • toasts land in the polite and assertive regions;
    • the fade races hold.

Not done / not checked

Took develop after #2695 and #2694 (2026-10-12)

Merged origin/develop as e9dcf82e9. The conflicts were all in shared files:

  • REUSE.toml, theming/oepstrap.scss, RESTYLED_PAGES and the changelog: kept both sides.
  • bootstrap.min.css: rebuilt with the pinned sass + bootstrap, not resolved by hand.

#2642's field sets the same text colour and border that this PR's typed-input rule set. That rule is deleted, and the scan still finds 0.

Results on the merged tree:

  • base, modelview, dataedit and login tests: 1,478, OK.
  • vitest: 294.
  • check_catalogue: 17 entries.
  • Literal scan: 0.
  • Homepage comparison against the original before-shots: 8/8 byte-identical.
  • gh pr view: MERGEABLE.

🤖 Generated with Claude Code

jh-RLI and others added 3 commits October 12, 2026 20:14
…2643)

Every shell includes base/_feedback.html once, outside every block: the
house component confirm_dialog (a Bootstrap modal on tokens), a toast region
(polite + assertive) with the toast include as a <template>, and the ES
module base/static/base/dialogs.js. One htmx:confirm listener routes every
hx-confirm through the dialog (issueRequest(true) only on confirm);
data-confirm holds a plain button's click back until confirm;
window.oep.confirm/toast serve classic scripts and the vite bundles.

Replaced: four hx-confirms (factsheet x2, organization delete, member
remove), the permission page's two onsubmit confirms and its alert(), the
tag editor's onclick confirm, the table view's delete modal (deleted; the
wizard's unused one too), the peer review confirm and every alert() in
backend.js, metaedit.js and peer review, and the four z-index:11 toast
containers. Every modal takes the dialog's look; the dashboard drawers its
header. Catalogue entries confirm_dialog and toast; dataedit:table-permission
joins the page lint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Bootstrap's modal ignores show() and hide() while it fades. An answer
  given during the fade-in left the dialog on screen with no handlers, and
  a confirmation asked during a fade was lost or answered "no" by the last
  one's close. Each call now waits for the fade it falls into, a hide counts
  as pending from when it is decided, and the backdrop "no" is listened for
  only once that confirmation's own show has happened. Three tests with a
  fading stand-in; checked in headless Chrome against real Bootstrap.
- The factsheet Delete is a <button>: as an <a> without href it could not
  take focus, so focus never returned to it after the dialog.
- The factsheet pages toast "not deleted" only for the delete request, not
  for any htmx error on the page.
- Dropped modal/toast variables that restated Bootstrap's own values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jh-RLI jh-RLI self-assigned this Oct 12, 2026
Conflicts were the shared files only: REUSE.toml, oepstrap.scss, RESTYLED_PAGES
and the changelog as unions; bootstrap.min.css rebuilt. #2642's field look
reads the same tokens as the dialog's own typed-input rule, so that rule is
gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jh-RLI
jh-RLI merged commit 6480731 into develop Oct 12, 2026
6 of 7 checks passed
jh-RLI added a commit that referenced this pull request Oct 12, 2026
Conflicts: RESTYLED_PAGES (union; the permission page is listed by #2643
now, so the comment names only the peer review pages) and
bootstrap.min.css (rebuilt with npm run build:theme). The catalogue's
text snapshot is recaptured from develop's catalogue, which gained the
entries #2642, #2643 and #2649 added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jh-RLI
jh-RLI deleted the feature-2643-confirm-dialog branch November 3, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One confirm dialog and one toast region replace every native dialog

1 participant