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..19d80d134 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,12 +525,21 @@ 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({ @@ -534,7 +547,7 @@ const ToolsTab = ({ [key]: undefined, }); } else { - // Field has value - try to convert to number, but store input either way + // Field has value - try to convert to number const num = Number(value); if (!isNaN(num)) { setParams({ @@ -550,6 +563,28 @@ 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 ( + prop.type === "integer" || + 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..451c8304c 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: "1.0" } }); + fireEvent.blur(input); + expect(input.value).toBe("1"); + }); }); });