add reset logic to global config view

This commit is contained in:
Josh Hawkins 2026-01-25 11:20:22 -06:00
parent f9e5969b7d
commit dbc8e4c2d1

View File

@ -1,7 +1,7 @@
// Global Configuration View // Global Configuration View
// Main view for configuring global Frigate settings // Main view for configuring global Frigate settings
import { useMemo, useCallback, useState, memo } from "react"; import { useMemo, useCallback, useState, useEffect, memo, useRef } from "react";
import useSWR from "swr"; import useSWR from "swr";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
@ -21,6 +21,7 @@ import type { RJSFSchema } from "@rjsf/utils";
import type { FrigateConfig } from "@/types/frigateConfig"; import type { FrigateConfig } from "@/types/frigateConfig";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { extractSchemaSection } from "@/lib/config-schema"; import { extractSchemaSection } from "@/lib/config-schema";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import Heading from "@/components/ui/heading"; import Heading from "@/components/ui/heading";
@ -380,6 +381,7 @@ interface GlobalConfigSectionProps {
schema: RJSFSchema | null; schema: RJSFSchema | null;
config: FrigateConfig | undefined; config: FrigateConfig | undefined;
onSave: () => void; onSave: () => void;
title: string;
} }
const GlobalConfigSection = memo(function GlobalConfigSection({ const GlobalConfigSection = memo(function GlobalConfigSection({
@ -387,6 +389,7 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
schema, schema,
config, config,
onSave, onSave,
title,
}: GlobalConfigSectionProps) { }: GlobalConfigSectionProps) {
const sectionConfig = globalSectionConfigs[sectionKey]; const sectionConfig = globalSectionConfigs[sectionKey];
const { t } = useTranslation([ const { t } = useTranslation([
@ -396,19 +399,52 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
]); ]);
const [pendingData, setPendingData] = useState<unknown | null>(null); const [pendingData, setPendingData] = useState<unknown | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [formKey, setFormKey] = useState(0);
const isResettingRef = useRef(false);
const formData = useMemo((): unknown => { const formData = useMemo((): unknown => {
if (!config) return {}; if (!config) return {};
return (config as unknown as Record<string, unknown>)[sectionKey]; return (config as unknown as Record<string, unknown>)[sectionKey];
}, [config, sectionKey]); }, [config, sectionKey]);
useEffect(() => {
setPendingData(null);
}, [formData]);
useEffect(() => {
if (isResettingRef.current) {
isResettingRef.current = false;
}
}, [formKey]);
const hasChanges = useMemo(() => { const hasChanges = useMemo(() => {
if (!pendingData) return false; if (!pendingData) return false;
return !isEqual(formData, pendingData); return !isEqual(formData, pendingData);
}, [formData, pendingData]); }, [formData, pendingData]);
const handleChange = useCallback((data: unknown) => { const handleChange = useCallback(
setPendingData(data); (data: unknown) => {
if (isResettingRef.current) {
setPendingData(null);
return;
}
if (!data || typeof data !== "object") {
setPendingData(null);
return;
}
if (isEqual(formData, data)) {
setPendingData(null);
return;
}
setPendingData(data);
},
[formData],
);
const handleReset = useCallback(() => {
isResettingRef.current = true;
setPendingData(null);
setFormKey((prev) => prev + 1);
}, []); }, []);
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
@ -450,7 +486,16 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3">
<Heading as="h4">{title}</Heading>
{hasChanges && (
<Badge variant="outline" className="text-xs">
{t("modified", { ns: "common", defaultValue: "Modified" })}
</Badge>
)}
</div>
<ConfigForm <ConfigForm
key={formKey}
schema={schema} schema={schema}
formData={pendingData || formData} formData={pendingData || formData}
onChange={handleChange} onChange={handleChange}
@ -463,7 +508,7 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
/> />
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2">
<div> <div className="flex items-center gap-2">
{hasChanges && ( {hasChanges && (
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{t("unsavedChanges", { {t("unsavedChanges", {
@ -473,16 +518,28 @@ const GlobalConfigSection = memo(function GlobalConfigSection({
</span> </span>
)} )}
</div> </div>
<Button <div className="flex items-center gap-2">
onClick={handleSave} {hasChanges && (
disabled={!hasChanges || isSaving} <Button
className="gap-2" onClick={handleReset}
> variant="outline"
<LuSave className="h-4 w-4" /> disabled={isSaving}
{isSaving className="gap-2"
? t("saving", { ns: "common", defaultValue: "Saving..." }) >
: t("save", { ns: "common", defaultValue: "Save" })} {t("reset", { ns: "common", defaultValue: "Reset" })}
</Button> </Button>
)}
<Button
onClick={handleSave}
disabled={!hasChanges || isSaving}
className="gap-2"
>
<LuSave className="h-4 w-4" />
{isSaving
? t("saving", { ns: "common", defaultValue: "Saving..." })
: t("save", { ns: "common", defaultValue: "Save" })}
</Button>
</div>
</div> </div>
</div> </div>
); );
@ -679,57 +736,61 @@ export default function GlobalConfigView() {
{activeTab === "system" && ( {activeTab === "system" && (
<> <>
{systemSections.map((sectionKey) => ( {systemSections.map((sectionKey) => {
<div const sectionTitle = t("label", {
key={sectionKey} ns: globalSectionConfigs[sectionKey].i18nNamespace,
className={cn( defaultValue:
activeSection === sectionKey ? "block" : "hidden", sectionKey.charAt(0).toUpperCase() +
)} sectionKey.slice(1).replace(/_/g, " "),
> });
<Heading as="h4" className="mb-4">
{t("label", { return (
ns: globalSectionConfigs[sectionKey].i18nNamespace, <div
defaultValue: key={sectionKey}
sectionKey.charAt(0).toUpperCase() + className={cn(
sectionKey.slice(1).replace(/_/g, " "), activeSection === sectionKey ? "block" : "hidden",
})} )}
</Heading> >
<GlobalConfigSection <GlobalConfigSection
sectionKey={sectionKey} sectionKey={sectionKey}
schema={extractSchemaSection(schema, sectionKey)} schema={extractSchemaSection(schema, sectionKey)}
config={config} config={config}
onSave={handleSave} onSave={handleSave}
/> title={sectionTitle}
</div> />
))} </div>
);
})}
</> </>
)} )}
{activeTab === "integrations" && ( {activeTab === "integrations" && (
<> <>
{integrationSections.map((sectionKey) => ( {integrationSections.map((sectionKey) => {
<div const sectionTitle = t("label", {
key={sectionKey} ns: globalSectionConfigs[sectionKey].i18nNamespace,
className={cn( defaultValue:
activeSection === sectionKey ? "block" : "hidden", sectionKey.charAt(0).toUpperCase() +
)} sectionKey.slice(1).replace(/_/g, " "),
> });
<Heading as="h4" className="mb-4">
{t("label", { return (
ns: globalSectionConfigs[sectionKey].i18nNamespace, <div
defaultValue: key={sectionKey}
sectionKey.charAt(0).toUpperCase() + className={cn(
sectionKey.slice(1).replace(/_/g, " "), activeSection === sectionKey ? "block" : "hidden",
})} )}
</Heading> >
<GlobalConfigSection <GlobalConfigSection
sectionKey={sectionKey} sectionKey={sectionKey}
schema={extractSchemaSection(schema, sectionKey)} schema={extractSchemaSection(schema, sectionKey)}
config={config} config={config}
onSave={handleSave} onSave={handleSave}
/> title={sectionTitle}
</div> />
))} </div>
);
})}
</> </>
)} )}
</div> </div>