From 8c65cbce22628c75e20be850b4fc0f0ddd100f45 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:28:06 -0600 Subject: [PATCH] deep merge schema for advanced fields --- web/src/components/config-form/ConfigForm.tsx | 29 +++++++------ web/src/lib/utils.ts | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/web/src/components/config-form/ConfigForm.tsx b/web/src/components/config-form/ConfigForm.tsx index 2987ac34d..279e11214 100644 --- a/web/src/components/config-form/ConfigForm.tsx +++ b/web/src/components/config-form/ConfigForm.tsx @@ -8,7 +8,7 @@ import { transformSchema } from "@/lib/config-schema"; import { createErrorTransformer } from "@/lib/config-schema/errorMessages"; import { useMemo, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { cn } from "@/lib/utils"; +import { cn, mergeUiSchema } from "@/lib/utils"; export interface ConfigFormProps { /** JSON Schema for the form */ @@ -90,17 +90,22 @@ export function ConfigForm({ ); // Merge generated uiSchema with custom overrides - const finalUiSchema = useMemo( - () => ({ - ...generatedUiSchema, - "ui:groups": fieldGroups, - ...customUiSchema, - "ui:submitButtonOptions": showSubmit - ? { norender: false } - : { norender: true }, - }), - [generatedUiSchema, customUiSchema, showSubmit, fieldGroups], - ); + const finalUiSchema = useMemo(() => { + // Start with generated schema + const merged = mergeUiSchema(generatedUiSchema, customUiSchema); + + // Add field groups + if (fieldGroups) { + merged["ui:groups"] = fieldGroups; + } + + // Set submit button options + merged["ui:submitButtonOptions"] = showSubmit + ? { norender: false } + : { norender: true }; + + return merged; + }, [generatedUiSchema, customUiSchema, showSubmit, fieldGroups]); // Create error transformer for user-friendly error messages const errorTransformer = useMemo(() => createErrorTransformer(i18n), [i18n]); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 365058ceb..665bc9e37 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -1,6 +1,49 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import type { UiSchema } from "@rjsf/utils"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** + * Deep merges uiSchema objects, preserving nested properties from the base schema + * when overrides don't explicitly replace them. + * + * Special handling for ui:options - merges nested options rather than replacing them. + */ +export function mergeUiSchema( + base: UiSchema = {}, + overrides: UiSchema = {}, +): UiSchema { + const result: UiSchema = { ...base }; + + for (const [key, value] of Object.entries(overrides)) { + if ( + key === "ui:options" && + base[key] && + typeof value === "object" && + value !== null + ) { + // Merge ui:options objects instead of replacing + result[key] = { + ...(typeof base[key] === "object" && base[key] !== null + ? base[key] + : {}), + ...value, + }; + } else if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) + ) { + // Recursively merge nested objects (field configurations) + result[key] = mergeUiSchema(base[key] as UiSchema, value as UiSchema); + } else { + // Replace primitive values and arrays + result[key] = value; + } + } + + return result; +}