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")}
/>