feat: add schema approval surface

This commit is contained in:
lda
2026-07-09 05:41:42 +07:00 Verified
parent f5a81106dd
commit a65da60a5c
3 changed files with 263 additions and 0 deletions
@@ -0,0 +1,86 @@
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SchemaApprovalSurface } from "./SchemaApprovalSurface.js";
afterEach(() => cleanup());
describe("SchemaApprovalSurface", () => {
it("renders explicit schema fields and outcome actions", () => {
const onSubmit = vi.fn();
const onCancel = vi.fn();
render(
<SchemaApprovalSurface
title="Issue review resume"
schema={{
type: "object",
required: ["selected_issue_ids"],
properties: {
selected_issue_ids: { type: "array", description: "Issue ids to create" },
comment: { type: "string" },
},
}}
payload={{ selected_issue_ids: ["risk-1"], comment: "Create the selected issue." }}
outcomes={["submitted", "cancelled"]}
runId="run_recorded_lda_report"
onSubmit={onSubmit}
onCancel={onCancel}
/>,
);
const surface = screen.getByRole("group", { name: /issue review resume/i });
expect(within(surface).getByText("selected issue ids")).toBeInTheDocument();
expect(within(surface).getByText("required")).toBeInTheDocument();
expect(within(surface).getByText("[\"risk-1\"]")).toBeInTheDocument();
expect(within(surface).getByText("run_recorded_lda_report")).toBeInTheDocument();
fireEvent.click(within(surface).getByRole("button", { name: /submit/i }));
fireEvent.click(within(surface).getByRole("button", { name: /cancel/i }));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onCancel).toHaveBeenCalledTimes(1);
});
it("renders payload preview for loose object schemas", () => {
render(
<SchemaApprovalSurface
title="Issue review resume"
schema={{ type: "object" }}
payload={{ selected_issue_ids: ["risk-1"], comment: "Create the selected issue." }}
outcomes={["submitted", "cancelled"]}
runId="run_recorded_lda_report"
/>,
);
expect(screen.getByText("No additional resume fields are declared by this schema.")).toBeInTheDocument();
expect(screen.getByText("selected_issue_ids")).toBeInTheDocument();
expect(screen.getByText("[\"risk-1\"]")).toBeInTheDocument();
});
it("shows submitted and cancelled states without active actions", () => {
const { rerender } = render(
<SchemaApprovalSurface
title="Issue review resume"
schema={{ type: "object" }}
payload={{}}
outcomes={["submitted", "cancelled"]}
runId={null}
state="submitted"
/>,
);
expect(screen.getByText("Outcome: submitted")).toBeInTheDocument();
rerender(
<SchemaApprovalSurface
title="Issue review resume"
schema={{ type: "object" }}
payload={{}}
outcomes={["submitted", "cancelled"]}
runId={null}
state="cancelled"
/>,
);
expect(screen.getByText("Outcome: cancelled")).toBeInTheDocument();
});
});
@@ -0,0 +1,83 @@
import { buildSchemaApprovalModel } from "./schema-approval-model.js";
export type SchemaApprovalSurfaceProps = {
readonly title: string;
readonly schema: unknown;
readonly payload: unknown;
readonly outcomes: ReadonlyArray<string>;
readonly runId: string | null;
readonly state?: "ready" | "submitted" | "cancelled";
readonly onSubmit?: (() => void) | undefined;
readonly onCancel?: (() => void) | undefined;
};
export const SchemaApprovalSurface = ({
title,
schema,
payload,
outcomes,
runId,
state = "ready",
onSubmit,
onCancel,
}: SchemaApprovalSurfaceProps) => {
const model = buildSchemaApprovalModel({ schema, payload, outcomes });
const isResolved = state !== "ready";
return (
<section className="schema-approval-surface" role="group" aria-label={title} data-state={state}>
<header className="schema-approval-surface__header">
<span>Schema-backed decision</span>
<strong>{title}</strong>
<code>{runId ?? "run unavailable"}</code>
</header>
<div className="schema-approval-surface__body">
{model.hasExplicitFields ? (
<dl className="schema-approval-surface__fields">
{model.fields.map((field) => (
<div key={field.name} className="schema-approval-surface__field" data-kind={field.kind}>
<dt>
<span>{field.label}</span>
{field.required ? <small>required</small> : <small>optional</small>}
</dt>
<dd>
<code>{field.valuePreview ?? "not provided"}</code>
{field.description ? <p>{field.description}</p> : null}
</dd>
</div>
))}
</dl>
) : (
<div className="schema-approval-surface__loose">
<p>No additional resume fields are declared by this schema.</p>
<dl>
{model.payloadPreview.map((entry) => (
<div key={entry.key}>
<dt>{entry.key}</dt>
<dd><code>{entry.value}</code></dd>
</div>
))}
</dl>
</div>
)}
</div>
<footer className="schema-approval-surface__actions">
{isResolved ? (
<strong>Outcome: {state}</strong>
) : (
<>
<button type="button" onClick={onSubmit} disabled={!onSubmit}>
Submit
</button>
<button type="button" onClick={onCancel} disabled={!onCancel}>
Cancel
</button>
</>
)}
<span>{model.outcomes.join(" / ")}</span>
</footer>
</section>
);
};