|
@@ -5,7 +5,7 @@ import {
|
|
|
CheckCircleIcon,
|
|
CheckCircleIcon,
|
|
|
EyeSlashIcon,
|
|
EyeSlashIcon,
|
|
|
Squares2X2Icon,
|
|
Squares2X2Icon,
|
|
|
- TrashIcon
|
|
|
|
|
|
|
+ TrashIcon,
|
|
|
} from "@heroicons/react/24/outline";
|
|
} from "@heroicons/react/24/outline";
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
@@ -61,14 +61,14 @@ export default function DuplicateList() {
|
|
|
data: duplicateGroups,
|
|
data: duplicateGroups,
|
|
|
isLoading,
|
|
isLoading,
|
|
|
error,
|
|
error,
|
|
|
- refetch
|
|
|
|
|
|
|
+ refetch,
|
|
|
} = useQuery<DuplicateGroup[]>({
|
|
} = useQuery<DuplicateGroup[]>({
|
|
|
queryKey: ["duplicate-files"],
|
|
queryKey: ["duplicate-files"],
|
|
|
- queryFn: async () => get("/maintenance/duplicates?status=pending")
|
|
|
|
|
|
|
+ queryFn: async () => get("/maintenance/duplicates?status=pending"),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const [enabledDatasets, setEnabledDatasets] = useState<Set<string>>(
|
|
const [enabledDatasets, setEnabledDatasets] = useState<Set<string>>(
|
|
|
- new Set()
|
|
|
|
|
|
|
+ new Set(),
|
|
|
);
|
|
);
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
const [sortField, setSortField] = useState<SortField>("count");
|
|
const [sortField, setSortField] = useState<SortField>("count");
|
|
@@ -78,7 +78,7 @@ export default function DuplicateList() {
|
|
|
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
|
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
|
|
const [deleteSelection, setDeleteSelection] = useState<DeleteSelection>({
|
|
const [deleteSelection, setDeleteSelection] = useState<DeleteSelection>({
|
|
|
isOpen: false,
|
|
isOpen: false,
|
|
|
- groups: []
|
|
|
|
|
|
|
+ groups: [],
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Initialize enabled datasets on first load
|
|
// Initialize enabled datasets on first load
|
|
@@ -91,22 +91,28 @@ export default function DuplicateList() {
|
|
|
}
|
|
}
|
|
|
}, [datasets]);
|
|
}, [datasets]);
|
|
|
|
|
|
|
|
|
|
+ const [scanController, setScanController] = useState<AbortController | null>(
|
|
|
|
|
+ null,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
const scanMutation = useMutation({
|
|
const scanMutation = useMutation({
|
|
|
- mutationFn: () =>
|
|
|
|
|
- post("/maintenance/duplicates/scan", { resetExisting: false }),
|
|
|
|
|
|
|
+ mutationFn: ({ signal }: { signal?: AbortSignal } = {}) =>
|
|
|
|
|
+ post("/maintenance/duplicates/scan", { resetExisting: false }, signal),
|
|
|
onSuccess: () => {
|
|
onSuccess: () => {
|
|
|
toast.success("Duplicate scan completed");
|
|
toast.success("Duplicate scan completed");
|
|
|
addNotification({
|
|
addNotification({
|
|
|
type: "info",
|
|
type: "info",
|
|
|
title: "Scan Finished",
|
|
title: "Scan Finished",
|
|
|
- message: "Duplicate scan completed. Refreshing results."
|
|
|
|
|
|
|
+ message: "Duplicate scan completed. Refreshing results.",
|
|
|
});
|
|
});
|
|
|
refetch();
|
|
refetch();
|
|
|
},
|
|
},
|
|
|
onError: (err: any) => {
|
|
onError: (err: any) => {
|
|
|
|
|
+ if (err.message === "Request aborted") return;
|
|
|
console.error(err);
|
|
console.error(err);
|
|
|
toast.error("Failed to start duplicate scan");
|
|
toast.error("Failed to start duplicate scan");
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ onSettled: () => setScanController(null),
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const markNotDuplicateMutation = useMutation({
|
|
const markNotDuplicateMutation = useMutation({
|
|
@@ -115,9 +121,9 @@ export default function DuplicateList() {
|
|
|
ids.map((id) =>
|
|
ids.map((id) =>
|
|
|
post(`/maintenance/duplicates/${id}/mark`, {
|
|
post(`/maintenance/duplicates/${id}/mark`, {
|
|
|
status: "reviewed",
|
|
status: "reviewed",
|
|
|
- note: "not_duplicate"
|
|
|
|
|
- })
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ note: "not_duplicate",
|
|
|
|
|
+ }),
|
|
|
|
|
+ ),
|
|
|
);
|
|
);
|
|
|
},
|
|
},
|
|
|
onSuccess: () => {
|
|
onSuccess: () => {
|
|
@@ -128,7 +134,7 @@ export default function DuplicateList() {
|
|
|
onError: (err: any) => {
|
|
onError: (err: any) => {
|
|
|
console.error(err);
|
|
console.error(err);
|
|
|
toast.error("Failed to update duplicates");
|
|
toast.error("Failed to update duplicates");
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const deleteMutation = useMutation({
|
|
const deleteMutation = useMutation({
|
|
@@ -137,9 +143,9 @@ export default function DuplicateList() {
|
|
|
await Promise.all(
|
|
await Promise.all(
|
|
|
groups.map((group) =>
|
|
groups.map((group) =>
|
|
|
post(`/maintenance/duplicates/${group.id}/purge`, {
|
|
post(`/maintenance/duplicates/${group.id}/purge`, {
|
|
|
- files: group.files
|
|
|
|
|
- })
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ files: group.files,
|
|
|
|
|
+ }),
|
|
|
|
|
+ ),
|
|
|
);
|
|
);
|
|
|
},
|
|
},
|
|
|
onSuccess: () => {
|
|
onSuccess: () => {
|
|
@@ -152,7 +158,7 @@ export default function DuplicateList() {
|
|
|
onError: (err: any) => {
|
|
onError: (err: any) => {
|
|
|
console.error(err);
|
|
console.error(err);
|
|
|
toast.error("Failed to delete selected files");
|
|
toast.error("Failed to delete selected files");
|
|
|
- }
|
|
|
|
|
|
|
+ },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const filteredData = useMemo(() => {
|
|
const filteredData = useMemo(() => {
|
|
@@ -250,7 +256,7 @@ export default function DuplicateList() {
|
|
|
|
|
|
|
|
const groups = Array.from(grouped.entries()).map(([id, files]) => ({
|
|
const groups = Array.from(grouped.entries()).map(([id, files]) => ({
|
|
|
id,
|
|
id,
|
|
|
- files
|
|
|
|
|
|
|
+ files,
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
setDeleteSelection({ isOpen: true, groups });
|
|
setDeleteSelection({ isOpen: true, groups });
|
|
@@ -304,12 +310,35 @@ export default function DuplicateList() {
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<div className="flex flex-wrap gap-2">
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => scanMutation.mutate()}
|
|
|
|
|
- className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ addNotification({
|
|
|
|
|
+ type: "info",
|
|
|
|
|
+ title: "Scan Started",
|
|
|
|
|
+ message: "Scanning for duplicates in /Volumes/Shares/.Private",
|
|
|
|
|
+ });
|
|
|
|
|
+ const controller = new AbortController();
|
|
|
|
|
+ setScanController(controller);
|
|
|
|
|
+ scanMutation.mutate({ signal: controller.signal });
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabled={scanMutation.isPending}
|
|
|
|
|
+ className="inline-flex items-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
>
|
|
>
|
|
|
- <ArrowPathIcon className="h-4 w-4" />
|
|
|
|
|
- Rescan
|
|
|
|
|
|
|
+ <ArrowPathIcon
|
|
|
|
|
+ className={`h-4 w-4 ${scanMutation.isPending ? "animate-spin" : ""}`}
|
|
|
|
|
+ />
|
|
|
|
|
+ {scanMutation.isPending ? "Scanning..." : "Rescan"}
|
|
|
</button>
|
|
</button>
|
|
|
|
|
+ {scanController && (
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ scanController.abort();
|
|
|
|
|
+ setScanController(null);
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="inline-flex items-center gap-2 rounded-md bg-gray-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ Cancel Scan
|
|
|
|
|
+ </button>
|
|
|
|
|
+ )}
|
|
|
<button
|
|
<button
|
|
|
onClick={markSelectedNotDuplicate}
|
|
onClick={markSelectedNotDuplicate}
|
|
|
disabled={selectedGroups.size === 0}
|
|
disabled={selectedGroups.size === 0}
|
|
@@ -438,7 +467,7 @@ export default function DuplicateList() {
|
|
|
{filteredData.map((group) => {
|
|
{filteredData.map((group) => {
|
|
|
const isExpanded = expandedRows.has(group.id);
|
|
const isExpanded = expandedRows.has(group.id);
|
|
|
const allSelected = group.files.every((f) =>
|
|
const allSelected = group.files.every((f) =>
|
|
|
- selectedFiles.has(makeFileKey(group.id, f))
|
|
|
|
|
|
|
+ selectedFiles.has(makeFileKey(group.id, f)),
|
|
|
);
|
|
);
|
|
|
return (
|
|
return (
|
|
|
<>
|
|
<>
|