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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user