diff --git a/frontend/src/components/ClusterGrid.tsx b/frontend/src/components/ClusterGrid.tsx index e77d222..7b7a781 100644 --- a/frontend/src/components/ClusterGrid.tsx +++ b/frontend/src/components/ClusterGrid.tsx @@ -179,7 +179,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [ if (s?.new_pota) badges.push(tok('success', t('clg2.newPota'))); if (badges.length === 0) return ; return ( - + {badges.map((bd, i) => ( (null); + const [selectedActiveIds, setSelectedActiveIds] = useState([]); // Add/edit-contact dialog. const [contactOpen, setContactOpen] = useState(false); const [contact, setContact] = useState(emptyStation()); const [looking, setLooking] = useState(false); - const activeGrid = useRef(null); const rosterGrid = useRef(null); // Worked-before for the clicked station (see if/when we contacted it before). @@ -230,18 +231,6 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) { } catch (e: any) { setError(String(e?.message ?? e)); } } - const activeCols = useMemo[]>(() => [ - { headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' }, - { headerName: t('ncp.colName'), field: 'name', flex: 1 }, - { headerName: 'QTH', field: 'qth', flex: 1 }, - { headerName: t('ncp.colTimeOn'), valueGetter: (p) => fmtTimeOn((p.data as any)?.qso_date), width: 90, cellClass: 'font-mono text-[11px]' }, - { headerName: t('ncp.colBand'), field: 'band', width: 70, cellClass: 'font-mono' }, - { headerName: t('ncp.colMode'), field: 'mode', width: 70, cellClass: 'font-mono' }, - { headerName: 'RST S', field: 'rst_sent', width: 70, cellClass: 'font-mono' }, - { headerName: 'RST R', field: 'rst_rcvd', width: 70, cellClass: 'font-mono' }, - { headerName: t('ncp.colComment'), field: 'comment', flex: 1.5 }, - ], [t]); - const rosterCols = useMemo[]>(() => [ { headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' }, { headerName: t('ncp.colName'), field: 'name', flex: 1 }, @@ -292,26 +281,20 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) { {t('ncp.onAirActive')} {t('ncp.activeHint')} -
-
- - ref={activeGrid} - theme={hamlogTheme} - rowData={active} - columnDefs={activeCols} - defaultColDef={defaultColDef} - onRowClicked={(e) => e.data && showWorkedBefore(e.data.callsign, (e.data as any).dxcc ?? 0)} - onRowDoubleClicked={(e) => e.data && setEditingDraft(e.data)} - rowSelection={{ mode: 'singleRow', checkboxes: false, enableClickSelection: true }} - animateRows={false} - getRowId={(p) => String((p.data as any).id)} - /> -
+
+ showWorkedBefore(q.callsign, (q as any).dxcc ?? 0)} + onRowDoubleClicked={(q) => setEditingDraft(q)} + onRowSelected={setSelectedActiveIds} + />
{isOpen && active.length > 0 && (
@@ -335,7 +318,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) { )}
-
+
{!wbCall ? (
{t('ncp.wbHint')}
) : wbBusy ? ( @@ -343,30 +326,11 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) { ) : !wb || wb.count === 0 ? (
{t('ncp.wbNone')} {wbCall}
) : ( - - - - - - - - - - - - - {((wb.entries ?? []) as any[]).map((q, i) => ( - - - - - - - - - ))} - -
{t('ncp.colDate')}{t('ncp.colTimeOn')}{t('ncp.colBand')}{t('ncp.colMode')}RST{t('ncp.colComment')}
{String(q.qso_date ?? '').slice(0, 10)}{String(q.time_on ?? '').slice(0, 5)}{q.band}{q.mode}{(q.rst_sent ?? '')}/{(q.rst_rcvd ?? '')}{q.comment}
+ )}
diff --git a/frontend/src/components/RecentQSOsGrid.tsx b/frontend/src/components/RecentQSOsGrid.tsx index c20943b..ca4e46e 100644 --- a/frontend/src/components/RecentQSOsGrid.tsx +++ b/frontend/src/components/RecentQSOsGrid.tsx @@ -30,7 +30,12 @@ type Props = { // Bump this number to programmatically select every row (e.g. after a search // in the QSL Manager, where the default is "all selected"). selectAllSignal?: number; + // storageKey scopes the persisted column layout/visibility. Omit for the main + // log (keeps the historical keys); pass a distinct value (e.g. "net.onair") to + // reuse this grid elsewhere with its OWN independent column config. + storageKey?: string; onRowDoubleClicked?: (q: QSOForm) => void; + onRowClicked?: (q: QSOForm) => void; onRowSelected?: (ids: number[]) => void; onUpdateFromCty?: (ids: number[]) => void; onUpdateFromQRZ?: (ids: number[]) => void; @@ -49,7 +54,7 @@ type Props = { awardCols?: { code: string; name: string }[]; }; -const COL_STATE_KEY = 'hamlog.qsoColState.v2'; +const BASE_COLSTATE_KEY = 'hamlog.qsoColState.v2'; function fmtMhzDots(hz?: number): string { if (!hz) return ''; @@ -240,10 +245,13 @@ export const groupLabel = (t: TFn, g: string): string => t(GRP_KEYS[g] ?? g); const stripAwardCols = (st: any[] | null | undefined): any[] => (st ?? []).filter((s) => !String(s?.colId ?? '').startsWith('award_')); -export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, awardCols }: Props) { +export function RecentQSOsGrid({ rows, selectAllSignal, storageKey, onRowDoubleClicked, onRowClicked, onRowSelected, onUpdateFromCty, onUpdateFromQRZ, onUpdateFromClublog, onSendTo, onSendRecording, onSendEQSL, onBulkEdit, onExportSelected, onExportFiltered, onExportCabrilloSelected, onExportCabrilloFiltered, onDelete, awardCols }: Props) { const { t } = useI18n(); const gridRef = useRef(null); const [pickerOpen, setPickerOpen] = useState(false); + // Column-layout persistence keys — scoped by storageKey so a reused instance + // (e.g. the Net panel) keeps its own layout independent of the main log. + const colStateKey = storageKey ? `hamlog.qsoColState.${storageKey}` : BASE_COLSTATE_KEY; const [menu, setMenu] = useState(null); // Localized column catalog — rebuilt when the language changes. @@ -283,7 +291,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo // 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 AWARD_SHOWN_KEY = storageKey ? `hamlog.awardColsShown.${storageKey}` : '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. @@ -341,21 +349,21 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo }), []); function onGridReady(e: GridReadyEvent) { - const local = loadLocal(COL_STATE_KEY); + const local = loadLocal(colStateKey); if (local) e.api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true }); // Fall back to the portable DB copy when the local cache is empty // (fresh machine / after a reinstall), then re-seed the cache. - loadRemote(COL_STATE_KEY).then((remote) => { + loadRemote(colStateKey).then((remote) => { if (remote && !local) { e.api.applyColumnState({ state: stripAwardCols(remote) as ColumnState[], applyOrder: true }); - seedLocal(COL_STATE_KEY, remote); + seedLocal(colStateKey, 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, stripAwardCols(state)); + if (state) saveState(colStateKey, stripAwardCols(state)); }, []); // The award columns load asynchronously; when they arrive (or change) the @@ -365,7 +373,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo // every rebuild so the user's choices win. No-op before the grid is ready. useEffect(() => { const api = gridRef.current?.api; - const local = loadLocal(COL_STATE_KEY); + const local = loadLocal(colStateKey); if (api && local) api.applyColumnState({ state: stripAwardCols(local) as ColumnState[], applyOrder: true }); // Re-enable saving once AG Grid has settled the column events from the rebuild. const t = window.setTimeout(() => { restoringRef.current = false; }, 0); @@ -465,6 +473,7 @@ export function RecentQSOsGrid({ rows, selectAllSignal, onRowDoubleClicked, onRo onColumnVisible={saveColumnState} onSortChanged={saveColumnState} onRowDoubleClicked={handleRowDoubleClicked} + onRowClicked={(e) => e.data && onRowClicked?.(e.data)} onSelectionChanged={onSelectionChanged} onCellContextMenu={onCellContextMenu} preventDefaultOnContextMenu diff --git a/frontend/src/components/StationControlPanel.tsx b/frontend/src/components/StationControlPanel.tsx index 38791a1..303193f 100644 --- a/frontend/src/components/StationControlPanel.tsx +++ b/frontend/src/components/StationControlPanel.tsx @@ -112,6 +112,8 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () = const [lengths, setLengths] = useState([]); // current element lengths (mm), from ReadElements const [reading, setReading] = useState(false); const [busyEl, setBusyEl] = useState(null); + const [editingEl, setEditingEl] = useState(null); // element whose mm is being typed + const [editVal, setEditVal] = useState(''); const run = (p: Promise) => p.then(refetch).catch((e) => setErr(String(e?.message ?? e))); const isUB = ant.type !== 'steppir'; @@ -125,16 +127,24 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () = // +/- starts from the real values rather than blind. useEffect(() => { if (isUB && ant.connected) readLengths(); }, [isUB, ant.connected, readLengths]); - // Nudge one element by ±2 mm from its current known length. - const nudge = async (i: number, delta: number) => { - const cur = lengths[i] ?? 0; - const next = Math.max(0, cur + delta); + // Send an absolute length to one element and remember it as the new baseline. + const setLen = async (i: number, mm: number) => { + const next = Math.max(0, Math.round(mm)); setBusyEl(i); setErr(''); setLengths((ls) => { const c = [...ls]; c[i] = next; return c; }); // optimistic try { await MotorSetElement(i, next); refetch(); } catch (e: any) { setErr(String(e?.message ?? e)); } finally { setBusyEl(null); } }; + // Nudge one element by ±2 mm from its current known length. + const nudge = (i: number, delta: number) => setLen(i, (lengths[i] ?? 0) + delta); + // Commit a typed exact length (click on the mm value). Lets the operator fix + // the baseline when the auto-read is off, so +/- then work reliably. + const commitEdit = (i: number) => { + const v = parseInt(editVal, 10); + setEditingEl(null); + if (!isNaN(v) && v >= 0 && v !== lengths[i]) setLen(i, v); + }; const dirs: [number, string][] = [[0, 'N'], [1, '180°'], [2, t('station.bi')]]; return (
@@ -191,9 +201,19 @@ function MotorAntennaWidget({ ant, refetch, t }: { ant: AntStatus; refetch: () = className="flex items-center justify-center size-7 rounded-md border border-border hover:bg-muted disabled:opacity-40 shrink-0"> - - {busyEl === i ? : `${mm} mm`} - + {editingEl === i ? ( + setEditVal(e.target.value)} + onBlur={() => commitEdit(i)} + onKeyDown={(e) => { if (e.key === 'Enter') commitEdit(i); if (e.key === 'Escape') setEditingEl(null); }} + className="text-sm font-mono font-bold flex-1 min-w-0 text-center tabular-nums bg-transparent border border-primary rounded px-1" /> + ) : ( + { setEditingEl(i); setEditVal(String(mm)); }}> + {busyEl === i ? : `${mm} mm`} + + )}