fix: a port field could not be cleared
Every port box was parseInt(e.target.value) || <default>. Delete the contents and parseInt gives NaN, so the default is written straight back: the digits reappear under the cursor and the only way to enter a different port is to overwrite it character by character. Reported on the cluster editor; the same line existed in eight other places. One PortInput component now holds the raw TEXT as its state and only tells the parent about a value that is actually a port. An empty box stays empty while you type; on blur an invalid one falls back to the default, so nothing is ever saved as 0 or NaN. It adopts a value arriving from the backend only while the operator has not started typing — re-syncing mid-edit is the behaviour being fixed. This is the controlled-input trap the project notes already warn about: keep the raw text in local state and derive the stored value from it.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ArrowDown, ArrowUp, ArrowLeft, ArrowRight, Copy, Plus, Star, StarOff, Trash2,
|
||||
ChevronDown, ChevronRight,
|
||||
@@ -2358,8 +2358,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.port')}</Label>
|
||||
<Input type="number" value={catCfg.flex_port || 4992}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, flex_port: parseInt(e.target.value) || 4992 }))} />
|
||||
<PortInput value={catCfg.flex_port || 4992}
|
||||
onChange={(n) => setCatCfg((s) => ({ ...s, flex_port: n }))} fallback={4992} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FlexDiscover onPick={(ip, port) => setCatCfg((s) => ({ ...s, flex_host: ip, flex_port: port }))} />
|
||||
@@ -2482,8 +2482,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{t('cat.port')}</Label>
|
||||
<Input type="number" value={catCfg.tci_port || 40001}
|
||||
onChange={(e) => setCatCfg((s) => ({ ...s, tci_port: parseInt(e.target.value) || 40001 }))} />
|
||||
<PortInput value={catCfg.tci_port || 40001}
|
||||
onChange={(n) => setCatCfg((s) => ({ ...s, tci_port: n }))} fallback={40001} />
|
||||
</div>
|
||||
<p className="col-span-2 text-xs text-muted-foreground">
|
||||
{t('cat.tciHint')}
|
||||
@@ -2654,10 +2654,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>TCP port</Label>
|
||||
<Input
|
||||
type="number" min={1} max={65535}
|
||||
<PortInput
|
||||
value={ultrabeam.port}
|
||||
onChange={(e) => setUltrabeam((s) => ({ ...s, port: parseInt(e.target.value) || 23 }))}
|
||||
onChange={(n) => setUltrabeam((s) => ({ ...s, port: n }))} fallback={23}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
@@ -2943,8 +2942,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>TCP port</Label>
|
||||
<Input type="number" min={1} max={65535} value={amp.port}
|
||||
onChange={(e) => patchAmp(i, { port: parseInt(e.target.value) || 9008 })} className="font-mono" />
|
||||
<PortInput value={amp.port}
|
||||
onChange={(n) => patchAmp(i, { port: n })} fallback={9008} className="font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -3114,10 +3113,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>{isRG || isARCO ? 'TCP port' : 'UDP port'}</Label>
|
||||
<Input
|
||||
type="number" min={1} max={65535}
|
||||
<PortInput
|
||||
value={rotator.port}
|
||||
onChange={(e) => setRotator((s) => ({ ...s, port: parseInt(e.target.value) || (isRG ? 9006 : isARCO ? 4001 : 12000) }))}
|
||||
onChange={(n) => setRotator((s) => ({ ...s, port: n }))} fallback={isRG ? 9006 : isARCO ? 4001 : 12000}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
@@ -4678,7 +4676,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Label className="text-sm">{t('db.host')}</Label>
|
||||
<Input className="h-8" placeholder="192.168.1.10 or db.example.com" value={mysqlCfg.host} onChange={(e) => setMysqlField({ host: e.target.value })} />
|
||||
<Label className="text-sm">{t('db.port')}</Label>
|
||||
<Input type="number" className="h-8 w-28 font-mono" value={mysqlCfg.port} onChange={(e) => setMysqlField({ port: parseInt(e.target.value, 10) || 0 })} />
|
||||
<PortInput className="h-8 w-28 font-mono" value={mysqlCfg.port} fallback={3306} onChange={(n) => setMysqlField({ port: n })} />
|
||||
<Label className="text-sm">{t('db.database')}</Label>
|
||||
<Input className="h-8" placeholder="opslog" value={mysqlCfg.database} onChange={(e) => setMysqlField({ database: e.target.value })} />
|
||||
<Label className="text-sm">{t('db.user')}</Label>
|
||||
@@ -5154,7 +5152,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Input className="h-8" placeholder="ex5.mail.ovh.net" value={emailCfg.smtp_host} onChange={(e) => setEmailField({ smtp_host: e.target.value })} />
|
||||
<Label className="text-sm">Port / encryption</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input type="number" className="h-8 w-24 font-mono" value={emailCfg.smtp_port} onChange={(e) => setEmailField({ smtp_port: parseInt(e.target.value, 10) || 0 })} />
|
||||
<PortInput className="h-8 w-24 font-mono" value={emailCfg.smtp_port} fallback={587} onChange={(n) => setEmailField({ smtp_port: n })} />
|
||||
<Select value={emailCfg.encryption} onValueChange={(v) => setEmailField({ encryption: v as any })}>
|
||||
<SelectTrigger className="h-8 w-44"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -5358,6 +5356,53 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
);
|
||||
}
|
||||
|
||||
// PortInput — a TCP port field you can actually clear.
|
||||
//
|
||||
// Every port box was written as `parseInt(e.target.value) || <default>`. Delete
|
||||
// the contents and parseInt gives NaN, so the default is written straight back:
|
||||
// the digits reappear under the cursor and the only way to enter a different
|
||||
// port is to overwrite character by character. Reported on the cluster editor;
|
||||
// the same line existed in eight other places.
|
||||
//
|
||||
// The raw TEXT is the state here, and the parent is only told about a value that
|
||||
// is actually a port. An empty box stays empty while you type; on blur it falls
|
||||
// back to the last good value, so nothing is ever saved as 0 or NaN.
|
||||
function PortInput({ value, onChange, fallback, className, min = 1, max = 65535 }: {
|
||||
value: number; onChange: (n: number) => void; fallback: number; className?: string; min?: number; max?: number;
|
||||
}) {
|
||||
const [text, setText] = useState(value ? String(value) : '');
|
||||
const typed = useRef(false);
|
||||
// Only adopt the incoming value while the operator has NOT started typing —
|
||||
// settings arrive from the backend after mount, and re-syncing mid-edit is
|
||||
// precisely the behaviour being fixed.
|
||||
useEffect(() => {
|
||||
if (!typed.current) setText(value ? String(value) : '');
|
||||
}, [value]);
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
className={className}
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
typed.current = true;
|
||||
const digits = e.target.value.replace(/[^0-9]/g, '').slice(0, 5);
|
||||
setText(digits);
|
||||
const n = parseInt(digits, 10);
|
||||
if (n >= min && n <= max) onChange(n);
|
||||
}}
|
||||
onBlur={() => {
|
||||
typed.current = false;
|
||||
const n = parseInt(text, 10);
|
||||
if (!(n >= min && n <= max)) {
|
||||
setText(String(fallback));
|
||||
onChange(fallback);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ClusterServerEditor edits one row of cluster_servers. Init commands are
|
||||
// free-form (one per line); the backend strips blanks and "//" comments.
|
||||
interface ClusterEditorProps {
|
||||
@@ -5389,7 +5434,7 @@ function ClusterServerEditor({ value, onCancel, onSave }: ClusterEditorProps) {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Port</Label>
|
||||
<Input type="number" min={1} max={65535} className="font-mono" value={s.port} onChange={(e) => update({ port: parseInt(e.target.value) || 7300 })} />
|
||||
<PortInput className="font-mono" value={s.port} fallback={7300} onChange={(n) => update({ port: n })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Login callsign (optional)</Label>
|
||||
|
||||
Reference in New Issue
Block a user