From 261b3958d6a339d83bdcedc597894c43b1e914c4 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:47:53 +0800 Subject: [PATCH] fix: resource form --- .../src/components/resource-form.test.tsx | 107 ++++++++++++++++++ .../src/components/resource-form.tsx | 59 ++++++++-- 2 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 packages/admin-next/src/components/resource-form.test.tsx diff --git a/packages/admin-next/src/components/resource-form.test.tsx b/packages/admin-next/src/components/resource-form.test.tsx new file mode 100644 index 0000000..ee3b466 --- /dev/null +++ b/packages/admin-next/src/components/resource-form.test.tsx @@ -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( + , + ); + + 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", + }); + }); + }); +}); diff --git a/packages/admin-next/src/components/resource-form.tsx b/packages/admin-next/src/components/resource-form.tsx index a2a68e5..da5d2e5 100644 --- a/packages/admin-next/src/components/resource-form.tsx +++ b/packages/admin-next/src/components/resource-form.tsx @@ -22,15 +22,58 @@ 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; + items?: SchemaNode; + dependencies?: Record; + 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(), + seen = new Set(), +): Set { + 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, schema: FormSchema, ): Record { - const properties = (schema.schema as { properties?: Record }) - .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 = {}; for (const [key, value] of Object.entries(data)) { if (allowed.has(key)) out[key] = value; @@ -38,12 +81,6 @@ function pickSchemaKeys( return out; } -type SchemaNode = { - type?: string | string[]; - properties?: Record; - items?: SchemaNode; -}; - function normalizeFormDataForSchema( data: Record, schema: FormSchema,