add custom validation and use it for ffmpeg input roles

This commit is contained in:
Josh Hawkins 2026-02-01 12:45:16 -06:00
parent 718757fc60
commit 09ceff9bd8
5 changed files with 685 additions and 574 deletions

View File

@ -16,5 +16,6 @@
"format": "Invalid format",
"additionalProperties": "Unknown property is not allowed",
"oneOf": "Must match exactly one of the allowed schemas",
"anyOf": "Must match at least one of the allowed schemas"
"anyOf": "Must match at least one of the allowed schemas",
"ffmpeg.inputs.rolesUnique": "Each role can only be assigned to one input stream."
}

View File

@ -1,7 +1,7 @@
// ConfigForm - Main RJSF form wrapper component
import Form from "@rjsf/shadcn";
import validator from "@rjsf/validator-ajv8";
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
import type { FormValidation, RJSFSchema, UiSchema } from "@rjsf/utils";
import type { IChangeEvent } from "@rjsf/core";
import { frigateTheme } from "./theme";
import { transformSchema } from "@/lib/config-schema";
@ -182,6 +182,11 @@ export interface ConfigFormProps {
formContext?: ConfigFormContext;
/** i18n namespace for field labels */
i18nNamespace?: string;
/** Optional custom validation */
customValidate?: (
formData: unknown,
errors: FormValidation,
) => FormValidation;
}
export function ConfigForm({
@ -202,6 +207,7 @@ export function ConfigForm({
liveValidate = true,
formContext,
i18nNamespace,
customValidate,
}: ConfigFormProps) {
const { t, i18n } = useTranslation([
i18nNamespace || "common",
@ -319,6 +325,7 @@ export function ConfigForm({
liveValidate={liveValidate}
formContext={extendedFormContext}
transformErrors={errorTransformer}
customValidate={customValidate}
{...frigateTheme}
/>
</div>

View File

@ -0,0 +1,47 @@
import type { FormValidation } from "@rjsf/utils";
import type { TFunction } from "i18next";
import { isJsonObject } from "@/lib/utils";
import type { JsonObject } from "@/types/configForm";
export function validateFfmpegInputRoles(
formData: unknown,
errors: FormValidation,
t: TFunction,
): FormValidation {
if (!isJsonObject(formData as JsonObject)) {
return errors;
}
const inputs = (formData as JsonObject).inputs;
if (!Array.isArray(inputs)) {
return errors;
}
const roleCounts = new Map<string, number>();
inputs.forEach((input) => {
if (!isJsonObject(input) || !Array.isArray(input.roles)) {
return;
}
input.roles.forEach((role) => {
if (typeof role !== "string") {
return;
}
roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
});
});
const hasDuplicates = Array.from(roleCounts.values()).some(
(count) => count > 1,
);
if (hasDuplicates) {
const inputsErrors = errors.inputs as {
addError?: (message: string) => void;
};
inputsErrors?.addError?.(
t("ffmpeg.inputs.rolesUnique", { ns: "config/validation" }),
);
}
return errors;
}

View File

@ -0,0 +1,26 @@
import type { FormValidation } from "@rjsf/utils";
import type { TFunction } from "i18next";
import { validateFfmpegInputRoles } from "./ffmpeg";
export type SectionValidation = (
formData: unknown,
errors: FormValidation,
) => FormValidation;
type SectionValidationOptions = {
sectionPath: string;
level: "global" | "camera";
t: TFunction;
};
export function getSectionValidation({
sectionPath,
level,
t,
}: SectionValidationOptions): SectionValidation | undefined {
if (sectionPath === "ffmpeg" && level === "camera") {
return (formData, errors) => validateFfmpegInputRoles(formData, errors, t);
}
return undefined;
}

File diff suppressed because it is too large Load Diff