From fd173e998b9b197c9abd8b7dbf592b54b65d29f4 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 2 Dec 2024 08:34:33 -0600 Subject: [PATCH] multi select in explore --- web/src/components/card/SearchThumbnail.tsx | 24 +++-- web/src/views/explore/ExploreView.tsx | 10 +-- web/src/views/search/SearchView.tsx | 99 +++++++++++++++------ 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/web/src/components/card/SearchThumbnail.tsx b/web/src/components/card/SearchThumbnail.tsx index e96632400..a7ea574b3 100644 --- a/web/src/components/card/SearchThumbnail.tsx +++ b/web/src/components/card/SearchThumbnail.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { useApiHost } from "@/api"; import { getIconForLabel } from "@/utils/iconUtil"; import useSWR from "swr"; @@ -12,10 +12,12 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { SearchResult } from "@/types/search"; import { cn } from "@/lib/utils"; import { TooltipPortal } from "@radix-ui/react-tooltip"; +import usePress from "@/hooks/use-press"; +import useContextMenu from "@/hooks/use-contextmenu"; type SearchThumbnailProps = { searchResult: SearchResult; - onClick: (searchResult: SearchResult) => void; + onClick: (searchResult: SearchResult, ctrl: boolean) => void; }; export default function SearchThumbnail({ @@ -28,9 +30,14 @@ export default function SearchThumbnail({ // interactions - const handleOnClick = useCallback(() => { - onClick(searchResult); - }, [searchResult, onClick]); + useContextMenu(imgRef, () => { + onClick(searchResult, true); + }); + + const bindClickAndLongPress = usePress({ + onLongPress: () => onClick(searchResult, true), + onPress: () => onClick(searchResult, false), + })(); const objectLabel = useMemo(() => { if ( @@ -45,7 +52,10 @@ export default function SearchThumbnail({ }, [config, searchResult]); return ( -
+
onClick(searchResult)} + onClick={() => onClick(searchResult, false)} > {getIconForLabel(objectLabel, "size-3 text-white")} {Math.round( diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index caff874d4..0ec825416 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -26,7 +26,7 @@ type ExploreViewProps = { searchDetail: SearchResult | undefined; setSearchDetail: (search: SearchResult | undefined) => void; setSimilaritySearch: (search: SearchResult) => void; - onSelectSearch: (item: SearchResult, page?: SearchTab) => void; + onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; export default function ExploreView({ @@ -125,7 +125,7 @@ type ThumbnailRowType = { setSearchDetail: (search: SearchResult | undefined) => void; mutate: () => void; setSimilaritySearch: (search: SearchResult) => void; - onSelectSearch: (item: SearchResult, page?: SearchTab) => void; + onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; function ThumbnailRow({ @@ -205,7 +205,7 @@ type ExploreThumbnailImageProps = { setSearchDetail: (search: SearchResult | undefined) => void; mutate: () => void; setSimilaritySearch: (search: SearchResult) => void; - onSelectSearch: (item: SearchResult, page?: SearchTab) => void; + onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; }; function ExploreThumbnailImage({ event, @@ -225,11 +225,11 @@ function ExploreThumbnailImage({ }; const handleShowObjectLifecycle = () => { - onSelectSearch(event, "object lifecycle"); + onSelectSearch(event, false, "object lifecycle"); }; const handleShowSnapshot = () => { - onSelectSearch(event, "snapshot"); + onSelectSearch(event, false, "snapshot"); }; return ( diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 5d29758fc..0b009ec1b 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -30,6 +30,7 @@ import { } from "@/components/ui/tooltip"; import Chip from "@/components/indicators/Chip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; +import SearchActionGroup from "@/components/filter/SearchActionGroup"; type SearchViewProps = { search: string; @@ -185,8 +186,8 @@ export default function SearchView({ const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const onSelectSearch = useCallback( - (item: SearchResult, page: SearchTab = "details") => { - if (selectedObjects.length > 1) { + (item: SearchResult, ctrl: boolean, page: SearchTab = "details") => { + if (selectedObjects.length > 1 || ctrl) { const index = selectedObjects.indexOf(item.id); if (index != -1) { @@ -207,19 +208,37 @@ export default function SearchView({ } else { setSelectedObjects([item.id]); } - setPage(page); - setSearchDetail(item); + if (!ctrl) { + setPage(page); + setSearchDetail(item); + } else { + setSearchDetail(undefined); + } }, [selectedObjects], ); + const onSelectAllObjects = useCallback(() => { + if (!uniqueResults || uniqueResults.length == 0) { + return; + } + + if (selectedObjects.length < uniqueResults.length) { + setSelectedObjects(uniqueResults.map((value) => value.id)); + } else { + setSelectedObjects([]); + } + }, [uniqueResults, selectedObjects]); + useEffect(() => { if (uniqueResults && uniqueResults.length > 0) { setSelectedObjects([uniqueResults[0].id]); } else { setSelectedObjects([]); } - }, [searchTerm, searchFilter, uniqueResults]); + // only select first item when search term or filter changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchTerm, searchFilter]); // confidence score @@ -267,6 +286,11 @@ export default function SearchView({ } switch (key) { + case "a": + if (modifiers.ctrl) { + onSelectAllObjects(); + } + break; case "ArrowLeft": setSelectedObjects((prevSelected) => { if (uniqueResults.length === 0) return prevSelected; @@ -324,11 +348,11 @@ export default function SearchView({ break; } }, - [uniqueResults, inputFocused], + [uniqueResults, inputFocused, onSelectAllObjects], ); useKeyboardListener( - ["ArrowLeft", "ArrowRight", "PageDown", "PageUp"], + ["a", "ArrowLeft", "ArrowRight", "PageDown", "PageUp"], onKeyboardShortcut, !inputFocused, ); @@ -336,7 +360,7 @@ export default function SearchView({ // scroll into view useEffect(() => { - if (selectedObjects.length > 0 && uniqueResults && itemRefs.current) { + if (selectedObjects.length == 1 && uniqueResults && itemRefs.current) { const selectedIndex = uniqueResults.findIndex( (result) => result.id === selectedObjects[0], ); @@ -420,22 +444,39 @@ export default function SearchView({ {hasExistingSearch && (
- - - + {selectedObjects.length <= 1 ? ( + <> + + + + + ) : ( +
+ +
+ )}
)} @@ -479,7 +520,9 @@ export default function SearchView({ > onSelectSearch(value)} + onClick={(value: SearchResult, ctrl: boolean) => { + onSelectSearch(value, ctrl); + }} /> {(searchTerm || searchFilter?.search_type?.includes("similarity")) && ( @@ -520,9 +563,11 @@ export default function SearchView({ }} refreshResults={refresh} showObjectLifecycle={() => - onSelectSearch(value, "object lifecycle") + onSelectSearch(value, false, "object lifecycle") + } + showSnapshot={() => + onSelectSearch(value, false, "snapshot") } - showSnapshot={() => onSelectSearch(value, "snapshot")} />