fix: canonicalize schema binding paths
This commit is contained in:
@@ -285,8 +285,7 @@ export const SchemaFieldControl = ({
|
||||
);
|
||||
}
|
||||
|
||||
const legacySourceKey = field.path.length === 0 ? "root" : field.path.map(String).join(".");
|
||||
const source = sources[pathKey(field)] ?? sources[legacySourceKey] ?? { mode: "literal", value };
|
||||
const source = sources[pathKey(field)] ?? { mode: "literal", value };
|
||||
return (
|
||||
<div className="schema-form__field">
|
||||
<FieldLabel field={field} id={id} />
|
||||
|
||||
@@ -287,4 +287,52 @@ describe("SchemaForm", () => {
|
||||
const hyphenated = screen.getByRole("textbox", { name: "A-b" });
|
||||
expect(dotted.id).not.toBe(hyphenated.id);
|
||||
});
|
||||
|
||||
it("edits a canonical literal source without aliasing a nested path", async () => {
|
||||
const user = userEvent.setup();
|
||||
const submissions: SchemaSerializationResult[] = [];
|
||||
render(
|
||||
<SchemaForm
|
||||
initialSources={{
|
||||
"a.b": { mode: "literal", value: "canonical nested" },
|
||||
}}
|
||||
initialValue={{ "a.b": "initial dotted", a: { b: "initial nested" } }}
|
||||
onSubmit={(result) => submissions.push(result)}
|
||||
schema={{
|
||||
type: "object",
|
||||
properties: {
|
||||
"a.b": { type: "string" },
|
||||
a: { type: "object", properties: { b: { type: "string" } } },
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dotted = screen.getByRole("textbox", { name: "A.b" });
|
||||
await user.clear(dotted);
|
||||
await user.type(dotted, "edited dotted");
|
||||
await user.click(screen.getByRole("button", { name: "Save form" }));
|
||||
|
||||
expect(submissions[0]?.value).toEqual({ "a.b": "edited dotted", a: { b: "canonical nested" } });
|
||||
});
|
||||
|
||||
it("updates the canonical literal source when its field is edited", async () => {
|
||||
const user = userEvent.setup();
|
||||
const submissions: SchemaSerializationResult[] = [];
|
||||
render(
|
||||
<SchemaForm
|
||||
initialSources={{ '"a.b"': { mode: "literal", value: "canonical dotted" } }}
|
||||
initialValue={{ "a.b": "initial dotted" }}
|
||||
onSubmit={(result) => submissions.push(result)}
|
||||
schema={{ type: "object", properties: { "a.b": { type: "string" } } }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dotted = screen.getByRole("textbox", { name: "A.b" });
|
||||
await user.clear(dotted);
|
||||
await user.type(dotted, "edited canonical");
|
||||
await user.click(screen.getByRole("button", { name: "Save form" }));
|
||||
|
||||
expect(submissions[0]?.value).toEqual({ "a.b": "edited canonical" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
||||
const isBareSegment = (value: string): boolean => BARE_TOML_KEY.test(value);
|
||||
|
||||
const isValidSegment = (value: string): boolean =>
|
||||
value.length > 0 && !CONTROL_CHARACTER.test(value);
|
||||
value.trim().length > 0 && !CONTROL_CHARACTER.test(value);
|
||||
|
||||
const parseDoubleQuoted = (raw: string, start: number): { readonly value: string; readonly next: number } | null => {
|
||||
let escaped = false;
|
||||
|
||||
@@ -134,6 +134,36 @@ describe("serializeSchemaValues", () => {
|
||||
expect(rootResult.bindings).toEqual([{ target: ".", path: "input.payload" }]);
|
||||
});
|
||||
|
||||
it("rejects whitespace-only quoted binding path segments", () => {
|
||||
const field = normalizeSchema({ type: "string" });
|
||||
const result = serializeSchemaValues(field, "literal", {
|
||||
".": { mode: "bind", sourcePath: 'input." "' },
|
||||
});
|
||||
|
||||
expect(result.bindings).toEqual([]);
|
||||
expect(result.issues).toEqual([
|
||||
{ path: [], message: "Binding path must start with input, state, or context." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not alias a nested path with a literal dotted property", () => {
|
||||
const field = normalizeSchema({
|
||||
type: "object",
|
||||
properties: {
|
||||
"a.b": { type: "string" },
|
||||
a: { type: "object", properties: { b: { type: "string" } } },
|
||||
},
|
||||
});
|
||||
|
||||
const result = serializeSchemaValues(
|
||||
field,
|
||||
{ "a.b": "initial dotted", a: { b: "initial nested" } },
|
||||
{ "a.b": { mode: "literal", value: "canonical nested" } },
|
||||
);
|
||||
|
||||
expect(result.value).toEqual({ "a.b": "initial dotted", a: { b: "canonical nested" } });
|
||||
});
|
||||
|
||||
it("rebases bindings for the second nested object array item", () => {
|
||||
const field = normalizeSchema({
|
||||
type: "object",
|
||||
|
||||
@@ -38,13 +38,10 @@ const isRecord = (value: unknown): value is ValueRecord =>
|
||||
|
||||
const pathKey = (path: ReadonlyArray<string | number>): string => formatTOMLPath(path);
|
||||
|
||||
const legacyPathKey = (path: ReadonlyArray<string | number>): string =>
|
||||
path.length === 0 ? "root" : path.map(String).join(".");
|
||||
|
||||
const sourceForPath = (
|
||||
sources: FieldSources,
|
||||
path: ReadonlyArray<string | number>,
|
||||
): FieldSource | undefined => sources[pathKey(path)] ?? sources[legacyPathKey(path)];
|
||||
): FieldSource | undefined => sources[pathKey(path)];
|
||||
|
||||
const targetPath = (path: ReadonlyArray<string | number>): string => formatTOMLPath(path);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user