feat(frontend): add CustomSchemaField wrapper for dynamic form field routing

(#11722)

### Changes 🏗️

This PR introduces automatic UI schema generation for custom form
fields, eliminating manual field mapping.

#### 1. **generateUiSchemaForCustomFields Utility**
(`generate-ui-schema.ts`) - New File
   - Auto-generates `ui:field` settings for custom fields
   - Detects custom fields using `findCustomFieldId()` matcher
   - Handles nested objects and array items recursively
   - Merges with existing UI schema without overwriting

#### 2. **FormRenderer Integration** (`FormRenderer.tsx`)
   - Imports and uses `generateUiSchemaForCustomFields`
   - Creates merged UI schema with `useMemo`
   - Passes merged schema to Form component
   - Enables automatic custom field detection

#### 3. **Preprocessor Cleanup** (`input-schema-pre-processor.ts`)
   - Removed manual `$id` assignment for custom fields
   - Removed unused `findCustomFieldId` import
   - Simplified to focus only on type validation

### Why these changes?

- Custom fields now auto-detect without manual `ui:field` configuration
- Uses standard RJSF approach (UI schema) for field routing
- Centralized custom field detection logic improves maintainability

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Verify custom fields render correctly when present in schema
- [x] Verify standard fields continue to render with default SchemaField
- [x] Verify multiple instances of same custom field type have unique
IDs
  - [x] Test form submission with custom fields

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved custom field rendering in forms by optimizing the UI schema
generation process.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Abhimanyu Yadav
2026-01-08 14:17:52 +05:30
committed by GitHub
parent 103a62c9da
commit 5e2146dd76
3 changed files with 78 additions and 8 deletions

View File

@@ -4,6 +4,7 @@ import { useMemo } from "react";
import { customValidator } from "./utils/custom-validator";
import Form from "./registry";
import { ExtendedFormContextType } from "./types";
import { generateUiSchemaForCustomFields } from "./utils/generate-ui-schema";
type FormRendererProps = {
jsonSchema: RJSFSchema;
@@ -24,6 +25,11 @@ export const FormRenderer = ({
return preprocessInputSchema(jsonSchema);
}, [jsonSchema]);
// Merge custom field ui:field settings with existing uiSchema
const mergedUiSchema = useMemo(() => {
return generateUiSchemaForCustomFields(preprocessedSchema, uiSchema);
}, [preprocessedSchema, uiSchema]);
return (
<div className={"mb-6 mt-4"}>
<Form
@@ -33,7 +39,7 @@ export const FormRenderer = ({
schema={preprocessedSchema}
validator={customValidator}
onChange={handleChange}
uiSchema={uiSchema}
uiSchema={mergedUiSchema}
formData={initialValues}
liveValidate={false}
/>

View File

@@ -0,0 +1,71 @@
import { RJSFSchema, UiSchema } from "@rjsf/utils";
import { findCustomFieldId } from "../custom/custom-registry";
/**
* Generates uiSchema with ui:field settings for custom fields based on schema matchers.
* This is the standard RJSF way to route fields to custom components.
*/
export function generateUiSchemaForCustomFields(
schema: RJSFSchema,
existingUiSchema: UiSchema = {},
): UiSchema {
const uiSchema: UiSchema = { ...existingUiSchema };
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (propSchema && typeof propSchema === "object") {
const customFieldId = findCustomFieldId(propSchema);
if (customFieldId) {
uiSchema[key] = {
...(uiSchema[key] as object),
"ui:field": customFieldId,
};
}
if (
propSchema.type === "object" &&
propSchema.properties &&
typeof propSchema.properties === "object"
) {
const nestedUiSchema = generateUiSchemaForCustomFields(
propSchema as RJSFSchema,
(uiSchema[key] as UiSchema) || {},
);
uiSchema[key] = {
...(uiSchema[key] as object),
...nestedUiSchema,
};
}
if (propSchema.type === "array" && propSchema.items) {
const itemsSchema = propSchema.items as RJSFSchema;
if (itemsSchema && typeof itemsSchema === "object") {
const itemsCustomFieldId = findCustomFieldId(itemsSchema);
if (itemsCustomFieldId) {
uiSchema[key] = {
...(uiSchema[key] as object),
items: {
"ui:field": itemsCustomFieldId,
},
};
} else if (itemsSchema.properties) {
const itemsUiSchema = generateUiSchemaForCustomFields(
itemsSchema,
((uiSchema[key] as UiSchema)?.items as UiSchema) || {},
);
if (Object.keys(itemsUiSchema).length > 0) {
uiSchema[key] = {
...(uiSchema[key] as object),
items: itemsUiSchema,
};
}
}
}
}
}
}
}
return uiSchema;
}

View File

@@ -1,5 +1,4 @@
import { RJSFSchema } from "@rjsf/utils";
import { findCustomFieldId } from "../custom/custom-registry";
/**
* Pre-processes the input schema to ensure all properties have a type defined.
@@ -21,12 +20,6 @@ export function preprocessInputSchema(schema: RJSFSchema): RJSFSchema {
if (property && typeof property === "object") {
const processedProperty = { ...property };
// adding $id for custom field
const customFieldId = findCustomFieldId(processedProperty);
if (customFieldId) {
processedProperty.$id = customFieldId;
}
// Only add type if no type is defined AND no anyOf/oneOf/allOf is present
if (
!processedProperty.type &&