feat(entry/stats/awards): Alt+W clear, slot drill-down, DXCC prefix column
Five operator-requested items: - Alt+W clears the QSO entry. Handled before the `typing` guard and above the keyer's key routing, so there is always one key that clears whatever else is running — Esc is not that key when the CW keyer reserves it. - The Grid box no longer pops outside the entry panel on a narrow window. Row 2 needed 300+130+76+gaps = 538 px inside a panel whose min-width is 520, so Grid was pushed out and clipped at every narrow width, not just extreme ones. QTH's min-width drops to 80 and the row wraps rather than overflowing if it ever still can't fit. - Selecting a QSO in the log now drives the Stats (F1) matrix. Uses its own WorkedBefore call into separate state, NOT runWorkedBefore: that one owns the entry form's wbRef and can trigger a field backfill, which browsing the log must never do. The entry form wins whenever it holds a call. - Clicking a coloured band/mode square lists the contacts behind it. Returns the exact callsign AND the rest of the entity, because that pair is what the cell's colour encodes; the call's own QSOs are bolded. The DXCC arm matches the stored dxcc column only — reconstructing it from the callsign here would disagree with the matrix above, which is built from that column. - The Awards DXCC list shows each entity's primary prefix in its own sortable column. Derived live from cty.dat into Ref.Group rather than stored on the reference row, so an existing installation needs no re-seed.
This commit is contained in:
+45
-3
@@ -1512,6 +1512,11 @@ export default function App() {
|
||||
const [deletingIds, setDeletingIds] = useState<number[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
// Call/band/mode of the QSO highlighted in the Recent-QSOs grid. Drives the
|
||||
// Stats (F1) matrix whenever the entry form is empty — clicking a past contact
|
||||
// is the natural way to ask "what else have I got with this one?", and until
|
||||
// now the panel just sat blank.
|
||||
const [selQso, setSelQso] = useState<{ call: string; band: string; mode: string } | null>(null);
|
||||
const [bulkEditIds, setBulkEditIds] = useState<number[]>([]);
|
||||
const [bulkEditOpen, setBulkEditOpen] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
@@ -3315,6 +3320,24 @@ export default function App() {
|
||||
finally { setDeletingAll(false); }
|
||||
}
|
||||
|
||||
// Worked-before for the QSO highlighted in the grid. Deliberately NOT
|
||||
// runWorkedBefore: that one owns the entry form's wbRef and can trigger a
|
||||
// field backfill, which browsing the log must never do.
|
||||
const [selWb, setSelWb] = useState<WB | null>(null);
|
||||
const [selWbBusy, setSelWbBusy] = useState(false);
|
||||
useEffect(() => {
|
||||
const call = (selQso?.call ?? '').trim().toUpperCase();
|
||||
// The entry form wins whenever it has a call — it's the live QSO.
|
||||
if (!call || callsign.trim()) { setSelWb(null); return; }
|
||||
let dead = false;
|
||||
setSelWbBusy(true);
|
||||
WorkedBefore(call, 0)
|
||||
.then((w: any) => { if (!dead) setSelWb(w); })
|
||||
.catch(() => { if (!dead) setSelWb(null); })
|
||||
.finally(() => { if (!dead) setSelWbBusy(false); });
|
||||
return () => { dead = true; };
|
||||
}, [selQso, callsign]);
|
||||
|
||||
async function runWorkedBefore(call: string, dxccHint: number = 0) {
|
||||
setWbBusy(true);
|
||||
try {
|
||||
@@ -3859,6 +3882,16 @@ export default function App() {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'o') {
|
||||
e.preventDefault(); importAdif(); return;
|
||||
}
|
||||
// Alt+W: wipe the entry. The near-universal logger convention (N1MM,
|
||||
// Log4OM, DXLog), and unlike ESC it is never swallowed by the CW keyer —
|
||||
// so there is always one key that clears, whatever else is running.
|
||||
// Alt-combos carry no character on Windows, so this is safe while typing.
|
||||
if (e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'w') {
|
||||
e.preventDefault();
|
||||
resetEntry();
|
||||
callsignRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
if (typing) return;
|
||||
if (selectedId !== null) {
|
||||
if (e.key === 'Delete') { e.preventDefault(); askDeleteSelected(); return; }
|
||||
@@ -4131,7 +4164,7 @@ export default function App() {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col flex-1 min-w-[130px]"><Label className="mb-1 h-3.5">{t('field.qth')}</Label>
|
||||
<div className="flex flex-col flex-1 min-w-[80px]"><Label className="mb-1 h-3.5">{t('field.qth')}</Label>
|
||||
<Input value={qth} onChange={(e) => { setQth(e.target.value); markEdited('qth'); }} />
|
||||
</div>
|
||||
);
|
||||
@@ -4412,7 +4445,7 @@ export default function App() {
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => { resetEntry(); callsignRef.current?.focus(); }} className="h-8 px-2.5 text-xs"
|
||||
title="Clear the QSO entry (always — unlike Esc which may be reserved for the CW keyer)">
|
||||
title="Clear the QSO entry — Alt+W (always works, unlike Esc which may be reserved for the CW keyer)">
|
||||
<Eraser className="size-3.5" />
|
||||
{t('btn.clear')}
|
||||
</Button>
|
||||
@@ -4746,6 +4779,7 @@ export default function App() {
|
||||
onExportFiltered={exportFilteredADIF}
|
||||
onDelete={(ids) => setDeletingIds(ids)}
|
||||
onRowSelected={(ids) => { setSelectedIds(ids); setSelectedId(ids[0] ?? null); }}
|
||||
onRowSelectedQso={(r) => setSelQso(r ? { call: String(r.callsign ?? ""), band: String(r.band ?? ""), mode: String(r.mode ?? "") } : null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -5483,7 +5517,10 @@ export default function App() {
|
||||
{/* Row 2: Name fixed to the Band/Mode/Country column width (300px) so
|
||||
its right edge lines up with that column below; QTH grows to fill.
|
||||
gap-4 matches Row 3 so QTH's left edge aligns with Comment/Note. */}
|
||||
<div className="flex gap-4 items-end">
|
||||
{/* flex-wrap, not overflow: at the narrowest window the Grid box used
|
||||
to be pushed outside the entry panel and clipped. Wrapping drops
|
||||
it to its own line instead, which stays readable. */}
|
||||
<div className="flex gap-4 items-end flex-wrap">
|
||||
<div className="flex flex-col w-[300px] shrink-0"><Label className="mb-1 h-3.5">Name</Label>
|
||||
<Input value={name} onChange={(e) => { setName(e.target.value); markEdited('name'); }} />
|
||||
</div>
|
||||
@@ -5549,6 +5586,10 @@ export default function App() {
|
||||
band={band}
|
||||
mode={mode}
|
||||
bands={bands}
|
||||
{...(!callsign.trim() && selQso ? {
|
||||
slotCall: selQso.call, slotBand: selQso.band, slotMode: selQso.mode,
|
||||
slotWb: selWb, slotWbBusy: selWbBusy,
|
||||
} : {})}
|
||||
tab={detailTab}
|
||||
onTab={setDetailTab}
|
||||
keyerActive={(wkEnabled && wkStatus.connected) || dvkEnabled}
|
||||
@@ -5999,6 +6040,7 @@ export default function App() {
|
||||
onExportCabrilloFiltered={exportFilteredCabrillo}
|
||||
onDelete={(ids) => setDeletingIds(ids)}
|
||||
onRowSelected={(ids) => { setSelectedIds(ids); setSelectedId(ids[0] ?? null); }}
|
||||
onRowSelectedQso={(r) => setSelQso(r ? { call: String(r.callsign ?? ""), band: String(r.band ?? ""), mode: String(r.mode ?? "") } : null)}
|
||||
/>
|
||||
<div className="px-3 py-1.5 border-t border-border/60 text-[11px] text-muted-foreground flex items-center justify-between gap-3 bg-muted/30">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user