From 0280c2ec43b2c157464b546d4b8fda999626d26b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:04:50 -0600 Subject: [PATCH] section fields --- web/src/components/config-form/ConfigForm.tsx | 28 +- .../config-form/sections/AudioSection.tsx | 39 +- .../config-form/sections/BaseSection.tsx | 392 +++++++++++++----- .../config-form/sections/DetectSection.tsx | 53 ++- .../config-form/sections/LiveSection.tsx | 19 +- .../config-form/sections/MotionSection.tsx | 63 ++- .../sections/NotificationsSection.tsx | 19 +- .../config-form/sections/ObjectsSection.tsx | 25 +- .../config-form/sections/RecordSection.tsx | 43 +- .../config-form/sections/ReviewSection.tsx | 19 +- .../config-form/sections/SnapshotsSection.tsx | 37 +- .../config-form/sections/TimestampSection.tsx | 23 +- .../components/config-form/sections/index.ts | 23 +- 13 files changed, 478 insertions(+), 305 deletions(-) diff --git a/web/src/components/config-form/ConfigForm.tsx b/web/src/components/config-form/ConfigForm.tsx index 4c26e51db..4597939de 100644 --- a/web/src/components/config-form/ConfigForm.tsx +++ b/web/src/components/config-form/ConfigForm.tsx @@ -43,6 +43,8 @@ export interface ConfigFormProps { liveValidate?: boolean; /** Form context passed to all widgets */ formContext?: Record; + /** i18n namespace for field labels */ + i18nNamespace?: string; } export function ConfigForm({ @@ -61,8 +63,9 @@ export function ConfigForm({ className, liveValidate = false, formContext, + i18nNamespace, }: ConfigFormProps) { - const { t } = useTranslation(["views/settings"]); + const { t } = useTranslation([i18nNamespace || "common", "views/settings"]); const [showAdvanced, setShowAdvanced] = useState(false); // Determine which fields to hide based on advanced toggle @@ -81,8 +84,16 @@ export function ConfigForm({ fieldOrder, hiddenFields: effectiveHiddenFields, advancedFields: showAdvanced ? advancedFields : [], + i18nNamespace, }), - [schema, fieldOrder, effectiveHiddenFields, advancedFields, showAdvanced], + [ + schema, + fieldOrder, + effectiveHiddenFields, + advancedFields, + showAdvanced, + i18nNamespace, + ], ); // Merge generated uiSchema with custom overrides @@ -116,6 +127,16 @@ export function ConfigForm({ const hasAdvancedFields = advancedFields && advancedFields.length > 0; + // Extended form context with i18n info + const extendedFormContext = useMemo( + () => ({ + ...formContext, + i18nNamespace, + t, + }), + [formContext, i18nNamespace, t], + ); + return (
{hasAdvancedFields && ( @@ -130,6 +151,7 @@ export function ConfigForm({ className="cursor-pointer text-sm text-muted-foreground" > {t("configForm.showAdvanced", { + ns: "views/settings", defaultValue: "Show Advanced Settings", })} @@ -146,7 +168,7 @@ export function ConfigForm({ disabled={disabled} readonly={readonly} liveValidate={liveValidate} - formContext={formContext} + formContext={extendedFormContext} transformErrors={errorTransformer} {...frigateTheme} /> diff --git a/web/src/components/config-form/sections/AudioSection.tsx b/web/src/components/config-form/sections/AudioSection.tsx index ae6d00911..7d9a94455 100644 --- a/web/src/components/config-form/sections/AudioSection.tsx +++ b/web/src/components/config-form/sections/AudioSection.tsx @@ -1,30 +1,27 @@ // Audio Section Component // Reusable for both global and camera-level audio settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the audio section -export const audioSectionConfig: SectionConfig = { - fieldOrder: [ - "enabled", - "listen", - "filters", - "min_volume", - "max_not_heard", - "num_threads", - ], - fieldGroups: { - detection: ["listen", "filters"], - sensitivity: ["min_volume", "max_not_heard"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["min_volume", "max_not_heard", "num_threads"], -}; +import { createConfigSection } from "./BaseSection"; export const AudioSection = createConfigSection({ sectionPath: "audio", - translationKey: "configForm.audio", - defaultConfig: audioSectionConfig, + i18nNamespace: "config/audio", + defaultConfig: { + fieldOrder: [ + "enabled", + "listen", + "filters", + "min_volume", + "max_not_heard", + "num_threads", + ], + fieldGroups: { + detection: ["listen", "filters"], + sensitivity: ["min_volume", "max_not_heard"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["min_volume", "max_not_heard", "num_threads"], + }, }); export default AudioSection; diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index bfb23a358..2f81db150 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -1,7 +1,7 @@ // Base Section Component for config form sections // Used as a foundation for reusable section components -import { useMemo, useCallback } from "react"; +import { useMemo, useCallback, useState } from "react"; import useSWR from "swr"; import axios from "axios"; import { toast } from "sonner"; @@ -10,11 +10,22 @@ import { ConfigForm } from "../ConfigForm"; import { useConfigOverride } from "@/hooks/use-config-override"; import { useSectionSchema } from "@/hooks/use-config-schema"; import type { FrigateConfig } from "@/types/frigateConfig"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { LuRotateCcw } from "react-icons/lu"; +import { + LuRotateCcw, + LuSave, + LuChevronDown, + LuChevronRight, +} from "react-icons/lu"; +import Heading from "@/components/ui/heading"; import get from "lodash/get"; +import isEqual from "lodash/isEqual"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; export interface SectionConfig { /** Field ordering within the section */ @@ -44,13 +55,19 @@ export interface BaseSectionProps { onSave?: () => void; /** Whether a restart is required after changes */ requiresRestart?: boolean; + /** Whether section is collapsible */ + collapsible?: boolean; + /** Default collapsed state */ + defaultCollapsed?: boolean; + /** Whether to show the section title (default: false for global, true for camera) */ + showTitle?: boolean; } export interface CreateSectionOptions { /** The config path for this section (e.g., "detect", "record") */ sectionPath: string; - /** Translation key prefix for this section */ - translationKey: string; + /** i18n namespace for this section (e.g., "config/detect") */ + i18nNamespace: string; /** Default section configuration */ defaultConfig: SectionConfig; } @@ -60,10 +77,10 @@ export interface CreateSectionOptions { */ export function createConfigSection({ sectionPath, - translationKey, + i18nNamespace, defaultConfig, }: CreateSectionOptions) { - return function ConfigSection({ + const ConfigSection = function ConfigSection({ level, cameraName, showOverrideIndicator = true, @@ -72,8 +89,20 @@ export function createConfigSection({ readonly = false, onSave, requiresRestart = true, + collapsible = false, + defaultCollapsed = false, + showTitle, }: BaseSectionProps) { - const { t } = useTranslation(["views/settings"]); + const { t } = useTranslation([i18nNamespace, "views/settings", "common"]); + const [isOpen, setIsOpen] = useState(!defaultCollapsed); + const [pendingData, setPendingData] = useState | null>(null); + const [isSaving, setIsSaving] = useState(false); + + // Default: show title for camera level (since it might be collapsible), hide for global + const shouldShowTitle = showTitle ?? level === "camera"; // Fetch config const { data: config, mutate: refreshConfig } = @@ -83,12 +112,11 @@ export function createConfigSection({ const sectionSchema = useSectionSchema(sectionPath, level); // Get override status - const { isOverridden, globalValue, cameraValue, resetToGlobal } = - useConfigOverride({ - config, - cameraName: level === "camera" ? cameraName : undefined, - sectionPath, - }); + const { isOverridden, globalValue, cameraValue } = useConfigOverride({ + config, + cameraName: level === "camera" ? cameraName : undefined, + sectionPath, + }); // Get current form data const formData = useMemo(() => { @@ -101,119 +129,286 @@ export function createConfigSection({ return get(config, sectionPath) || {}; }, [config, level, cameraName]); - // Handle form submission - const handleSubmit = useCallback( - async (data: Record) => { - try { - const basePath = - level === "camera" && cameraName - ? `cameras.${cameraName}.${sectionPath}` - : sectionPath; + // Track if there are unsaved changes + const hasChanges = useMemo(() => { + if (!pendingData) return false; + return !isEqual(formData, pendingData); + }, [formData, pendingData]); - await axios.put("config/set", { - requires_restart: requiresRestart ? 1 : 0, - config_data: { - [basePath]: data, - }, - }); + // Handle form data change + const handleChange = useCallback((data: Record) => { + setPendingData(data); + }, []); - toast.success( - t(`${translationKey}.toast.success`, { - defaultValue: "Settings saved successfully", - }), - ); + // Handle save button click + const handleSave = useCallback(async () => { + if (!pendingData) return; - refreshConfig(); - onSave?.(); - } catch (error) { - // Parse Pydantic validation errors from API response - if (axios.isAxiosError(error) && error.response?.data) { - const responseData = error.response.data; - // Pydantic errors come as { detail: [...] } or { message: "..." } - if (responseData.detail && Array.isArray(responseData.detail)) { - const validationMessages = responseData.detail - .map((err: { loc?: string[]; msg?: string }) => { - const field = err.loc?.slice(1).join(".") || "unknown"; - return `${field}: ${err.msg || "Invalid value"}`; - }) - .join(", "); - toast.error( - t(`${translationKey}.toast.validationError`, { - defaultValue: `Validation failed: ${validationMessages}`, - }), - ); - } else if (responseData.message) { - toast.error(responseData.message); - } else { - toast.error( - t(`${translationKey}.toast.error`, { - defaultValue: "Failed to save settings", - }), - ); - } + setIsSaving(true); + try { + const basePath = + level === "camera" && cameraName + ? `cameras.${cameraName}.${sectionPath}` + : sectionPath; + + await axios.put("config/set", { + requires_restart: requiresRestart ? 1 : 0, + config_data: { + [basePath]: pendingData, + }, + }); + + toast.success( + t("toast.success", { + ns: "views/settings", + defaultValue: "Settings saved successfully", + }), + ); + + setPendingData(null); + refreshConfig(); + onSave?.(); + } catch (error) { + // Parse Pydantic validation errors from API response + if (axios.isAxiosError(error) && error.response?.data) { + const responseData = error.response.data; + if (responseData.detail && Array.isArray(responseData.detail)) { + const validationMessages = responseData.detail + .map((err: { loc?: string[]; msg?: string }) => { + const field = err.loc?.slice(1).join(".") || "unknown"; + return `${field}: ${err.msg || "Invalid value"}`; + }) + .join(", "); + toast.error( + t("toast.validationError", { + ns: "views/settings", + defaultValue: `Validation failed: ${validationMessages}`, + }), + ); + } else if (responseData.message) { + toast.error(responseData.message); } else { toast.error( - t(`${translationKey}.toast.error`, { + t("toast.error", { + ns: "views/settings", defaultValue: "Failed to save settings", }), ); } + } else { + toast.error( + t("toast.error", { + ns: "views/settings", + defaultValue: "Failed to save settings", + }), + ); } - }, - [level, cameraName, requiresRestart, t, refreshConfig, onSave], - ); + } finally { + setIsSaving(false); + } + }, [ + pendingData, + level, + cameraName, + requiresRestart, + t, + refreshConfig, + onSave, + ]); - // Handle reset to global + // Handle reset to global - removes camera-level override by deleting the section const handleResetToGlobal = useCallback(async () => { if (level !== "camera" || !cameraName) return; try { const basePath = `cameras.${cameraName}.${sectionPath}`; - // Reset by setting to null/undefined or removing the override + // Send empty string to delete the key from config (see update_yaml in backend) await axios.put("config/set", { requires_restart: requiresRestart ? 1 : 0, config_data: { - [basePath]: resetToGlobal(), + [basePath]: "", }, }); toast.success( - t(`${translationKey}.toast.resetSuccess`, { + t("toast.resetSuccess", { + ns: "views/settings", defaultValue: "Reset to global defaults", }), ); + setPendingData(null); refreshConfig(); } catch { toast.error( - t(`${translationKey}.toast.resetError`, { + t("toast.resetError", { + ns: "views/settings", defaultValue: "Failed to reset settings", }), ); } - }, [level, cameraName, requiresRestart, t, refreshConfig, resetToGlobal]); + }, [level, cameraName, requiresRestart, t, refreshConfig]); if (!sectionSchema) { return null; } - const title = t(`${translationKey}.title`, { - defaultValue: sectionPath.charAt(0).toUpperCase() + sectionPath.slice(1), + // Get section title from config namespace + const title = t("label", { + ns: i18nNamespace, + defaultValue: + sectionPath.charAt(0).toUpperCase() + + sectionPath.slice(1).replace(/_/g, " "), }); - return ( - - + const sectionContent = ( +
+ + + {/* Save button */} +
- {title} - {showOverrideIndicator && level === "camera" && isOverridden && ( - - {t("common.overridden", { defaultValue: "Overridden" })} - + {hasChanges && ( + + {t("unsavedChanges", { + ns: "views/settings", + defaultValue: "You have unsaved changes", + })} + )}
- {level === "camera" && isOverridden && ( + +
+
+ ); + + if (collapsible) { + return ( + +
+ +
+
+ {isOpen ? ( + + ) : ( + + )} + {title} + {showOverrideIndicator && + level === "camera" && + isOverridden && ( + + {t("overridden", { + ns: "common", + defaultValue: "Overridden", + })} + + )} + {hasChanges && ( + + {t("modified", { + ns: "common", + defaultValue: "Modified", + })} + + )} +
+ {level === "camera" && isOverridden && ( + + )} +
+
+ + +
{sectionContent}
+
+
+
+ ); + } + + return ( +
+ {shouldShowTitle && ( +
+
+ {title} + {showOverrideIndicator && level === "camera" && isOverridden && ( + + {t("overridden", { + ns: "common", + defaultValue: "Overridden", + })} + + )} + {hasChanges && ( + + {t("modified", { ns: "common", defaultValue: "Modified" })} + + )} +
+ {level === "camera" && isOverridden && ( + + )} +
+ )} + + {/* Reset button when title is hidden but we're at camera level with override */} + {!shouldShowTitle && level === "camera" && isOverridden && ( +
- )} - - - - - +
+ )} + + {sectionContent} +
); }; + + return ConfigSection; } diff --git a/web/src/components/config-form/sections/DetectSection.tsx b/web/src/components/config-form/sections/DetectSection.tsx index ab6bff8b2..30d6b4736 100644 --- a/web/src/components/config-form/sections/DetectSection.tsx +++ b/web/src/components/config-form/sections/DetectSection.tsx @@ -1,37 +1,34 @@ // Detect Section Component // Reusable for both global and camera-level detect settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the detect section -export const detectSectionConfig: SectionConfig = { - fieldOrder: [ - "enabled", - "fps", - "width", - "height", - "min_initialized", - "max_disappeared", - "annotation_offset", - "stationary", - ], - fieldGroups: { - resolution: ["width", "height"], - tracking: ["min_initialized", "max_disappeared"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: [ - "min_initialized", - "max_disappeared", - "annotation_offset", - "stationary", - ], -}; +import { createConfigSection } from "./BaseSection"; export const DetectSection = createConfigSection({ sectionPath: "detect", - translationKey: "configForm.detect", - defaultConfig: detectSectionConfig, + i18nNamespace: "config/detect", + defaultConfig: { + fieldOrder: [ + "enabled", + "fps", + "width", + "height", + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + ], + fieldGroups: { + resolution: ["width", "height"], + tracking: ["min_initialized", "max_disappeared"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: [ + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + ], + }, }); export default DetectSection; diff --git a/web/src/components/config-form/sections/LiveSection.tsx b/web/src/components/config-form/sections/LiveSection.tsx index 7612b8204..ec033eb71 100644 --- a/web/src/components/config-form/sections/LiveSection.tsx +++ b/web/src/components/config-form/sections/LiveSection.tsx @@ -1,20 +1,17 @@ // Live Section Component // Reusable for both global and camera-level live settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the live section -export const liveSectionConfig: SectionConfig = { - fieldOrder: ["stream_name", "height", "quality"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: ["quality"], -}; +import { createConfigSection } from "./BaseSection"; export const LiveSection = createConfigSection({ sectionPath: "live", - translationKey: "configForm.live", - defaultConfig: liveSectionConfig, + i18nNamespace: "config/live", + defaultConfig: { + fieldOrder: ["stream_name", "height", "quality"], + fieldGroups: {}, + hiddenFields: ["enabled_in_config"], + advancedFields: ["quality"], + }, }); export default LiveSection; diff --git a/web/src/components/config-form/sections/MotionSection.tsx b/web/src/components/config-form/sections/MotionSection.tsx index b8f584191..87b1a4682 100644 --- a/web/src/components/config-form/sections/MotionSection.tsx +++ b/web/src/components/config-form/sections/MotionSection.tsx @@ -1,42 +1,39 @@ // Motion Section Component // Reusable for both global and camera-level motion settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the motion section -export const motionSectionConfig: SectionConfig = { - fieldOrder: [ - "enabled", - "threshold", - "lightning_threshold", - "improve_contrast", - "contour_area", - "delta_alpha", - "frame_alpha", - "frame_height", - "mask", - "mqtt_off_delay", - ], - fieldGroups: { - sensitivity: ["threshold", "lightning_threshold", "contour_area"], - algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: [ - "lightning_threshold", - "improve_contrast", - "contour_area", - "delta_alpha", - "frame_alpha", - "frame_height", - "mqtt_off_delay", - ], -}; +import { createConfigSection } from "./BaseSection"; export const MotionSection = createConfigSection({ sectionPath: "motion", - translationKey: "configForm.motion", - defaultConfig: motionSectionConfig, + i18nNamespace: "config/motion", + defaultConfig: { + fieldOrder: [ + "enabled", + "threshold", + "lightning_threshold", + "improve_contrast", + "contour_area", + "delta_alpha", + "frame_alpha", + "frame_height", + "mask", + "mqtt_off_delay", + ], + fieldGroups: { + sensitivity: ["threshold", "lightning_threshold", "contour_area"], + algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: [ + "lightning_threshold", + "improve_contrast", + "contour_area", + "delta_alpha", + "frame_alpha", + "frame_height", + "mqtt_off_delay", + ], + }, }); export default MotionSection; diff --git a/web/src/components/config-form/sections/NotificationsSection.tsx b/web/src/components/config-form/sections/NotificationsSection.tsx index e95665f9a..72a29caab 100644 --- a/web/src/components/config-form/sections/NotificationsSection.tsx +++ b/web/src/components/config-form/sections/NotificationsSection.tsx @@ -1,20 +1,17 @@ // Notifications Section Component // Reusable for both global and camera-level notification settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the notifications section -export const notificationsSectionConfig: SectionConfig = { - fieldOrder: ["enabled", "email"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: [], -}; +import { createConfigSection } from "./BaseSection"; export const NotificationsSection = createConfigSection({ sectionPath: "notifications", - translationKey: "configForm.notifications", - defaultConfig: notificationsSectionConfig, + i18nNamespace: "config/notifications", + defaultConfig: { + fieldOrder: ["enabled", "email"], + fieldGroups: {}, + hiddenFields: ["enabled_in_config"], + advancedFields: [], + }, }); export default NotificationsSection; diff --git a/web/src/components/config-form/sections/ObjectsSection.tsx b/web/src/components/config-form/sections/ObjectsSection.tsx index 7ed21e39b..fd3ab1ba4 100644 --- a/web/src/components/config-form/sections/ObjectsSection.tsx +++ b/web/src/components/config-form/sections/ObjectsSection.tsx @@ -1,23 +1,20 @@ // Objects Section Component // Reusable for both global and camera-level objects settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the objects section -export const objectsSectionConfig: SectionConfig = { - fieldOrder: ["track", "alert", "detect", "filters", "mask"], - fieldGroups: { - tracking: ["track", "alert", "detect"], - filtering: ["filters", "mask"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["filters", "mask"], -}; +import { createConfigSection } from "./BaseSection"; export const ObjectsSection = createConfigSection({ sectionPath: "objects", - translationKey: "configForm.objects", - defaultConfig: objectsSectionConfig, + i18nNamespace: "config/objects", + defaultConfig: { + fieldOrder: ["track", "alert", "detect", "filters", "mask"], + fieldGroups: { + tracking: ["track", "alert", "detect"], + filtering: ["filters", "mask"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["filters", "mask"], + }, }); export default ObjectsSection; diff --git a/web/src/components/config-form/sections/RecordSection.tsx b/web/src/components/config-form/sections/RecordSection.tsx index c5166f296..3263f0464 100644 --- a/web/src/components/config-form/sections/RecordSection.tsx +++ b/web/src/components/config-form/sections/RecordSection.tsx @@ -1,32 +1,29 @@ // Record Section Component // Reusable for both global and camera-level record settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the record section -export const recordSectionConfig: SectionConfig = { - fieldOrder: [ - "enabled", - "expire_interval", - "continuous", - "motion", - "alerts", - "detections", - "preview", - "export", - ], - fieldGroups: { - retention: ["continuous", "motion"], - events: ["alerts", "detections"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["expire_interval", "preview", "export"], -}; +import { createConfigSection } from "./BaseSection"; export const RecordSection = createConfigSection({ sectionPath: "record", - translationKey: "configForm.record", - defaultConfig: recordSectionConfig, + i18nNamespace: "config/record", + defaultConfig: { + fieldOrder: [ + "enabled", + "expire_interval", + "continuous", + "motion", + "alerts", + "detections", + "preview", + "export", + ], + fieldGroups: { + retention: ["continuous", "motion"], + events: ["alerts", "detections"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["expire_interval", "preview", "export"], + }, }); export default RecordSection; diff --git a/web/src/components/config-form/sections/ReviewSection.tsx b/web/src/components/config-form/sections/ReviewSection.tsx index fe89ad698..5dbee249a 100644 --- a/web/src/components/config-form/sections/ReviewSection.tsx +++ b/web/src/components/config-form/sections/ReviewSection.tsx @@ -1,20 +1,17 @@ // Review Section Component // Reusable for both global and camera-level review settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the review section -export const reviewSectionConfig: SectionConfig = { - fieldOrder: ["alerts", "detections"], - fieldGroups: {}, - hiddenFields: ["enabled_in_config"], - advancedFields: [], -}; +import { createConfigSection } from "./BaseSection"; export const ReviewSection = createConfigSection({ sectionPath: "review", - translationKey: "configForm.review", - defaultConfig: reviewSectionConfig, + i18nNamespace: "config/review", + defaultConfig: { + fieldOrder: ["alerts", "detections"], + fieldGroups: {}, + hiddenFields: ["enabled_in_config"], + advancedFields: [], + }, }); export default ReviewSection; diff --git a/web/src/components/config-form/sections/SnapshotsSection.tsx b/web/src/components/config-form/sections/SnapshotsSection.tsx index f7095646a..bab0ad511 100644 --- a/web/src/components/config-form/sections/SnapshotsSection.tsx +++ b/web/src/components/config-form/sections/SnapshotsSection.tsx @@ -1,29 +1,26 @@ // Snapshots Section Component // Reusable for both global and camera-level snapshots settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the snapshots section -export const snapshotsSectionConfig: SectionConfig = { - fieldOrder: [ - "enabled", - "bounding_box", - "crop", - "quality", - "timestamp", - "retain", - ], - fieldGroups: { - display: ["bounding_box", "crop", "quality", "timestamp"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["quality", "retain"], -}; +import { createConfigSection } from "./BaseSection"; export const SnapshotsSection = createConfigSection({ sectionPath: "snapshots", - translationKey: "configForm.snapshots", - defaultConfig: snapshotsSectionConfig, + i18nNamespace: "config/snapshots", + defaultConfig: { + fieldOrder: [ + "enabled", + "bounding_box", + "crop", + "quality", + "timestamp", + "retain", + ], + fieldGroups: { + display: ["bounding_box", "crop", "quality", "timestamp"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["quality", "retain"], + }, }); export default SnapshotsSection; diff --git a/web/src/components/config-form/sections/TimestampSection.tsx b/web/src/components/config-form/sections/TimestampSection.tsx index b4cf88574..8732c5131 100644 --- a/web/src/components/config-form/sections/TimestampSection.tsx +++ b/web/src/components/config-form/sections/TimestampSection.tsx @@ -1,22 +1,19 @@ // Timestamp Section Component // Reusable for both global and camera-level timestamp_style settings -import { createConfigSection, type SectionConfig } from "./BaseSection"; - -// Configuration for the timestamp_style section -export const timestampSectionConfig: SectionConfig = { - fieldOrder: ["position", "format", "color", "thickness", "effect"], - fieldGroups: { - appearance: ["color", "thickness", "effect"], - }, - hiddenFields: ["enabled_in_config"], - advancedFields: ["thickness", "effect"], -}; +import { createConfigSection } from "./BaseSection"; export const TimestampSection = createConfigSection({ sectionPath: "timestamp_style", - translationKey: "configForm.timestampStyle", - defaultConfig: timestampSectionConfig, + i18nNamespace: "config/timestamp_style", + defaultConfig: { + fieldOrder: ["position", "format", "color", "thickness", "effect"], + fieldGroups: { + appearance: ["color", "thickness", "effect"], + }, + hiddenFields: ["enabled_in_config"], + advancedFields: ["thickness", "effect"], + }, }); export default TimestampSection; diff --git a/web/src/components/config-form/sections/index.ts b/web/src/components/config-form/sections/index.ts index 0fd932b51..3a16ca73a 100644 --- a/web/src/components/config-form/sections/index.ts +++ b/web/src/components/config-form/sections/index.ts @@ -8,16 +8,13 @@ export { type CreateSectionOptions, } from "./BaseSection"; -export { DetectSection, detectSectionConfig } from "./DetectSection"; -export { RecordSection, recordSectionConfig } from "./RecordSection"; -export { SnapshotsSection, snapshotsSectionConfig } from "./SnapshotsSection"; -export { MotionSection, motionSectionConfig } from "./MotionSection"; -export { ObjectsSection, objectsSectionConfig } from "./ObjectsSection"; -export { ReviewSection, reviewSectionConfig } from "./ReviewSection"; -export { AudioSection, audioSectionConfig } from "./AudioSection"; -export { - NotificationsSection, - notificationsSectionConfig, -} from "./NotificationsSection"; -export { LiveSection, liveSectionConfig } from "./LiveSection"; -export { TimestampSection, timestampSectionConfig } from "./TimestampSection"; +export { DetectSection } from "./DetectSection"; +export { RecordSection } from "./RecordSection"; +export { SnapshotsSection } from "./SnapshotsSection"; +export { MotionSection } from "./MotionSection"; +export { ObjectsSection } from "./ObjectsSection"; +export { ReviewSection } from "./ReviewSection"; +export { AudioSection } from "./AudioSection"; +export { NotificationsSection } from "./NotificationsSection"; +export { LiveSection } from "./LiveSection"; +export { TimestampSection } from "./TimestampSection";