Commit Graph
2 Commits
Author SHA1 Message Date
Guillem Arias Fauste 2bcef99441 fix(insights): drop the dismiss toast and fix both empty states (#2965)
* fix(insights): drop the dismiss toast and fix both empty states

Three problems with acknowledging an insight, all in the turbo-stream path.

Every dismissal appended an undo toast to the notification tray. Acknowledging
already means "hidden until these numbers change" — GenerateInsightsJob
resurfaces a row whose metadata moves materially, and 6 of 8 generators scope
`dedup_key` to a month — so a toast interrupting the flow bought little.
Removed, along with the now-orphaned `_undo_toast` partial and its two locale
keys.

Dismissing the last insight left the dashboard widget on screen: the stream
re-rendered the well unconditionally, so the section shell stayed with its
header above an empty box until a reload. A full render already drops it
(PagesController#insights_feed_section sets `visible: @feed_insights.any?`);
the stream now removes the whole section to match, targeted by
`[data-section-key='insights_feed']` because the shared dashboard loop emits no
id on the section element.

Dismissing the last insight on /insights left a blank page. The card left via
`turbo_stream.remove`, which emptied #insights-list without re-rendering the
partial that owns the empty state, so "No insights yet" only appeared after a
reload. The list is now replaced rather than the card removed — the same thing
unacknowledge already did.

InsightsController#unacknowledge, its route and Insight#unacknowledge! are kept
and still work; only the toast that reached them is gone, so undo can be
re-wired to a different surface without resurrecting them.

* fix(insights): announce the dismissal now the toast is gone

Removing the undo toast took the only `role=status` element with it, and the
stream also replaces the list containing the "Got it" control the user just
activated — so a screen-reader or keyboard user was left with no confirmation
that anything happened.

Add a shared, visually hidden live region to the notification tray and update
it from the acknowledge stream. It sits outside every stream target and is
rendered with the page, which matters: a live region that arrives together
with its own content is not announced. Updated rather than appended, so
messages replace instead of piling up, and it stays empty (and free) until
something uses it.

This is a general primitive, not an insights one — the tray already holds
`#sync-toast` and `#cta` as stable stream targets, and any flow that changes
the page without leaving something on screen to read can use it.

Verified in a browser: after dismissing, the region reads "Insight dismissed"
at 1x1px with `clip: rect(0,0,0,0)` — announced, invisible.
2026-08-11 06:15:02 +02:00
Guillem Arias Fauste cedf28a5a9 fix(accounts): make the sync toolbar and toast agree, fix toast overlap (#2813)
* fix(accounts): make the sync toolbar and toast agree, fix toast overlap

The Accounts page's own sync toolbar (refresh icon, "Cancel sync") and the
global sync-complete toast were three inconsistently-styled, disconnected
pieces of UI representing one action, and the toast overlapped the page's
own header instead of sitting near it.

- "Cancel sync" was hand-rolled markup instead of a DS::Button, unlike its
  sibling refresh icon right next to it — now both are DS::Button (:ghost).
- The refresh icon just went `disabled` with no visible "working" state —
  now shows a spinning loader-circle while a family sync is in progress,
  matching the pattern already used in provider_sync_summary.html.erb.
- The toolbar was plain server-rendered HTML with no way to know a sync
  finished, so it stayed stuck showing "still syncing" indefinitely next to
  a toast now saying otherwise. Family::SyncCompleteEvent now broadcasts a
  second replace target for the toolbar alongside the existing toast
  replace, so both resolve together.
- The notification tray was a <body>-level fixed overlay centered on the
  full viewport, but every layout that renders it has a sidebar of some
  kind — so it never actually centered on the visible content pane, and
  landed on top of the settings-layout header. It now renders in-flow at
  the top of each layout's own content region (opt-in via
  notification_tray_inline, since the simpler single-column layouts don't
  have this mismatch and are unaffected).
- Added the Catalan sync_toast/cancel_sync strings that were missing
  entirely, which is why the toast/toolbar showed English text on an
  otherwise-Catalan page.

Verified live in a real browser via a new system test covering the idle,
syncing, and cancel-flash states, plus a model test on the new broadcast
target.

* fix(accounts): keep the tray a floating overlay, sidebar-aware instead

Codex on this PR: with the tray as first-child-of-scrollable-main, a
notification delivered while scrolled down is inserted above the
viewport and stays unseen — breaking the sync toast's manual-refresh
path specifically, since sync_toast_controller.js suppresses
auto-refresh while a form is focused and relies on the toast being
visible to offer that manual refresh.

Reverts the tray to a position: fixed overlay for every layout (so it
can't be scrolled out of view), and fixes the actual bug that made it
overlap the accounts toolbar in the first place — a ResizeObserver on
<main> centers it on the real content pane instead of the viewport,
for the two layouts with a sidebar (application, settings passed via
sidebar_aware:). The five single-column layouts are untouched; for
those, viewport-center already is content-pane-center.

One trap worth flagging: this app renders turbo_refreshes_with
method: :morph, and idiomorph resets any inline style a client script
set that isn't in the freshly-fetched HTML — including the JS-set
`left`. data-turbo-permanent looked like the fix but isn't: it invokes
idiomorph's node-identity matching (same id preserved across ANY
morphed page), which broke navigation once the id existed on
structurally different layouts (app vs settings) — a real, reproduced
bug, caught by the system test before it shipped. Went with the
narrower turbo:before-morph-attribute event instead, which blocks only
the `style` attribute on this one element, with no node-identity
system involved.

Rewrote the system test's positioning assertion to match: it now
asserts the tray centers on <main> rather than sitting above the page
header, since a fixed overlay was never going to satisfy the latter by
construction.

Verified: full bin/rails test (6023 runs, 0 failures), rubocop, erb_lint,
brakeman (0 warnings) all clean. Live-verified in a browser across both
sidebar-aware layouts and a simple layout, including the full
cancel-sync -> morph -> re-render cycle.

* fix(accounts): use declarative Stimulus action for morph-attribute guard

Replace the manual addEventListener/removeEventListener pair for
turbo:before-morph-attribute with a data-action, per the repo's
declarative-actions convention. Same element, same listener — just no
manual lifecycle management.
2026-07-30 02:57:16 +02:00