Skip to content

Commit 7c8b031

Browse files
authored
Merge pull request #1127 from MumuTW/fix-tools-tab-decimal-input-1080
Fix decimal typing behavior for number inputs in tools form
2 parents db0827b + fdbf896 commit 7c8b031

File tree

2 files changed

+88
-3
lines changed

2 files changed

+88
-3
lines changed

client/src/components/DynamicJsonForm.tsx

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
125125
const [rawJsonValue, setRawJsonValue] = useState<string>(
126126
JSON.stringify(value ?? generateDefaultValue(schema), null, 2),
127127
);
128+
const [numericInputDrafts, setNumericInputDrafts] = useState<
129+
Record<string, string>
130+
>({});
128131

129132
// Use a ref to manage debouncing timeouts to avoid parsing JSON
130133
// on every keystroke which would be inefficient and error-prone
@@ -134,6 +137,37 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
134137
return !!jsonError;
135138
};
136139

140+
const getPathKey = (path: string[]) =>
141+
path.length === 0 ? "$root" : path.join(".");
142+
143+
const getNumericDisplayValue = (
144+
path: string[],
145+
currentValue: JsonValue,
146+
): string => {
147+
const pathKey = getPathKey(path);
148+
if (Object.prototype.hasOwnProperty.call(numericInputDrafts, pathKey)) {
149+
return numericInputDrafts[pathKey];
150+
}
151+
return typeof currentValue === "number" ? currentValue.toString() : "";
152+
};
153+
154+
const updateNumericDraft = (path: string[], draftValue: string) => {
155+
const pathKey = getPathKey(path);
156+
setNumericInputDrafts((prev) => ({ ...prev, [pathKey]: draftValue }));
157+
};
158+
159+
const clearNumericDraft = (path: string[]) => {
160+
const pathKey = getPathKey(path);
161+
setNumericInputDrafts((prev) => {
162+
if (!Object.prototype.hasOwnProperty.call(prev, pathKey)) {
163+
return prev;
164+
}
165+
const next = { ...prev };
166+
delete next[pathKey];
167+
return next;
168+
});
169+
};
170+
137171
// Debounce JSON parsing and parent updates to handle typing gracefully
138172
const debouncedUpdateParent = useCallback(
139173
(jsonString: string) => {
@@ -429,9 +463,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
429463
return (
430464
<Input
431465
type="number"
432-
value={(currentValue as number)?.toString() ?? ""}
466+
value={getNumericDisplayValue(path, currentValue)}
433467
onChange={(e) => {
434468
const val = e.target.value;
469+
updateNumericDraft(path, val);
435470
if (!val && !isRequired) {
436471
handleFieldChange(path, undefined);
437472
} else {
@@ -441,6 +476,19 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
441476
}
442477
}
443478
}}
479+
onBlur={(e) => {
480+
const val = e.target.value;
481+
if (!val) {
482+
clearNumericDraft(path);
483+
return;
484+
}
485+
486+
const num = Number(val);
487+
if (!isNaN(num)) {
488+
handleFieldChange(path, num);
489+
}
490+
clearNumericDraft(path);
491+
}}
444492
placeholder={propSchema.description}
445493
required={isRequired}
446494
min={propSchema.minimum}
@@ -453,9 +501,10 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
453501
<Input
454502
type="number"
455503
step="1"
456-
value={(currentValue as number)?.toString() ?? ""}
504+
value={getNumericDisplayValue(path, currentValue)}
457505
onChange={(e) => {
458506
const val = e.target.value;
507+
updateNumericDraft(path, val);
459508
if (!val && !isRequired) {
460509
handleFieldChange(path, undefined);
461510
} else {
@@ -465,6 +514,19 @@ const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(
465514
}
466515
}
467516
}}
517+
onBlur={(e) => {
518+
const val = e.target.value;
519+
if (!val) {
520+
clearNumericDraft(path);
521+
return;
522+
}
523+
524+
const num = Number(val);
525+
if (!isNaN(num) && Number.isInteger(num)) {
526+
handleFieldChange(path, num);
527+
}
528+
clearNumericDraft(path);
529+
}}
468530
placeholder={propSchema.description}
469531
required={isRequired}
470532
min={propSchema.minimum}

client/src/components/__tests__/DynamicJsonForm.test.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
22
import "@testing-library/jest-dom";
33
import { describe, it, expect, jest } from "@jest/globals";
4-
import { useRef } from "react";
4+
import { useRef, useState } from "react";
55
import DynamicJsonForm, { DynamicJsonFormRef } from "../DynamicJsonForm";
66
import type { JsonSchemaType } from "@/utils/jsonUtils";
77

@@ -402,6 +402,29 @@ describe("DynamicJsonForm Number Fields", () => {
402402

403403
expect(onChange).toHaveBeenCalledWith(98.6);
404404
});
405+
406+
it("should preserve decimal zero while typing", () => {
407+
const schema: JsonSchemaType = {
408+
type: "number",
409+
description: "Coordinate",
410+
};
411+
412+
const WrappedForm = () => {
413+
const [value, setValue] = useState<number>(0);
414+
return (
415+
<DynamicJsonForm schema={schema} value={value} onChange={setValue} />
416+
);
417+
};
418+
419+
render(<WrappedForm />);
420+
const input = screen.getByRole("spinbutton") as HTMLInputElement;
421+
422+
fireEvent.change(input, { target: { value: "-74.0" } });
423+
expect(input.value).toBe("-74.0");
424+
425+
fireEvent.change(input, { target: { value: "-74.01" } });
426+
expect(input.value).toBe("-74.01");
427+
});
405428
});
406429
});
407430

0 commit comments

Comments
 (0)