2026-01-23 17:23:52 +03:00
|
|
|
// ConfigForm - Main RJSF form wrapper component
|
|
|
|
|
import Form from "@rjsf/core";
|
|
|
|
|
import validator from "@rjsf/validator-ajv8";
|
|
|
|
|
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
|
|
|
|
|
import type { IChangeEvent } from "@rjsf/core";
|
|
|
|
|
import { frigateTheme } from "./theme";
|
|
|
|
|
import { transformSchema } from "@/lib/config-schema";
|
|
|
|
|
import { createErrorTransformer } from "@/lib/config-schema/errorMessages";
|
|
|
|
|
import { useMemo, useCallback, useState } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
|
|
|
|
export interface ConfigFormProps {
|
|
|
|
|
/** JSON Schema for the form */
|
|
|
|
|
schema: RJSFSchema;
|
|
|
|
|
/** Current form data */
|
2026-01-25 19:33:57 +03:00
|
|
|
formData?: unknown;
|
2026-01-23 17:23:52 +03:00
|
|
|
/** Called when form data changes */
|
2026-01-25 19:33:57 +03:00
|
|
|
onChange?: (data: unknown) => void;
|
2026-01-23 17:23:52 +03:00
|
|
|
/** Called when form is submitted */
|
2026-01-25 19:33:57 +03:00
|
|
|
onSubmit?: (data: unknown) => void;
|
2026-01-23 17:23:52 +03:00
|
|
|
/** Called when form has errors on submit */
|
|
|
|
|
onError?: (errors: unknown[]) => void;
|
|
|
|
|
/** Additional uiSchema overrides */
|
|
|
|
|
uiSchema?: UiSchema;
|
|
|
|
|
/** Field ordering */
|
|
|
|
|
fieldOrder?: string[];
|
2026-01-26 02:29:52 +03:00
|
|
|
/** Field groups for layout */
|
|
|
|
|
fieldGroups?: Record<string, string[]>;
|
2026-01-23 17:23:52 +03:00
|
|
|
/** Fields to hide */
|
|
|
|
|
hiddenFields?: string[];
|
|
|
|
|
/** Fields marked as advanced (collapsed by default) */
|
|
|
|
|
advancedFields?: string[];
|
|
|
|
|
/** Whether form is disabled */
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
/** Whether form is read-only */
|
|
|
|
|
readonly?: boolean;
|
|
|
|
|
/** Whether to show submit button */
|
|
|
|
|
showSubmit?: boolean;
|
|
|
|
|
/** Custom class name */
|
|
|
|
|
className?: string;
|
|
|
|
|
/** Live validation mode */
|
|
|
|
|
liveValidate?: boolean;
|
|
|
|
|
/** Form context passed to all widgets */
|
|
|
|
|
formContext?: Record<string, unknown>;
|
2026-01-23 19:04:50 +03:00
|
|
|
/** i18n namespace for field labels */
|
|
|
|
|
i18nNamespace?: string;
|
2026-01-23 17:23:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ConfigForm({
|
|
|
|
|
schema,
|
|
|
|
|
formData,
|
|
|
|
|
onChange,
|
|
|
|
|
onSubmit,
|
|
|
|
|
onError,
|
|
|
|
|
uiSchema: customUiSchema,
|
|
|
|
|
fieldOrder,
|
2026-01-26 02:29:52 +03:00
|
|
|
fieldGroups,
|
2026-01-23 17:23:52 +03:00
|
|
|
hiddenFields,
|
|
|
|
|
advancedFields,
|
|
|
|
|
disabled = false,
|
|
|
|
|
readonly = false,
|
|
|
|
|
showSubmit = true,
|
|
|
|
|
className,
|
|
|
|
|
liveValidate = false,
|
|
|
|
|
formContext,
|
2026-01-23 19:04:50 +03:00
|
|
|
i18nNamespace,
|
2026-01-23 17:23:52 +03:00
|
|
|
}: ConfigFormProps) {
|
2026-01-23 19:04:50 +03:00
|
|
|
const { t } = useTranslation([i18nNamespace || "common", "views/settings"]);
|
2026-01-23 17:23:52 +03:00
|
|
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Determine which fields to hide based on advanced toggle
|
|
|
|
|
const effectiveHiddenFields = useMemo(() => {
|
|
|
|
|
if (showAdvanced || !advancedFields || advancedFields.length === 0) {
|
|
|
|
|
return hiddenFields;
|
|
|
|
|
}
|
|
|
|
|
// Hide advanced fields when toggle is off
|
|
|
|
|
return [...(hiddenFields || []), ...advancedFields];
|
|
|
|
|
}, [hiddenFields, advancedFields, showAdvanced]);
|
|
|
|
|
|
|
|
|
|
// Transform schema and generate uiSchema
|
|
|
|
|
const { schema: transformedSchema, uiSchema: generatedUiSchema } = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
transformSchema(schema, {
|
|
|
|
|
fieldOrder,
|
|
|
|
|
hiddenFields: effectiveHiddenFields,
|
|
|
|
|
advancedFields: showAdvanced ? advancedFields : [],
|
2026-01-23 19:04:50 +03:00
|
|
|
i18nNamespace,
|
2026-01-23 17:23:52 +03:00
|
|
|
}),
|
2026-01-23 19:04:50 +03:00
|
|
|
[
|
|
|
|
|
schema,
|
|
|
|
|
fieldOrder,
|
|
|
|
|
effectiveHiddenFields,
|
|
|
|
|
advancedFields,
|
|
|
|
|
showAdvanced,
|
|
|
|
|
i18nNamespace,
|
|
|
|
|
],
|
2026-01-23 17:23:52 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Merge generated uiSchema with custom overrides
|
|
|
|
|
const finalUiSchema = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...generatedUiSchema,
|
2026-01-26 02:29:52 +03:00
|
|
|
"ui:groups": fieldGroups,
|
2026-01-23 17:23:52 +03:00
|
|
|
...customUiSchema,
|
|
|
|
|
"ui:submitButtonOptions": showSubmit
|
|
|
|
|
? { norender: false }
|
|
|
|
|
: { norender: true },
|
|
|
|
|
}),
|
2026-01-26 02:29:52 +03:00
|
|
|
[generatedUiSchema, customUiSchema, showSubmit, fieldGroups],
|
2026-01-23 17:23:52 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create error transformer for user-friendly error messages
|
|
|
|
|
const errorTransformer = useMemo(() => createErrorTransformer(), []);
|
|
|
|
|
|
|
|
|
|
const handleChange = useCallback(
|
|
|
|
|
(e: IChangeEvent) => {
|
|
|
|
|
onChange?.(e.formData);
|
|
|
|
|
},
|
|
|
|
|
[onChange],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleSubmit = useCallback(
|
|
|
|
|
(e: IChangeEvent) => {
|
|
|
|
|
onSubmit?.(e.formData);
|
|
|
|
|
},
|
|
|
|
|
[onSubmit],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hasAdvancedFields = advancedFields && advancedFields.length > 0;
|
|
|
|
|
|
2026-01-23 19:04:50 +03:00
|
|
|
// Extended form context with i18n info
|
|
|
|
|
const extendedFormContext = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...formContext,
|
|
|
|
|
i18nNamespace,
|
|
|
|
|
t,
|
|
|
|
|
}),
|
|
|
|
|
[formContext, i18nNamespace, t],
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-23 17:23:52 +03:00
|
|
|
return (
|
|
|
|
|
<div className={cn("config-form", className)}>
|
|
|
|
|
{hasAdvancedFields && (
|
|
|
|
|
<div className="mb-4 flex items-center justify-end gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="show-advanced"
|
|
|
|
|
checked={showAdvanced}
|
|
|
|
|
onCheckedChange={setShowAdvanced}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="show-advanced"
|
|
|
|
|
className="cursor-pointer text-sm text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
{t("configForm.showAdvanced", {
|
2026-01-23 19:04:50 +03:00
|
|
|
ns: "views/settings",
|
2026-01-23 17:23:52 +03:00
|
|
|
defaultValue: "Show Advanced Settings",
|
|
|
|
|
})}
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<Form
|
|
|
|
|
schema={transformedSchema}
|
|
|
|
|
uiSchema={finalUiSchema}
|
|
|
|
|
formData={formData}
|
|
|
|
|
validator={validator}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
onError={onError}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
readonly={readonly}
|
|
|
|
|
liveValidate={liveValidate}
|
2026-01-23 19:04:50 +03:00
|
|
|
formContext={extendedFormContext}
|
2026-01-23 17:23:52 +03:00
|
|
|
transformErrors={errorTransformer}
|
|
|
|
|
{...frigateTheme}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default ConfigForm;
|