Skip to content

feat(settings): add save feedback for preferences changes#2261

Open
lspassos1 wants to merge 3 commits intokoala73:mainfrom
lspassos1:feat/settings-save-feedback
Open

feat(settings): add save feedback for preferences changes#2261
lspassos1 wants to merge 3 commits intokoala73:mainfrom
lspassos1:feat/settings-save-feedback

Conversation

@lspassos1
Copy link
Copy Markdown
Collaborator

Summary

Reapplies the settings save feedback work from #1269 on top of current main, without carrying the stale branch drift that is now breaking that PR.

Root cause

The app already had inline Saved feedback for the Panels tab, but Preferences changes (theme, map provider, language, AI toggles, etc.) still had no equivalent confirmation. The older PR branch also diverged badly from main and no longer typechecked cleanly.

Changes

  • add a shared global toast helper in src/utils/toast.ts with role="status"
  • wire renderPreferences() to accept onSettingSaved and fire it once after persisted preference changes
  • show the global Saved toast only for Preferences changes
  • keep the Panels tab on its existing inline Saved status, so there is no duplicate feedback there
  • replace duplicate body-level toast implementations in UnifiedSettings, event-handlers, and country-intel
  • add a guardrail test covering the shared toast path and the no-duplicate Panels behavior

Validation

  • npx tsx --test tests/settings-save-feedback.test.mjs
  • npm run typecheck
  • npx biome lint src/utils/toast.ts src/components/UnifiedSettings.ts src/services/preferences-content.ts src/app/event-handlers.ts src/app/country-intel.ts tests/settings-save-feedback.test.mjs

Risk

Low risk. The change is limited to settings-related feedback paths and a shared toast helper, with the Panels tab intentionally left on its existing inline status behavior.

Ref #1247
Supersedes #1269

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

@lspassos1 is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR introduces a shared showToast utility in src/utils/toast.ts, wires it into the Preferences tab via an onSettingSaved callback on renderPreferences, and removes three duplicate body-level toast implementations from UnifiedSettings, event-handlers, and country-intel. The result is that all preference changes (theme, map provider, language, AI toggles, etc.) now show a brief "Saved" toast, while the Panels tab retains its existing inline status indicator.

Key changes:

  • src/utils/toast.ts — new shared helper; adds role="status" for accessibility, standardises dismiss timeout at 3 000 ms
  • src/services/preferences-content.ts — inlines change logic split into handleSettingsImport / handlePreferenceChange helpers; onSettingSaved is called only when a recognised preference is persisted, never for the import/export path
  • src/components/UnifiedSettings.ts — drops local showToast, passes onSettingSaved to renderPreferences; Panels inline-save path is unchanged
  • src/app/event-handlers.ts / country-intel.ts — duplicate showToast methods removed
  • tests/settings-save-feedback.test.mjs — guardrail test covering the shared utility, callback wiring, and the no-duplicate-panels-toast invariant; one regex is slightly brittle (depends on exact indentation of savePanelChanges closing brace)

Confidence Score: 5/5

  • Safe to merge — changes are limited to settings feedback paths with no data-loss or security surface.
  • Logic is correct: onSettingSaved fires only for recognised preference changes, import/export uses its own inline toast, and the Panels tab inline status is untouched. The two flagged items (test regex fragility, silent 4 000 ms → 3 000 ms timeout change) are non-blocking P2s that don't affect runtime correctness or the primary user path.
  • No files require special attention.

Important Files Changed

Filename Overview
src/utils/toast.ts New shared toast helper; adds role="status" for accessibility and consolidates three duplicate implementations. Minor: timeout silently reduced from 4 000 ms (UnifiedSettings) to 3 000 ms.
src/services/preferences-content.ts Extracts inline change-handler logic into handleSettingsImport / handlePreferenceChange helpers, wires onSettingSaved callback correctly: called only when a recognised preference changes, never for the import path. Local showToast(container, msg, success) remains for the data-management inline toast.
src/components/UnifiedSettings.ts Removes duplicate local showToast, imports shared utility, passes onSettingSaved callback to renderPreferences. Panels tab's existing inline Saved status is untouched.
src/app/event-handlers.ts Drops duplicate showToast method, imports shared utility — straightforward cleanup with no logic change.
src/app/country-intel.ts Drops duplicate showToast method, imports shared utility — straightforward cleanup with no logic change.
tests/settings-save-feedback.test.mjs Guardrail test validating the shared toast utility and the no-duplicate-panels-toast invariant. One regex is brittle (depends on exact 2-space indentation of savePanelChanges closing brace).

Sequence Diagram

sequenceDiagram
    participant User
    participant UnifiedSettings
    participant renderPreferences
    participant handlePreferenceChange
    participant handleSettingsImport
    participant showToast (global)
    participant showToast (inline)

    User->>UnifiedSettings: opens Settings → Preferences tab
    UnifiedSettings->>renderPreferences: renderPreferences({ onSettingSaved: () => showToast("Saved") })
    renderPreferences-->>UnifiedSettings: { html, attach() }

    User->>renderPreferences: changes a preference (e.g. theme)
    renderPreferences->>handleSettingsImport: handleSettingsImport(target, container)
    handleSettingsImport-->>renderPreferences: false (not import input)
    renderPreferences->>handlePreferenceChange: handlePreferenceChange(target, container, host)
    handlePreferenceChange-->>renderPreferences: true (preference persisted)
    renderPreferences->>showToast (global): host.onSettingSaved?.() → showToast("Saved")
    showToast (global)-->>User: body-level toast for 3 000 ms

    User->>renderPreferences: imports settings file
    renderPreferences->>handleSettingsImport: handleSettingsImport(target, container)
    handleSettingsImport->>showToast (inline): showToast(container, "Imported N keys", true)
    showToast (inline)-->>User: inline #usDataMgmtToast message
    handleSettingsImport-->>renderPreferences: true (import handled — early return)
    Note over renderPreferences: onSettingSaved NOT called

    User->>UnifiedSettings: clicks Save on Panels tab
    UnifiedSettings->>UnifiedSettings: savePanelChanges()
    UnifiedSettings-->>User: inline #usPanelsStatus "Saved" (no global toast)
Loading

Reviews (1): Last reviewed commit: "feat(settings): add save feedback for pr..." | Re-trigger Greptile

Comment on lines +37 to +40
});

it('removes duplicate global toast implementations from event handlers and country intel', () => {
assert.match(handlersSrc, /import \{ showToast \} from '@\/utils\/toast';/);
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 Fragile closing-brace regex in guardrail test

The pattern /private savePanelChanges\(\): void \{([\s\S]*?)\n {2}\}/ assumes the closing } of savePanelChanges is indented with exactly 2 spaces. Any formatting change (e.g., reformatter runs, indentation style shift) will silently break the assertion — saveMatch will be null, the assert.ok will fail with a confusing message, and the real assertion about showToast will never be reached.

Consider anchoring on the method name itself rather than its closing brace, or reading the entire file and checking the region between known method boundaries:

// alternative: just assert the method body does not contain showToast anywhere nearby
const methodStart = settingsSrc.indexOf('private savePanelChanges()');
const methodEnd = settingsSrc.indexOf('\n  private ', methodStart + 1);
const methodBody = settingsSrc.slice(methodStart, methodEnd === -1 ? undefined : methodEnd);
assert.ok(methodBody.length > 0, 'savePanelChanges() not found');
assert.doesNotMatch(methodBody, /showToast\(/);

Comment on lines +12 to +14
toast.classList.remove('visible');
setTimeout(() => toast.remove(), 300);
}, 3000);
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 Silent timeout regression for UnifiedSettings toasts

The old local showToast in UnifiedSettings.ts used a 4 000 ms dismiss timeout; event-handlers.ts and country-intel.ts used 3 000 ms. The new shared helper standardises on 3 000 ms, which silently shortens the display time for two toasts in UnifiedSettings (the free-panel-limit toast fired from toggleDraftPanel, and the new "Saved" toast). This may be intentional, but it's worth confirming — users who currently see the panel-limit warning for 4 s will now only see it for 3 s.

If 3 000 ms is the desired standard, a brief comment here would make the intention explicit:

  }, 3000); // 3 s display — standardised across all global toasts

@koala73 koala73 added High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value labels Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants