feat(settings): auto-save preferences instead of draft + Save
Every Settings change now persists on its own (debounced ~500ms so a slider drag is one PUT), with a small transient indicator; the draft/dirty-save UI and the navigation leave-guard are gone. Also unblocks the navigation extraction — there is no longer a cross-page "unsaved settings" guard to thread through routing.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Bell, Monitor, RefreshCw, Trash2, User } from "lucide-react";
|
||||
import { Bell, Check, Loader2, Monitor, RefreshCw, Trash2, User } from "lucide-react";
|
||||
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import { LS, useAccountPersistedState } from "../lib/storage";
|
||||
@@ -9,12 +9,12 @@ import Avatar from "./Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { Section, SettingRow, Switch } from "./ui/form";
|
||||
import { DraftSaveBar, type SaveState } from "./ui/DraftSaveBar";
|
||||
import { type SaveState } from "./ui/DraftSaveBar";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
// The Settings page edits server-persisted preferences as a draft: changes apply locally
|
||||
// for instant preview but reach the server only on an explicit Save (or revert on Discard).
|
||||
// App owns the draft + baseline (so the leave-the-page guard can see it); this controller is
|
||||
// The Settings page edits server-persisted preferences. Each change applies locally for instant
|
||||
// preview AND auto-saves (debounced) — no draft, no Save button; a small indicator flashes the
|
||||
// outcome. App owns the state + the save; this controller is
|
||||
// how the panel reads/writes it. See App.tsx.
|
||||
export type PrefsSaveState = SaveState;
|
||||
export interface PrefsController {
|
||||
@@ -26,9 +26,6 @@ export interface PrefsController {
|
||||
setHints: (v: boolean) => void;
|
||||
notif: NotifSettings;
|
||||
setNotif: (n: NotifSettings) => void;
|
||||
dirty: boolean;
|
||||
save: () => void;
|
||||
discard: () => void;
|
||||
saveState: PrefsSaveState;
|
||||
}
|
||||
|
||||
@@ -94,32 +91,39 @@ export default function SettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save/Discard bar — spans the whole card (a change can come from any tab). Only the
|
||||
Appearance/Notifications prefs are drafted; the Sync/Account tabs act immediately. */}
|
||||
<PrefsSaveBar prefs={prefs} />
|
||||
{/* Auto-save indicator — a change on any tab persists immediately; this just flashes the
|
||||
outcome (the Account tab's actions have their own feedback). */}
|
||||
<SaveIndicator state={prefs.saveState} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PrefsSaveBar({ prefs }: { prefs: PrefsController }) {
|
||||
// A small transient indicator of the auto-save outcome. Renders nothing while idle; it sits at the
|
||||
// bottom of the settings card and fades after a success (the state resets to "idle" in App).
|
||||
function SaveIndicator({ state }: { state: PrefsSaveState }) {
|
||||
const { t } = useTranslation();
|
||||
if (state === "idle") return null;
|
||||
return (
|
||||
<DraftSaveBar
|
||||
variant="inline"
|
||||
dirty={prefs.dirty}
|
||||
state={prefs.saveState}
|
||||
onSave={prefs.save}
|
||||
onDiscard={prefs.discard}
|
||||
labels={{
|
||||
saved: t("settings.save.saved"),
|
||||
failed: t("settings.save.failed"),
|
||||
unsaved: t("settings.save.unsaved"),
|
||||
discard: t("settings.save.discard"),
|
||||
save: t("settings.save.save"),
|
||||
saving: t("settings.save.saving"),
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
data-testid="settings-save-indicator"
|
||||
data-state={state}
|
||||
className="flex items-center justify-end gap-1.5 border-t border-border/60 px-4 py-2.5 text-sm"
|
||||
>
|
||||
{state === "saving" ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-muted">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t("settings.save.saving")}
|
||||
</span>
|
||||
) : state === "saved" ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-emerald-500">
|
||||
<Check className="w-4 h-4" />
|
||||
{t("settings.save.saved")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-red-500">{t("settings.save.failed")}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user