From b6eaeaad8a9638c0b41fcefdb4568df222238838 Mon Sep 17 00:00:00 2001 From: notsafeforgit Date: Fri, 13 Mar 2026 16:37:38 -0700 Subject: [PATCH] feat: add edit and delete actions to image duplicate checker This adds checkboxes to select duplicate images and integrates the existing EditImagesDialog and DeleteImagesDialog, allowing users to resolve duplicates directly from the tool. --- .../ImageDuplicateChecker.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx b/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx index f78141335..b96fab050 100644 --- a/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx +++ b/ui/v2.5/src/components/ImageDuplicateChecker/ImageDuplicateChecker.tsx @@ -7,6 +7,9 @@ import { Row, Col, Card, + ButtonGroup, + OverlayTrigger, + Tooltip, } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { useFindDuplicateImagesQuery } from "src/core/generated-graphql"; @@ -17,6 +20,10 @@ import { ErrorMessage } from "../Shared/ErrorMessage"; import { FileSize } from "../Shared/FileSize"; import { Pagination } from "src/components/List/Pagination"; import { useHistory } from "react-router-dom"; +import { DeleteImagesDialog } from "../Images/DeleteImagesDialog"; +import { EditImagesDialog } from "../Images/EditImagesDialog"; +import { Icon } from "../Shared/Icon"; +import { faPencilAlt, faTrash } from "@fortawesome/free-solid-svg-icons"; const ImageDuplicateCheckerSection = PatchContainerComponent( "ImageDuplicateCheckerSection" @@ -32,6 +39,10 @@ const ImageDuplicateChecker: React.FC = () => { const [isSearching, setIsSearching] = useState(false); const [hasSearched, setHasSearched] = useState(false); + const [checkedImages, setCheckedImages] = useState>({}); + const [selectedImages, setSelectedImages] = useState(); + const [deletingImages, setDeletingImages] = useState(false); + const [editingImages, setEditingImages] = useState(false); const { data, loading, error, refetch } = useFindDuplicateImagesQuery({ variables: { distance: hashDistance }, @@ -42,6 +53,7 @@ const ImageDuplicateChecker: React.FC = () => { const handleSearch = () => { setIsSearching(true); setHasSearched(true); + setCheckedImages({}); refetch({ distance: hashDistance }).finally(() => setIsSearching(false)); }; @@ -67,6 +79,40 @@ const ImageDuplicateChecker: React.FC = () => { return allGroups.slice(start, start + pageSize); }, [allGroups, currentPage, pageSize]); + const checkCount = Object.keys(checkedImages).filter((id) => checkedImages[id]).length; + + const handleCheck = (checked: boolean, imageID: string) => { + setCheckedImages({ ...checkedImages, [imageID]: checked }); + }; + + const handleDeleteChecked = () => { + setSelectedImages(allGroups.flat().filter((i) => checkedImages[i.id])); + setDeletingImages(true); + }; + + const onEdit = () => { + setSelectedImages(allGroups.flat().filter((i) => checkedImages[i.id])); + setEditingImages(true); + setCheckedImages({}); + }; + + const onDeleteDialogClosed = (confirmed: boolean) => { + setDeletingImages(false); + setSelectedImages(undefined); + if (confirmed) { + setCheckedImages({}); + refetch(); + } + }; + + const onEditDialogClosed = (applied: boolean) => { + setEditingImages(false); + setSelectedImages(undefined); + if (applied) { + refetch(); + } + }; + if (error) return ; const renderGroup = (group: GQL.ImageDataFragment[], index: number) => { @@ -83,6 +129,7 @@ const ImageDuplicateChecker: React.FC = () => { + @@ -94,6 +141,12 @@ const ImageDuplicateChecker: React.FC = () => { const file = img.visual_files[0]; return ( +

@@ -191,6 +256,40 @@ const ImageDuplicateChecker: React.FC = () => { )} + {hasSearched && !loading && !error && allGroups.length > 0 && ( +
+
+ Found {allGroups.length} duplicate groups +
+ {checkCount > 0 && ( + + + {intl.formatMessage({ id: "actions.edit" })} + + } + > + + + + {intl.formatMessage({ id: "actions.delete" })} + + } + > + + + + )} +
+ )} + {pagedGroups.map((group, index) => renderGroup(group, index))} {allGroups.length > pageSize && (

Image Details Size
+ handleCheck(e.currentTarget.checked, img.id)} + /> + { return (
+ {deletingImages && selectedImages && ( + + )} + {editingImages && selectedImages && ( + + )}