diff --git a/changelog.json b/changelog.json index 518ceef..18fbaaa 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,16 @@ [ + { + "version": "0.21.5", + "date": "2026-07-27", + "en": [ + "Column layouts (widths, order, hidden columns) now stick — five faults were undoing them.", + "ACOM and SPE amplifiers can follow OpsLog's frequency, on a second COM port (Settings → Amplifier)." + ], + "fr": [ + "Les dispositions de colonnes (largeurs, ordre, colonnes masquées) tiennent enfin — cinq défauts les défaisaient.", + "Les amplificateurs ACOM et SPE peuvent suivre la fréquence d'OpsLog, sur un second port COM (Réglages → Amplificateur)." + ] + }, { "version": "0.21.4", "date": "2026-07-26", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f2ee1cc..c05040e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -95,7 +95,7 @@ import { SendSpotModal, type RecentSpotQSO } from '@/components/SendSpotModal'; import { WinkeyerPanel, type WKStatus, type WKMacro } from '@/components/WinkeyerPanel'; import { RotorCompass } from '@/components/RotorCompass'; import { writeUiPref } from '@/lib/uiPref'; -import { setGridPrefsProfile } from '@/lib/gridPrefs'; +import { setGridPrefsProfile, flushGridPrefs } from '@/lib/gridPrefs'; import { DvkPanel, type DVKMsg, type DVKStat } from '@/components/DvkPanel'; import { Button } from '@/components/ui/button'; @@ -2356,6 +2356,14 @@ export default function App() { }).catch(() => {}); }, []); + // A column resized in the last seconds before quitting must still reach the + // database: the DB write is debounced, so flush it on the way out. + useEffect(() => { + const flush = () => flushGridPrefs(); + window.addEventListener('beforeunload', flush); + return () => { window.removeEventListener('beforeunload', flush); flush(); }; + }, []); + // Every setting is per-profile, so when the active profile changes the whole // main UI re-reads its config (station identity, lists, CAT, keyer). The Go // side reloads its managers; this keeps the React state in sync. @@ -4147,7 +4155,7 @@ export default function App() {
- +
{clusterShowFilters && renderClusterFilters()}
@@ -4156,7 +4164,7 @@ export default function App() { case 'worked': return (
- openEdit(q.id as number)} + openEdit(q.id as number)} onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields} @@ -5485,6 +5493,7 @@ export default function App() { } return ( - openEdit(q.id as number)} + openEdit(q.id as number)} onUpdateFromCty={bulkUpdateFromCty} onUpdateFromQRZ={bulkUpdateFromQRZ} onUpdateFromClublog={bulkUpdateFromClublog} onSendTo={bulkSendTo} onSendRecording={bulkSendRecording} onSendEQSL={(ids) => setEqslQsoId(ids[0] ?? null)} onBulkEdit={openBulkEdit} onExportSelected={exportSelectedADIF} onExportSelectedFields={exportSelectedFields} diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index e36cce3..f47846e 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { cleanSpotter, inferSpotMode, spotStatusKey } from '@/lib/spot'; -import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs'; +import { loadLocal, loadRemote, saveState, seedLocal, whenGridPrefsReady } from '@/lib/gridPrefs'; import { useI18n } from '@/lib/i18n'; type TFn = (key: string, vars?: Record) => string; @@ -371,10 +371,28 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { // Localized column catalog — rebuilt when the language changes. const COL_CATALOG = useMemo(() => makeColCatalog(t), [t]); - const columnDefs = useMemo[]>(() => COL_CATALOG.map((c) => { - const { group: _g, label: _l, defaultVisible, ...rest } = c; - return { ...rest, hide: !defaultVisible }; - }), [COL_CATALOG]); + // A rebuild makes AG Grid re-apply every colDef hide/width DEFAULT and fire the + // matching column events. Without this guard those events were persisted, so a + // single language change overwrote the saved cluster layout — in the cache AND + // in the database, with nothing to restore from. + const restoringRef = useRef(true); + + const columnDefs = useMemo[]>(() => { + restoringRef.current = true; + return COL_CATALOG.map((c) => { + const { group: _g, label: _l, defaultVisible, ...rest } = c; + return { ...rest, hide: !defaultVisible }; + }); + }, [COL_CATALOG]); + + // Re-apply the saved state after every rebuild, then re-enable saving. + useEffect(() => { + const api = gridRef.current?.api; + const local = loadLocal(COL_STATE_KEY); + if (api && local) api.applyColumnState({ state: local as ColumnState[], applyOrder: true }); + const tm = window.setTimeout(() => { restoringRef.current = false; }, 0); + return () => window.clearTimeout(tm); + }, [columnDefs]); const defaultColDef = useMemo(() => ({ sortable: true, resizable: true, filter: true, suppressMovable: false, @@ -394,17 +412,20 @@ export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) { gridRef.current?.api?.refreshCells({ force: true }); }, [spotStatus]); - function onGridReady(e: GridReadyEvent) { + // Restore AFTER the profile scope is known — this grid has no key= remount to + // save it from reading the wrong (unscoped) cache key at first paint. + async function onGridReady(e: GridReadyEvent) { + await whenGridPrefsReady(); const local = loadLocal(COL_STATE_KEY); if (local) e.api.applyColumnState({ state: local as ColumnState[], applyOrder: true }); - loadRemote(COL_STATE_KEY).then((remote) => { - if (remote && !local) { - e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true }); - seedLocal(COL_STATE_KEY, remote); - } - }); + const remote = await loadRemote(COL_STATE_KEY); + if (remote && !local) { + e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true }); + seedLocal(COL_STATE_KEY, remote); + } } const saveColumnState = useCallback(() => { + if (restoringRef.current) return; // ignore the events fired by a column rebuild const state = gridRef.current?.api?.getColumnState(); if (state) saveState(COL_STATE_KEY, state); }, []); diff --git a/frontend/src/components/QSLManagerModal.tsx b/frontend/src/components/QSLManagerModal.tsx index 6f0472a..b470cec 100644 --- a/frontend/src/components/QSLManagerModal.tsx +++ b/frontend/src/components/QSLManagerModal.tsx @@ -427,6 +427,10 @@ export function QSLManagerPanel({ onEditQSO }: { onEditQSO?: (id: number) => voi // drives which QSOs the apply-form below updates; a search selects all.