From 6e3490701bca91c6d0b722dbbdc2f7375204aa1d Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Sun, 29 Mar 2026 16:21:28 +0800 Subject: [PATCH 1/2] fix: preserve trailing decimal zeros in number inputs after blur The number input's onBlur handler was unconditionally clearing the numeric draft, causing values like "1.0" to revert to "1". Now the draft is only cleared when the raw text matches the stringified number (e.g. "5" === "5"), so meaningful decimal notation like "1.0" persists. Also adds numeric draft tracking to ToolsTab's top-level number inputs which previously had no draft state at all. Fixes #918 Signed-off-by: majiayu000 <1835304752@qq.com> --- client/src/components/DynamicJsonForm.tsx | 7 +++- client/src/components/ToolsTab.tsx | 41 +++++++++++++++--- .../__tests__/DynamicJsonForm.test.tsx | 42 +++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 90249ab1c..0880d5986 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -487,7 +487,12 @@ const DynamicJsonForm = forwardRef( if (!isNaN(num)) { handleFieldChange(path, num); } - clearNumericDraft(path); + // Keep the draft if the raw text differs from the + // stringified number (e.g. "1.0" vs "1") so the + // display preserves the user's decimal notation. + if (val === String(num)) { + clearNumericDraft(path); + } }} placeholder={propSchema.description} required={isRequired} diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index febea1d8f..924dd0c7f 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -199,6 +199,9 @@ const ToolsTab = ({ serverSupportsTaskRequests: boolean; }) => { const [params, setParams] = useState>({}); + const [numericDrafts, setNumericDrafts] = useState>( + {}, + ); const [runAsTask, setRunAsTask] = useState(false); const [isToolRunning, setIsToolRunning] = useState(false); const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); @@ -241,6 +244,7 @@ const ToolsTab = ({ ]; }); setParams(Object.fromEntries(params)); + setNumericDrafts({}); const toolTaskSupport = serverSupportsTaskRequests ? getTaskSupport(selectedTool) : "forbidden"; @@ -521,20 +525,27 @@ const ToolsTab = ({ name={key} placeholder={prop.description} value={ - params[key] === undefined - ? "" - : String(params[key]) + Object.prototype.hasOwnProperty.call( + numericDrafts, + key, + ) + ? numericDrafts[key] + : params[key] === undefined + ? "" + : String(params[key]) } onChange={(e) => { const value = e.target.value; + setNumericDrafts((prev) => ({ + ...prev, + [key]: value, + })); if (value === "") { - // Field cleared - set to undefined setParams({ ...params, [key]: undefined, }); } else { - // Field has value - try to convert to number, but store input either way const num = Number(value); if (!isNaN(num)) { setParams({ @@ -542,7 +553,6 @@ const ToolsTab = ({ [key]: num, }); } else { - // Store invalid input as string - let server validate setParams({ ...params, [key]: value, @@ -550,6 +560,25 @@ const ToolsTab = ({ } } }} + onBlur={(e) => { + const val = e.target.value; + if (!val) { + setNumericDrafts((prev) => { + const next = { ...prev }; + delete next[key]; + return next; + }); + return; + } + const num = Number(val); + if (val === String(num)) { + setNumericDrafts((prev) => { + const next = { ...prev }; + delete next[key]; + return next; + }); + } + }} className="mt-1" /> ) : ( diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index a273f75ae..8df9948b3 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -425,6 +425,48 @@ describe("DynamicJsonForm Number Fields", () => { fireEvent.change(input, { target: { value: "-74.01" } }); expect(input.value).toBe("-74.01"); }); + + it("should preserve decimal zero after blur", () => { + const schema: JsonSchemaType = { + type: "number", + description: "Coordinate", + }; + + const WrappedForm = () => { + const [value, setValue] = useState(0); + return ( + + ); + }; + + render(); + const input = screen.getByRole("spinbutton") as HTMLInputElement; + + fireEvent.change(input, { target: { value: "1.0" } }); + fireEvent.blur(input); + expect(input.value).toBe("1.0"); + }); + + it("should not preserve decimal zero for integer fields after blur", () => { + const schema: JsonSchemaType = { + type: "integer", + description: "Count", + }; + + const WrappedForm = () => { + const [value, setValue] = useState(0); + return ( + + ); + }; + + render(); + const input = screen.getByRole("spinbutton") as HTMLInputElement; + + fireEvent.change(input, { target: { value: "5" } }); + fireEvent.blur(input); + expect(input.value).toBe("5"); + }); }); }); From 3184d10cfa444847b64c85a81f4c26c2150ae016 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Sun, 29 Mar 2026 16:29:12 +0800 Subject: [PATCH 2/2] fix: differentiate number vs integer draft handling in ToolsTab onBlur Integer fields now unconditionally clear numericDrafts on blur, matching DynamicJsonForm behavior. Number fields still preserve trailing decimal notation (e.g. "1.0") in the draft. Updated test to verify "1.0" typed in an integer field is stripped to "1" after blur. Signed-off-by: majiayu000 <1835304752@qq.com> --- client/src/components/ToolsTab.tsx | 8 +++++++- client/src/components/__tests__/DynamicJsonForm.test.tsx | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 924dd0c7f..19d80d134 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -541,11 +541,13 @@ const ToolsTab = ({ [key]: value, })); if (value === "") { + // Field cleared - set to undefined setParams({ ...params, [key]: undefined, }); } else { + // Field has value - try to convert to number const num = Number(value); if (!isNaN(num)) { setParams({ @@ -553,6 +555,7 @@ const ToolsTab = ({ [key]: num, }); } else { + // Store invalid input as string - let server validate setParams({ ...params, [key]: value, @@ -571,7 +574,10 @@ const ToolsTab = ({ return; } const num = Number(val); - if (val === String(num)) { + if ( + prop.type === "integer" || + val === String(num) + ) { setNumericDrafts((prev) => { const next = { ...prev }; delete next[key]; diff --git a/client/src/components/__tests__/DynamicJsonForm.test.tsx b/client/src/components/__tests__/DynamicJsonForm.test.tsx index 8df9948b3..451c8304c 100644 --- a/client/src/components/__tests__/DynamicJsonForm.test.tsx +++ b/client/src/components/__tests__/DynamicJsonForm.test.tsx @@ -463,9 +463,9 @@ describe("DynamicJsonForm Number Fields", () => { render(); const input = screen.getByRole("spinbutton") as HTMLInputElement; - fireEvent.change(input, { target: { value: "5" } }); + fireEvent.change(input, { target: { value: "1.0" } }); fireEvent.blur(input); - expect(input.value).toBe("5"); + expect(input.value).toBe("1"); }); }); });