multi select in explore

This commit is contained in:
Josh Hawkins 2024-12-02 08:34:33 -06:00
parent 96a8caa9b2
commit fd173e998b
3 changed files with 94 additions and 39 deletions

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import useSWR from "swr"; import useSWR from "swr";
@ -12,10 +12,12 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil";
import { SearchResult } from "@/types/search"; import { SearchResult } from "@/types/search";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import usePress from "@/hooks/use-press";
import useContextMenu from "@/hooks/use-contextmenu";
type SearchThumbnailProps = { type SearchThumbnailProps = {
searchResult: SearchResult; searchResult: SearchResult;
onClick: (searchResult: SearchResult) => void; onClick: (searchResult: SearchResult, ctrl: boolean) => void;
}; };
export default function SearchThumbnail({ export default function SearchThumbnail({
@ -28,9 +30,14 @@ export default function SearchThumbnail({
// interactions // interactions
const handleOnClick = useCallback(() => { useContextMenu(imgRef, () => {
onClick(searchResult); onClick(searchResult, true);
}, [searchResult, onClick]); });
const bindClickAndLongPress = usePress({
onLongPress: () => onClick(searchResult, true),
onPress: () => onClick(searchResult, false),
})();
const objectLabel = useMemo(() => { const objectLabel = useMemo(() => {
if ( if (
@ -45,7 +52,10 @@ export default function SearchThumbnail({
}, [config, searchResult]); }, [config, searchResult]);
return ( return (
<div className="relative size-full cursor-pointer" onClick={handleOnClick}> <div
className="relative size-full cursor-pointer"
{...bindClickAndLongPress}
>
<ImageLoadingIndicator <ImageLoadingIndicator
className="absolute inset-0" className="absolute inset-0"
imgLoaded={imgLoaded} imgLoaded={imgLoaded}
@ -79,7 +89,7 @@ export default function SearchThumbnail({
<div className="mx-3 pb-1 text-sm text-white"> <div className="mx-3 pb-1 text-sm text-white">
<Chip <Chip
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs`} className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs`}
onClick={() => onClick(searchResult)} onClick={() => onClick(searchResult, false)}
> >
{getIconForLabel(objectLabel, "size-3 text-white")} {getIconForLabel(objectLabel, "size-3 text-white")}
{Math.round( {Math.round(

View File

@ -26,7 +26,7 @@ type ExploreViewProps = {
searchDetail: SearchResult | undefined; searchDetail: SearchResult | undefined;
setSearchDetail: (search: SearchResult | undefined) => void; setSearchDetail: (search: SearchResult | undefined) => void;
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
onSelectSearch: (item: SearchResult, page?: SearchTab) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
}; };
export default function ExploreView({ export default function ExploreView({
@ -125,7 +125,7 @@ type ThumbnailRowType = {
setSearchDetail: (search: SearchResult | undefined) => void; setSearchDetail: (search: SearchResult | undefined) => void;
mutate: () => void; mutate: () => void;
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
onSelectSearch: (item: SearchResult, page?: SearchTab) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
}; };
function ThumbnailRow({ function ThumbnailRow({
@ -205,7 +205,7 @@ type ExploreThumbnailImageProps = {
setSearchDetail: (search: SearchResult | undefined) => void; setSearchDetail: (search: SearchResult | undefined) => void;
mutate: () => void; mutate: () => void;
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
onSelectSearch: (item: SearchResult, page?: SearchTab) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
}; };
function ExploreThumbnailImage({ function ExploreThumbnailImage({
event, event,
@ -225,11 +225,11 @@ function ExploreThumbnailImage({
}; };
const handleShowObjectLifecycle = () => { const handleShowObjectLifecycle = () => {
onSelectSearch(event, "object lifecycle"); onSelectSearch(event, false, "object lifecycle");
}; };
const handleShowSnapshot = () => { const handleShowSnapshot = () => {
onSelectSearch(event, "snapshot"); onSelectSearch(event, false, "snapshot");
}; };
return ( return (

View File

@ -30,6 +30,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import SearchActionGroup from "@/components/filter/SearchActionGroup";
type SearchViewProps = { type SearchViewProps = {
search: string; search: string;
@ -185,8 +186,8 @@ export default function SearchView({
const itemRefs = useRef<(HTMLDivElement | null)[]>([]); const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const onSelectSearch = useCallback( const onSelectSearch = useCallback(
(item: SearchResult, page: SearchTab = "details") => { (item: SearchResult, ctrl: boolean, page: SearchTab = "details") => {
if (selectedObjects.length > 1) { if (selectedObjects.length > 1 || ctrl) {
const index = selectedObjects.indexOf(item.id); const index = selectedObjects.indexOf(item.id);
if (index != -1) { if (index != -1) {
@ -207,19 +208,37 @@ export default function SearchView({
} else { } else {
setSelectedObjects([item.id]); setSelectedObjects([item.id]);
} }
setPage(page); if (!ctrl) {
setSearchDetail(item); setPage(page);
setSearchDetail(item);
} else {
setSearchDetail(undefined);
}
}, },
[selectedObjects], [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(() => { useEffect(() => {
if (uniqueResults && uniqueResults.length > 0) { if (uniqueResults && uniqueResults.length > 0) {
setSelectedObjects([uniqueResults[0].id]); setSelectedObjects([uniqueResults[0].id]);
} else { } else {
setSelectedObjects([]); 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 // confidence score
@ -267,6 +286,11 @@ export default function SearchView({
} }
switch (key) { switch (key) {
case "a":
if (modifiers.ctrl) {
onSelectAllObjects();
}
break;
case "ArrowLeft": case "ArrowLeft":
setSelectedObjects((prevSelected) => { setSelectedObjects((prevSelected) => {
if (uniqueResults.length === 0) return prevSelected; if (uniqueResults.length === 0) return prevSelected;
@ -324,11 +348,11 @@ export default function SearchView({
break; break;
} }
}, },
[uniqueResults, inputFocused], [uniqueResults, inputFocused, onSelectAllObjects],
); );
useKeyboardListener( useKeyboardListener(
["ArrowLeft", "ArrowRight", "PageDown", "PageUp"], ["a", "ArrowLeft", "ArrowRight", "PageDown", "PageUp"],
onKeyboardShortcut, onKeyboardShortcut,
!inputFocused, !inputFocused,
); );
@ -336,7 +360,7 @@ export default function SearchView({
// scroll into view // scroll into view
useEffect(() => { useEffect(() => {
if (selectedObjects.length > 0 && uniqueResults && itemRefs.current) { if (selectedObjects.length == 1 && uniqueResults && itemRefs.current) {
const selectedIndex = uniqueResults.findIndex( const selectedIndex = uniqueResults.findIndex(
(result) => result.id === selectedObjects[0], (result) => result.id === selectedObjects[0],
); );
@ -420,22 +444,39 @@ export default function SearchView({
{hasExistingSearch && ( {hasExistingSearch && (
<ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]"> <ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<SearchFilterGroup {selectedObjects.length <= 1 ? (
className={cn( <>
"w-full justify-between md:justify-start lg:justify-end", <SearchFilterGroup
)} className={cn(
filter={searchFilter} "w-full justify-between md:justify-start lg:justify-end",
onUpdateFilter={onUpdateFilter} )}
/> filter={searchFilter}
<SearchSettings onUpdateFilter={onUpdateFilter}
columns={columns} />
setColumns={setColumns} <SearchSettings
defaultView={defaultView} columns={columns}
setDefaultView={setDefaultView} setColumns={setColumns}
filter={searchFilter} defaultView={defaultView}
onUpdateFilter={onUpdateFilter} setDefaultView={setDefaultView}
/> filter={searchFilter}
<ScrollBar orientation="horizontal" className="h-0" /> onUpdateFilter={onUpdateFilter}
/>
<ScrollBar orientation="horizontal" className="h-0" />
</>
) : (
<div
className={cn(
"scrollbar-container flex justify-center gap-2 overflow-x-auto",
"h-10 w-full justify-between md:justify-start lg:justify-end",
)}
>
<SearchActionGroup
selectedObjects={selectedObjects}
setSelectedObjects={setSelectedObjects}
pullLatestData={refresh}
/>
</div>
)}
</div> </div>
</ScrollArea> </ScrollArea>
)} )}
@ -479,7 +520,9 @@ export default function SearchView({
> >
<SearchThumbnail <SearchThumbnail
searchResult={value} searchResult={value}
onClick={() => onSelectSearch(value)} onClick={(value: SearchResult, ctrl: boolean) => {
onSelectSearch(value, ctrl);
}}
/> />
{(searchTerm || {(searchTerm ||
searchFilter?.search_type?.includes("similarity")) && ( searchFilter?.search_type?.includes("similarity")) && (
@ -520,9 +563,11 @@ export default function SearchView({
}} }}
refreshResults={refresh} refreshResults={refresh}
showObjectLifecycle={() => showObjectLifecycle={() =>
onSelectSearch(value, "object lifecycle") onSelectSearch(value, false, "object lifecycle")
}
showSnapshot={() =>
onSelectSearch(value, false, "snapshot")
} }
showSnapshot={() => onSelectSearch(value, "snapshot")}
/> />
</div> </div>
</div> </div>