Skip to content

Commit b715ce5

Browse files
authored
Added a delete all imported folders button/action. (#101)
closes #82
1 parent ec3ce15 commit b715ce5

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

frontend/src/api/inbox.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,14 @@ function deleteFromFolder(
177177
}
178178
}
179179
}
180+
181+
export function* walkFolder(folder: Folder): Generator<Folder | File> {
182+
yield folder;
183+
for (const child of folder.children) {
184+
if (child.type === "directory") {
185+
yield* walkFolder(child);
186+
} else {
187+
yield child;
188+
}
189+
}
190+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { Trash2Icon } from "lucide-react";
2+
import { useMemo, useState } from "react";
3+
import {
4+
Box,
5+
Button,
6+
DialogContent,
7+
Divider,
8+
Typography,
9+
useTheme,
10+
} from "@mui/material";
11+
import { useMutation, useQueries } from "@tanstack/react-query";
12+
13+
import { deleteFoldersMutationOptions, walkFolder } from "@/api/inbox";
14+
import { sessionQueryOptions } from "@/api/session";
15+
import { FolderStatusChip } from "@/components/common/chips";
16+
import { Dialog } from "@/components/common/dialogs";
17+
import { FolderTypeIcon } from "@/components/common/icons";
18+
import { Folder, Progress } from "@/pythonTypes";
19+
20+
/**
21+
* A button component that allows bulk deletion of imported folders.
22+
*
23+
* Features:
24+
* - Lists all imported folders (with `IMPORT_COMPLETED` status) under the given root folder.
25+
* - Provides a confirmation dialog before deletion.
26+
* - Supports shift+click to skip confirmation and delete immediately.
27+
*
28+
*/
29+
export function DeleteImportedFoldersButton({ folder }: { folder: Folder }) {
30+
const theme = useTheme();
31+
const [open, setOpen] = useState(false);
32+
33+
/** Get all folders that have a session with
34+
* `status.progress` equal to `Progress.IMPORT_COMPLETED`.
35+
*/
36+
const folders = useMemo(() => {
37+
const fs = [];
38+
for (const f of walkFolder(folder)) {
39+
if (f.type === "file") continue; // skip files
40+
if (f.full_path === folder.full_path) continue; // skip the root folder
41+
fs.push(f);
42+
}
43+
return fs;
44+
}, [folder]);
45+
46+
const sessions = useQueries({
47+
queries: folders.map((f) =>
48+
sessionQueryOptions({ folderHash: f.hash, folderPath: f.full_path })
49+
),
50+
});
51+
52+
const importedFolders = useMemo(() => {
53+
return folders.filter((f, i) => {
54+
const session = sessions[i];
55+
return session.data?.status.progress === Progress.IMPORT_COMPLETED;
56+
});
57+
}, [folders, sessions]);
58+
59+
/** Delete folder mutation */
60+
const { mutateAsync: deleteFolders, isPending } = useMutation(
61+
deleteFoldersMutationOptions
62+
);
63+
64+
return (
65+
<>
66+
<Button
67+
onClick={(e) => {
68+
// On shift + click, delete all imported folders without confirmation
69+
if (e.shiftKey) {
70+
deleteFolders({
71+
folderPaths: importedFolders.map((f) => f.full_path),
72+
folderHashes: importedFolders.map((f) => f.hash),
73+
}).catch((err) => {
74+
console.error("Failed to delete folders:", err);
75+
});
76+
return;
77+
}
78+
79+
setOpen(true);
80+
}}
81+
loading={isPending}
82+
color="secondary"
83+
startIcon={<Trash2Icon size={theme.iconSize.md} />}
84+
variant="outlined"
85+
>
86+
Delete All Imported Folders
87+
</Button>
88+
89+
<Dialog
90+
open={open}
91+
onClose={() => setOpen(false)}
92+
title="Delete all imported folders? "
93+
title_icon={<Trash2Icon size={theme.iconSize.lg} />}
94+
color="secondary"
95+
>
96+
<DialogContent>
97+
<Typography variant="body2" color="text.secondary">
98+
Are you sure you want to delete all imported folders? This will
99+
delete the following folders:
100+
</Typography>
101+
<Box
102+
sx={{
103+
overflowY: "auto",
104+
marginTop: 2,
105+
marginBottom: 2,
106+
display: "flex",
107+
flexDirection: "column",
108+
gap: 1,
109+
}}
110+
>
111+
{importedFolders.map((f, i) => (
112+
<Box
113+
key={i}
114+
sx={{
115+
display: "flex",
116+
justifyContent: "space-between",
117+
alignItems: "center",
118+
pl: 1,
119+
}}
120+
>
121+
<Box
122+
sx={{
123+
display: "flex",
124+
alignItems: "center",
125+
gap: 1,
126+
}}
127+
>
128+
<FolderTypeIcon
129+
isAlbum={f.is_album}
130+
isOpen={false}
131+
size={theme.iconSize.md}
132+
/>
133+
<Typography variant="body1" fontWeight={"bold"}>
134+
{f.full_path}
135+
</Typography>
136+
</Box>
137+
<FolderStatusChip folder={f} />
138+
</Box>
139+
))}
140+
{importedFolders.length === 0 && (
141+
<Typography variant="body1">
142+
No imported folders to delete!
143+
</Typography>
144+
)}
145+
</Box>
146+
<Typography variant="body2" color="text.secondary">
147+
This action cannot be undone! All files inside the folders will
148+
be permanently deleted.
149+
</Typography>
150+
<Divider sx={{ marginY: 2 }} />
151+
<Box
152+
sx={{
153+
display: "flex",
154+
justifyContent: "space-between",
155+
gap: 1,
156+
}}
157+
>
158+
<Button
159+
variant="outlined"
160+
color="secondary"
161+
onClick={() => setOpen(false)}
162+
>
163+
Cancel
164+
</Button>
165+
<Button
166+
variant="contained"
167+
color="secondary"
168+
onClick={() => {
169+
deleteFolders({
170+
folderPaths: importedFolders.map(
171+
(f) => f.full_path
172+
),
173+
folderHashes: importedFolders.map((f) => f.hash),
174+
}).catch((err) => {
175+
console.error("Failed to delete folders:", err);
176+
});
177+
setOpen(false);
178+
}}
179+
disabled={isPending || importedFolders.length === 0}
180+
>
181+
Delete
182+
</Button>
183+
</Box>
184+
</DialogContent>
185+
</Dialog>
186+
</>
187+
);
188+
}

frontend/src/components/inbox/cards/inboxCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from "@/components/inbox/fileTree";
1313
import { Folder } from "@/pythonTypes";
1414

15+
import { DeleteImportedFoldersButton } from "../actions2/deleteFolders";
16+
1517
export function InboxCard({ folder }: { folder: Folder }) {
1618
const config = useConfig();
1719

@@ -158,6 +160,7 @@ export function InboxCard({ folder }: { folder: Folder }) {
158160
<SelectedStats />
159161
</CardContent>
160162
<FolderActionDesktopBar />
163+
<DeleteImportedFoldersButton folder={folder} />
161164
{/* <FolderActionsSpeedDial /> */}
162165
</Card>
163166
);

0 commit comments

Comments
 (0)