fix(webpub): the column picker is a dropdown, not a wall

The first attempt laid 123 fields out on the page, sectioned by group. That
buried every other setting in the panel and was no easier to read than the flat
wrap it replaced — more surface, same problem.

One dropdown now, alphabetical, with a search box pinned at its top. Alphabetical
because any other order means hunting: the operator arrives knowing the name of
the field they want. The menu stays open while ticking, since picking eight
columns should be one visit rather than eight.

The chosen columns keep their place above it, in publication order — that list
answers "what will the page look like", which no catalogue can.
This commit is contained in:
2026-08-11 11:11:24 +02:00
parent 67fe5bcff0
commit 735dfe69db
3 changed files with 50 additions and 40 deletions
+46 -36
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Upload, FolderOpen, Loader2 } from 'lucide-react';
import { Upload, FolderOpen, Loader2, ChevronsUpDown } from 'lucide-react';
import {
GetWebPublishConfig, SaveWebPublishConfig, WebPublishColumns,
TestWebPublishFTP, PublishLogNow, GetWebPublishStatus, PickBackupFolder,
@@ -9,6 +9,9 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuCheckboxItem,
} from '@/components/ui/dropdown-menu';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
@@ -24,10 +27,10 @@ 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'.
// The catalogue is 123 fields, so the picker is a dropdown with a search box
// rather than anything laid out on the page. Chosen columns stay visible above
// it, in publication order: after picking eight out of a hundred the question
// stops being 'what exists' and becomes 'what did I pick, and how will it print'.
const [colSearch, setColSearch] = useState('');
const [busy, setBusy] = useState<'' | 'test' | 'publish'>('');
const [msg, setMsg] = useState('');
@@ -166,37 +169,44 @@ export function WebPublishPanel() {
</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 (
<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>
{/* One dropdown, alphabetical, filtered as you type. The catalogue is
123 fields: laid out on the page it buries every other setting, and
sorting by anything but the alphabet means hunting. The menu stays
open while ticking — picking eight columns should be one visit. */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between h-8 text-xs font-normal">
{t('wpub.columnsPick')}
<ChevronsUpDown className="size-3.5 opacity-60" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-72 max-h-80 overflow-auto">
<div className="p-1.5 sticky top-0 bg-popover z-10">
<Input className="h-7 text-xs" placeholder={t('wpub.columnsSearch')}
value={colSearch}
onChange={(e) => setColSearch(e.target.value)}
onKeyDown={(e) => e.stopPropagation()} />
</div>
{[...cols]
.sort((a, b) => a.header.localeCompare(b.header))
.filter((c) => {
const q = colSearch.trim().toLowerCase();
return !q || c.header.toLowerCase().includes(q) || c.key.toLowerCase().includes(q);
})
.map((c) => (
<DropdownMenuCheckboxItem
key={c.key}
checked={cfg.columns?.includes(c.key) ?? false}
onCheckedChange={() => toggleCol(c.key)}
onSelect={(e) => e.preventDefault()}
className="text-xs"
>
{c.header}
<span className="ml-auto pl-2 text-[10px] text-muted-foreground font-mono">{c.group}</span>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<p className="text-[11px] text-muted-foreground">{t('wpub.columnsHint')}</p>
</div>