mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-30 10:20:30 +03:00
Export dialog improvements (#24787)
* show validation errors in multi-camera export Clearing part of a time field in the export time picker set the range to NaN, while the picker kept showing a valid fallback time. The multi-camera Export button then stayed disabled with no explanation, and non-admins could never start an export because a case name was required even though they can't see the case field. The picker now ignores incomplete time values. The multi-camera tab shows inline errors for an invalid range, no selected cameras, and a blank case name, only requires a case name from admins, and prefills a default case name. * add viewer export test and remove stale non-admin case text * fix test
This commit is contained in:
@@ -151,6 +151,11 @@ export function CustomTimeSelector({
|
||||
step={isIOS ? "60" : "1"}
|
||||
onChange={(e) => {
|
||||
const clock = e.target.value;
|
||||
|
||||
if (!clock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [hour, minute, second] = isIOS
|
||||
? [...clock.split(":"), "00"]
|
||||
: clock.split(":");
|
||||
@@ -217,6 +222,11 @@ export function CustomTimeSelector({
|
||||
step={isIOS ? "60" : "1"}
|
||||
onChange={(e) => {
|
||||
const clock = e.target.value;
|
||||
|
||||
if (!clock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [hour, minute, second] = isIOS
|
||||
? [...clock.split(":"), "00"]
|
||||
: clock.split(":");
|
||||
|
||||
@@ -65,6 +65,8 @@ import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { isReplayCamera } from "@/utils/cameraUtil";
|
||||
import { isValidIconName } from "@/utils/iconUtil";
|
||||
|
||||
@@ -441,6 +443,7 @@ export function ExportContent({
|
||||
}: ExportContentProps) {
|
||||
const { t } = useTranslation(["components/dialog"]);
|
||||
const isAdmin = useIsAdmin();
|
||||
const locale = useDateLocale();
|
||||
const [selectedOption, setSelectedOption] = useState<ExportOption>("1");
|
||||
const { data: cases } = useSWR<ExportCase[]>(isAdmin ? "cases" : null);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@@ -663,12 +666,25 @@ export function ExportContent({
|
||||
]);
|
||||
|
||||
const selectedCameraCount = selectedCameraIds.length;
|
||||
|
||||
const batchRangeError = !range
|
||||
? t("export.toast.error.noValidTimeSelected")
|
||||
: range.before <= range.after
|
||||
? t("export.toast.error.endTimeMustAfterStartTime")
|
||||
: undefined;
|
||||
const batchCameraError =
|
||||
!isEventsLoading && selectedCameraCount === 0
|
||||
? t("export.multiCamera.noCamerasSelected")
|
||||
: undefined;
|
||||
const batchCaseNameError =
|
||||
isAdmin && batchCaseSelection === "new" && newCaseName.trim().length === 0
|
||||
? t("export.case.nameRequired")
|
||||
: undefined;
|
||||
const canStartBatchExport =
|
||||
Boolean(range && range.before > range.after) &&
|
||||
!batchRangeError &&
|
||||
!batchCaseNameError &&
|
||||
selectedCameraCount > 0 &&
|
||||
!isStartingBatchExport &&
|
||||
(batchCaseSelection !== "new" || newCaseName.trim().length > 0) &&
|
||||
batchCaseSelection.length > 0;
|
||||
!isStartingBatchExport;
|
||||
|
||||
const onSelectTime = useCallback(
|
||||
(option: ExportOption) => {
|
||||
@@ -965,6 +981,17 @@ export function ExportContent({
|
||||
if (tab === "multi") {
|
||||
setRange(buildRangeAroundCurrentTime(MULTI_CAMERA_RANGE_SECONDS));
|
||||
setBatchCaseSelection(selectedCaseId ?? "new");
|
||||
if (newCaseName.trim().length === 0) {
|
||||
setNewCaseName(
|
||||
t("export.multiCamera.caseNameDefault", {
|
||||
date: formatUnixTimestampToDateTime(Date.now() / 1000, {
|
||||
date_style: "medium",
|
||||
time_style: "short",
|
||||
locale,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onSelectTime(selectedOption);
|
||||
}
|
||||
@@ -1132,6 +1159,11 @@ export function ExportContent({
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{batchRangeError && (
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{batchRangeError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -1315,6 +1347,11 @@ export function ExportContent({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{batchCameraError && (
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{batchCameraError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -1367,6 +1404,11 @@ export function ExportContent({
|
||||
value={newCaseName}
|
||||
onChange={(event) => setNewCaseName(event.target.value)}
|
||||
/>
|
||||
{batchCaseNameError && (
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{batchCaseNameError}
|
||||
</p>
|
||||
)}
|
||||
<Textarea
|
||||
placeholder={t("export.case.newCaseDescriptionPlaceholder")}
|
||||
value={newCaseDescription}
|
||||
|
||||
@@ -68,9 +68,7 @@ export default function MultiExportDialog({
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
// Only admins can attach exports to an existing case (enforced server-side
|
||||
// by POST /exports/batch). Skip fetching the case list entirely for
|
||||
// non-admins — they can only ever use the "Create new case" branch.
|
||||
// Only admins can assign exports to a case, so non-admins skip the case list
|
||||
const { data: cases } = useSWR<ExportCase[]>(isAdmin ? "cases" : null);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -284,9 +282,6 @@ export default function MultiExportDialog({
|
||||
t,
|
||||
]);
|
||||
|
||||
// New-case inputs: rendered below the Select when caseSelection === "new",
|
||||
// or rendered standalone for non-admins (who never see the Select since
|
||||
// they cannot attach to an existing case).
|
||||
const newCaseInputs = (
|
||||
<div className="space-y-2 pt-1">
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user