import { useCallback, useEffect, useMemo, useState } from 'react'; import { Plus, Trash2, RotateCcw, Save, Download, Upload, Loader2, Search, FolderOpen, ArrowUpCircle } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Combobox } from '@/components/ui/combobox'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { GetAwardDefs, SaveAwardDefs, ResetAwardDefs, AwardFields, GetAwardReferenceMeta, UpdateAwardReferenceList, ListAwardReferences, SearchAwardReferences, SaveAwardReference, DeleteAwardReference, ImportAwardReferencesText, GetAwardPresets, ApplyAwardPreset, ListCountries, DXCCForCountry, DXCCName, PopulateBuiltinReferences, HasBuiltinReferences, ExportAwards, ImportAwards, InspectAwardImport, ApplyAwardImport, GetCatalogCodes, OpenAwardsFolder, GetAwardUpdates, ApplyAwardUpdate, DismissAwardUpdate, ExplainAward, } from '../../wailsjs/go/main/App'; // Above this many references the editor stops loading the whole list and // switches to search-only (mirrors Log4OM's "Too many items" behaviour). const REF_LIST_CAP = 1000; type RefMeta = { code: string; count: number; updated_at: string; can_update: boolean }; export type AwardDef = { code: string; name: string; description?: string; valid?: boolean; protected?: boolean; url?: string; download_url?: string; ref_url?: string; valid_from?: string; valid_to?: string; alias?: string; ref_display?: string; // grid column shows: ref | name | both type?: string; field: string; match_by?: string; exact_match?: boolean; pattern: string; leading_str?: string; trailing_str?: string; dynamic?: boolean; or_rules?: AwardOrRule[]; dxcc_filter: number[] | null; valid_bands?: string[]; valid_modes?: string[]; emission?: string[]; confirm: string[] | null; validate?: string[] | null; grant_codes?: string; export_credit_granted?: boolean; total: number; builtin?: boolean; }; type AwardOrRule = { field: string; match_by?: string; exact_match?: boolean; pattern?: string; leading_str?: string; trailing_str?: string; prefix?: string; }; type AwardRef = { code: string; name: string; dxcc: number; group: string; subgrp: string; dxcc_list?: number[]; pattern?: string; valid: boolean; valid_from?: string; valid_to?: string; score?: number; bonus?: number; gridsquare?: string; alias?: string; }; type Preset = { key: string; name: string; field: string; dxcc: number; refs: AwardRef[] }; // Award types mirror Log4OM: REFERENCE (we supply a list), QSOFIELDS (scan any // QSO field — handles state/grid4/zones/etc.), CALLSIGN. The type is // organizational only; matching is driven by the field/pattern/dynamic options, // so there's no need for separate GRID/DXCC types (use QSOFIELDS + the field). const AWARD_TYPES = ['REFERENCE', 'QSOFIELDS', 'CALLSIGN']; const CONFIRM_SRC = [ { id: 'lotw', label: 'LoTW' }, { id: 'qsl', label: 'QSL' }, { id: 'eqsl', label: 'eQSL' }, { id: 'qrzcom', label: 'QRZ.com' }, { id: 'custom', label: 'Custom' }, ]; const BANDS = ['2190m','630m','160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','1.25m','70cm','23cm','13cm']; const MODES = ['CW','SSB','USB','LSB','AM','FM','RTTY','PSK31','FT8','FT4','JT65','JT9','MFSK','OLIVIA','DIGITALVOICE']; const EMISSIONS = ['CW', 'PHONE', 'DIGITAL']; const emptyAward = (): AwardDef => ({ code: 'NEW', name: 'New award', description: '', valid: true, type: 'QSOFIELDS', field: 'note', match_by: 'code', exact_match: true, pattern: '', dxcc_filter: null, confirm: ['lotw', 'qsl'], validate: ['lotw', 'qsl'], total: 0, }); interface Props { open: boolean; onClose: () => void; onSaved: () => void; } // Small reusable multi-toggle chip group. function Chips({ all, value, onToggle }: { all: string[]; value: string[]; onToggle: (v: string) => void }) { return (
{all.map((v) => { const on = value.includes(v); return ( ); })}
); } function Field2({ label, children }: { label: string; children: React.ReactNode }) { return (
{children}
); } // DxccFilter — pick the entities an award is scoped to by country name (like the // QSO editor), resolving each to its ADIF DXCC number. Stored as number[]. function DxccFilter({ value, onChange, countries }: { value: number[]; onChange: (v: number[]) => void; countries: string[] }) { const { t } = useI18n(); const [names, setNames] = useState>({}); useEffect(() => { let live = true; (async () => { const miss = value.filter((n) => names[n] === undefined); if (miss.length === 0) return; const got: Record = {}; for (const n of miss) { try { got[n] = await DXCCName(n); } catch { got[n] = ''; } } if (live) setNames((m) => ({ ...m, ...got })); })(); return () => { live = false; }; }, [value]); // eslint-disable-line react-hooks/exhaustive-deps async function addCountry(name: string) { const n = await DXCCForCountry(name); if (n && n > 0 && !value.includes(n)) { setNames((m) => ({ ...m, [n]: name })); onChange([...value, n]); } } return (
{value.length > 0 && (
{value.map((n) => ( #{n} {names[n] || '…'} ))}
)}
); } export function AwardEditor({ open, onClose, onSaved }: Props) { const { t } = useI18n(); const [defs, setDefs] = useState([]); const [fields, setFields] = useState([]); const [meta, setMeta] = useState>({}); const [presets, setPresets] = useState([]); const [countries, setCountries] = useState([]); const [sel, setSel] = useState(0); const [search, setSearch] = useState(''); const [updating, setUpdating] = useState(null); const [err, setErr] = useState(''); // The err banner doubles as a success/notice area (export path, import counts, // "populated N refs"). Auto-dismiss it after a few seconds so it doesn't stay // forever; the longer text (export path) gets a bit more time. useEffect(() => { if (!err) return; const t = window.setTimeout(() => setErr(''), 8000); return () => window.clearTimeout(t); }, [err]); const loadMeta = () => GetAwardReferenceMeta() .then((m) => setMeta(Object.fromEntries(((m ?? []) as RefMeta[]).map((x) => [x.code.toUpperCase(), x])))) .catch(() => {}); useEffect(() => { if (!open) return; setErr(''); Promise.all([GetAwardDefs(), AwardFields(), GetAwardPresets(), ListCountries()]) .then(([d, f, p, c]) => { setDefs((d ?? []) as any); setFields(((f ?? []) as string[]).slice().sort((a, b) => a.localeCompare(b))); setPresets((p ?? []) as any); setCountries((c ?? []) as any); }) .catch((e) => setErr(String(e?.message ?? e))); loadMeta(); }, [open]); // Codes present in the SHIPPED catalog. Anything in the database that isn't in // here exists only on this machine — it is yours alone, and a reinstall loses it // unless it has been exported. The list flags those. const [catalogCodes, setCatalogCodes] = useState([]); useEffect(() => { if (!open) return; GetCatalogCodes().then((c: any) => setCatalogCodes(((c ?? []) as string[]).map((s) => s.toUpperCase()))).catch(() => {}); }, [open]); // Shipped fixes we did NOT apply, because this award carries the operator's own // changes and we will not overwrite those behind their back. Offered, not forced. type AwardUpdate = { code: string; name: string; from: number; to: number }; const [updates, setUpdates] = useState([]); const loadUpdates = useCallback(() => { GetAwardUpdates().then((u: any) => setUpdates(Array.isArray(u) ? u : [])).catch(() => {}); }, []); useEffect(() => { if (open) loadUpdates(); }, [open, loadUpdates]); // Pending import awaiting the operator's decision on the awards that collide. type ImportEntry = { code: string; name: string; references: number; exists: boolean; mine_name: string; mine_refs: number; protected: boolean }; type ImportPreview = { path: string; awards: ImportEntry[] }; const [importPreview, setImportPreview] = useState(null); const [decisions, setDecisions] = useState>({}); useEffect(() => { if (!importPreview) return; // Default to the SAFE choice: keep what you have. An import must never destroy // an award because the operator clicked through a dialog without reading it. const d: Record = {}; for (const e of importPreview.awards) d[e.code] = e.exists ? 'skip' : 'replace'; setDecisions(d); }, [importPreview]); const cur = defs[sel]; const selUpdate = updates.find((u) => (u.code ?? '').toUpperCase() === (cur?.code ?? '').toUpperCase()) ?? null; // ── Award tester: run the award's rules against a real QSO and show every step. type Rejected = { candidate: string; reason: string }; type Step = { rule: string; field: string; match_by?: string; exact?: boolean; pattern?: string; field_value?: string; candidates?: string[]; kept?: string[]; rejected?: Rejected[]; skipped?: boolean; error?: string; }; type Explanation = { code: string; in_scope: boolean; scope_error?: string; predefined: boolean; ref_count: number; steps: Step[]; manual?: string[]; result: string[]; }; type TestRow = { qso: any; explanation: Explanation }; const [testCall, setTestCall] = useState(''); const [testRows, setTestRows] = useState(null); const [testErr, setTestErr] = useState(''); const [testing, setTesting] = useState(false); const runTest = async () => { if (!cur) return; setTesting(true); setTestErr(''); setTestRows(null); try { const r = await ExplainAward(cur.code, testCall); setTestRows((Array.isArray(r) ? r : []) as TestRow[]); } catch (e: any) { setTestErr(String(e?.message ?? e)); } finally { setTesting(false); } }; // The tester reads the SAVED award, not the unsaved edits in this dialog — so say // so, rather than let the operator test a rule they only think they applied. useEffect(() => { setTestRows(null); setTestErr(''); }, [sel]); // Several QSOs with the same station usually trace IDENTICALLY, and twenty copies // of the same trace is noise. But they don't always: scope is judged per QSO // (band, mode, date — FFMA's 1983 cut-off), a manual override lives ON a QSO, and // two contacts can even hold different QTHs. That divergence is often the actual // answer ("why does my 2019 QSO count and my 2024 one not?"), so we group by // trace instead of dropping it: one card per distinct outcome, with its count. const testGroups = useMemo(() => { if (!testRows) return null; const groups: { key: string; rows: TestRow[] }[] = []; for (const row of testRows) { const key = JSON.stringify(row.explanation); const g = groups.find((x) => x.key === key); if (g) g.rows.push(row); else groups.push({ key, rows: [row] }); } return groups; }, [testRows]); const patch = (p: Partial) => setDefs((ds) => ds.map((d, j) => (j === sel ? { ...d, ...p } : d))); const toggleIn = (key: keyof AwardDef, v: string) => { const arr = ((cur?.[key] as string[]) ?? []); patch({ [key]: arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v] } as any); }; function addAward() { setDefs((ds) => [...ds, emptyAward()]); setSel(defs.length); } function removeAward(i: number) { setDefs((ds) => ds.filter((_, j) => j !== i)); setSel((s) => Math.max(0, s >= i ? s - 1 : s)); } async function save() { setErr(''); try { const clean = defs.filter((d) => d.code.trim()) .map((d) => ({ ...d, code: d.code.trim().toUpperCase(), confirm: d.confirm ?? [], validate: d.validate ?? [] })); await SaveAwardDefs(clean as any); onSaved(); onClose(); } catch (e: any) { setErr(String(e?.message ?? e)); } } async function reset() { try { setDefs((await ResetAwardDefs()) as any); setSel(0); } catch (e: any) { setErr(String(e?.message ?? e)); } } // Export every award definition + reference list to a JSON bundle (backup // that survives a reinstall / PC change, independent of the database). async function exportAwards() { setErr(''); try { const p = await ExportAwards(); if (p) setErr(t('awed.exportedTo', { path: p })); } catch (e: any) { setErr(String(e?.message ?? e)); } } // Import: LOOK FIRST, then ask. // // This used to merge by code with "imported wins", silently — import a WAPC // someone shared and YOUR WAPC (its province list, its city regexes) was // destroyed without a word. Sharing awards is exactly what we want people to do, // so it must not be a data-loss trap. async function importAwards() { setErr(''); try { const p: any = await InspectAwardImport(); if (!p?.path) return; // cancelled const clashes = (p.awards ?? []).filter((e: any) => e.exists); if (clashes.length === 0) { // Nothing collides — nothing to ask. This is the common case. const dec: Record = {}; for (const e of p.awards) dec[e.code] = 'replace'; await applyImport(p.path, dec); return; } setImportPreview(p); // → the collision dialog decides } catch (e: any) { setErr(String(e?.message ?? e)); } } async function applyImport(path: string, decisions: Record) { try { const r: any = await ApplyAwardImport(path, decisions); setImportPreview(null); const [d] = await Promise.all([GetAwardDefs(), loadMeta()]); setDefs((d ?? []) as any); setSel(0); onSaved(); if (r?.awards || r?.references) { setErr(t('awed.importedMsg', { awards: r.awards, references: r.references })); } } catch (e: any) { setErr(String(e?.message ?? e)); } } async function updateList(code: string) { setUpdating(code); setErr(''); try { await UpdateAwardReferenceList(code); await loadMeta(); } catch (e: any) { setErr(`${code}: ${String(e?.message ?? e)}`); } finally { setUpdating(null); } } const filtered = useMemo(() => { const q = search.trim().toUpperCase(); // Keep the original index `i` (used for selection/patch) but display the // list sorted alphabetically by code — scales as the catalogue grows. return defs.map((d, i) => ({ d, i })) .filter(({ d }) => !q || d.code.toUpperCase().includes(q) || d.name.toUpperCase().includes(q)) .sort((a, b) => a.d.code.localeCompare(b.d.code)); }, [defs, search]); return ( { if (!o) onClose(); }}> {t('awed.awardManagement')}
{/* Left: award list */}
setSearch(e.target.value)} />
{filtered.map(({ d, i }) => { // An award that is NOT in the shipped catalog exists only in THIS // database: nobody else has it, and a reinstall loses it unless it // has been exported. That deserves to be visible at a glance, not // discovered the hard way. const onlyHere = !catalogCodes.includes((d.code ?? '').toUpperCase()); // A pending update is only reachable from the award's own banner, so // the list has to say which award to open — otherwise the fix waits // behind a click nobody knows to make. const hasUpdate = updates.some((u) => (u.code ?? '').toUpperCase() === (d.code ?? '').toUpperCase()); return ( ); })}
{/* Right: tabbed editor for selected award */}
{err &&
setErr('')} title={t('awed.clickToDismiss')} className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all cursor-pointer">{err}
} {/* A fix shipped for an award this operator has customised. We did NOT apply it — that would destroy their work — so we offer it, and say plainly what accepting costs. */} {cur && selUpdate && (
{t('awed.updateAvailable')}
{t('awed.updateOverwrites')}
)} {!cur ? (
{t('awed.selectOrCreate')}
) : ( {t('awed.tabInfo')} {t('awed.tabType')} {t('awed.tabConfirmation')} {t('awed.tabReferences')} {t('awed.tabTest')}
{/* ── Award info ── */}
patch({ code: e.target.value })} placeholder="CODE" /> patch({ name: e.target.value })} placeholder={t('awed.awardName')} /> {/* No "Built-in" checkbox: an award OpsLog ships IS built-in, and the catalog derives that on load. Asking the author to tick a box to declare it would be one more step nobody can guess — forget it and the award silently misses every future catalog correction. "Protected" stays: whether an award can be deleted IS a real choice. */}
patch({ description: e.target.value })} /> patch({ url: e.target.value })} /> patch({ ref_url: e.target.value })} placeholder="https://…/" />
patch({ valid_from: e.target.value })} /> patch({ valid_to: e.target.value })} />
patch({ dxcc_filter: v })} countries={countries} />
toggleIn('valid_bands', v)} />
toggleIn('emission', v)} />
toggleIn('valid_modes', v)} />
{/* ── Award type ── */} {/* No "allow multiple references" switch: a QSO always counts for every reference its field holds (an n-fer POTA activation, a VUCC grid-line contact). The old checkbox was read by nothing. */}

{t('awed.qsoParams')}

{['code', 'description', 'pattern'].map((m) => ( ))}
patch({ pattern: e.target.value })} placeholder={t('awed.patternPlaceholder')} />
patch({ leading_str: e.target.value })} /> patch({ trailing_str: e.target.value })} />
{/* Fallback searches: tried in order, only while nothing has matched yet — the first that hits wins (short-circuit), so a value already resolved by the primary rule isn't re-derived differently by a later one. */}

{t('awed.additionalSearches')} (OR) {t('awed.orAlsoMatch')}

{(cur.or_rules ?? []).map((r, ri) => { const upd = (p: Partial) => patch({ or_rules: (cur.or_rules ?? []).map((x, j) => (j === ri ? { ...x, ...p } : x)) }); const del = () => patch({ or_rules: (cur.or_rules ?? []).filter((_, j) => j !== ri) }); return (
{t('awed.orSearchIn')}
{['code', 'description', 'pattern'].map((m) => ( ))}
upd({ pattern: e.target.value })} placeholder={t('awed.orPatternPlaceholder')} /> upd({ prefix: e.target.value })} placeholder={t('awed.prefixPlaceholder')} title={t('awed.prefixTitle')} />
); })}
{/* ── Confirmation ── */}
{CONFIRM_SRC.map((c) => ( ))}
{CONFIRM_SRC.map((c) => ( ))}
{/* "Grant codes" and "export credit_granted" used to live here. No ADIF export has ever written CREDIT_GRANTED, so both controls did nothing at all. The stored values are kept (see award.Def); put the controls back when the export is actually wired up. */}
{/* ── Test ── Runs the SAVED award against a real QSO and shows every rule: what it scanned, what it produced, and why each candidate was rejected. An award that matches nothing used to be a black box. */}
setTestCall(e.target.value.toUpperCase())} onKeyDown={(e) => { if (e.key === 'Enter') runTest(); }} />
{t('awed.testSavedOnly')}
{testErr &&
{testErr}
} {testGroups?.map((g, ri) => { const row = g.rows[0]; const ex = row.explanation; const matched = (ex.result ?? []).length > 0; const qsoLabel = (q: any) => `${String(q?.qso_date ?? '').slice(0, 10)} · ${q?.band} · ${q?.mode}`; return (
{row.qso?.callsign} {qsoLabel(row.qso)} {g.rows.length > 1 && ( qsoLabel(r.qso)).join('\n')}> {t('awed.testSameAs', { n: String(g.rows.length - 1) })} )} {matched ? (ex.result ?? []).join(', ') : t('awed.testNoMatch')}
{!ex.in_scope ? (
{t('awed.testOutOfScope')}{' '} {ex.scope_error}
) : (
{(ex.steps ?? []).map((s, si) => (
{s.rule} {s.field}{s.match_by ? ` / ${s.match_by}` : ''}{s.exact ? ` / ${t('awed.exact')}` : ''} {s.skipped && — {t('awed.testSkipped')}} {s.error && — {s.error}}
{!s.skipped && !s.error && (
{t('awed.testFieldValue')}: {s.field_value ? {s.field_value} : {t('awed.testEmptyField')}}
{(s.kept ?? []).map((k) => (
✓ {k}
))} {(s.rejected ?? []).map((r, i2) => (
✗ {r.candidate} — {r.reason}
))} {!(s.kept ?? []).length && !(s.rejected ?? []).length && s.field_value && (
{t('awed.testNoCandidate')}
)}
)}
))} {(ex.manual ?? []).length > 0 && (
{t('awed.testManual')}{' '} {(ex.manual ?? []).join(', ')}
)}
)}
); })}
{/* ── References ── */} updateList(cur.code.toUpperCase())} updating={updating === cur.code.toUpperCase()} onChanged={loadMeta} setErr={setErr} />
)}
{/* The drop folder: put an award JSON here and it installs on restart — no rebuild, nobody to ask. Opening it beats printing a path someone then has to retype. */}
{/* Collision dialog. The import cannot silently replace an award you already have — importing a shared WAPC used to destroy yours (province list, city regexes, band scope) without a word. You decide, per award. */} {importPreview && ( setImportPreview(null)}> {t('awed.importCollisionTitle')}

{t('awed.importCollisionHint')}

{importPreview.awards.map((e) => (
{e.code} {e.name} {t('awed.importRefs', { n: e.references })}
{!e.exists ? (

{t('awed.importNew')}

) : ( <>

{t('awed.importExists', { name: e.mine_name || e.code, n: e.mine_refs })} {e.protected && ` · ${t('awed.importProtected')}`}

{([ ['skip', t('awed.importKeepMine')], ['replace', t('awed.importReplace')], ['copy', t('awed.importCopy', { code: e.code })], ] as [string, string][]).map(([v, label]) => ( ))}
)}
))}
)}
); } // ReferencesPanel — manage the reference list of one award: search/list on the // left, a per-reference editor on the right, plus bulk paste/CSV, presets and // the online updater (POTA/SOTA/WWFF). function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChanged, setErr }: { code: string; presets: Preset[]; meta?: RefMeta; onUpdateOnline: () => void; updating: boolean; onChanged: () => void; setErr: (s: string) => void; }) { const { t } = useI18n(); const [refs, setRefs] = useState([]); const [q, setQ] = useState(''); const [selCode, setSelCode] = useState(null); const [busy, setBusy] = useState(false); const [bulk, setBulk] = useState(''); const [showBulk, setShowBulk] = useState(false); const [hasBuiltin, setHasBuiltin] = useState(false); const total = meta?.count ?? 0; const large = total > REF_LIST_CAP; // Small lists are loaded whole and filtered client-side; large lists (POTA, // 85k parks) are search-only to stay responsive. const load = () => { if (!code) return; if (total > REF_LIST_CAP) { setRefs([]); return; } setBusy(true); ListAwardReferences(code).then((r) => setRefs((r ?? []) as any)).catch(() => {}).finally(() => setBusy(false)); }; useEffect(load, [code, total]); useEffect(() => { HasBuiltinReferences(code).then(setHasBuiltin).catch(() => setHasBuiltin(false)); }, [code]); // Server-side search for large lists (debounced, min 2 chars). useEffect(() => { if (!large) return; const s = q.trim(); if (s.length < 2) { setRefs([]); return; } const t = window.setTimeout(() => { setBusy(true); SearchAwardReferences(code, s, 0, 200).then((r) => setRefs((r ?? []) as any)).catch(() => setRefs([])).finally(() => setBusy(false)); }, 200); return () => window.clearTimeout(t); }, [code, q, large]); async function populateBuiltin() { try { const n = await PopulateBuiltinReferences(code); load(); onChanged(); setErr(t('awed.populatedMsg', { n })); } catch (e: any) { setErr(String(e?.message ?? e)); } } const sel = refs.find((r) => r.code === selCode) || null; // Large lists are already filtered by the server; small lists filter locally. const filtered = useMemo(() => { if (large) return refs; const s = q.trim().toUpperCase(); return refs.filter((r) => !s || r.code.toUpperCase().includes(s) || (r.name ?? '').toUpperCase().includes(s)); }, [refs, q, large]); const patchSel = (p: Partial) => setRefs((rs) => rs.map((r) => (r.code === selCode ? { ...r, ...p } : r))); async function saveRef(r: AwardRef) { try { await SaveAwardReference(code, r as any); load(); onChanged(); } catch (e: any) { setErr(String(e?.message ?? e)); } } async function addRef() { const c = prompt(t('awed.newRefCodePrompt'))?.trim().toUpperCase(); if (!c) return; const r: AwardRef = { code: c, name: '', dxcc: 0, group: '', subgrp: '', valid: true }; await saveRef(r); setSelCode(c); } async function delRef(c: string) { try { await DeleteAwardReference(code, c); setSelCode(null); load(); onChanged(); } catch (e: any) { setErr(String(e?.message ?? e)); } } async function applyPreset(key: string) { if (!key) return; try { await ApplyAwardPreset(code, key); load(); onChanged(); } catch (e: any) { setErr(String(e?.message ?? e)); } } async function importBulk() { try { const n = await ImportAwardReferencesText(code, bulk); setBulk(''); setShowBulk(false); load(); onChanged(); setErr(t('awed.importedRefsMsg', { n })); } catch (e: any) { setErr(String(e?.message ?? e)); } } return (
{/* Toolbar */}
{t('awed.referenceCount')} {total.toLocaleString()}
{hasBuiltin && ( )} {meta?.can_update && ( )}
{showBulk && (

{t('awed.onePerLine')} CODE,Description,Group,Subgroup,DXCC {t('awed.replacesList')}