Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions packages/admin-next/src/components/resource-form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { AdminActions } from "../server/actions.js";
import type { FormSchema } from "../types.js";
import { ResourceForm } from "./resource-form.js";

afterEach(cleanup);

describe("ResourceForm", () => {
it("prefills edit fields declared inside conditional dependencies", async () => {
const schema: FormSchema = {
uiType: "form",
type: "edit",
schema: {
type: "object",
additionalProperties: false,
properties: {
models_mode: {
type: "string",
enum: ["all", "only"],
},
voices_mode: {
type: "string",
enum: ["all", "only"],
},
},
dependencies: {
models_mode: {
oneOf: [
{
properties: {
models_mode: { enum: ["all"] },
},
},
{
properties: {
models_mode: { enum: ["only"] },
models_allow: {
type: "array",
items: { type: "string" },
},
},
},
],
},
voices_mode: {
oneOf: [
{
properties: {
voices_mode: { enum: ["all"] },
},
},
{
properties: {
voices_mode: { enum: ["only"] },
voices_allow: {
type: "array",
items: { type: "string" },
},
},
},
],
},
},
},
uiSchema: {},
supportedActions: [],
};
const actions: AdminActions = {
listResources: vi.fn(),
getSchema: vi.fn(),
fetchUrl: vi.fn(),
submitAction: vi.fn(),
fetchAction: vi.fn().mockResolvedValue({
ok: true,
data: {
data: {
id: "free",
models_mode: "only",
models_allow: ["gpt-4o-mini"],
voices_mode: "only",
voices_allow: ["en-US-E2EAvaNeural"],
},
},
}),
};

render(
<ResourceForm
resourceId="permissions"
action="edit"
dynamicPath="free"
schema={schema}
actions={actions}
onDone={vi.fn()}
/>,
);

expect(await screen.findByDisplayValue("gpt-4o-mini")).toBeTruthy();
expect(await screen.findByDisplayValue("en-US-E2EAvaNeural")).toBeTruthy();
await waitFor(() => {
expect(actions.fetchAction).toHaveBeenCalledWith("permissions", "edit", {
dynamicPath: "free",
});
});
});
});
59 changes: 48 additions & 11 deletions packages/admin-next/src/components/resource-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,65 @@ import {
} from "./array-templates.js";
import { Button } from "./ui.js";

/** Keep only the keys declared in the form schema's `properties`. */
type SchemaNode = {
type?: string | string[];
properties?: Record<string, SchemaNode>;
items?: SchemaNode;
dependencies?: Record<string, SchemaNode | string[]>;
oneOf?: SchemaNode[];
anyOf?: SchemaNode[];
allOf?: SchemaNode[];
if?: SchemaNode;
then?: SchemaNode;
else?: SchemaNode;
};

/**
* Collect every property key a schema can hold, including keys declared in
* conditional draft-07 branches. RJSF resolves these branches at render time,
* so edit-prefill filtering must account for them before handing over data.
*/
function collectSchemaKeys(
schema: SchemaNode,
keys = new Set<string>(),
seen = new Set<SchemaNode>(),
): Set<string> {
if (seen.has(schema)) return keys;
seen.add(schema);

for (const key of Object.keys(schema.properties ?? {})) keys.add(key);

for (const branch of [schema.if, schema.then, schema.else]) {
if (branch) collectSchemaKeys(branch, keys, seen);
}
for (const branches of [schema.oneOf, schema.anyOf, schema.allOf]) {
for (const branch of branches ?? []) {
collectSchemaKeys(branch, keys, seen);
}
}
for (const dependency of Object.values(schema.dependencies ?? {})) {
if (!Array.isArray(dependency)) {
collectSchemaKeys(dependency, keys, seen);
}
}

return keys;
}

/** Keep only the keys the form schema can hold (declared or conditional). */
function pickSchemaKeys(
data: Record<string, unknown>,
schema: FormSchema,
): Record<string, unknown> {
const properties = (schema.schema as { properties?: Record<string, unknown> })
.properties;
if (!properties) return data;
const allowed = new Set(Object.keys(properties));
const allowed = collectSchemaKeys(schema.schema as SchemaNode);
if (allowed.size === 0) return data;
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
if (allowed.has(key)) out[key] = value;
}
return out;
}

type SchemaNode = {
type?: string | string[];
properties?: Record<string, SchemaNode>;
items?: SchemaNode;
};

function normalizeFormDataForSchema(
data: Record<string, unknown>,
schema: FormSchema,
Expand Down