diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index f90675d..1b6f630 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -262,6 +262,30 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo // auto-save during that window. Set in the memo (runs at render, before the // column events) and cleared by the re-apply effect below. const restoringRef = useRef(true); + + // Award-column visibility is stored as an explicit set of award CODES, NOT via + // AG Grid's column-state round-trip. Those columns load asynchronously and + // race the state restore, and AG Grid preserves an existing column's visibility + // across a columnDefs rebuild instead of re-reading `hide` — which is exactly + // why previously-shown award columns kept vanishing on reopen. A dedicated set + // is deterministic: it drives `hide` directly and is re-enforced below. + const AWARD_SHOWN_KEY = 'hamlog.awardColsShown'; + const [awardShown, setAwardShown] = useState>(() => new Set((loadLocal(AWARD_SHOWN_KEY) ?? []).map((s) => String(s).toUpperCase()))); + useEffect(() => { + // Fresh machine: hydrate the set from the portable DB copy, then seed the cache. + if (loadLocal(AWARD_SHOWN_KEY)) return; + loadRemote(AWARD_SHOWN_KEY).then((remote) => { + if (remote && remote.length) { + seedLocal(AWARD_SHOWN_KEY, remote); + setAwardShown(new Set(remote.map((s: any) => String(s).toUpperCase()))); + } + }); + }, []); + const persistAwardShown = useCallback((next: Set) => { + setAwardShown(next); + saveState(AWARD_SHOWN_KEY, [...next]); + }, []); + const columnDefs = useMemo[]>(() => { restoringRef.current = true; const base = COL_CATALOG.map((c) => { @@ -274,16 +298,26 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo headerTooltip: t('rqg.awardTip', { name: a.name }), width: 110, cellClass: 'text-[11px]', - // Hidden by DEFAULT (award columns are opt-in). Without this, AG Grid shows - // them whenever the saved column state doesn't cover them — a newly-added - // award, an empty cache, or a rebuild that runs before the saved state is - // re-applied — which is why "all award columns" kept reappearing. The user's - // saved state (a column they explicitly showed) still overrides this. - hide: true, + // Visibility comes from the persisted award-code set, so a column the user + // showed reappears on reopen and one they didn't stays hidden. + hide: !awardShown.has(a.code.toUpperCase()), valueGetter: (p) => (p.data as any)?.award_refs?.[a.code.toUpperCase()] ?? '', })); return [...base, ...awards]; - }, [awardCols, COL_CATALOG, t]); + }, [awardCols, COL_CATALOG, t, awardShown]); + + // Enforce award-column visibility via the API after the columns (re)appear — + // colDef.hide alone isn't honoured by AG Grid for a column that already exists, + // so we set it explicitly whenever the award set or the loaded columns change. + useEffect(() => { + const api = gridRef.current?.api; + if (!api || !awardCols?.length) return; + for (const a of awardCols) { + const want = awardShown.has(a.code.toUpperCase()); + const col = api.getColumn(`award_${a.code}`); + if (col && col.isVisible() !== want) api.setColumnsVisible([`award_${a.code}`], want); + } + }, [awardCols, awardShown]); const defaultColDef = useMemo(() => ({ sortable: true, @@ -342,10 +376,22 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo // state for "which columns are visible" — AG Grid's column state is the // source of truth, and saveColumnState persists it. function isColVisible(colId: string): boolean { + // Award columns: the persisted set is the source of truth (see awardShown). + if (colId.startsWith('award_')) return awardShown.has(colId.slice('award_'.length).toUpperCase()); const col = gridRef.current?.api?.getColumn(colId); return col ? col.isVisible() : !!COL_CATALOG.find((c) => c.colId === colId)?.defaultVisible; } function setColVisible(colId: string, visible: boolean) { + // Award columns are driven by the persisted code set — update it and let the + // enforce effect apply visibility. This is what makes them stick across reopen. + if (colId.startsWith('award_')) { + const code = colId.slice('award_'.length).toUpperCase(); + const next = new Set(awardShown); + if (visible) next.add(code); else next.delete(code); + persistAwardShown(next); + gridRef.current?.api?.setColumnsVisible([colId], visible); + return; + } const api = gridRef.current?.api; if (!api) return; api.setColumnsVisible([colId], visible); @@ -373,6 +419,9 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo api.setColumnsVisible(visible, true); api.setColumnsVisible(hidden, false); saveColumnState(); + // Award columns are opt-in: reset hides them all. + persistAwardShown(new Set()); + (awardCols ?? []).forEach((a) => api.setColumnsVisible([`award_${a.code}`], false)); } return ( @@ -469,8 +518,8 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo
{t('rqg.grpAwards')}
- - + +
diff --git a/frontend/src/lib/uiPref.ts b/frontend/src/lib/uiPref.ts index 37687d0..76ba2b7 100644 --- a/frontend/src/lib/uiPref.ts +++ b/frontend/src/lib/uiPref.ts @@ -31,6 +31,7 @@ const PORTABLE_KEYS = [ 'opslog.clusterLockBand', 'opslog.clusterLockMode', 'opslog.clusterStatusFilter', 'opslog.clusterModeFilter', 'opslog.clusterSearch', 'opslog.clusterHideWorked', 'opslog.activeTab', // last selected tab + 'hamlog.awardColsShown', // which award columns are shown in the QSO grid ]; // syncPortablePrefs reconciles the DB with the local cache at startup: