feat(webpub): offer every field, and make choosing among them possible
The catalogue held 23 hand-picked columns, so publishing a county, a satellite pass or an award reference was simply not possible. It is generated from the qso.QSO struct now — 123 fields — which also means a new ADIF field cannot be forgotten here, as a hand-written list always eventually is. Three keys were renamed to their ADIF names on the way (pota, sota, station). An alias map keeps existing configurations publishing the same columns; without it three would have vanished silently on upgrade, which is the worst way for a setting to change. The picker had to change with it: 123 chips in one wrap is a wall nobody reads to the end of. It is sectioned by group, filtered as you type, and the chosen columns sit on top in publication order — after picking eight out of a hundred, the question stops being "what exists" and becomes "what did I pick". The package doc said columns were curated so the operator's address could not be published. That is no longer true and the comment now says so plainly: the judgement moved to the operator, the default selection is unchanged, and nothing is published that was not chosen. Also collapses the changelog: four separate shared-CAT entries were one thing from where the operator sits, and all of them were far longer than the one or two sentences this project asks for.
This commit is contained in:
@@ -18,12 +18,17 @@ type Cfg = {
|
||||
ftp_enabled: boolean; ftp_host: string; ftp_port: number; ftp_user: string;
|
||||
ftp_password: string; ftp_tls: boolean; ftp_folder: string; ftp_file_name: string;
|
||||
};
|
||||
type Col = { key: string; header: string };
|
||||
type Col = { key: string; header: string; group: string };
|
||||
|
||||
export function WebPublishPanel() {
|
||||
const { t } = useI18n();
|
||||
const [cfg, setCfg] = useState<Cfg | null>(null);
|
||||
const [cols, setCols] = useState<Col[]>([]);
|
||||
// With every ADIF field on offer the list is long, so it is searchable and
|
||||
// sectioned. Chosen columns are pulled to the top: after picking eight out of
|
||||
// a hundred, the question stops being 'what exists' and becomes 'what did I
|
||||
// pick, and in what order does it print'.
|
||||
const [colSearch, setColSearch] = useState('');
|
||||
const [busy, setBusy] = useState<'' | 'test' | 'publish'>('');
|
||||
const [msg, setMsg] = useState('');
|
||||
const [err, setErr] = useState('');
|
||||
@@ -137,16 +142,58 @@ export function WebPublishPanel() {
|
||||
|
||||
{/* ── Columns ── */}
|
||||
<div className="space-y-2 border-t border-border/60 pt-3">
|
||||
<Label className="text-xs font-semibold">{t('wpub.columns')}</Label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{cols.map((c) => {
|
||||
const on = cfg.columns?.includes(c.key);
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-semibold">{t('wpub.columns')}</Label>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{t('wpub.columnsCount', { n: cfg.columns?.length ?? 0, total: cols.length })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* CHOSEN, in publication order — this is the list that answers "what
|
||||
will the page look like", which a sectioned catalogue cannot. */}
|
||||
{(cfg.columns?.length ?? 0) > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{cfg.columns.map((k) => {
|
||||
const c = cols.find((x) => x.key === k);
|
||||
return (
|
||||
<button key={k} type="button" onClick={() => toggleCol(k)}
|
||||
title={t('wpub.removeColumn')}
|
||||
className="px-2 py-0.5 rounded-full border border-primary bg-primary text-primary-foreground text-[11px] font-medium">
|
||||
{c?.header ?? k} ×
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Input className="h-7 text-xs" placeholder={t('wpub.columnsSearch')}
|
||||
value={colSearch} onChange={(e) => setColSearch(e.target.value)} />
|
||||
|
||||
{/* The catalogue: sectioned, and filtered as you type. 123 fields in
|
||||
one flat wrap is a wall nobody reads to the end of. */}
|
||||
<div className="max-h-64 overflow-auto rounded border border-border/60 p-2 space-y-2">
|
||||
{Array.from(new Set(cols.map((c) => c.group))).map((g) => {
|
||||
const q = colSearch.trim().toLowerCase();
|
||||
const inGroup = cols.filter((c) => c.group === g &&
|
||||
(!q || c.header.toLowerCase().includes(q) || c.key.toLowerCase().includes(q)));
|
||||
if (inGroup.length === 0) return null;
|
||||
return (
|
||||
<button key={c.key} type="button" onClick={() => toggleCol(c.key)}
|
||||
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||
on ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
{c.header}
|
||||
</button>
|
||||
<div key={g}>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">{g}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{inGroup.map((c) => {
|
||||
const on = cfg.columns?.includes(c.key);
|
||||
return (
|
||||
<button key={c.key} type="button" onClick={() => toggleCol(c.key)}
|
||||
title={c.key}
|
||||
className={cn('px-2 py-0.5 rounded-full border text-[11px] font-medium transition-colors',
|
||||
on ? 'border-primary bg-primary text-primary-foreground' : 'border-border text-muted-foreground hover:bg-muted')}>
|
||||
{c.header}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user