fix: address Copilot review round 11 — recurse constants into nested objects
applySchemaConstants only corrected top-level pinned fields, but the deep
link's overlay replaces a nested object wholesale rather than merging into it —
so `{ config: { kind: "sms" } }` slipped past a nested read-only
`kind: { const: "email" }` the form was displaying. It now recurses into
object-valued properties, resolving each one's own root union the same way.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
775fec0f6a
commit
a69cb58451
2 changed files with 49 additions and 8 deletions
|
|
@ -445,6 +445,27 @@ describe("root composition (#2123)", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-applies a nested object's constants", () => {
|
||||||
|
// The overlay replaces the whole nested object rather than merging into it,
|
||||||
|
// so a link naming `{ config: { kind: "sms" } }` would otherwise slip past
|
||||||
|
// the pinned `kind` the nested form displays.
|
||||||
|
const schema: InspectorFormSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
config: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
kind: { type: "string", const: "email" },
|
||||||
|
to: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
applySchemaConstants(schema, { config: { kind: "sms", to: "a@b.c" } }),
|
||||||
|
).toEqual({ config: { kind: "email", to: "a@b.c" } });
|
||||||
|
});
|
||||||
|
|
||||||
it("leaves values alone when nothing is pinned", () => {
|
it("leaves values alone when nothing is pinned", () => {
|
||||||
const values = { a: 1 };
|
const values = { a: 1 };
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -194,14 +194,34 @@ export function applySchemaConstants(
|
||||||
const { base, branches } = resolveRootUnion(schema);
|
const { base, branches } = resolveRootUnion(schema);
|
||||||
const selected = selectBranchIndex(branches, values) ?? 0;
|
const selected = selectBranchIndex(branches, values) ?? 0;
|
||||||
const properties = (branches[selected]?.schema ?? base).properties ?? {};
|
const properties = (branches[selected]?.schema ?? base).properties ?? {};
|
||||||
const pinned = Object.entries(properties).filter(
|
|
||||||
([, fieldSchema]) => fieldSchema.const !== undefined,
|
const corrections: [string, unknown][] = [];
|
||||||
);
|
for (const [name, rawSchema] of Object.entries(properties)) {
|
||||||
if (pinned.length === 0) return values;
|
const fieldSchema = normalizeNullableUnion(rawSchema);
|
||||||
return Object.fromEntries([
|
if (fieldSchema.const !== undefined) {
|
||||||
...Object.entries(values),
|
corrections.push([name, fieldSchema.const]);
|
||||||
...pinned.map(([name, fieldSchema]) => [name, fieldSchema.const]),
|
continue;
|
||||||
]);
|
}
|
||||||
|
// Recurse: a nested object renders its own read-only fields, and the
|
||||||
|
// overlay replaces the whole object rather than merging into it — so a
|
||||||
|
// link naming `{ config: { kind: "sms" } }` would otherwise slip a value
|
||||||
|
// past the pinned `kind` the nested form is displaying.
|
||||||
|
const nested = values[name];
|
||||||
|
if (
|
||||||
|
typeof nested === "object" &&
|
||||||
|
nested !== null &&
|
||||||
|
!Array.isArray(nested)
|
||||||
|
) {
|
||||||
|
const corrected = applySchemaConstants(
|
||||||
|
fieldSchema,
|
||||||
|
nested as Record<string, unknown>,
|
||||||
|
);
|
||||||
|
if (corrected !== nested) corrections.push([name, corrected]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (corrections.length === 0) return values;
|
||||||
|
return Object.fromEntries([...Object.entries(values), ...corrections]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue