fix(awards): Missing refs froze the window on a big reference list
Reported as POTA: missing references, and it hangs. The Missing-refs modal builds a dropdown of every reference the award has, one menu item each. A POTA log with the park list imported holds tens of thousands of them, and putting that many items in the DOM stops the window answering. Russian districts and the bigger European lists are the same shape. Two things, and the second is the sharper one. The list is now searched rather than scrolled: above 300 references a filter box appears beside the dropdown, and the menu renders at most that many, saying how many it is holding back. Nobody scrolls to K-4521 — they type it. And the modal no longer fetches the references at all until there is something to assign them to. Missing-reference detection needs a DXCC scope, so a worldwide award like POTA always has zero rows here and the modal says so — it was loading every park behind that message, for a dropdown that could not be used for anything. The freeze happened on a screen with nothing to offer. Whether this is the freeze that was reported I cannot say from here; it is a freeze on exactly that screen, for exactly that award.
This commit is contained in:
+4
-2
@@ -3,10 +3,12 @@
|
|||||||
"version": "0.25.7",
|
"version": "0.25.7",
|
||||||
"date": "",
|
"date": "",
|
||||||
"en": [
|
"en": [
|
||||||
"Opening the Awards panel no longer pulls the whole logbook several times at once — a large log briefly took gigabytes of memory."
|
"Opening the Awards panel no longer pulls the whole logbook several times at once — a large log briefly took gigabytes of memory.",
|
||||||
|
"Awards → Missing refs no longer freezes on an award with a huge reference list, POTA above all: the list is searched, not scrolled."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Ouvrir le panneau Awards ne tire plus plusieurs fois le journal entier en même temps — un gros log occupait brièvement des gigaoctets de mémoire."
|
"Ouvrir le panneau Awards ne tire plus plusieurs fois le journal entier en même temps — un gros log occupait brièvement des gigaoctets de mémoire.",
|
||||||
|
"Awards → Réf. manquantes ne fige plus sur un diplôme à très longue liste de références, POTA en tête : la liste se cherche, elle ne se déroule plus."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -608,6 +608,10 @@ export function AwardsPanel({ onEditQSO, onAwardsChanged }: { onEditQSO?: (id: n
|
|||||||
// the missing reference (e.g. a department for DDFM).
|
// the missing reference (e.g. a department for DDFM).
|
||||||
type MissingSortKey = 'qso_date' | 'callsign' | 'band' | 'mode' | 'country' | 'qth';
|
type MissingSortKey = 'qso_date' | 'callsign' | 'band' | 'mode' | 'country' | 'qth';
|
||||||
|
|
||||||
|
// How many references the assign dropdown will put in the DOM at once. Above
|
||||||
|
// this a search box appears beside it and the rest are held back, counted.
|
||||||
|
const REF_MENU_MAX = 300;
|
||||||
|
|
||||||
function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; name: string; onClose: () => void; onEditQSO?: (id: number) => void }) {
|
function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; name: string; onClose: () => void; onEditQSO?: (id: number) => void }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [qsos, setQsos] = useState<any[]>([]);
|
const [qsos, setQsos] = useState<any[]>([]);
|
||||||
@@ -616,6 +620,7 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
|
|||||||
const [sortKey, setSortKey] = useState<MissingSortKey>('callsign');
|
const [sortKey, setSortKey] = useState<MissingSortKey>('callsign');
|
||||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||||
const [refs, setRefs] = useState<Array<{ code: string; name: string }>>([]);
|
const [refs, setRefs] = useState<Array<{ code: string; name: string }>>([]);
|
||||||
|
const [refSearch, setRefSearch] = useState('');
|
||||||
const [assignRef, setAssignRef] = useState('');
|
const [assignRef, setAssignRef] = useState('');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [msg, setMsg] = useState('');
|
const [msg, setMsg] = useState('');
|
||||||
@@ -629,12 +634,38 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
useEffect(() => { load(); }, [code]);
|
useEffect(() => { load(); }, [code]);
|
||||||
// The award's reference list drives the "assign" dropdown (e.g. China provinces).
|
// The award's reference list drives the "assign" dropdown (e.g. China
|
||||||
|
// provinces) — fetched ONLY once there is something to assign it to.
|
||||||
|
//
|
||||||
|
// Missing-reference detection needs a DXCC scope, so a worldwide award like
|
||||||
|
// POTA always has zero rows here and says so. It was still loading every
|
||||||
|
// reference behind that message: on a log with the full POTA park list
|
||||||
|
// imported, tens of thousands of them, feeding a dropdown that could not be
|
||||||
|
// used for anything. That is the window freezing on a screen with nothing to
|
||||||
|
// offer.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loading || qsos.length === 0) { setRefs([]); return; }
|
||||||
ListAwardReferences(code)
|
ListAwardReferences(code)
|
||||||
.then((r) => setRefs(((r ?? []) as any[]).map((x) => ({ code: String(x.code).toUpperCase(), name: String(x.name ?? '') }))))
|
.then((r) => setRefs(((r ?? []) as any[]).map((x) => ({ code: String(x.code).toUpperCase(), name: String(x.name ?? '') }))))
|
||||||
.catch(() => setRefs([]));
|
.catch(() => setRefs([]));
|
||||||
}, [code]);
|
}, [code, loading, qsos.length]);
|
||||||
|
|
||||||
|
// What the assign dropdown actually renders. Bounded, and it says how many it
|
||||||
|
// is holding back rather than silently showing the first few hundred.
|
||||||
|
const shownRefs = useMemo(() => {
|
||||||
|
const q = refSearch.trim().toUpperCase();
|
||||||
|
const rows = q
|
||||||
|
? refs.filter((r) => r.code.includes(q) || r.name.toUpperCase().includes(q))
|
||||||
|
: refs;
|
||||||
|
return rows.slice(0, REF_MENU_MAX);
|
||||||
|
}, [refs, refSearch]);
|
||||||
|
const hiddenRefs = useMemo(() => {
|
||||||
|
const q = refSearch.trim().toUpperCase();
|
||||||
|
const total = q
|
||||||
|
? refs.filter((r) => r.code.includes(q) || r.name.toUpperCase().includes(q)).length
|
||||||
|
: refs.length;
|
||||||
|
return Math.max(0, total - shownRefs.length);
|
||||||
|
}, [refs, refSearch, shownRefs.length]);
|
||||||
|
|
||||||
const qthOf = (q: any) => String(q.qth || q.notes || '');
|
const qthOf = (q: any) => String(q.qth || q.notes || '');
|
||||||
const sorted = useMemo(() => {
|
const sorted = useMemo(() => {
|
||||||
@@ -711,14 +742,29 @@ function MissingQSOModal({ code, name, onClose, onEditQSO }: { code: string; nam
|
|||||||
{/* Bulk-assign toolbar */}
|
{/* Bulk-assign toolbar */}
|
||||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border/50 bg-muted/20">
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-border/50 bg-muted/20">
|
||||||
<span className="text-xs text-muted-foreground">{t('awp.selectedArrow', { n: sel.size })}</span>
|
<span className="text-xs text-muted-foreground">{t('awp.selectedArrow', { n: sel.size })}</span>
|
||||||
|
{/* A search box beside the dropdown, and a bounded list inside it.
|
||||||
|
A reference list can hold tens of thousands of entries (POTA
|
||||||
|
parks, Russian districts); every one of them as a menu item is
|
||||||
|
hundreds of thousands of DOM nodes and a window that stops
|
||||||
|
answering. Nobody scrolls to K-4521 anyway — they type it. */}
|
||||||
|
{refs.length > REF_MENU_MAX && (
|
||||||
|
<Input className="h-7 w-40 text-xs font-mono" value={refSearch}
|
||||||
|
placeholder={t('awp.filterReferences')}
|
||||||
|
onChange={(e) => setRefSearch(e.target.value)} />
|
||||||
|
)}
|
||||||
<Select value={assignRef} onValueChange={setAssignRef}>
|
<Select value={assignRef} onValueChange={setAssignRef}>
|
||||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue placeholder={t('awp.chooseReference')} /></SelectTrigger>
|
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue placeholder={t('awp.chooseReference')} /></SelectTrigger>
|
||||||
<SelectContent className="max-h-72">
|
<SelectContent className="max-h-72">
|
||||||
{refs.map((r) => (
|
{shownRefs.map((r) => (
|
||||||
<SelectItem key={r.code} value={r.code}>
|
<SelectItem key={r.code} value={r.code}>
|
||||||
<span className="font-mono font-semibold">{r.code}</span>{r.name ? <span className="text-muted-foreground"> · {r.name}</span> : ''}
|
<span className="font-mono font-semibold">{r.code}</span>{r.name ? <span className="text-muted-foreground"> · {r.name}</span> : ''}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
{hiddenRefs > 0 && (
|
||||||
|
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">
|
||||||
|
{t('awp.refsNarrow', { n: hiddenRefs })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button size="sm" disabled={!assignRef || sel.size === 0 || busy} onClick={applyAssign}>
|
<Button size="sm" disabled={!assignRef || sel.size === 0 || busy} onClick={applyAssign}>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user