Skip to content

Commit a1f16d5

Browse files
Feature: Delete file/folder
1 parent 92da289 commit a1f16d5

6 files changed

Lines changed: 268 additions & 2 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client";
2+
3+
import Modal from "./shared/Modal";
4+
import Button from "./shared/Button";
5+
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
6+
import { FileItemData } from "./FileItem";
7+
8+
interface DeleteConfirmDialogProps {
9+
isOpen: boolean;
10+
onClose: () => void;
11+
file: FileItemData | null;
12+
onConfirm: () => void;
13+
isDeleting?: boolean;
14+
}
15+
16+
export default function DeleteConfirmDialog({
17+
isOpen,
18+
onClose,
19+
file,
20+
onConfirm,
21+
isDeleting = false,
22+
}: DeleteConfirmDialogProps) {
23+
if (!file) return null;
24+
25+
return (
26+
<Modal
27+
isOpen={isOpen}
28+
onClose={onClose}
29+
title={`Delete ${file.type === "folder" ? "Folder" : "File"}`}
30+
maxWidth="md"
31+
footer={
32+
<div className="flex justify-end gap-2">
33+
<Button
34+
variant="ghost"
35+
onClick={onClose}
36+
disabled={isDeleting}
37+
>
38+
Cancel
39+
</Button>
40+
<Button
41+
variant="primary"
42+
onClick={onConfirm}
43+
isLoading={isDeleting}
44+
disabled={isDeleting}
45+
className="bg-red-600 hover:bg-red-700 text-white"
46+
>
47+
Delete
48+
</Button>
49+
</div>
50+
}
51+
>
52+
<section className="py-4">
53+
<div className="flex items-start gap-3">
54+
<ExclamationTriangleIcon className="h-6 w-6 text-red-600 flex-shrink-0 mt-0.5" />
55+
<div className="flex-1">
56+
<p className="text-sm text-gray-700 mb-2">
57+
Are you sure you want to delete{" "}
58+
<span className="font-medium">"{file.name}"</span>?
59+
</p>
60+
{file.type === "folder" && (
61+
<p className="text-sm text-gray-500">
62+
This will permanently delete the folder and all its contents. This action cannot be undone.
63+
</p>
64+
)}
65+
{file.type === "file" && (
66+
<p className="text-sm text-gray-500">
67+
This action cannot be undone.
68+
</p>
69+
)}
70+
</div>
71+
</div>
72+
</section>
73+
</Modal>
74+
);
75+
}
76+

app/components/FileItem.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface FileItemProps {
2626
onCopy?: (file: FileItemData) => void;
2727
onMove?: (file: FileItemData) => void;
2828
onDownload?: (file: FileItemData) => void;
29+
onDelete?: (file: FileItemData) => void;
2930
isSelected?: boolean;
3031
}
3132

@@ -39,6 +40,7 @@ export default function FileItem({
3940
onCopy,
4041
onMove,
4142
onDownload,
43+
onDelete,
4244
isSelected = false,
4345
}: FileItemProps) {
4446
const [isHovered, setIsHovered] = useState(false);
@@ -136,7 +138,7 @@ export default function FileItem({
136138
onDownload={onDownload}
137139
onCopy={onCopy}
138140
onMove={onMove}
139-
onDelete={(f) => console.log("Delete:", f.name)}
141+
onDelete={onDelete}
140142
/>
141143
)}
142144
<div className="mb-1 flex h-12 w-12 items-center justify-center sm:mb-2 sm:h-16 sm:w-16">
@@ -184,7 +186,7 @@ export default function FileItem({
184186
onDownload={onDownload}
185187
onCopy={onCopy}
186188
onMove={onMove}
187-
onDelete={(f) => console.log("Delete:", f.name)}
189+
onDelete={onDelete}
188190
/>
189191
)}
190192
</section>

app/components/FileList.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface FileListProps {
1515
onFileCopy?: (file: FileItemData) => void;
1616
onFileMove?: (file: FileItemData) => void;
1717
onFileDownload?: (file: FileItemData) => void;
18+
onFileDelete?: (file: FileItemData) => void;
1819
selectedFileIds: string[];
1920
}
2021

@@ -30,6 +31,7 @@ export default function FileList({
3031
onFileCopy,
3132
onFileMove,
3233
onFileDownload,
34+
onFileDelete,
3335
selectedFileIds,
3436
}: FileListProps) {
3537
const [view, setView] = useState<"grid" | "list">(() => {
@@ -68,6 +70,7 @@ export default function FileList({
6870
onCopy={onFileCopy}
6971
onMove={onFileMove}
7072
onDownload={onFileDownload}
73+
onDelete={onFileDelete}
7174
isSelected={selectedFileIds.includes(file.id)}
7275
/>
7376
))}
@@ -86,6 +89,7 @@ export default function FileList({
8689
onCopy={onFileCopy}
8790
onMove={onFileMove}
8891
onDownload={onFileDownload}
92+
onDelete={onFileDelete}
8993
isSelected={selectedFileIds.includes(file.id)}
9094
/>
9195
))}

app/components/FileManager.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import NewFolderDialog from "./NewFolderDialog";
1313
import RenameDialog from "./RenameDialog";
1414
import PreviewModal from "./PreviewModal";
1515
import MoveDialog from "./MoveDialog";
16+
import DeleteConfirmDialog from "./DeleteConfirmDialog";
1617
import FileUploadHandler from "./FileUploadHandler";
1718
import { FileItemData } from "./FileItem";
1819
import LoadingSpinner from "./shared/LoadingSpinner";
@@ -25,6 +26,8 @@ import {
2526
copyFolderResource,
2627
downloadFile,
2728
downloadFolderAsZip,
29+
deleteFileResource,
30+
deleteFolderResource,
2831
} from "../lib/helpers";
2932

3033

@@ -51,6 +54,9 @@ export default function FileManager() {
5154
const [fileToPreview, setFileToPreview] = useState<FileItemData | null>(null);
5255
const [showMoveDialog, setShowMoveDialog] = useState(false);
5356
const [fileToMove, setFileToMove] = useState<FileItemData | null>(null);
57+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
58+
const [fileToDelete, setFileToDelete] = useState<FileItemData | null>(null);
59+
const [isDeleting, setIsDeleting] = useState(false);
5460

5561
const [savedUrl, setSavedUrl] = useState<string | null>(() => {
5662
if (typeof window === "undefined") return null;
@@ -264,6 +270,47 @@ export default function FileManager() {
264270
setRefreshKey((prev) => prev + 1);
265271
};
266272

273+
const handleDelete = (file: FileItemData) => {
274+
setFileToDelete(file);
275+
setShowDeleteDialog(true);
276+
};
277+
278+
const handleDeleteConfirm = async () => {
279+
if (!fileToDelete) {
280+
return;
281+
}
282+
283+
setIsDeleting(true);
284+
const toastId = toast.loading(
285+
`Deleting "${fileToDelete.name}"...`
286+
);
287+
288+
try {
289+
const { fetch: fetchFn } = getAuthenticatedSession();
290+
291+
if (fileToDelete.type === "folder") {
292+
await deleteFolderResource(fileToDelete.url, fetchFn);
293+
} else {
294+
await deleteFileResource(fileToDelete.url, fetchFn);
295+
}
296+
297+
toast.success(`Deleted "${fileToDelete.name}"`, { id: toastId });
298+
setShowDeleteDialog(false);
299+
setFileToDelete(null);
300+
setRefreshKey((prev) => prev + 1);
301+
} catch (error) {
302+
console.error("Failed to delete resource:", error);
303+
toast.error(
304+
error instanceof Error
305+
? `Failed to delete: ${error.message}`
306+
: "Failed to delete resource",
307+
{ id: toastId }
308+
);
309+
} finally {
310+
setIsDeleting(false);
311+
}
312+
};
313+
267314
const handleDownload = async (file: FileItemData) => {
268315
if (!file) {
269316
return;
@@ -500,6 +547,7 @@ export default function FileManager() {
500547
onFileCopy={handleCopy}
501548
onFileMove={handleMove}
502549
onFileDownload={handleDownload}
550+
onFileDelete={handleDelete}
503551
selectedFileIds={selectedFileIds}
504552
/>
505553
</div>
@@ -554,6 +602,16 @@ export default function FileManager() {
554602
currentLocationUrl={getCurrentLocationUrl()}
555603
onMoved={handleMoved}
556604
/>
605+
<DeleteConfirmDialog
606+
isOpen={showDeleteDialog}
607+
onClose={() => {
608+
setShowDeleteDialog(false);
609+
setFileToDelete(null);
610+
}}
611+
file={fileToDelete}
612+
onConfirm={handleDeleteConfirm}
613+
isDeleting={isDeleting}
614+
/>
557615
<FileUploadHandler
558616
currentContainerUrl={containerUrlToBrowse}
559617
onUploadComplete={handleFileUploaded}

app/lib/helpers/deleteUtils.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {
2+
deleteFile,
3+
getSolidDataset,
4+
getContainedResourceUrlAll,
5+
isContainer,
6+
UrlString,
7+
} from "@inrupt/solid-client";
8+
import { ensureTrailingSlash } from "./copyUtils";
9+
10+
/**
11+
* Recursively deletes all contents of a folder, then deletes the folder itself
12+
*/
13+
async function deleteFolderContents(
14+
folderUrl: string,
15+
fetchFn: typeof fetch,
16+
visited: Set<string> = new Set()
17+
): Promise<void> {
18+
const normalizedUrl = ensureTrailingSlash(folderUrl);
19+
20+
// Prevent infinite loops
21+
if (visited.has(normalizedUrl)) {
22+
return;
23+
}
24+
visited.add(normalizedUrl);
25+
26+
try {
27+
const dataset = await getSolidDataset(normalizedUrl as UrlString, { fetch: fetchFn });
28+
const containedResources = getContainedResourceUrlAll(dataset);
29+
30+
// Delete all contained resources
31+
for (const resourceUrl of containedResources) {
32+
try {
33+
if (isContainer(resourceUrl)) {
34+
// It's a folder - recursively delete its contents first, then the folder
35+
await deleteFolderContents(resourceUrl, fetchFn, visited);
36+
// Delete the folder itself
37+
await deleteFile(resourceUrl as UrlString, { fetch: fetchFn });
38+
39+
// Also delete the .meta file if it exists
40+
try {
41+
const metaUrl = `${resourceUrl}.meta` as UrlString;
42+
await deleteFile(metaUrl, { fetch: fetchFn });
43+
} catch {
44+
// Ignore if .meta file doesn't exist
45+
}
46+
} else {
47+
// It's a file - delete it
48+
await deleteFile(resourceUrl as UrlString, { fetch: fetchFn });
49+
50+
// Also delete the .meta file if it exists
51+
try {
52+
const metaUrl = `${resourceUrl}.meta` as UrlString;
53+
await deleteFile(metaUrl, { fetch: fetchFn });
54+
} catch {
55+
// Ignore if .meta file doesn't exist
56+
}
57+
}
58+
} catch (error) {
59+
console.warn(`Failed to delete resource ${resourceUrl}:`, error);
60+
61+
}
62+
}
63+
} catch (error) {
64+
console.error(`Failed to access folder ${folderUrl}:`, error);
65+
throw error;
66+
}
67+
}
68+
69+
/**
70+
* Deletes a file resource
71+
*/
72+
export async function deleteFileResource(
73+
fileUrl: string,
74+
fetchFn: typeof fetch
75+
): Promise<void> {
76+
try {
77+
// Delete the file
78+
await deleteFile(fileUrl as UrlString, { fetch: fetchFn });
79+
80+
// Also try to delete the .meta file if it exists
81+
try {
82+
const metaUrl = `${fileUrl}.meta` as UrlString;
83+
await deleteFile(metaUrl, { fetch: fetchFn });
84+
} catch {
85+
// Ignore if .meta file doesn't exist
86+
}
87+
} catch (error) {
88+
console.error("Failed to delete file:", error);
89+
throw new Error(
90+
`Failed to delete file: ${error instanceof Error ? error.message : "Unknown error"}`
91+
);
92+
}
93+
}
94+
95+
/**
96+
* Deletes a folder resource and all its contents recursively
97+
*/
98+
export async function deleteFolderResource(
99+
folderUrl: string,
100+
fetchFn: typeof fetch
101+
): Promise<void> {
102+
try {
103+
const normalizedUrl = ensureTrailingSlash(folderUrl);
104+
105+
// First, delete all contents recursively
106+
await deleteFolderContents(normalizedUrl, fetchFn);
107+
108+
// Then delete the folder itself
109+
await deleteFile(normalizedUrl as UrlString, { fetch: fetchFn });
110+
111+
// Also try to delete the .meta file if it exists
112+
try {
113+
const metaUrl = `${normalizedUrl}.meta` as UrlString;
114+
await deleteFile(metaUrl, { fetch: fetchFn });
115+
} catch {
116+
// Ignore if .meta file doesn't exist
117+
}
118+
} catch (error) {
119+
console.error("Failed to delete folder:", error);
120+
throw new Error(
121+
`Failed to delete folder: ${error instanceof Error ? error.message : "Unknown error"}`
122+
);
123+
}
124+
}
125+

app/lib/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export * from "./sessionUtils";
1010
export * from "./metaFileUtils";
1111
export * from "./copyUtils";
1212
export * from "./downloadUtils";
13+
export * from "./deleteUtils";
1314

0 commit comments

Comments
 (0)