fix: one table per band instead of two
Antennas and power were two tables listing the same bands, one under the other, so configuring a band meant matching rows by eye between them. They are now one row per band. That the two settings are stored separately and applied on different triggers — antennas on band change, power on band or mode change — is a backend detail, and no reason to split what is a single decision per band for the person configuring it. A rule in the header keeps the two groups readable.
This commit is contained in:
@@ -953,158 +953,124 @@ function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
// FlexBandPowerPanel — TX power per band and per class of mode.
|
||||
// FlexBandPanel — everything that follows the band, in ONE row per band:
|
||||
// antennas and TX power.
|
||||
//
|
||||
// Three classes, not one row per mode: what decides the power is duty cycle and
|
||||
// what the amplifier will take of it, not the mode's name. Blank leaves the
|
||||
// power alone, which is why the boxes are empty rather than showing 0.
|
||||
function FlexBandPowerPanel({ bands }: { bands: string[] }) {
|
||||
// They were two tables listing the same bands one under the other, which made
|
||||
// the operator match rows by eye between them. The two settings are stored and
|
||||
// applied separately (antennas on band change, power on band or mode change) —
|
||||
// that is a backend detail, and no reason to split what is one decision per
|
||||
// band for the person configuring it.
|
||||
function FlexBandPanel({ bands }: { bands: string[] }) {
|
||||
const { t } = useI18n();
|
||||
const [map, setMap] = useState<Record<string, { phone: number; cw: number; digi: number }>>({});
|
||||
const [rxList, setRxList] = useState<string[]>([]);
|
||||
const [txList, setTxList] = useState<string[]>([]);
|
||||
const [ant, setAnt] = useState<Record<string, { rx: string; tx: string }>>({});
|
||||
const [pwr, setPwr] = useState<Record<string, { phone: number; cw: number; digi: number }>>({});
|
||||
const [msg, setMsg] = useState('');
|
||||
useEffect(() => { GetFlexBandPower().then((m: any) => setMap(m ?? {})).catch(() => {}); }, []);
|
||||
const set = (band: string, kind: 'phone' | 'cw' | 'digi', v: string) => {
|
||||
|
||||
useEffect(() => {
|
||||
GetFlexState().then((s: any) => {
|
||||
setRxList((s?.ant_list ?? []) as string[]);
|
||||
setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
|
||||
}).catch(() => {});
|
||||
GetFlexBandAntennas().then((m: any) => setAnt(m ?? {})).catch(() => {});
|
||||
GetFlexBandPower().then((m: any) => setPwr(m ?? {})).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const saved = () => { setMsg(t('flxpw.saved')); window.setTimeout(() => setMsg(''), 1200); };
|
||||
|
||||
const setAntenna = (band: string, side: 'rx' | 'tx', v: string) => {
|
||||
const key = band.toUpperCase();
|
||||
const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0));
|
||||
setMap((m) => {
|
||||
const cur = m[key] ?? { phone: 0, cw: 0, digi: 0 };
|
||||
const next = { ...m, [key]: { ...cur, [kind]: n } };
|
||||
SaveFlexBandPower(next as any)
|
||||
.then(() => { setMsg(t('flxpw.saved')); window.setTimeout(() => setMsg(''), 1200); })
|
||||
.catch((e: any) => setMsg(String(e?.message ?? e)));
|
||||
setAnt((m) => {
|
||||
const next = { ...m, [key]: { ...(m[key] ?? { rx: '', tx: '' }), [side]: v } };
|
||||
SaveFlexBandAntennas(next as any).then(saved).catch((e: any) => setMsg(String(e?.message ?? e)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const cell = (b: string, kind: 'phone' | 'cw' | 'digi') => {
|
||||
const e = map[b.toUpperCase()] ?? { phone: 0, cw: 0, digi: 0 };
|
||||
|
||||
const setPower = (band: string, kind: 'phone' | 'cw' | 'digi', v: string) => {
|
||||
const key = band.toUpperCase();
|
||||
const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0));
|
||||
setPwr((m) => {
|
||||
const next = { ...m, [key]: { ...(m[key] ?? { phone: 0, cw: 0, digi: 0 }), [kind]: n } };
|
||||
SaveFlexBandPower(next as any).then(saved).catch((e: any) => setMsg(String(e?.message ?? e)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const antCell = (b: string, side: 'rx' | 'tx', list: string[]) => {
|
||||
const e = ant[b.toUpperCase()] ?? { rx: '', tx: '' };
|
||||
return (
|
||||
<td className="px-2 py-1">
|
||||
<td className="px-2 py-1.5">
|
||||
<select value={e[side] ?? ''} onChange={(ev) => setAntenna(b, side, ev.target.value)}
|
||||
className="h-8 w-28 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
|
||||
<option value="">— none —</option>
|
||||
{list.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
const pwrCell = (b: string, kind: 'phone' | 'cw' | 'digi') => {
|
||||
const e = pwr[b.toUpperCase()] ?? { phone: 0, cw: 0, digi: 0 };
|
||||
return (
|
||||
<td className="px-2 py-1.5">
|
||||
<Input
|
||||
type="number" min={0} max={100}
|
||||
className="h-7 w-20 text-sm"
|
||||
className="h-8 w-16 text-xs"
|
||||
value={e[kind] ? String(e[kind]) : ''}
|
||||
placeholder="—"
|
||||
onChange={(ev) => set(b, kind, ev.target.value)}
|
||||
onChange={(ev) => setPower(b, kind, ev.target.value)}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">{t('flxpw.title')}</h3>
|
||||
<p className="text-xs text-muted-foreground">{t('flxpw.hint')}</p>
|
||||
<h3 className="text-base font-semibold text-foreground">{t('flxb.title')}</h3>
|
||||
<p className="text-xs text-muted-foreground">{t('flxb.hint')}</p>
|
||||
</div>
|
||||
{rxList.length === 0 && (
|
||||
<div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-2xl">
|
||||
{t('flxb.noRadio')}
|
||||
</div>
|
||||
)}
|
||||
{bands.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">{t('flxpw.noBands')}</p>
|
||||
) : (
|
||||
<div className="rounded-md border border-border overflow-hidden max-w-xl">
|
||||
<div className="rounded-md border border-border overflow-x-auto max-w-3xl">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/40 text-muted-foreground text-xs">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">{t('flxpw.band')}</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.phone')}</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold">{t('flxb.rxAnt')}</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold">{t('flxb.txAnt')}</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold border-l border-border/60">{t('flxpw.phone')}</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold">CW</th>
|
||||
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.digi')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bands.map((b) => (
|
||||
<tr key={b} className="border-t border-border/60">
|
||||
<td className="px-3 py-1 font-mono">{b}</td>
|
||||
{cell(b, 'phone')}
|
||||
{cell(b, 'cw')}
|
||||
{cell(b, 'digi')}
|
||||
<tr key={b} className="border-t border-border/50">
|
||||
<td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
|
||||
{antCell(b, 'rx', rxList)}
|
||||
{antCell(b, 'tx', txList)}
|
||||
{/* The rule separates what follows the BAND from what follows
|
||||
the MODE — two different triggers, side by side. */}
|
||||
<td className="border-l border-border/60 p-0" />
|
||||
{pwrCell(b, 'phone')}
|
||||
{pwrCell(b, 'cw')}
|
||||
{pwrCell(b, 'digi')}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{msg && <span className="text-xs text-muted-foreground">{msg}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// FlexBandAntennasPanel — pick the RX/TX antenna per band. Applied automatically
|
||||
// when the band changes (frequency change / spot click). Antennas come live from
|
||||
// the connected FlexRadio; bands come from Lists → Bands.
|
||||
function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
|
||||
const [rxList, setRxList] = useState<string[]>([]);
|
||||
const [txList, setTxList] = useState<string[]>([]);
|
||||
const [map, setMap] = useState<Record<string, { rx: string; tx: string }>>({});
|
||||
const [msg, setMsg] = useState('');
|
||||
useEffect(() => {
|
||||
GetFlexState().then((s: any) => {
|
||||
setRxList((s?.ant_list ?? []) as string[]);
|
||||
setTxList(((s?.tx_ant_list?.length ? s.tx_ant_list : s?.ant_list) ?? []) as string[]);
|
||||
}).catch(() => {});
|
||||
GetFlexBandAntennas().then((m: any) => setMap(m ?? {})).catch(() => {});
|
||||
}, []);
|
||||
const set = (band: string, side: 'rx' | 'tx', v: string) => {
|
||||
const key = band.toUpperCase();
|
||||
setMap((m) => {
|
||||
const cur = m[key] ?? { rx: '', tx: '' };
|
||||
const next = { ...m, [key]: { ...cur, [side]: v } };
|
||||
SaveFlexBandAntennas(next as any)
|
||||
.then(() => { setMsg('Saved'); window.setTimeout(() => setMsg(''), 1200); })
|
||||
.catch((e: any) => setMsg(String(e?.message ?? e)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-foreground">FlexRadio — per-band antennas</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose the RX and TX antenna for each band. They're applied automatically when the band
|
||||
changes (frequency change or clicking a spot).
|
||||
</p>
|
||||
</div>
|
||||
{rxList.length === 0 && (
|
||||
<div className="text-xs text-warning-muted-foreground bg-warning-muted border border-warning-border rounded-md px-3 py-2 max-w-xl">
|
||||
No antennas reported yet — make sure the FlexRadio is connected (CAT interface), then reopen this panel.
|
||||
</div>
|
||||
)}
|
||||
{bands.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No bands configured — add them in Lists → Bands.</p>
|
||||
) : (
|
||||
<div className="rounded-md border border-border overflow-hidden max-w-xl">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/40 text-muted-foreground text-xs">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">Band</th>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">RX antenna</th>
|
||||
<th className="text-left px-3 py-1.5 font-semibold">TX antenna</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bands.map((b) => {
|
||||
const e = map[b.toUpperCase()] ?? { rx: '', tx: '' };
|
||||
return (
|
||||
<tr key={b} className="border-t border-border/50">
|
||||
<td className="px-3 py-1.5 font-mono font-semibold">{b}</td>
|
||||
<td className="px-3 py-1.5">
|
||||
<select value={e.rx ?? ''} onChange={(ev) => set(b, 'rx', ev.target.value)}
|
||||
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
|
||||
<option value="">— none —</option>
|
||||
{rxList.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
<td className="px-3 py-1.5">
|
||||
<select value={e.tx ?? ''} onChange={(ev) => set(b, 'tx', ev.target.value)}
|
||||
className="h-8 w-32 rounded-md border border-input bg-background px-1.5 text-xs font-mono">
|
||||
<option value="">— none —</option>
|
||||
{txList.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{msg && <span className="text-[11px] text-muted-foreground">{msg}</span>}
|
||||
</div>
|
||||
);
|
||||
@@ -5664,13 +5630,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
antgenius: AntGeniusPanelSettings,
|
||||
tunergenius: TunerGeniusPanelSettings,
|
||||
pgxl: PGXLPanelSettings,
|
||||
flex: () => (
|
||||
<>
|
||||
<FlexBandAntennasPanel bands={lists.bands ?? []} />
|
||||
<div className="h-6" />
|
||||
<FlexBandPowerPanel bands={lists.bands ?? []} />
|
||||
</>
|
||||
),
|
||||
flex: () => <FlexBandPanel bands={lists.bands ?? []} />,
|
||||
audio: AudioPanel,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user