fix: close workflow console review remediation

This commit is contained in:
lda
2026-08-12 14:51:02 +07:00 Verified
parent e0e1b718b8
commit a9bb2ab837
22 changed files with 359 additions and 178 deletions
@@ -17,6 +17,7 @@ export type SchemaFieldControlProps = {
readonly onValueChange: (field: SchemaField, value: unknown) => void;
readonly onSourceChange: (field: SchemaField, source: FieldSource) => void;
readonly onArrayItemRemove: (field: SchemaField, index: number) => void;
readonly arrayItemKey?: ((field: SchemaField, index: number) => string) | undefined;
readonly sourceSuggestions?: ReadonlyArray<string>;
readonly showSourceControl?: boolean;
readonly idPrefix?: string;
@@ -216,6 +217,7 @@ const LeafControl = ({
};
export const SchemaFieldControl = ({
arrayItemKey,
field,
value,
sources,
@@ -246,8 +248,9 @@ export const SchemaFieldControl = ({
{field.children.map((child) => (
<SchemaFieldControl
diagnostics={diagnostics.filter((diagnostic) => isPathPrefix(child.path, diagnostic.path))}
arrayItemKey={arrayItemKey}
field={child}
key={pathKey(child)}
key={String(child.key)}
onArrayItemRemove={onArrayItemRemove}
onSourceChange={onSourceChange}
onValueChange={onValueChange}
@@ -282,8 +285,12 @@ export const SchemaFieldControl = ({
: null;
if (!itemField) return null;
return (
<div className="schema-form__array-item" key={pathKey(itemField)}>
<div
className="schema-form__array-item"
key={arrayItemKey?.(field, index) ?? pathKey(itemField)}
>
<SchemaFieldControl
arrayItemKey={arrayItemKey}
diagnostics={diagnostics.filter((diagnostic) => isPathPrefix(itemField.path, diagnostic.path))}
field={itemField}
onSourceChange={onSourceChange}
@@ -57,6 +57,42 @@ describe("SchemaForm", () => {
expect(screen.getByRole("button", { name: "Call capability" })).toBeInTheDocument();
});
it("isolates control ids and radio groups across form instances", () => {
render(
<>
<SchemaForm schema={{ type: "object", properties: { value: { type: "string" } } }} />
<SchemaForm schema={{ type: "object", properties: { value: { type: "string" } } }} />
</>,
);
const literalRadios = screen.getAllByRole("radio", { name: /Literal/ });
expect(literalRadios[0]).not.toHaveAttribute("name", literalRadios[1]?.getAttribute("name"));
const valueInputs = screen.getAllByRole("textbox", { name: "Value" });
expect(valueInputs[0]?.id).not.toBe(valueInputs[1]?.id);
});
it("resets values when the represented schema changes", async () => {
const user = userEvent.setup();
const { rerender } = render(
<SchemaForm
initialValue={{ first: "initial" }}
schema={{ type: "object", properties: { first: { type: "string" } } }}
/>,
);
await user.clear(screen.getByRole("textbox", { name: "First" }));
await user.type(screen.getByRole("textbox", { name: "First" }), "edited");
rerender(
<SchemaForm
initialValue={{ second: "replacement" }}
schema={{ type: "object", properties: { second: { type: "string" } } }}
/>,
);
expect(screen.queryByRole("textbox", { name: "First" })).not.toBeInTheDocument();
expect(screen.getByRole("textbox", { name: "Second" })).toHaveValue("replacement");
});
it("renders unsupported fields as JSON editors with their exact fallback reason", () => {
render(
<SchemaForm
@@ -152,6 +188,29 @@ describe("SchemaForm", () => {
]);
});
it("keeps later array controls mounted when an earlier row is removed", async () => {
const user = userEvent.setup();
render(
<SchemaForm
initialValue={{ items: [{ name: "first" }, { name: "second" }] }}
schema={{
type: "object",
properties: {
items: {
type: "array",
items: { type: "object", properties: { name: { type: "string" } } },
},
},
}}
/>,
);
const secondControl = screen.getAllByRole("textbox", { name: "Name" })[1];
await user.click(screen.getByRole("button", { name: "Remove item 1" }));
expect(screen.getByRole("textbox", { name: "Name" })).toBe(secondControl);
expect(secondControl).toHaveValue("second");
});
it("omits an untouched optional boolean but preserves explicit false", async () => {
const user = userEvent.setup();
const untouched: SchemaSerializationResult[] = [];
@@ -1,4 +1,4 @@
import { useState, type FormEvent, type ReactNode } from "react";
import { useId, useRef, useState, type FormEvent, type ReactNode } from "react";
import { normalizeSchema, type FieldSource, type SchemaField } from "./schema-field.js";
import { SchemaFieldControl } from "./SchemaFieldControl.js";
import {
@@ -93,7 +93,7 @@ const rawSchemaText = (schema: unknown): string => {
}
};
export const SchemaForm = ({
const SchemaFormState = ({
schema,
initialValue,
initialSources = EMPTY_SOURCES,
@@ -106,6 +106,9 @@ export const SchemaForm = ({
sourceSuggestions = EMPTY_SUGGESTIONS,
showSourceControls = true,
}: SchemaFormProps) => {
const formId = useId();
const nextArrayRowId = useRef(0);
const arrayRowIds = useRef(new Map<string, string[]>());
const field = normalizeSchema(schema);
const [values, setValues] = useState<unknown>(() =>
initialValue !== undefined ? initialValue : emptyValueFor(field),
@@ -147,6 +150,7 @@ export const SchemaForm = ({
onDirtyChange?.(true);
const arrayValue = readAtPath(values, arrayField.path);
if (!Array.isArray(arrayValue)) return;
arrayRowIds.current.get(sourceKey(arrayField))?.splice(index, 1);
const nextValues = setAtPath(
values,
arrayField.path,
@@ -160,6 +164,16 @@ export const SchemaForm = ({
onValueChange?.(serializeSchemaValues(field, nextValues, nextSources));
};
const arrayItemKey = (arrayField: SchemaField, index: number): string => {
const path = sourceKey(arrayField);
const ids = arrayRowIds.current.get(path) ?? [];
while (ids.length <= index) {
ids.push(`${formId}-array-row-${nextArrayRowId.current++}`);
}
arrayRowIds.current.set(path, ids);
return ids[index] ?? `${formId}-array-row-missing`;
};
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
event.preventDefault();
const result = serializeSchemaValues(field, values, sources);
@@ -171,8 +185,10 @@ export const SchemaForm = ({
<form className="schema-form" noValidate onSubmit={handleSubmit}>
{renderBeforeFields}
<SchemaFieldControl
arrayItemKey={arrayItemKey}
diagnostics={allDiagnostics}
field={field}
idPrefix={`schema-form-${formId}`}
onArrayItemRemove={handleArrayItemRemove}
onSourceChange={handleSourceChange}
onValueChange={handleValueChange}
@@ -189,3 +205,8 @@ export const SchemaForm = ({
</form>
);
};
/** Remounts form-local state only when the represented schema changes. */
export const SchemaForm = (props: SchemaFormProps) => (
<SchemaFormState key={rawSchemaText(props.schema)} {...props} />
);