add reordering save text to camera management view

This commit is contained in:
Josh Hawkins 2026-05-18 14:04:55 -05:00
parent be8c81e173
commit e33da68fff
2 changed files with 84 additions and 16 deletions

View File

@ -482,6 +482,8 @@
"disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.", "disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.",
"enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.", "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.",
"reorderHandle": "Drag to reorder", "reorderHandle": "Drag to reorder",
"saving": "Saving…",
"saved": "Saved",
"friendlyName": { "friendlyName": {
"edit": "Edit camera display name", "edit": "Edit camera display name",
"title": "Edit Display Name", "title": "Edit Display Name",

View File

@ -15,6 +15,7 @@ import CameraEditForm from "@/components/settings/CameraEditForm";
import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import CameraWizardDialog from "@/components/settings/CameraWizardDialog";
import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog"; import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog";
import { import {
LuCheck,
LuExternalLink, LuExternalLink,
LuGripVertical, LuGripVertical,
LuPencil, LuPencil,
@ -52,6 +53,10 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
const REORDER_SAVED_INDICATOR_MS = 1500;
type ReorderSaveStatus = "idle" | "saving" | "saved";
type CameraManagementViewProps = { type CameraManagementViewProps = {
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>; setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
profileState?: ProfileState; profileState?: ProfileState;
@ -113,6 +118,19 @@ export default function CameraManagementView({
}); });
}, [enabledCameras]); }, [enabledCameras]);
const [reorderSaveStatus, setReorderSaveStatus] =
useState<ReorderSaveStatus>("idle");
const reorderSavedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null,
);
useEffect(() => {
return () => {
if (reorderSavedTimerRef.current) {
clearTimeout(reorderSavedTimerRef.current);
}
};
}, []);
const handleReorderDragEnd = useCallback(async () => { const handleReorderDragEnd = useCallback(async () => {
const current = orderedCamerasRef.current; const current = orderedCamerasRef.current;
if ( if (
@ -127,14 +145,26 @@ export default function CameraManagementView({
cameraUpdates[cam] = { ui: { order: i * 10 } }; cameraUpdates[cam] = { ui: { order: i * 10 } };
}); });
if (reorderSavedTimerRef.current) {
clearTimeout(reorderSavedTimerRef.current);
reorderSavedTimerRef.current = null;
}
setReorderSaveStatus("saving");
try { try {
await axios.put("config/set", { await axios.put("config/set", {
requires_restart: 0, requires_restart: 0,
config_data: { cameras: cameraUpdates }, config_data: { cameras: cameraUpdates },
}); });
await updateConfig(); await updateConfig();
setReorderSaveStatus("saved");
reorderSavedTimerRef.current = setTimeout(() => {
setReorderSaveStatus("idle");
reorderSavedTimerRef.current = null;
}, REORDER_SAVED_INDICATOR_MS);
} catch (error) { } catch (error) {
setOrderedCameras(enabledCameras); setOrderedCameras(enabledCameras);
setReorderSaveStatus("idle");
const errorMessage = const errorMessage =
axios.isAxiosError(error) && axios.isAxiosError(error) &&
(error.response?.data?.message || error.response?.data?.detail) (error.response?.data?.message || error.response?.data?.detail)
@ -238,22 +268,27 @@ export default function CameraManagementView({
</p> </p>
</Label> </Label>
</div> </div>
<Reorder.Group <div className="max-w-md space-y-1.5">
as="div" <Reorder.Group
axis="y" as="div"
values={orderedCameras} axis="y"
onReorder={setOrderedCameras} values={orderedCameras}
className="max-w-md space-y-2 rounded-lg bg-secondary p-4" onReorder={setOrderedCameras}
> className="space-y-2 rounded-lg bg-secondary p-4"
{orderedCameras.map((camera) => ( >
<EnabledCameraRow {orderedCameras.map((camera) => (
key={camera} <EnabledCameraRow
camera={camera} key={camera}
onConfigChanged={updateConfig} camera={camera}
onDragEnd={handleReorderDragEnd} onConfigChanged={updateConfig}
/> onDragEnd={handleReorderDragEnd}
))} />
</Reorder.Group> ))}
</Reorder.Group>
<ReorderSaveStatusIndicator
status={reorderSaveStatus}
/>
</div>
<p className="text-sm text-muted-foreground md:hidden"> <p className="text-sm text-muted-foreground md:hidden">
<Trans ns="views/settings"> <Trans ns="views/settings">
cameraManagement.streams.enableDesc cameraManagement.streams.enableDesc
@ -373,6 +408,37 @@ export default function CameraManagementView({
); );
} }
type ReorderSaveStatusIndicatorProps = {
status: ReorderSaveStatus;
};
function ReorderSaveStatusIndicator({
status,
}: ReorderSaveStatusIndicatorProps) {
const { t } = useTranslation(["views/settings"]);
return (
<div
aria-live="polite"
className={cn(
"flex h-4 items-center justify-start gap-1 text-xs transition-opacity duration-200",
status === "idle" ? "opacity-0" : "opacity-100",
)}
>
{status === "saving" && (
<span className="text-muted-foreground">
{t("cameraManagement.streams.saving")}
</span>
)}
{status === "saved" && (
<span className="flex items-center gap-1 text-success">
<LuCheck className="size-3.5" />
{t("cameraManagement.streams.saved")}
</span>
)}
</div>
);
}
type EnabledCameraRowProps = { type EnabledCameraRowProps = {
camera: string; camera: string;
onConfigChanged: () => Promise<unknown>; onConfigChanged: () => Promise<unknown>;