Skip to content

fix(accounts): make the sync toolbar and toast agree, fix toast overlap#2813

Open
gariasf wants to merge 2 commits into
mainfrom
fix-accounts-sync-toast
Open

fix(accounts): make the sync toolbar and toast agree, fix toast overlap#2813
gariasf wants to merge 2 commits into
mainfrom
fix-accounts-sync-toast

Conversation

@gariasf

@gariasf gariasf commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Screenshots showed a "New data available" toast crowding the Accounts page's own sync toolbar (refresh icon, "Cancel sync", "+ Nou compte"). Tracing the full click-to-completion flow showed the overlap is a symptom of a bigger issue: the same "sync my accounts" action was represented by three inconsistently-styled, disconnected pieces of UI that don't agree with each other while a sync runs.

  • "Cancel sync" is now a real DS::Button (:ghost, :sm) instead of hand-rolled markup — it was the only non-DS::Button control in a toolbar where its sibling refresh icon already was one.
  • The refresh icon now shows an actual "in progress" state — a spinning loader-circle while Current.family.syncing?, instead of just going disabled with no other visible change. Mirrors the existing pattern in provider_sync_summary.html.erb.
  • The toolbar now resolves itself when a sync completes. Previously only the #sync-toast slot was broadcast-replaced on Family::SyncCompleteEvent, so the toolbar (disabled icon + "Cancel sync") stayed stuck showing "still syncing" indefinitely next to a toast that had already announced completion. The event now also broadcasts a replace for the extracted accounts/sync_controls partial, so both update together.
  • The notification tray no longer overlaps the page header. It was a <body>-level fixed overlay centered on the full browser viewport, but every layout that renders it has a sidebar of some kind (settings nav, icon rail, resizable app sidebars) — so it never actually centered on the visible content pane, and just happened to land on top of the settings-layout header. It now renders in-flow at the top of each layout's own content region. This is opt-in per layout (content_for :notification_tray_inline) — the five simpler single-column layouts (auth, onboarding, wizard, imports, blank) don't have this sidebar mismatch and keep the original floating behavior unchanged.
  • Added the missing Catalan sync_toast/cancel_sync stringsshared.sync_toast didn't exist in ca.yml at all, which is why the toast/toolbar showed English text on an otherwise-Catalan page.

Note: this same gap exists for sync_toast/cancel_sync in most of the other 15 locale files (not just Catalan) — pre-existing and out of scope here since I can't reliably hand-author translations for languages I don't speak; flagging for a separate i18n pass.

Before / After

Captured against fixture/demo data, not production data.

Before — toast overlaps the header New data available toast card floating directly on top of the Home/Accounts breadcrumb and page title, crowding the New account button
After — toast pushes content down instead Same toast now rendered above the breadcrumb and page title in normal document flow, no overlap with any toolbar control
Before — flat icon + plain text link Static refresh icon and plain unstyled Cancel sync text next to the New account button
After — spinner + real button Loader icon mid-rotation and a proper Cancel sync DS::Button next to the New account button

Shown at 4× crop for the toolbar pair so the change is visible; the loader icon looks like a partial ring because it's a still frame of a spinning animation, not a rendering issue.

Test plan

  • New system test (test/system/accounts_sync_ui_test.rb) covering: idle state (no spinner, no Cancel sync), in-progress state (spinner + matching Cancel sync button), and the positioning fix itself — asserts the notification tray's bounding rect sits above the page header rather than overlapping it.
  • New model test (test/models/family/sync_complete_event_test.rb) asserting both broadcast targets fire with correct target/partial/locals.
  • Full bin/rails test — 6023 runs, 0 failures, 0 errors.
  • erb_lint on all touched templates — clean.
  • bin/rubocop on touched/new Ruby files — clean.
  • bin/brakeman — 0 warnings.

Summary by CodeRabbit

  • New Features
    • Added clearer account synchronization controls with loading and “Cancel sync” options.
    • Added in-page notification tray support on Accounts and Settings to keep alerts visible and properly positioned.
    • Added Catalan translations for sync actions and notifications.
  • Bug Fixes
    • Sync controls now refresh automatically after background synchronization completes, preventing the “still syncing” state from getting stuck.
    • Improved notification tray alignment when cancelling synchronization.

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.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR extracts account sync controls into a reusable partial, updates them after sync completion broadcasts, and adds sidebar-aware notification tray positioning to application and settings layouts. Catalan translations and model/system coverage are included.

Changes

Sync UI flow

Layer / File(s) Summary
Account sync controls and completion broadcast
app/views/accounts/..., app/models/family/..., config/locales/views/accounts/ca.yml, config/locales/views/shared/ca.yml, test/models/family/...
The accounts page uses a shared sync-controls partial, completion broadcasts replace both sync targets, and translations and broadcast tests cover the updated rendering.
Sidebar-aware notification tray
app/views/layouts/..., app/javascript/controllers/notification_tray_controller.js, test/system/accounts_sync_ui_test.rb
Application and settings layouts expose tray targets, the Stimulus controller aligns the tray with main, and system tests cover sync states, cancellation, and tray positioning.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FamilySync as Family::SyncCompleteEvent
  participant Turbo as Turbo Stream
  participant Accounts as Accounts page
  FamilySync->>Turbo: Replace sync toast
  FamilySync->>Turbo: Replace account sync controls
  Turbo->>Accounts: Render updated sync state
Loading

Possibly related PRs

Suggested reviewers: jjmata

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main UI changes: syncing toolbar/toast synchronization and notification tray overlap fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-accounts-sync-toast

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c28f4ef4d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/views/layouts/application.html.erb Outdated

<%# SHARED - Main content %>
<%= tag.main id: "main", class: class_names("grow overflow-y-auto px-3 lg:px-10 w-full mx-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0"), data: { app_layout_target: "content", viewport_target: "content" } do %>
<%= render "layouts/shared/notification_tray" %>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep background notifications visible while scrolled

Because the tray is now the first child of the scrollable main element (and is similarly placed inside the settings scroll container), any notification delivered while the user is farther down a page is inserted above the viewport and remains unseen due to scroll anchoring. This particularly breaks the sync toast's mid-form path: sync_toast_controller.js deliberately suppresses auto-refresh while a form is focused and expects the visible toast to offer a manual refresh, but a user editing a form below the fold now receives neither a visible prompt nor refreshed data. Keep asynchronously delivered notifications sticky/fixed within the content pane, or otherwise ensure the scroll position cannot hide them.

Useful? React with 👍 / 👎.

gariasf added a commit that referenced this pull request Jul 26, 2026

@jjmata jjmata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good root-cause work — tracing the toast/toolbar overlap back to "three independently-styled pieces of UI representing one sync action, only one of which was updated by the completion broadcast" is the right level of fix rather than just repositioning the toast.

A few things worth calling out as done well:

  • Extracting accounts/_sync_controls and passing family: explicitly (rather than Current.family) is correct and necessary — Family::SyncCompleteEvent#broadcast runs outside a request, so Current.family wouldn't be set there. The comment explaining this is helpful for future readers.
  • The content_for :notification_tray_inline opt-in keeps the fix scoped to the two layouts that actually have the sidebar-mismatch problem, and leaves the five single-column layouts' floating behavior untouched — reasonable, low-blast-radius approach.
  • Deriving both the icon spinner state and the "Cancel sync" visibility from family.syncing?/family.syncs.visible.first in the same partial, broadcast-replaced alongside the toast, directly fixes the "toolbar stuck showing syncing after the toast already says done" bug.
  • Test coverage (system test asserting the tray's bounding rect sits above the header, model test asserting both broadcast targets fire) matches the two behaviors being fixed.

The PR description already flags the remaining sync_toast/cancel_sync locale gap in the other ~15 language files as pre-existing and out of scope — agreed that's reasonable to defer separately rather than block this fix on unrelated translation work.


Generated by Claude Code

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/javascript/controllers/notification_tray_controller.js (1)

27-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare the Turbo morph handler in the template.

Turbo dispatches turbo:before-morph-attribute on the element being morphed, so the tray can handle it through a Stimulus data-action without manual listener lifecycle management. (turbo.hotwired.dev)

  • app/javascript/controllers/notification_tray_controller.js#L27-L32: remove the manual add/remove listener calls.
  • app/views/layouts/shared/_htmldoc.html.erb#L45-L45: add data-action="turbo:before-morph-attribute->notification-tray#preserveStyle" alongside the conditional tray target.

As per coding guidelines, use declarative actions in Stimulus controllers rather than imperative event listeners.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/javascript/controllers/notification_tray_controller.js` around lines 27 -
32, Replace the manual turbo:before-morph-attribute add/remove listener calls in
notification_tray_controller.js, including connect and disconnect, with a
declarative Stimulus action. In app/views/layouts/shared/_htmldoc.html.erb line
45, add
data-action="turbo:before-morph-attribute->notification-tray#preserveStyle"
alongside the conditional tray target so preserveStyle remains invoked during
morphing.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/javascript/controllers/notification_tray_controller.js`:
- Around line 27-32: Replace the manual turbo:before-morph-attribute add/remove
listener calls in notification_tray_controller.js, including connect and
disconnect, with a declarative Stimulus action. In
app/views/layouts/shared/_htmldoc.html.erb line 45, add
data-action="turbo:before-morph-attribute->notification-tray#preserveStyle"
alongside the conditional tray target so preserveStyle remains invoked during
morphing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 13e41fe9-79cb-454d-937d-d38624c318a5

📥 Commits

Reviewing files that changed from the base of the PR and between 8c28f4e and 7b53b6b.

📒 Files selected for processing (6)
  • app/javascript/controllers/notification_tray_controller.js
  • app/views/layouts/application.html.erb
  • app/views/layouts/settings.html.erb
  • app/views/layouts/shared/_htmldoc.html.erb
  • app/views/layouts/shared/_notification_tray.html.erb
  • test/system/accounts_sync_ui_test.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/views/layouts/application.html.erb
  • app/views/layouts/shared/_notification_tray.html.erb
  • test/system/accounts_sync_ui_test.rb

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.

2 participants