mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +00:00
* fix(sync-toast): morph refresh, defer behind modals, DS conformance Follow-up to #1964 (addresses #2071). - Refresh via Turbo morph visit instead of window.location.reload, so scroll position and data-turbo-permanent elements (the AI chat panel) survive and there is no white flash. - Defer the toast while a <dialog> is open and reveal it on close. A refresh mid-modal closes the dialog and discards its in-progress input, which is the exact data loss this toast exists to prevent. Handles stacked modals. - Refresh CTA and close button now use DS::Button (secondary / icon). The close is always visible, inside the card, focusable, and has an aria-label; the old hover-only corner chip was unreachable on touch and not keyboard-focusable. - Add role="status" / aria-live="polite" to the toast. - Fix icon color: "inverse" is not a key in the icon helper color map, so it silently rendered no color class (dark icon on bg-info). Use "white", which maps to the functional text-inverse token. - Tighten copy: "New data available" / "Refresh". - Sync the broadcast comment with the actual replace/morph behavior. * fix(sync-toast): detach deferred dialog listener on disconnect A toast replaced by a newer broadcast_replace_to while a <dialog> was open kept its 'close' listener attached, so the detached controller fired #reveal()/#arm() when the dialog closed — a spurious auto-refresh from a stale toast (and repeated syncs could queue several). Store the dialog + handler refs and remove the listener in disconnect(). Flagged by codex + coderabbit on #2105. * fix(sync-toast): re-check interaction and dialogs at refresh-fire time The interaction check ran once at arm time but the refresh fired two seconds later. The post-dialog reveal made that window matter: the user closes a dialog sitting on a form, resumes typing, and the timer morphs the page — wiping non-turbo-permanent input, the exact data-loss class this toast exists to prevent. A dialog opened during the window had the mirror problem (the refresh would close it). Bail inside the callback instead, leaving the toast visible for a manual refresh, matching the mid-form behavior. Also documents the dialog-removed-without-close edge on the deferred listener.
32 lines
1.2 KiB
Ruby
32 lines
1.2 KiB
Ruby
class Family::SyncCompleteEvent
|
|
attr_reader :family
|
|
|
|
def initialize(family)
|
|
@family = family
|
|
end
|
|
|
|
def broadcast
|
|
# Replace the #sync-toast slot with a lightweight toast instead of a full
|
|
# page refresh. The sync-toast Stimulus controller handles three cases:
|
|
# - User is idle → morph-refreshes after a short delay
|
|
# - User is mid-form → toast stays visible; user clicks "Refresh"
|
|
# - A modal is open → toast defers until the dialog closes
|
|
#
|
|
# This avoids wiping in-progress form state when a background sync fires.
|
|
# The partial contains no user-scoped data (Current.user is nil here), so
|
|
# each browser re-fetches the page on its own authenticated request.
|
|
family.broadcast_replace_to(
|
|
family,
|
|
target: "sync-toast",
|
|
partial: "shared/notifications/sync_toast"
|
|
)
|
|
|
|
# Schedule recurring transaction pattern identification (debounced to run after all syncs complete)
|
|
begin
|
|
RecurringTransaction.identify_patterns_for(family)
|
|
rescue => e
|
|
Rails.logger.error("Family::SyncCompleteEvent recurring transaction identification failed: #{e.message}\n#{e.backtrace&.join("\n")}")
|
|
end
|
|
end
|
|
end
|