mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
fix(dashboard): persist the currency & timezone widgets so an upgrade keeps them (#1311)
The currency and timezone widgets stored their state only in browser localStorage, so a (docker) upgrade that clears site storage reset them to defaults — unlike every other preference, which is saved server-side. Persist them through the per-user settings store (no schema change; the settings table takes arbitrary keys) and migrate any existing localStorage values on first load so users keep their picks. dashboard_timezones is left unset by default so the widget can tell 'never chosen' from an explicitly emptied list.
This commit is contained in:
@@ -874,4 +874,32 @@ describe('DashboardPage', () => {
|
||||
expect(screen.getByText(/my trips/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FE-PAGE-DASH-034: dashboard widgets persist to settings, not localStorage (#1311)', () => {
|
||||
it('reads the timezone widget zones from the settings store', async () => {
|
||||
// A zone that is NOT in the hardcoded default ([home, London, Tokyo]) — its presence
|
||||
// proves the widget reads the stored preference rather than the old localStorage default.
|
||||
seedStore(useSettingsStore, { settings: buildSettings({ dashboard_timezones: ['America/New_York'] }), isLoaded: true });
|
||||
render(<DashboardPage />);
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /add timezone/i })).toBeInTheDocument());
|
||||
expect(screen.getByText('New York')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('migrates the pre-3.1.3 localStorage prefs into settings and clears the legacy keys', async () => {
|
||||
localStorage.setItem('trek_fx_from', 'CAD');
|
||||
localStorage.setItem('trek_fx_to', 'CHF');
|
||||
localStorage.setItem('trek_dashboard_tz', JSON.stringify(['America/New_York']));
|
||||
seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true });
|
||||
render(<DashboardPage />);
|
||||
// The one-time migration runs on mount (settings already loaded) and removes the keys.
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem('trek_fx_from')).toBeNull();
|
||||
expect(localStorage.getItem('trek_dashboard_tz')).toBeNull();
|
||||
});
|
||||
const s = useSettingsStore.getState().settings;
|
||||
expect(s.dashboard_fx_from).toBe('CAD');
|
||||
expect(s.dashboard_fx_to).toBe('CHF');
|
||||
expect(s.dashboard_timezones).toEqual(['America/New_York']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,8 +491,12 @@ const FX_FALLBACK = ['EUR', 'USD', 'GBP', 'CHF', 'JPY', 'CAD', 'AUD', 'CNY', 'SE
|
||||
|
||||
function CurrencyTool(): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const [from, setFrom] = useState(() => localStorage.getItem('trek_fx_from') || 'EUR')
|
||||
const [to, setTo] = useState(() => localStorage.getItem('trek_fx_to') || 'USD')
|
||||
const isLoaded = useSettingsStore(s => s.isLoaded)
|
||||
const updateSetting = useSettingsStore(s => s.updateSetting)
|
||||
const from = useSettingsStore(s => s.settings.dashboard_fx_from) || 'EUR'
|
||||
const to = useSettingsStore(s => s.settings.dashboard_fx_to) || 'USD'
|
||||
const setFrom = (v: string) => { updateSetting('dashboard_fx_from', v).catch(() => {}) }
|
||||
const setTo = (v: string) => { updateSetting('dashboard_fx_to', v).catch(() => {}) }
|
||||
const [amount, setAmount] = useState('100')
|
||||
const [rates, setRates] = useState<Record<string, number> | null>(null)
|
||||
|
||||
@@ -510,7 +514,18 @@ function CurrencyTool(): React.ReactElement {
|
||||
}, [from])
|
||||
|
||||
useEffect(() => { fetchRate() }, [fetchRate])
|
||||
useEffect(() => { localStorage.setItem('trek_fx_from', from); localStorage.setItem('trek_fx_to', to) }, [from, to])
|
||||
// One-time migration of the pre-3.1.3 localStorage values into the user's settings,
|
||||
// so a (docker) upgrade no longer resets the widget (#1311).
|
||||
useEffect(() => {
|
||||
if (!isLoaded) return
|
||||
const lf = localStorage.getItem('trek_fx_from')
|
||||
const lt = localStorage.getItem('trek_fx_to')
|
||||
if (!lf && !lt) return
|
||||
if (lf) updateSetting('dashboard_fx_from', lf).catch(() => {})
|
||||
if (lt) updateSetting('dashboard_fx_to', lt).catch(() => {})
|
||||
localStorage.removeItem('trek_fx_from')
|
||||
localStorage.removeItem('trek_fx_to')
|
||||
}, [isLoaded, updateSetting])
|
||||
|
||||
const currencies = rates ? Object.keys(rates).sort() : FX_FALLBACK
|
||||
const ccyOptions = currencies.map(c => ({ value: c, label: c }))
|
||||
@@ -565,13 +580,12 @@ function TimezoneTool({ locale }: { locale: string }): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const home = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
const [now, setNow] = useState(() => new Date())
|
||||
const [zones, setZones] = useState<string[]>(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem('trek_dashboard_tz')
|
||||
if (raw) return JSON.parse(raw)
|
||||
} catch { /* ignore malformed storage */ }
|
||||
return [home, ...DEFAULT_ZONES]
|
||||
})
|
||||
const isLoaded = useSettingsStore(s => s.isLoaded)
|
||||
const updateSetting = useSettingsStore(s => s.updateSetting)
|
||||
const stored = useSettingsStore(s => s.settings.dashboard_timezones)
|
||||
// Unset (never chosen) falls back to home + defaults; an explicit list is honoured.
|
||||
const zones = stored ?? [home, ...DEFAULT_ZONES]
|
||||
const setZones = (next: string[]) => { updateSetting('dashboard_timezones', next).catch(() => {}) }
|
||||
const [adding, setAdding] = useState(false)
|
||||
|
||||
// A minute's resolution is plenty for clocks and keeps re-renders cheap.
|
||||
@@ -580,7 +594,18 @@ function TimezoneTool({ locale }: { locale: string }): React.ReactElement {
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { localStorage.setItem('trek_dashboard_tz', JSON.stringify(zones)) }, [zones])
|
||||
// One-time migration of the pre-3.1.3 localStorage value into the user's settings,
|
||||
// so a (docker) upgrade no longer resets the widget (#1311).
|
||||
useEffect(() => {
|
||||
if (!isLoaded) return
|
||||
const raw = localStorage.getItem('trek_dashboard_tz')
|
||||
if (!raw) return
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed)) updateSetting('dashboard_timezones', parsed).catch(() => {})
|
||||
} catch { /* ignore malformed storage */ }
|
||||
localStorage.removeItem('trek_dashboard_tz')
|
||||
}, [isLoaded, updateSetting])
|
||||
|
||||
const allZones = React.useMemo<string[]>(() => {
|
||||
const supported = (Intl as unknown as { supportedValuesOf?: (k: string) => string[] }).supportedValuesOf
|
||||
@@ -591,8 +616,8 @@ function TimezoneTool({ locale }: { locale: string }): React.ReactElement {
|
||||
.filter(z => !zones.includes(z))
|
||||
.map(z => ({ value: z, label: z.replace(/_/g, ' '), searchLabel: z }))
|
||||
|
||||
const addZone = (tz: string) => { if (tz) setZones(prev => prev.includes(tz) ? prev : [...prev, tz]); setAdding(false) }
|
||||
const removeZone = (tz: string) => setZones(prev => prev.filter(z => z !== tz))
|
||||
const addZone = (tz: string) => { if (tz && !zones.includes(tz)) setZones([...zones, tz]); setAdding(false) }
|
||||
const removeZone = (tz: string) => setZones(zones.filter(z => z !== tz))
|
||||
|
||||
const timeIn = (tz: string) => now.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: tz })
|
||||
const offsetLabel = (tz: string) => {
|
||||
|
||||
@@ -41,6 +41,10 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
|
||||
maplibre_style: '',
|
||||
mapbox_3d_enabled: true,
|
||||
mapbox_quality_mode: false,
|
||||
dashboard_fx_from: 'EUR',
|
||||
dashboard_fx_to: 'USD',
|
||||
// dashboard_timezones is intentionally left unset so the widget can tell "never
|
||||
// chosen" (fall back to home + defaults) from an explicitly emptied list.
|
||||
},
|
||||
isLoaded: false,
|
||||
|
||||
|
||||
@@ -124,6 +124,10 @@ export interface Settings {
|
||||
maplibre_style?: string
|
||||
mapbox_3d_enabled?: boolean
|
||||
mapbox_quality_mode?: boolean
|
||||
// Dashboard widget prefs — persisted server-side so a (docker) upgrade keeps them (#1311).
|
||||
dashboard_fx_from?: string
|
||||
dashboard_fx_to?: string
|
||||
dashboard_timezones?: string[]
|
||||
}
|
||||
|
||||
export interface AssignmentsMap {
|
||||
|
||||
Reference in New Issue
Block a user