mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 20:46:28 +01:00
fix(ui): omit optional scalar defaults from JsonSchemaForm so empty number fields don't submit as 0 (PAPA-407)
getDefaultForSchema previously materialised a value of 0 for any number field without an explicit default (and "" for any optional string), which is what `getDefaultValues` then seeded the form with. That meant the exe.dev environment form opened with `cpu: 0` baked in even though the user never touched the field, and submitting the form failed server-side validation: "cpu must be greater than 0 when provided." Fix: only materialise scalar (string / number / integer / secret-ref) defaults when the schema actually declares one. Booleans, enums, arrays and objects still get sensible empty values so checkbox/array UI keeps working. Object recursion now skips properties whose defaults resolve to undefined so they stay out of the submitted payload. Adds a regression test that getDefaultValues omits optional scalars without an explicit default while still honouring fields that do declare one. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
58ff7db6a2
commit
980bfc7301
2 files changed
+38
-6
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { JsonSchemaForm } from "./JsonSchemaForm";
|
||||
import { JsonSchemaForm, getDefaultValues } from "./JsonSchemaForm";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
@@ -350,6 +350,31 @@ describe("JsonSchemaForm secret-ref rendering", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("omits optional scalar fields from getDefaultValues so empty inputs aren't submitted as 0/''", () => {
|
||||
const defaults = getDefaultValues({
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: { type: "string", format: "secret-ref" },
|
||||
sshPort: { type: "number", default: 22 },
|
||||
cpu: { type: "number" },
|
||||
memory: { type: "string" },
|
||||
reuseLease: { type: "boolean", default: false },
|
||||
tags: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
});
|
||||
|
||||
// Fields with explicit defaults round-trip.
|
||||
expect(defaults.sshPort).toBe(22);
|
||||
expect(defaults.reuseLease).toBe(false);
|
||||
expect(defaults.tags).toEqual([]);
|
||||
|
||||
// Optional scalars without explicit defaults stay out of the payload so
|
||||
// the server doesn't see e.g. `cpu: 0` and reject the submission.
|
||||
expect("apiKey" in defaults).toBe(false);
|
||||
expect("cpu" in defaults).toBe(false);
|
||||
expect("memory" in defaults).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the password fallback for short raw values", async () => {
|
||||
const root = createRoot(container);
|
||||
|
||||
|
||||
@@ -134,7 +134,14 @@ export function labelFromKey(key: string, schema: JsonSchemaNode): string {
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Produce a sensible default value for a schema node. */
|
||||
/**
|
||||
* Produce a sensible default value for a schema node.
|
||||
*
|
||||
* Optional scalar fields (string, number, integer, secret-ref) without an
|
||||
* explicit `default` return `undefined` so they stay out of the submitted
|
||||
* payload — otherwise an empty field would round-trip as `""` or `0` and
|
||||
* trip server-side "X must be greater than 0 when provided" style validators.
|
||||
*/
|
||||
export function getDefaultForSchema(schema: JsonSchemaNode): unknown {
|
||||
if (schema.default !== undefined) return schema.default;
|
||||
|
||||
@@ -142,10 +149,9 @@ export function getDefaultForSchema(schema: JsonSchemaNode): unknown {
|
||||
switch (type) {
|
||||
case "string":
|
||||
case "secret-ref":
|
||||
return "";
|
||||
case "number":
|
||||
case "integer":
|
||||
return schema.minimum ?? 0;
|
||||
return undefined;
|
||||
case "boolean":
|
||||
return false;
|
||||
case "enum":
|
||||
@@ -156,12 +162,13 @@ export function getDefaultForSchema(schema: JsonSchemaNode): unknown {
|
||||
if (!schema.properties) return {};
|
||||
const obj: Record<string, unknown> = {};
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
obj[key] = getDefaultForSchema(propSchema);
|
||||
const def = getDefaultForSchema(propSchema);
|
||||
if (def !== undefined) obj[key] = def;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user