fix: address Copilot review on the nullable-union collapse
Four findings, all valid: 1. Returning `T` was unsound. The collapse genuinely changes two fields — `type` stops being an array, `anyOf` becomes `undefined` — so a caller whose `T` typed either that way could keep treating it so afterwards. The result is now `T | NormalizedNullableUnion<T>`, which states exactly that: `Omit<T, "type" | "anyOf">` plus a `RenderableType` `type`, a cleared `anyOf`, and `nullable`. The one assertion left is isolated in a `collapsed()` helper, needed only because `Omit` stays deferred while `T` is open. 2. A typeless `enum` does not imply strings. JSON Schema allows `enum: [1, 2]`, and inferring `"string"` for it would hand numbers to a `Select` declared `string[]`, and make the TUI submit `"1"` for `1`. Now only an all-string enum earns the inference; anything else stays on the JSON fallback. An explicit `type` is still authoritative. 3. `hasMissingRequiredFields` counted every null as missing, so clearing a *required* nullable enum — newly possible via the clear button — would disable submit on a value the schema permits. `required` constrains presence, not content, so null now counts as present exactly when the field's schema admits it. 4. `collectSchemaDefaults` read the raw union, so a default nested inside a nullable object's branch was displayed by the form but never seeded into the submitted values. It normalizes each property first, like the renderer does. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
17a4c78dd3
commit
e4e16bb3e6
1 changed files with 207 additions and 20 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import { normalizeNullableUnion } from "@inspector/core/json/nullableUnion.js";
|
||||
|
||||
export type JsonValue =
|
||||
| string
|
||||
| number
|
||||
|
|
@ -116,7 +118,12 @@ export function collectSchemaDefaults(
|
|||
): Record<string, unknown> {
|
||||
const properties = schema.properties ?? {};
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
||||
for (const [fieldName, rawSchema] of Object.entries(properties)) {
|
||||
// Collapse a nullable union first, for the same reason `SchemaForm` does:
|
||||
// a nested object's `properties` live on the union's surviving branch, so
|
||||
// without this the form would *display* a hoisted default that never
|
||||
// reached the seeded values — the field would submit empty (#1928).
|
||||
const fieldSchema = normalizeNullableUnion(rawSchema);
|
||||
if (fieldSchema.default !== undefined) {
|
||||
result[fieldName] = fieldSchema.default;
|
||||
} else if (fieldSchema.type === "object" && fieldSchema.properties) {
|
||||
|
|
@ -133,15 +140,29 @@ export function collectSchemaDefaults(
|
|||
* Whether any of the schema's required top-level fields is missing a value in
|
||||
* `values` (absent, null, or empty string). Used to gate a form's submit
|
||||
* action until required fields are supplied.
|
||||
*
|
||||
* `null` counts as missing only for a field that does not admit it. JSON
|
||||
* Schema's `required` constrains *presence*, not content, so a required field
|
||||
* whose schema is a nullable union is satisfied by an explicit `null` — and
|
||||
* since #1928 the user can produce exactly that by clearing a nullable enum.
|
||||
* Treating it as missing would leave the form's submit permanently disabled on
|
||||
* a value the schema calls valid.
|
||||
*/
|
||||
export function hasMissingRequiredFields(
|
||||
schema: InspectorFormSchema,
|
||||
values: Record<string, unknown>,
|
||||
): boolean {
|
||||
const required = schema.required ?? [];
|
||||
const properties = schema.properties ?? {};
|
||||
return required.some((field) => {
|
||||
const value = values[field];
|
||||
return value === undefined || value === null || value === "";
|
||||
if (value === null) {
|
||||
const fieldSchema = properties[field];
|
||||
return fieldSchema === undefined
|
||||
? true
|
||||
: normalizeNullableUnion(fieldSchema).nullable !== true;
|
||||
}
|
||||
return value === undefined || value === "";
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue