Compare commits

..

No commits in common. "e593ac9599bdc440b180e11e9abbf230db7df07d" and "13e4a9406531ada6b4a638dac1d28c1f04e07c10" have entirely different histories.

6 changed files with 18 additions and 108 deletions

View File

@ -596,7 +596,7 @@ def export_recordings_batch(
)
try:
start_export_job(request.app.frigate_config, export_job)
except Exception:
except Exception as err:
logger.exception("Failed to queue export job %s", export_job.id)
results.append(
{
@ -604,7 +604,7 @@ def export_recordings_batch(
"export_id": None,
"success": False,
"status": None,
"error": "Failed to queue export job",
"error": str(err),
"item_index": index,
"client_item_id": item.client_item_id,
}

View File

@ -460,9 +460,10 @@ test.describe("Multi-Review Export @high", () => {
.filter({ hasText: /Export 2 reviews/i });
await expect(dialog).toBeVisible({ timeout: 5_000 });
// The dialog uses a Select trigger for case selection (admins). The
// default "None" value is shown on the trigger.
// default "Create new case" value is shown on the trigger and the
// New-case inputs render directly below.
await expect(dialog.locator("button[role='combobox']")).toBeVisible();
await expect(dialog.getByText(/None/)).toBeVisible();
await expect(dialog.getByText(/Create new case/i)).toBeVisible();
});
test("starting an export posts the expected payload and navigates to the case", async ({
@ -512,12 +513,6 @@ test.describe("Multi-Review Export @high", () => {
.filter({ hasText: /Export 2 reviews/i });
await expect(dialog).toBeVisible({ timeout: 5_000 });
// Select "Create new case" from the case dropdown (default is "None")
await dialog.locator("button[role='combobox']").click();
await frigateApp.page
.getByRole("option", { name: /Create new case/i })
.click();
const nameInput = dialog.locator("input").first();
await nameInput.fill("E2E Incident");

View File

@ -81,13 +81,11 @@
"exportButton_other": "Export {{count}} Cameras"
},
"multi": {
"title": "Export {{count}} reviews",
"title_one": "Export 1 review",
"title_other": "Export {{count}} reviews",
"description": "Export each selected review. All exports will be grouped under a single case.",
"descriptionNoCase": "Export each selected review.",
"caseNamePlaceholder": "Review export - {{date}}",
"exportButton": "Export {{count}} reviews",
"exportButton_one": "Export 1 review",
"exportButton_other": "Export {{count}} reviews",
"exportingButton": "Exporting...",

View File

@ -93,8 +93,6 @@ export default function ExportDialog({
const { t } = useTranslation(["components/dialog"]);
const [name, setName] = useState("");
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>();
const [singleNewCaseName, setSingleNewCaseName] = useState("");
const [singleNewCaseDescription, setSingleNewCaseDescription] = useState("");
const [activeTab, setActiveTab] = useState<ExportTab>("export");
const [isStartingExport, setIsStartingExport] = useState(false);
const previousModeRef = useRef<ExportMode>(mode);
@ -139,24 +137,12 @@ export default function ExportDialog({
setIsStartingExport(true);
try {
let exportCaseId: string | undefined = selectedCaseId;
if (selectedCaseId === "new" && singleNewCaseName.trim().length > 0) {
const caseResp = await axios.post("cases", {
name: singleNewCaseName.trim(),
description: singleNewCaseDescription.trim() || undefined,
});
exportCaseId = caseResp.data?.id;
} else if (selectedCaseId === "new" || selectedCaseId === "none") {
exportCaseId = undefined;
}
await axios.post<StartExportResponse>(
`export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`,
{
source: "recordings",
name,
export_case_id: exportCaseId,
export_case_id: selectedCaseId || undefined,
},
);
@ -170,8 +156,6 @@ export default function ExportDialog({
});
setName("");
setSelectedCaseId(undefined);
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setRange(undefined);
setMode("none");
return true;
@ -199,8 +183,6 @@ export default function ExportDialog({
name,
range,
selectedCaseId,
singleNewCaseDescription,
singleNewCaseName,
setMode,
setRange,
t,
@ -209,8 +191,6 @@ export default function ExportDialog({
const handleCancel = useCallback(() => {
setName("");
setSelectedCaseId(undefined);
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setMode("none");
setRange(undefined);
setActiveTab("export");
@ -292,16 +272,12 @@ export default function ExportDialog({
range={range}
name={name}
selectedCaseId={selectedCaseId}
singleNewCaseName={singleNewCaseName}
singleNewCaseDescription={singleNewCaseDescription}
activeTab={activeTab}
isStartingExport={isStartingExport}
onStartExport={onStartExport}
setActiveTab={setActiveTab}
setName={setName}
setSelectedCaseId={setSelectedCaseId}
setSingleNewCaseName={setSingleNewCaseName}
setSingleNewCaseDescription={setSingleNewCaseDescription}
setRange={setRange}
setMode={setMode}
onCancel={handleCancel}
@ -318,16 +294,12 @@ type ExportContentProps = {
range?: TimeRange;
name: string;
selectedCaseId?: string;
singleNewCaseName: string;
singleNewCaseDescription: string;
activeTab: ExportTab;
isStartingExport: boolean;
onStartExport: () => Promise<boolean>;
setActiveTab: (tab: ExportTab) => void;
setName: (name: string) => void;
setSelectedCaseId: (caseId: string | undefined) => void;
setSingleNewCaseName: (name: string) => void;
setSingleNewCaseDescription: (description: string) => void;
setRange: (range: TimeRange | undefined) => void;
setMode: (mode: ExportMode) => void;
onCancel: () => void;
@ -339,16 +311,12 @@ export function ExportContent({
range,
name,
selectedCaseId,
singleNewCaseName,
singleNewCaseDescription,
activeTab,
isStartingExport,
onStartExport,
setActiveTab,
setName,
setSelectedCaseId,
setSingleNewCaseName,
setSingleNewCaseDescription,
setRange,
setMode,
onCancel,
@ -364,7 +332,7 @@ export function ExportContent({
);
const [selectedCameraIds, setSelectedCameraIds] = useState<string[]>([]);
const [batchCaseSelection, setBatchCaseSelection] = useState<string>(
selectedCaseId || "none",
selectedCaseId || "new",
);
const [hasManualCameraSelection, setHasManualCameraSelection] =
useState(false);
@ -515,8 +483,7 @@ export function ExportContent({
Boolean(range && range.before > range.after) &&
selectedCameraCount > 0 &&
!isStartingBatchExport &&
(batchCaseSelection !== "new" || newCaseName.trim().length > 0) &&
batchCaseSelection.length > 0;
(batchCaseSelection !== "new" || newCaseName.trim().length > 0);
const onSelectTime = useCallback(
(option: ExportOption) => {
@ -600,7 +567,7 @@ export function ExportContent({
})),
};
if (isAdmin && batchCaseSelection !== "none") {
if (isAdmin) {
if (batchCaseSelection === "new") {
payload.new_case_name = newCaseName.trim();
payload.new_case_description = newCaseDescription.trim() || undefined;
@ -819,30 +786,8 @@ export function ExportContent({
{caseItem.name}
</SelectItem>
))}
<SelectSeparator />
<SelectItem value="new">
{t("export.case.newCaseOption")}
</SelectItem>
</SelectContent>
</Select>
{selectedCaseId === "new" && (
<div className="space-y-2 pt-1">
<Input
className="text-md"
placeholder={t("export.case.newCaseNamePlaceholder")}
value={singleNewCaseName}
onChange={(e) => setSingleNewCaseName(e.target.value)}
/>
<Textarea
className="text-md"
placeholder={t("export.case.newCaseDescriptionPlaceholder")}
value={singleNewCaseDescription}
onChange={(e) =>
setSingleNewCaseDescription(e.target.value)
}
/>
</div>
)}
</div>
)}
</TabsContent>
@ -1002,9 +947,6 @@ export function ExportContent({
<SelectValue placeholder={t("export.case.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
{t("label.none", { ns: "common" })}
</SelectItem>
{cases
?.sort((a, b) => a.name.localeCompare(b.name))
.map((caseItem) => (

View File

@ -115,8 +115,6 @@ export default function MobileReviewSettingsDrawer({
const [selectedCaseId, setSelectedCaseId] = useState<string | undefined>(
undefined,
);
const [singleNewCaseName, setSingleNewCaseName] = useState("");
const [singleNewCaseDescription, setSingleNewCaseDescription] = useState("");
const [isStartingExport, setIsStartingExport] = useState(false);
const onStartExport = useCallback(async () => {
if (isStartingExport) {
@ -150,24 +148,12 @@ export default function MobileReviewSettingsDrawer({
setIsStartingExport(true);
try {
let exportCaseId: string | undefined = selectedCaseId;
if (selectedCaseId === "new" && singleNewCaseName.trim().length > 0) {
const caseResp = await axios.post("cases", {
name: singleNewCaseName.trim(),
description: singleNewCaseDescription.trim() || undefined,
});
exportCaseId = caseResp.data?.id;
} else if (selectedCaseId === "new" || selectedCaseId === "none") {
exportCaseId = undefined;
}
await axios.post<StartExportResponse>(
`export/${camera}/start/${Math.round(range.after)}/end/${Math.round(range.before)}`,
{
source: "recordings",
name,
export_case_id: exportCaseId,
export_case_id: selectedCaseId || undefined,
},
);
@ -183,8 +169,6 @@ export default function MobileReviewSettingsDrawer({
});
setName("");
setSelectedCaseId(undefined);
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setRange(undefined);
setMode("none");
return true;
@ -215,8 +199,6 @@ export default function MobileReviewSettingsDrawer({
name,
range,
selectedCaseId,
singleNewCaseDescription,
singleNewCaseName,
setRange,
setMode,
t,
@ -379,16 +361,12 @@ export default function MobileReviewSettingsDrawer({
range={range}
name={name}
selectedCaseId={selectedCaseId}
singleNewCaseName={singleNewCaseName}
singleNewCaseDescription={singleNewCaseDescription}
activeTab={exportTab}
isStartingExport={isStartingExport}
onStartExport={onStartExport}
setActiveTab={setExportTab}
setName={setName}
setSelectedCaseId={setSelectedCaseId}
setSingleNewCaseName={setSingleNewCaseName}
setSingleNewCaseDescription={setSingleNewCaseDescription}
setRange={setRange}
setMode={(mode) => {
setMode(mode);
@ -401,8 +379,6 @@ export default function MobileReviewSettingsDrawer({
setMode("none");
setRange(undefined);
setSelectedCaseId(undefined);
setSingleNewCaseName("");
setSingleNewCaseDescription("");
setExportTab("export");
setDrawerMode("select");
}}

View File

@ -55,7 +55,6 @@ type MultiExportDialogProps = {
children: React.ReactNode;
};
const NONE_CASE_OPTION = "none";
const NEW_CASE_OPTION = "new";
export default function MultiExportDialog({
@ -75,7 +74,10 @@ export default function MultiExportDialog({
const { data: cases } = useSWR<ExportCase[]>(isAdmin ? "cases" : null);
const [open, setOpen] = useState(false);
const [caseSelection, setCaseSelection] = useState<string>(NONE_CASE_OPTION);
// Single unified state: either NEW_CASE_OPTION or an existing case id.
// Defaults to NEW_CASE_OPTION, which is also the only valid value for
// non-admins since they can't attach to existing cases.
const [caseSelection, setCaseSelection] = useState<string>(NEW_CASE_OPTION);
const [newCaseName, setNewCaseName] = useState("");
const [newCaseDescription, setNewCaseDescription] = useState("");
const [isExporting, setIsExporting] = useState(false);
@ -132,7 +134,7 @@ export default function MultiExportDialog({
}, [t, locale]);
const resetState = useCallback(() => {
setCaseSelection(NONE_CASE_OPTION);
setCaseSelection(NEW_CASE_OPTION);
setNewCaseName("");
setNewCaseDescription("");
setIsExporting(false);
@ -144,7 +146,7 @@ export default function MultiExportDialog({
resetState();
} else {
// Freshly reset each time so the default name reflects "now"
setCaseSelection(NONE_CASE_OPTION);
setCaseSelection(NEW_CASE_OPTION);
setNewCaseName(defaultCaseName);
setNewCaseDescription("");
setIsExporting(false);
@ -183,7 +185,7 @@ export default function MultiExportDialog({
const payload: BatchExportBody = { items };
if (isAdmin && caseSelection !== NONE_CASE_OPTION) {
if (isAdmin) {
if (isNewCase) {
payload.new_case_name = newCaseName.trim();
payload.new_case_description = newCaseDescription.trim() || undefined;
@ -321,15 +323,12 @@ export default function MultiExportDialog({
<SelectValue placeholder={t("export.case.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectItem value={NONE_CASE_OPTION}>
{t("label.none", { ns: "common" })}
</SelectItem>
{existingCases.map((caseItem) => (
<SelectItem key={caseItem.id} value={caseItem.id}>
{caseItem.name}
</SelectItem>
))}
<SelectSeparator />
{existingCases.length > 0 && <SelectSeparator />}
<SelectItem value={NEW_CASE_OPTION}>
{t("export.case.newCaseOption")}
</SelectItem>