feat: enum support in dynamic json form
This commit is contained in:
parent
3a121bc8fc
commit
efd5f8a17c
2 changed files with 110 additions and 10 deletions
|
|
@ -15,10 +15,10 @@ interface DynamicJsonFormProps {
|
|||
|
||||
const isSimpleObject = (schema: JsonSchemaType): boolean => {
|
||||
const supportedTypes = ["string", "number", "integer", "boolean", "null"];
|
||||
if (supportedTypes.includes(schema.type)) return true;
|
||||
if (schema.type && supportedTypes.includes(schema.type)) return true;
|
||||
if (schema.type === "object") {
|
||||
return Object.values(schema.properties ?? {}).every((prop) =>
|
||||
supportedTypes.includes(prop.type),
|
||||
return Object.values(schema.properties ?? {}).every(
|
||||
(prop) => prop.type && supportedTypes.includes(prop.type),
|
||||
);
|
||||
}
|
||||
if (schema.type === "array") {
|
||||
|
|
@ -178,6 +178,41 @@ const DynamicJsonForm = ({
|
|||
|
||||
switch (propSchema.type) {
|
||||
case "string": {
|
||||
if (
|
||||
propSchema.oneOf &&
|
||||
propSchema.oneOf.every(
|
||||
(option) =>
|
||||
typeof option.const === "string" &&
|
||||
typeof option.title === "string",
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<select
|
||||
value={(currentValue as string) ?? ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (!val && !isRequired) {
|
||||
handleFieldChange(path, undefined);
|
||||
} else {
|
||||
handleFieldChange(path, val);
|
||||
}
|
||||
}}
|
||||
required={isRequired}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<option value="">Select an option...</option>
|
||||
{propSchema.oneOf.map((option) => (
|
||||
<option
|
||||
key={option.const as string}
|
||||
value={option.const as string}
|
||||
>
|
||||
{option.title as string}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
if (propSchema.enum) {
|
||||
return (
|
||||
<select
|
||||
|
|
@ -194,9 +229,9 @@ const DynamicJsonForm = ({
|
|||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
|
||||
>
|
||||
<option value="">Select an option...</option>
|
||||
{propSchema.enum.map((option, index) => (
|
||||
{propSchema.enum.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{propSchema.enumNames?.[index] || option}
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -233,6 +268,9 @@ const DynamicJsonForm = ({
|
|||
}}
|
||||
placeholder={propSchema.description}
|
||||
required={isRequired}
|
||||
minLength={propSchema.minLength}
|
||||
maxLength={propSchema.maxLength}
|
||||
pattern={propSchema.pattern}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -255,6 +293,8 @@ const DynamicJsonForm = ({
|
|||
}}
|
||||
placeholder={propSchema.description}
|
||||
required={isRequired}
|
||||
min={propSchema.minimum}
|
||||
max={propSchema.maximum}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -277,6 +317,8 @@ const DynamicJsonForm = ({
|
|||
}}
|
||||
placeholder={propSchema.description}
|
||||
required={isRequired}
|
||||
min={propSchema.minimum}
|
||||
max={propSchema.maximum}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -103,11 +103,13 @@ describe("DynamicJsonForm String Fields", () => {
|
|||
expect(options).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("should use enumNames for option labels", () => {
|
||||
it("should use oneOf with const and title for labeled options", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "string",
|
||||
enum: ["val1", "val2"],
|
||||
enumNames: ["Label 1", "Label 2"],
|
||||
oneOf: [
|
||||
{ const: "val1", title: "Label 1" },
|
||||
{ const: "val2", title: "Label 2" },
|
||||
],
|
||||
description: "Select with labels",
|
||||
};
|
||||
render(<DynamicJsonForm schema={schema} value="" onChange={jest.fn()} />);
|
||||
|
|
@ -117,6 +119,24 @@ describe("DynamicJsonForm String Fields", () => {
|
|||
expect(options[2]).toHaveProperty("textContent", "Label 2");
|
||||
});
|
||||
|
||||
it("should call onChange with selected oneOf value", () => {
|
||||
const onChange = jest.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "string",
|
||||
oneOf: [
|
||||
{ const: "option1", title: "Option 1" },
|
||||
{ const: "option2", title: "Option 2" },
|
||||
],
|
||||
description: "Select an option",
|
||||
};
|
||||
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
|
||||
|
||||
const select = screen.getByRole("combobox");
|
||||
fireEvent.change(select, { target: { value: "option1" } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("option1");
|
||||
});
|
||||
|
||||
it("should call onChange with selected enum value", () => {
|
||||
const onChange = jest.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
|
|
@ -131,6 +151,44 @@ describe("DynamicJsonForm String Fields", () => {
|
|||
|
||||
expect(onChange).toHaveBeenCalledWith("option1");
|
||||
});
|
||||
|
||||
it("should render JSON Schema spec compliant oneOf with const for labeled enums", () => {
|
||||
// Example from JSON Schema spec: labeled enums using oneOf with const
|
||||
const onChange = jest.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "string",
|
||||
title: "Traffic Light",
|
||||
description: "Select a traffic light color",
|
||||
oneOf: [
|
||||
{ const: "red", title: "Stop" },
|
||||
{ const: "amber", title: "Caution" },
|
||||
{ const: "green", title: "Go" },
|
||||
],
|
||||
};
|
||||
render(<DynamicJsonForm schema={schema} value="" onChange={onChange} />);
|
||||
|
||||
// Should render as a select dropdown
|
||||
const select = screen.getByRole("combobox");
|
||||
expect(select.tagName).toBe("SELECT");
|
||||
|
||||
// Should have options with proper labels
|
||||
const options = screen.getAllByRole("option");
|
||||
expect(options).toHaveLength(4); // 3 options + 1 default "Select an option..."
|
||||
|
||||
expect(options[0]).toHaveProperty("textContent", "Select an option...");
|
||||
expect(options[1]).toHaveProperty("textContent", "Stop");
|
||||
expect(options[2]).toHaveProperty("textContent", "Caution");
|
||||
expect(options[3]).toHaveProperty("textContent", "Go");
|
||||
|
||||
// Should have proper values
|
||||
expect(options[1]).toHaveProperty("value", "red");
|
||||
expect(options[2]).toHaveProperty("value", "amber");
|
||||
expect(options[3]).toHaveProperty("value", "green");
|
||||
|
||||
// Test onChange behavior
|
||||
fireEvent.change(select, { target: { value: "amber" } });
|
||||
expect(onChange).toHaveBeenCalledWith("amber");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation Attributes", () => {
|
||||
|
|
@ -464,8 +522,8 @@ describe("DynamicJsonForm Object Fields", () => {
|
|||
<DynamicJsonForm schema={schema} value={{}} onChange={jest.fn()} />,
|
||||
);
|
||||
|
||||
const nameLabel = screen.getByText("Name");
|
||||
const optionalLabel = screen.getByText("Optional");
|
||||
const nameLabel = screen.getByText("name");
|
||||
const optionalLabel = screen.getByText("optional");
|
||||
|
||||
const nameInput = nameLabel.closest("div")?.querySelector("input");
|
||||
const optionalInput = optionalLabel
|
||||
|
|
|
|||
Loading…
Reference in a new issue