// Field Template - wraps each form field with label and description import { FieldTemplateProps, StrictRJSFSchema, UiSchema } from "@rjsf/utils"; import { getTemplate, getUiOptions, ADDITIONAL_PROPERTY_FLAG, } from "@rjsf/utils"; import { ComponentType, ReactNode } from "react"; import { isValidElement } from "react"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { isNullableUnionSchema } from "../fields/nullableUtils"; import { getTranslatedLabel } from "@/utils/i18n"; import { ConfigFormContext } from "@/types/configForm"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { requiresRestartForFieldPath } from "@/utils/configUtil"; import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator"; import { buildTranslationPath, getFilterObjectLabel, hasOverrideAtPath, humanizeKey, normalizeFieldValue, } from "../utils"; import { normalizeOverridePath } from "../utils/overrides"; import get from "lodash/get"; import isEqual from "lodash/isEqual"; function _isArrayItemInAdditionalProperty( pathSegments: Array, ): boolean { // // If we find a numeric index, this is an array item for (let i = 0; i < pathSegments.length; i++) { const segment = pathSegments[i]; if (typeof segment === "number") { // Consider any array item as being inside additional properties if it's not at the root level return i > 0; } } return false; } type FieldRenderSpec = | ReactNode | ComponentType | { render: string; props?: Record; }; export function FieldTemplate(props: FieldTemplateProps) { const { id, label, children, classNames, style, errors, help, description, hidden, required, displayLabel, schema, uiSchema, registry, fieldPathId, onKeyRename, onKeyRenameBlur, onRemoveProperty, rawDescription, rawErrors, formData: fieldFormData, disabled, readonly, } = props; // Get i18n namespace from form context (passed through registry) const formContext = registry?.formContext as ConfigFormContext | undefined; const i18nNamespace = formContext?.i18nNamespace as string | undefined; const sectionI18nPrefix = formContext?.sectionI18nPrefix as | string | undefined; const isCameraLevel = formContext?.level === "camera"; const effectiveNamespace = isCameraLevel ? "config/cameras" : i18nNamespace; const { t, i18n } = useTranslation([ effectiveNamespace || i18nNamespace || "common", i18nNamespace || "common", "views/settings", ]); const { getLocaleDocUrl } = useDocDomain(); if (hidden) { return
{children}
; } // Get UI options const uiOptionsFromSchema = uiSchema?.["ui:options"] || {}; const suppressDescription = uiOptionsFromSchema.suppressDescription === true; // Determine field characteristics const isBoolean = schema.type === "boolean" || (Array.isArray(schema.type) && schema.type.includes("boolean")); const isObjectField = schema.type === "object"; const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema); const isAdditionalProperty = ADDITIONAL_PROPERTY_FLAG in schema; const suppressMultiSchema = (uiSchema?.["ui:options"] as UiSchema["ui:options"] | undefined) ?.suppressMultiSchema === true; const schemaTypes = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : []; const nonNullSchemaTypes = schemaTypes.filter((type) => type !== "null"); const isScalarValueField = nonNullSchemaTypes.length === 1 && ["string", "number", "integer"].includes(nonNullSchemaTypes[0]); // Only suppress labels/descriptions if this is a multi-schema field (anyOf/oneOf) with suppressMultiSchema flag // This prevents duplicate labels while still showing the inner field's label const isMultiSchemaWrapper = (schema.anyOf || schema.oneOf) && (suppressMultiSchema || isNullableUnion); const useSplitBooleanLayout = uiOptionsFromSchema.splitLayout !== false && isBoolean && !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty; const useSplitLayout = uiOptionsFromSchema.splitLayout !== false && isScalarValueField && !isBoolean && !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty; // Get translation path for this field const pathSegments = fieldPathId.path.filter( (segment): segment is string => typeof segment === "string", ); // Check if this is an array item inside an object with additionalProperties const isArrayItemInAdditionalProp = _isArrayItemInAdditionalProperty( fieldPathId.path, ); // Conditions for showing descriptions/docs links const shouldShowDescription = !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty && !isArrayItemInAdditionalProp && !suppressDescription; const translationPath = buildTranslationPath( pathSegments, sectionI18nPrefix, formContext, ); const fieldPath = fieldPathId.path; const overrides = formContext?.overrides; const baselineFormData = formContext?.baselineFormData; const normalizedFieldPath = normalizeOverridePath( fieldPath, formContext?.formData, ); let baselineValue = baselineFormData ? get(baselineFormData, normalizedFieldPath) : undefined; if (baselineValue === undefined || baselineValue === null) { if (schema.default !== undefined && schema.default !== null) { baselineValue = schema.default; } } const isBaselineModified = baselineFormData !== undefined && !isEqual( normalizeFieldValue(fieldFormData), normalizeFieldValue(baselineValue), ); const isModified = baselineFormData ? isBaselineModified : hasOverrideAtPath(overrides, fieldPath, formContext?.formData); const filterObjectLabel = getFilterObjectLabel(pathSegments); const translatedFilterObjectLabel = filterObjectLabel ? getTranslatedLabel(filterObjectLabel, "object") : undefined; const fieldDocsKey = translationPath || pathSegments.join("."); const fieldDocsPath = fieldDocsKey ? formContext?.fieldDocs?.[fieldDocsKey] : undefined; const fieldDocsUrl = fieldDocsPath ? getLocaleDocUrl(fieldDocsPath) : undefined; const restartRequired = formContext?.restartRequired; const defaultRequiresRestart = formContext?.requiresRestart ?? true; const fieldRequiresRestart = requiresRestartForFieldPath( normalizedFieldPath, restartRequired, defaultRequiresRestart, ); // Use schema title/description as primary source (from JSON Schema) const schemaTitle = schema.title; const schemaDescription = schema.description; // Try to get translated label, falling back to schema title, then RJSF label let finalLabel = label; if (effectiveNamespace && translationPath) { // Prefer camera-scoped translations when a section prefix is provided const prefixedTranslationKey = sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`) ? `${sectionI18nPrefix}.${translationPath}.label` : undefined; const translationKey = `${translationPath}.label`; if ( prefixedTranslationKey && i18n.exists(prefixedTranslationKey, { ns: effectiveNamespace }) ) { finalLabel = t(prefixedTranslationKey, { ns: effectiveNamespace }); } else if (i18n.exists(translationKey, { ns: effectiveNamespace })) { finalLabel = t(translationKey, { ns: effectiveNamespace }); } else if (schemaTitle) { finalLabel = schemaTitle; } else if (translatedFilterObjectLabel) { const filtersIndex = pathSegments.indexOf("filters"); const isFilterObjectField = filtersIndex > -1 && pathSegments.length === filtersIndex + 2; if (isFilterObjectField) { finalLabel = translatedFilterObjectLabel; } else { // Try to get translated field label, fall back to humanized const fieldName = pathSegments[pathSegments.length - 1] || ""; let fieldLabel = schemaTitle; if (!fieldLabel) { const fieldTranslationKey = `${fieldName}.label`; const prefixedFieldTranslationKey = sectionI18nPrefix && !fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`) ? `${sectionI18nPrefix}.${fieldTranslationKey}` : undefined; if ( prefixedFieldTranslationKey && effectiveNamespace && i18n.exists(prefixedFieldTranslationKey, { ns: effectiveNamespace }) ) { fieldLabel = t(prefixedFieldTranslationKey, { ns: effectiveNamespace, }); } else if ( effectiveNamespace && i18n.exists(fieldTranslationKey, { ns: effectiveNamespace }) ) { fieldLabel = t(fieldTranslationKey, { ns: effectiveNamespace }); } else { fieldLabel = humanizeKey(fieldName); } } if (fieldLabel) { finalLabel = t("configForm.filters.objectFieldLabel", { ns: "views/settings", field: fieldLabel, label: translatedFilterObjectLabel, }); } } } } else if (schemaTitle) { finalLabel = schemaTitle; } else if (translatedFilterObjectLabel) { const filtersIndex = pathSegments.indexOf("filters"); const isFilterObjectField = filtersIndex > -1 && pathSegments.length === filtersIndex + 2; if (isFilterObjectField) { finalLabel = translatedFilterObjectLabel; } else { // Try to get translated field label, fall back to humanized const fieldName = pathSegments[pathSegments.length - 1] || ""; let fieldLabel = schemaTitle; if (!fieldLabel) { const fieldTranslationKey = `${fieldName}.label`; const prefixedFieldTranslationKey = sectionI18nPrefix && !fieldTranslationKey.startsWith(`${sectionI18nPrefix}.`) ? `${sectionI18nPrefix}.${fieldTranslationKey}` : undefined; if ( prefixedFieldTranslationKey && effectiveNamespace && i18n.exists(prefixedFieldTranslationKey, { ns: effectiveNamespace }) ) { fieldLabel = t(prefixedFieldTranslationKey, { ns: effectiveNamespace, }); } else if ( effectiveNamespace && i18n.exists(fieldTranslationKey, { ns: effectiveNamespace }) ) { fieldLabel = t(fieldTranslationKey, { ns: effectiveNamespace }); } else { fieldLabel = humanizeKey(fieldName); } } if (fieldLabel) { finalLabel = t("configForm.filters.objectFieldLabel", { ns: "views/settings", field: fieldLabel, label: translatedFilterObjectLabel, }); } } } // Try to get translated description, falling back to schema description let finalDescription = description || ""; if (effectiveNamespace && translationPath) { const prefixedDescriptionKey = sectionI18nPrefix && !translationPath.startsWith(`${sectionI18nPrefix}.`) ? `${sectionI18nPrefix}.${translationPath}.description` : undefined; const descriptionKey = `${translationPath}.description`; if ( prefixedDescriptionKey && i18n.exists(prefixedDescriptionKey, { ns: effectiveNamespace }) ) { finalDescription = t(prefixedDescriptionKey, { ns: effectiveNamespace }); } else if (i18n.exists(descriptionKey, { ns: effectiveNamespace })) { finalDescription = t(descriptionKey, { ns: effectiveNamespace }); } else if (schemaDescription) { finalDescription = schemaDescription; } } else if (schemaDescription) { finalDescription = schemaDescription; } const uiOptions = getUiOptions(uiSchema); const beforeSpec = uiSchema?.["ui:before"] as FieldRenderSpec | undefined; const afterSpec = uiSchema?.["ui:after"] as FieldRenderSpec | undefined; const renderCustom = (spec: FieldRenderSpec | undefined) => { if (spec === undefined || spec === null) { return null; } if (isValidElement(spec) || typeof spec === "string") { return spec; } if (typeof spec === "number") { return {spec}; } if (typeof spec === "function") { const SpecComponent = spec as ComponentType; return ; } if (typeof spec === "object" && "render" in spec) { const renderKey = spec.render; const renderers = formContext?.renderers; const RenderComponent = renderers?.[renderKey]; if (RenderComponent) { return ( ); } } return null; }; const beforeContent = renderCustom(beforeSpec); const afterContent = renderCustom(afterSpec); const WrapIfAdditionalTemplate = getTemplate( "WrapIfAdditionalTemplate", registry, uiOptions, ); const shouldRenderStandardLabel = displayLabel && finalLabel && !isBoolean && !useSplitLayout && !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty; const shouldRenderSplitLabel = displayLabel && finalLabel && !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty; const shouldRenderBooleanLabel = displayLabel && finalLabel; const renderDocsLink = (className?: string) => { if (!fieldDocsUrl || !shouldShowDescription) { return null; } return (
{t("readTheDocumentation", { ns: "common" })}
); }; const renderDescription = (className?: string) => { if (!finalDescription || !shouldShowDescription) { return null; } return (

{finalDescription}

); }; const renderStandardLabel = () => { if (!shouldRenderStandardLabel) { return null; } return ( ); }; const renderBooleanLabel = () => { if (!shouldRenderBooleanLabel) { return null; } return ( ); }; const renderSplitLabel = () => { if (!shouldRenderSplitLabel) { return null; } return ( ); }; const renderBooleanSplitLayout = () => ( <>
{renderBooleanLabel()}
{children}
{renderDescription()} {renderDocsLink()}
{renderBooleanLabel()} {renderDescription()} {renderDocsLink()}
{children}
); const renderBooleanInlineLayout = () => (
{renderBooleanLabel()} {renderDescription()} {renderDocsLink()}
{children}
); const renderSplitValueLayout = () => (
{renderSplitLabel()} {renderDescription("hidden md:block")} {renderDocsLink("hidden md:flex")}
{children} {renderDescription("md:hidden")} {renderDocsLink("md:hidden")}
); const renderDefaultValueLayout = () => ( <> {children} {renderDescription()} {renderDocsLink()} ); const renderFieldLayout = () => { if (isBoolean) { return useSplitBooleanLayout ? renderBooleanSplitLayout() : renderBooleanInlineLayout(); } if (useSplitLayout) { return renderSplitValueLayout(); } return renderDefaultValueLayout(); }; return (
{beforeContent}
{renderStandardLabel()} {renderFieldLayout()} {errors} {help}
{afterContent}
); }