diff --git a/src/components/form/form.test.tsx b/src/components/form/form.test.tsx index a0167bb..80656c8 100644 --- a/src/components/form/form.test.tsx +++ b/src/components/form/form.test.tsx @@ -1,11 +1,13 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react"; -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { describe, expect, it, vi, beforeEach, type Mock } from "vitest"; import { BrowserRouter } from "react-router-dom"; import { Form } from "./form"; import { ResourceSchema, PropertySchema } from "@/state/openapi"; +import { Schema } from "@aep_dev/aep-lib-ts"; import { ResourceInstance } from "@/state/fetch"; import fs from "fs"; import { parseOpenAPI } from "@/state/openapi"; +import "@testing-library/jest-dom/vitest"; // Mock ResourceSchema for testing different property types const createMockResourceSchema = ( @@ -1206,4 +1208,49 @@ describe("Form", () => { }); }); }); + + describe("ReadOnly properties", () => { + it("does not render readOnly properties in the form", () => { + const properties = [ + new PropertySchema("id", "string", { + readOnly: true, + } as unknown as Schema), + new PropertySchema("name", "string"), + ]; + const resource = createMockResourceSchema(properties); + renderForm(resource); + + expect(screen.queryByLabelText("id")).not.toBeInTheDocument(); + expect(screen.getByLabelText("name")).toBeInTheDocument(); + }); + + it("excludes readOnly properties from the validation schema and submission", async () => { + const properties = [ + new PropertySchema("id", "string", { + readOnly: true, + } as unknown as Schema), + new PropertySchema("name", "string"), + ]; + const resource = createMockResourceSchema(properties); + renderForm(resource); + + fireEvent.change(screen.getByLabelText("name"), { + target: { value: "Test Name" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Submit" })); + + await waitFor(() => { + expect(resource.create).toHaveBeenCalledWith( + { + name: "Test Name", + }, + "", + ); + }); + + // Ensure id was NOT included + const call = (resource.create as Mock).mock.calls[0]; + expect(call[0]).not.toHaveProperty("id"); + }); + }); }); diff --git a/src/components/form/form.tsx b/src/components/form/form.tsx index 349a3e4..330e29b 100644 --- a/src/components/form/form.tsx +++ b/src/components/form/form.tsx @@ -32,7 +32,7 @@ type FormProps = { onError: (error: unknown) => void; // Current resource state used to fill in the form's default values for updating (optional) resourceInstance?: ResourceInstance; - onSubmitOperation: (value: Record) => Promise; + onSubmitOperation: (value: Record) => Promise; }; // Form is responsible for rendering a form based on the resource schema. @@ -142,6 +142,10 @@ export function Form(props: FormProps) { return ; } + if (p.readOnly) { + return null; + } + const fieldPath = parentPath ? `${parentPath}.${p.name}` : p.name; if (p.type === "object") { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0880567..88daf0f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -40,7 +40,7 @@ export function createValidationSchema( const schemaObject: Record = {}; for (const property of properties) { - if (!property) continue; // Skip null properties + if (!property || property.readOnly) continue; // Skip null or readOnly properties let fieldSchema: z.ZodTypeAny; const isRequired = requiredFields.includes(property.name); @@ -105,6 +105,7 @@ export function createValidationSchemaFromRawSchema( string, Record, ][]) { + if (propSchema.readOnly) continue; // Skip readOnly properties const isFieldRequired = required.includes(name); let fieldSchema: z.ZodTypeAny; diff --git a/src/state/openapi.ts b/src/state/openapi.ts index 41f9391..f29112f 100644 --- a/src/state/openapi.ts +++ b/src/state/openapi.ts @@ -68,11 +68,11 @@ class ResourceSchema { return Get(url, this, headers); } - create(body: object, headers: string = ""): Promise { + create(body: Record, headers: string = ""): Promise { const baseUrl = this.base_url(); let url = `${this.server_url}${baseUrl}`; if (this.properties().find((prop) => prop.name === "id")) { - url += `?id=${body.id}`; + url += `?id=${(body as Record).id}`; } url = this.substituteUrlParameters(url); return Create(url, body, headers); @@ -124,7 +124,7 @@ class PropertySchema { type: string; schema: Schema; - constructor(name: string, type: string, schema: Schema) { + constructor(name: string, type: string, schema: Schema = {}) { this.name = name; this.type = type; this.schema = schema; @@ -149,6 +149,10 @@ class PropertySchema { } return []; } + + get readOnly(): boolean { + return !!(this.schema as Record)?.readOnly; + } } // Adapter class that wraps aep-lib-ts APIClient with UI-specific functionality