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.',