Fix clamping on value inputs to allow empty while focused

This commit is contained in:
Jack Kingsman
2026-03-29 22:38:06 -07:00
parent f01e91defc
commit 7aa4f76064
6 changed files with 457 additions and 26 deletions
+24 -2
View File
@@ -39,6 +39,7 @@ export function CrackerPanel({
}: CrackerPanelProps) {
const [isRunning, setIsRunning] = useState(false);
const [maxLength, setMaxLength] = useState(6);
const [maxLengthInput, setMaxLengthInput] = useState('6');
const [retryFailedAtNextLength, setRetryFailedAtNextLength] = useState(false);
const [decryptHistorical, setDecryptHistorical] = useState(true);
const [turboMode, setTurboMode] = useState(false);
@@ -191,6 +192,10 @@ export function CrackerPanel({
maxLengthRef.current = maxLength;
}, [maxLength]);
useEffect(() => {
setMaxLengthInput(String(maxLength));
}, [maxLength]);
useEffect(() => {
decryptHistoricalRef.current = decryptHistorical;
}, [decryptHistorical]);
@@ -434,8 +439,25 @@ export function CrackerPanel({
type="number"
min={1}
max={10}
value={maxLength}
onChange={(e) => setMaxLength(Math.min(10, Math.max(1, parseInt(e.target.value) || 6)))}
value={maxLengthInput}
onChange={(e) => {
const nextValue = e.target.value;
setMaxLengthInput(nextValue);
if (nextValue === '') return;
const parsed = Number.parseInt(nextValue, 10);
if (Number.isNaN(parsed)) return;
setMaxLength(Math.min(10, Math.max(1, parsed)));
}}
onBlur={() => {
const parsed = Number.parseInt(maxLengthInput, 10);
const nextValue = Number.isNaN(parsed)
? maxLength
: Math.min(10, Math.max(1, parsed));
setMaxLengthInput(String(nextValue));
if (nextValue !== maxLength) {
setMaxLength(nextValue);
}
}}
className="w-14 px-2 py-1 text-sm bg-muted border border-border rounded"
/>
</div>
@@ -316,6 +316,80 @@ const CREATE_INTEGRATION_DEFINITIONS_BY_VALUE = Object.fromEntries(
CREATE_INTEGRATION_DEFINITIONS.map((definition) => [definition.value, definition])
) as Record<DraftType, CreateIntegrationDefinition>;
function getNumberInputValue(value: unknown, fallback: number): string | number {
if (value === '') return '';
if (typeof value === 'string') return value;
if (typeof value === 'number' && Number.isFinite(value)) return value;
return fallback;
}
function getOptionalNumberInputValue(value: unknown): string | number {
if (value === '') return '';
if (typeof value === 'string') return value;
if (typeof value === 'number' && Number.isFinite(value)) return value;
return '';
}
function parseIntegerInputValue(value: string): number | string {
if (value === '') return '';
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? value : parsed;
}
function parseFloatInputValue(value: string): number | string {
if (value === '') return '';
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? value : parsed;
}
function normalizeIntegrationConfigForSave(
configType: string,
config: Record<string, unknown>
): Record<string, unknown> {
const normalized = { ...config };
if (configType === 'mqtt_private') {
const port = normalized.broker_port;
if (port === '' || port === undefined || port === null) {
normalized.broker_port = 1883;
} else if (typeof port === 'string') {
const parsed = Number.parseInt(port, 10);
normalized.broker_port = Number.isNaN(parsed) ? 1883 : parsed;
}
const topicPrefix = String(normalized.topic_prefix ?? '').trim();
normalized.topic_prefix = topicPrefix || 'meshcore';
}
if (configType === 'mqtt_community') {
const brokerHost = String(normalized.broker_host ?? '').trim();
normalized.broker_host = brokerHost || DEFAULT_COMMUNITY_BROKER_HOST;
const port = normalized.broker_port;
if (port === '' || port === undefined || port === null) {
normalized.broker_port = DEFAULT_COMMUNITY_BROKER_PORT;
} else if (typeof port === 'string') {
const parsed = Number.parseInt(port, 10);
normalized.broker_port = Number.isNaN(parsed) ? DEFAULT_COMMUNITY_BROKER_PORT : parsed;
}
const topicTemplate = String(normalized.topic_template ?? '').trim();
normalized.topic_template = topicTemplate || DEFAULT_COMMUNITY_PACKET_TOPIC_TEMPLATE;
}
if (configType === 'map_upload') {
const radius = normalized.geofence_radius_km;
if (radius === '' || radius === undefined || radius === null) {
normalized.geofence_radius_km = 0;
} else if (typeof radius === 'string') {
const parsed = Number.parseFloat(radius);
normalized.geofence_radius_km = Number.isNaN(parsed) ? 0 : parsed;
}
}
return normalized;
}
function isDraftType(value: string): value is DraftType {
return value in CREATE_INTEGRATION_DEFINITIONS_BY_VALUE;
}
@@ -338,7 +412,7 @@ function normalizeDraftConfig(draftType: DraftType, config: Record<string, unkno
throw new Error('MeshRank packet topic is required');
}
return {
return normalizeIntegrationConfigForSave('mqtt_community', {
...config,
broker_host: DEFAULT_MESHRANK_BROKER_HOST,
broker_port: DEFAULT_MESHRANK_BROKER_PORT,
@@ -352,7 +426,7 @@ function normalizeDraftConfig(draftType: DraftType, config: Record<string, unkno
topic_template: topicTemplate,
username: '',
password: '',
};
});
}
if (draftType === 'mqtt_community_letsmesh_us' || draftType === 'mqtt_community_letsmesh_eu') {
@@ -360,7 +434,7 @@ function normalizeDraftConfig(draftType: DraftType, config: Record<string, unkno
draftType === 'mqtt_community_letsmesh_eu'
? DEFAULT_COMMUNITY_BROKER_HOST_EU
: DEFAULT_COMMUNITY_BROKER_HOST;
return {
return normalizeIntegrationConfigForSave('mqtt_community', {
...config,
broker_host: brokerHost,
broker_port: DEFAULT_COMMUNITY_BROKER_PORT,
@@ -372,10 +446,13 @@ function normalizeDraftConfig(draftType: DraftType, config: Record<string, unkno
topic_template: (config.topic_template as string) || DEFAULT_COMMUNITY_PACKET_TOPIC_TEMPLATE,
username: '',
password: '',
};
});
}
return config;
return normalizeIntegrationConfigForSave(
getCreateIntegrationDefinition(draftType).savedType,
config
);
}
function normalizeDraftScope(draftType: DraftType, scope: Record<string, unknown>) {
@@ -649,9 +726,9 @@ function MqttPrivateConfigEditor({
type="number"
min="1"
max="65535"
value={(config.broker_port as number) || 1883}
value={getNumberInputValue(config.broker_port, 1883)}
onChange={(e) =>
onChange({ ...config, broker_port: parseInt(e.target.value, 10) || 1883 })
onChange({ ...config, broker_port: parseIntegerInputValue(e.target.value) })
}
/>
</div>
@@ -709,7 +786,8 @@ function MqttPrivateConfigEditor({
<Input
id="fanout-mqtt-prefix"
type="text"
value={(config.topic_prefix as string) || 'meshcore'}
placeholder="meshcore"
value={(config.topic_prefix as string | undefined) ?? ''}
onChange={(e) => onChange({ ...config, topic_prefix: e.target.value })}
/>
</div>
@@ -745,7 +823,7 @@ function MqttCommunityConfigEditor({
id="fanout-comm-host"
type="text"
placeholder={DEFAULT_COMMUNITY_BROKER_HOST}
value={(config.broker_host as string) || DEFAULT_COMMUNITY_BROKER_HOST}
value={(config.broker_host as string | undefined) ?? ''}
onChange={(e) => onChange({ ...config, broker_host: e.target.value })}
/>
</div>
@@ -756,11 +834,11 @@ function MqttCommunityConfigEditor({
type="number"
min="1"
max="65535"
value={(config.broker_port as number) || DEFAULT_COMMUNITY_BROKER_PORT}
value={getNumberInputValue(config.broker_port, DEFAULT_COMMUNITY_BROKER_PORT)}
onChange={(e) =>
onChange({
...config,
broker_port: parseInt(e.target.value, 10) || DEFAULT_COMMUNITY_BROKER_PORT,
broker_port: parseIntegerInputValue(e.target.value),
})
}
/>
@@ -895,7 +973,8 @@ function MqttCommunityConfigEditor({
<Input
id="fanout-comm-topic-template"
type="text"
value={(config.topic_template as string) || DEFAULT_COMMUNITY_PACKET_TOPIC_TEMPLATE}
placeholder={DEFAULT_COMMUNITY_PACKET_TOPIC_TEMPLATE}
value={(config.topic_template as string | undefined) ?? ''}
onChange={(e) => onChange({ ...config, topic_template: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
@@ -1215,11 +1294,11 @@ function MapUploadConfigEditor({
min="0"
step="any"
placeholder="e.g. 100"
value={(config.geofence_radius_km as number | undefined) ?? ''}
value={getOptionalNumberInputValue(config.geofence_radius_km)}
onChange={(e) =>
onChange({
...config,
geofence_radius_km: e.target.value === '' ? 0 : parseFloat(e.target.value),
geofence_radius_km: parseFloatInputValue(e.target.value),
})
}
/>
@@ -1997,9 +2076,10 @@ export function SettingsFanoutSection({
if (!currentEditingId) {
throw new Error('Missing fanout config id for update');
}
const editingType = configs.find((cfg) => cfg.id === currentEditingId)?.type ?? '';
const update: Record<string, unknown> = {
name: editName,
config: editConfig,
config: normalizeIntegrationConfigForSave(editingType, editConfig),
scope: editScope,
};
if (enabled !== undefined) update.enabled = enabled;
@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { Checkbox } from '../ui/checkbox';
import { PACKET_LEGEND_ITEMS } from '../../utils/visualizerUtils';
import { NODE_LEGEND_ITEMS } from './shared';
@@ -71,6 +72,19 @@ export function VisualizerControls({
onExpandContract,
onClearAndReset,
}: VisualizerControlsProps) {
const [observationWindowInput, setObservationWindowInput] = useState(
String(observationWindowSec)
);
const [pruneWindowInput, setPruneWindowInput] = useState(String(pruneStaleMinutes));
useEffect(() => {
setObservationWindowInput(String(observationWindowSec));
}, [observationWindowSec]);
useEffect(() => {
setPruneWindowInput(String(pruneStaleMinutes));
}, [pruneStaleMinutes]);
return (
<>
{showControls && (
@@ -212,12 +226,25 @@ export function VisualizerControls({
type="number"
min="1"
max="60"
value={observationWindowSec}
onChange={(e) =>
setObservationWindowSec(
Math.max(1, Math.min(60, parseInt(e.target.value, 10) || 1))
)
}
value={observationWindowInput}
onChange={(e) => {
const nextValue = e.target.value;
setObservationWindowInput(nextValue);
if (nextValue === '') return;
const parsed = Number.parseInt(nextValue, 10);
if (Number.isNaN(parsed)) return;
setObservationWindowSec(Math.max(1, Math.min(60, parsed)));
}}
onBlur={() => {
const parsed = Number.parseInt(observationWindowInput, 10);
const nextValue = Number.isNaN(parsed)
? observationWindowSec
: Math.max(1, Math.min(60, parsed));
setObservationWindowInput(String(nextValue));
if (nextValue !== observationWindowSec) {
setObservationWindowSec(nextValue);
}
}}
className="w-12 px-1 py-0.5 bg-background border border-border rounded text-xs text-center"
/>
<span className="text-muted-foreground">sec</span>
@@ -247,10 +274,25 @@ export function VisualizerControls({
type="number"
min={1}
max={60}
value={pruneStaleMinutes}
value={pruneWindowInput}
onChange={(e) => {
const v = parseInt(e.target.value, 10);
if (!isNaN(v) && v >= 1 && v <= 60) setPruneStaleMinutes(v);
const nextValue = e.target.value;
setPruneWindowInput(nextValue);
if (nextValue === '') return;
const parsed = Number.parseInt(nextValue, 10);
if (Number.isNaN(parsed)) return;
if (parsed >= 1 && parsed <= 60) setPruneStaleMinutes(parsed);
}}
onBlur={() => {
const parsed = Number.parseInt(pruneWindowInput, 10);
const nextValue =
Number.isNaN(parsed) || parsed < 1 || parsed > 60
? pruneStaleMinutes
: parsed;
setPruneWindowInput(String(nextValue));
if (nextValue !== pruneStaleMinutes) {
setPruneStaleMinutes(nextValue);
}
}}
className="w-14 rounded border border-border bg-background px-2 py-0.5 text-sm"
/>