From 735dfe69db2eebd00b68137dbb998e86e75a6412 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Tue, 11 Aug 2026 11:11:24 +0200 Subject: [PATCH] fix(webpub): the column picker is a dropdown, not a wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- changelog.json | 4 +- frontend/src/components/WebPublishPanel.tsx | 82 ++++++++++++--------- frontend/src/lib/i18n.tsx | 4 +- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/changelog.json b/changelog.json index 5452642..6cc4f8a 100644 --- a/changelog.json +++ b/changelog.json @@ -6,13 +6,13 @@ "Digital decodes: a station's grid square could be logged as its callsign when the message text was unusual. A grid in the callsign position is now refused.", "CW over CAT now works on a Kenwood. The KY command was sent in Elecraft's variable-length form; a Kenwood needs exactly 24 characters, so every message was refused.", "Shared CAT is steadier and now diagnoses itself. It survives a rig answering \"busy\" just after transmit instead of dropping the link, stops repeating a PTT state the client never changed, and logs a plain explanation when another program has taken its port or when a client is set to a rig model instead of Hamlib NET rigctl.", - "Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — grouped and searchable, with the chosen columns shown in publication order. Choose carefully: the page is public and the list includes addresses and e-mail." + "Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — from a searchable dropdown, with the chosen columns listed above it in publication order. Choose carefully: the page is public and the list includes addresses and e-mail." ], "fr": [ "Décodes digitaux : le carré locator d une station pouvait être enregistré comme son indicatif quand le texte du message sortait de l ordinaire. Un grid à la place de l indicatif est maintenant refusé.", "Le CW par CAT fonctionne sur Kenwood. La commande KY partait sous la forme Elecraft à longueur libre ; un Kenwood exige exactement 24 caractères, donc chaque message était refusé.", "Le CAT partagé est plus solide et se diagnostique tout seul. Il survit à un rig qui répond « occupé » juste après une émission au lieu de lâcher le lien, cesse de répéter un état PTT que le client n a pas changé, et écrit une explication claire quand un autre programme lui a pris son port ou qu un client est réglé sur un modèle de rig au lieu de Hamlib NET rigctl.", - "La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — groupés et cherchables, avec les colonnes choisies affichées dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails." + "La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — depuis une liste déroulante cherchable, les colonnes choisies étant listées au-dessus dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails." ] }, { diff --git a/frontend/src/components/WebPublishPanel.tsx b/frontend/src/components/WebPublishPanel.tsx index c28f692..cd51857 100644 --- a/frontend/src/components/WebPublishPanel.tsx +++ b/frontend/src/components/WebPublishPanel.tsx @@ -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(null); const [cols, setCols] = useState([]); - // 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() { )} - 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. */} -
- {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 ( -
-
{g}
-
- {inGroup.map((c) => { - const on = cfg.columns?.includes(c.key); - return ( - - ); - })} -
-
- ); - })} -
+ {/* 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. */} + + + + + +
+ setColSearch(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} /> +
+ {[...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) => ( + toggleCol(c.key)} + onSelect={(e) => e.preventDefault()} + className="text-xs" + > + {c.header} + {c.group} + + ))} +
+

{t('wpub.columnsHint')}

diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 6aac309..03628a3 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -117,7 +117,7 @@ const en: Dict = { 'sec.general': 'General', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': 'Callsign Lookup', 'sec.bands': 'Bands', 'sec.modes': 'Modes & default RST', 'sec.cluster': 'DX Cluster', 'sec.udp': 'UDP integrations', 'sec.database': 'Database', 'sec.autostart': 'Autostart', 'sec.backup': 'Database backup', 'sec.uscounties': 'US Counties', - 'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor', + 'sec.webpublish': 'Web publishing', 'wpub.hint': 'Publishes your log as a file for a website: a standalone HTML page or a CSV, written locally and optionally uploaded by FTP. It is refreshed when you log a QSO and, if you set an interval, on a timer.', 'wpub.enable': 'Publish the log to a file', 'wpub.fileSection': 'The file', 'wpub.format': 'Format', 'wpub.formatHtml': 'HTML page', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Output folder', 'wpub.browse': 'Browse…', 'wpub.fileName': 'File name', 'wpub.title': 'Page title', 'wpub.titlePh': 'blank = your callsign', 'wpub.count': 'Last N QSOs', 'wpub.every': 'Refresh every', 'wpub.everyHint': 'minutes — 0 = only when a QSO is logged', 'wpub.columns': 'Columns', 'wpub.columnsCount': '{n} of {total} chosen', 'wpub.columnsPick': 'Choose columns…', 'wpub.columnsSearch': 'Search a field…', 'wpub.removeColumn': 'Click to remove', 'wpub.columnsHint': 'Click to add or remove. The order shown here is the order in the file.', 'wpub.ftpEnable': 'Upload by FTP', 'wpub.ftpHost': 'Server / port', 'wpub.ftpUser': 'User', 'wpub.ftpPassword': 'Password', 'wpub.ftpFolder': 'Remote folder', 'wpub.ftpFileName': 'Remote file name', 'wpub.ftpTls': 'Use TLS (FTPS)', 'wpub.publishNow': 'Publish now', 'wpub.testFtp': 'Test connection', 'wpub.lastRun': 'Last run:', 'sec.adifmon': 'ADIF monitor', 'adifmon.hint': 'Watch external ADIF files and import new QSOs automatically — e.g. fldigi logging RTTY, or N1MM/VarAC. Imported QSOs are enriched, de-duplicated and uploaded to your external services just like a QSO logged here.', 'adifmon.enable': 'Enable ADIF monitor', 'adifmon.empty': 'No file watched yet. Add an ADIF file below.', @@ -544,7 +544,7 @@ const fr: Dict = { 'sec.general': 'Général', 'sec.email': 'E-mail (SMTP)', 'sec.lookup': "Recherche d'indicatif", 'sec.bands': 'Bandes', 'sec.modes': 'Modes & RST par défaut', 'sec.cluster': 'DX Cluster', 'sec.udp': 'Intégrations UDP', 'sec.database': 'Base de données', 'sec.autostart': 'Démarrage auto', 'sec.backup': 'Sauvegarde base', 'sec.uscounties': 'Comtés US', - 'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF', + 'sec.webpublish': 'Publication web', 'wpub.hint': "Publie ton journal dans un fichier destiné à un site web : une page HTML autonome ou un CSV, écrit en local et envoyé par FTP si tu le souhaites. Il est rafraîchi à chaque QSO enregistré et, si tu règles un intervalle, périodiquement.", 'wpub.enable': 'Publier le journal dans un fichier', 'wpub.fileSection': 'Le fichier', 'wpub.format': 'Format', 'wpub.formatHtml': 'Page HTML', 'wpub.formatCsv': 'CSV', 'wpub.folder': 'Dossier de sortie', 'wpub.browse': 'Parcourir…', 'wpub.fileName': 'Nom du fichier', 'wpub.title': 'Titre de la page', 'wpub.titlePh': 'vide = ton indicatif', 'wpub.count': 'N derniers QSO', 'wpub.every': 'Rafraîchir toutes les', 'wpub.everyHint': 'minutes — 0 = seulement à chaque QSO', 'wpub.columns': 'Colonnes', 'wpub.columnsCount': '{n} sur {total} choisis', 'wpub.columnsPick': 'Choisir les colonnes…', 'wpub.columnsSearch': 'Chercher un champ…', 'wpub.removeColumn': 'Cliquer pour retirer', 'wpub.columnsHint': 'Clique pour ajouter ou retirer. L ordre affiché ici est celui du fichier.', 'wpub.ftpEnable': 'Envoyer par FTP', 'wpub.ftpHost': 'Serveur / port', 'wpub.ftpUser': 'Utilisateur', 'wpub.ftpPassword': 'Mot de passe', 'wpub.ftpFolder': 'Dossier distant', 'wpub.ftpFileName': 'Nom du fichier distant', 'wpub.ftpTls': 'Utiliser TLS (FTPS)', 'wpub.publishNow': 'Publier maintenant', 'wpub.testFtp': 'Tester la connexion', 'wpub.lastRun': 'Dernière exécution :', 'sec.adifmon': 'Moniteur ADIF', 'adifmon.hint': "Surveille des fichiers ADIF externes et importe les nouveaux QSO automatiquement — ex. fldigi en RTTY, ou N1MM/VarAC. Les QSO importés sont enrichis, dédoublonnés et envoyés à tes services externes comme un QSO loggé ici.", 'adifmon.enable': 'Activer le moniteur ADIF', 'adifmon.empty': 'Aucun fichier surveillé. Ajoute un fichier ADIF ci-dessous.',