Refactor multi-select: replace icon with checkbox on hover for minimal selection UI

This commit is contained in:
marko-kraemer 2025-05-23 11:41:14 +02:00
parent 8904918ea7
commit b647889a7d
1 changed files with 84 additions and 109 deletions

View File

@ -62,7 +62,6 @@ export function NavAgents() {
const isPerformingActionRef = useRef(false); const isPerformingActionRef = useRef(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [isMultiSelectActive, setIsMultiSelectActive] = useState(false);
const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set()); const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set());
const [deleteProgress, setDeleteProgress] = useState(0); const [deleteProgress, setDeleteProgress] = useState(0);
const [totalToDelete, setTotalToDelete] = useState(0); const [totalToDelete, setTotalToDelete] = useState(0);
@ -146,10 +145,9 @@ export function NavAgents() {
// Function to handle thread click with loading state // Function to handle thread click with loading state
const handleThreadClick = (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => { const handleThreadClick = (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => {
// If multi-select is active, prevent navigation and toggle selection // If thread is selected, prevent navigation
if (isMultiSelectActive) { if (selectedThreads.has(threadId)) {
e.preventDefault(); e.preventDefault();
toggleThreadSelection(threadId);
return; return;
} }
@ -159,7 +157,12 @@ export function NavAgents() {
} }
// Toggle thread selection for multi-select // Toggle thread selection for multi-select
const toggleThreadSelection = (threadId: string) => { const toggleThreadSelection = (threadId: string, e?: React.MouseEvent) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}
setSelectedThreads(prev => { setSelectedThreads(prev => {
const newSelection = new Set(prev); const newSelection = new Set(prev);
if (newSelection.has(threadId)) { if (newSelection.has(threadId)) {
@ -171,15 +174,6 @@ export function NavAgents() {
}); });
}; };
// Toggle multi-select mode
const toggleMultiSelect = () => {
setIsMultiSelectActive(!isMultiSelectActive);
// Clear selections when toggling off
if (isMultiSelectActive) {
setSelectedThreads(new Set());
}
};
// Select all threads // Select all threads
const selectAllThreads = () => { const selectAllThreads = () => {
const allThreadIds = combinedThreads.map(thread => thread.threadId); const allThreadIds = combinedThreads.map(thread => thread.threadId);
@ -309,7 +303,6 @@ export function NavAgents() {
// Reset states // Reset states
setSelectedThreads(new Set()); setSelectedThreads(new Set());
setIsMultiSelectActive(false);
setDeleteProgress(0); setDeleteProgress(0);
setTotalToDelete(0); setTotalToDelete(0);
}, },
@ -331,7 +324,6 @@ export function NavAgents() {
// Reset states // Reset states
setSelectedThreads(new Set()); setSelectedThreads(new Set());
setIsMultiSelectActive(false);
setThreadToDelete(null); setThreadToDelete(null);
isPerformingActionRef.current = false; isPerformingActionRef.current = false;
setDeleteProgress(0); setDeleteProgress(0);
@ -351,16 +343,15 @@ export function NavAgents() {
return ( return (
<SidebarGroup> <SidebarGroup>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<SidebarGroupLabel>Agents</SidebarGroupLabel> <SidebarGroupLabel>Tasks</SidebarGroupLabel>
{state !== 'collapsed' ? ( {state !== 'collapsed' ? (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{isMultiSelectActive ? ( {selectedThreads.size > 0 ? (
<> <>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={deselectAllThreads} onClick={deselectAllThreads}
disabled={selectedThreads.size === 0}
className="h-7 w-7" className="h-7 w-7"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@ -378,56 +369,26 @@ export function NavAgents() {
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={handleMultiDelete} onClick={handleMultiDelete}
disabled={selectedThreads.size === 0}
className="h-7 w-7 text-destructive" className="h-7 w-7 text-destructive"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={toggleMultiSelect}
className="h-7 px-2 text-xs"
>
Done
</Button>
</> </>
) : ( ) : (
<> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <div>
<div> <Link
<Button href="/dashboard"
variant="ghost" className="text-muted-foreground hover:text-foreground h-7 w-7 flex items-center justify-center rounded-md"
size="icon" >
onClick={toggleMultiSelect} <Plus className="h-4 w-4" />
className="h-7 w-7" <span className="sr-only">New Agent</span>
disabled={combinedThreads.length === 0} </Link>
> </div>
<div className="h-4 w-4 border rounded border-foreground/30 flex items-center justify-center"> </TooltipTrigger>
{isMultiSelectActive && <Check className="h-3 w-3" />} <TooltipContent>New Agent</TooltipContent>
</div> </Tooltip>
<span className="sr-only">Select</span>
</Button>
</div>
</TooltipTrigger>
<TooltipContent>Select Multiple</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Link
href="/dashboard"
className="text-muted-foreground hover:text-foreground h-7 w-7 flex items-center justify-center rounded-md"
>
<Plus className="h-4 w-4" />
<span className="sr-only">New Agent</span>
</Link>
</div>
</TooltipTrigger>
<TooltipContent>New Agent</TooltipContent>
</Tooltip>
</>
)} )}
</div> </div>
) : null} ) : null}
@ -472,7 +433,7 @@ export function NavAgents() {
const isSelected = selectedThreads.has(thread.threadId); const isSelected = selectedThreads.has(thread.threadId);
return ( return (
<SidebarMenuItem key={`thread-${thread.threadId}`}> <SidebarMenuItem key={`thread-${thread.threadId}`} className="group">
{state === 'collapsed' ? ( {state === 'collapsed' ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -490,13 +451,7 @@ export function NavAgents() {
handleThreadClick(e, thread.threadId, thread.url) handleThreadClick(e, thread.threadId, thread.url)
} }
> >
{isMultiSelectActive ? ( {isThreadLoading ? (
<div
className={`h-4 w-4 border rounded flex items-center justify-center ${isSelected ? 'bg-primary border-primary' : 'border-foreground'}`}
>
{isSelected && <Check className="h-3 w-3 text-white" />}
</div>
) : isThreadLoading ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
) : ( ) : (
<MessagesSquare className="h-4 w-4" /> <MessagesSquare className="h-4 w-4" />
@ -509,48 +464,68 @@ export function NavAgents() {
<TooltipContent>{thread.projectName}</TooltipContent> <TooltipContent>{thread.projectName}</TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<SidebarMenuButton <div className="relative">
asChild <SidebarMenuButton
className={ asChild
isActive className={`relative ${
? 'bg-accent text-accent-foreground font-medium' isActive
: isSelected ? 'bg-accent text-accent-foreground font-medium'
? 'bg-primary/10' : isSelected
: '' ? 'bg-primary/10'
} : ''
> }`}
<Link
href={thread.url}
onClick={(e) =>
handleThreadClick(e, thread.threadId, thread.url)
}
className="flex items-center"
> >
{isMultiSelectActive ? ( <Link
<div href={thread.url}
className={`h-4 w-4 border flex-shrink-0 hover:bg-muted transition rounded mr-2 flex items-center justify-center ${isSelected ? 'bg-primary border-primary' : 'border-foreground/30'}`} onClick={(e) =>
onClick={(e) => { handleThreadClick(e, thread.threadId, thread.url)
e.preventDefault(); }
e.stopPropagation(); className="flex items-center"
toggleThreadSelection(thread.threadId); >
}} <div className="flex items-center group/icon relative">
> {/* Show checkbox on hover or when selected, otherwise show MessagesSquare */}
{isSelected && <Check className="h-3 w-3 text-white" />} {isThreadLoading ? (
</div> <Loader2 className="h-4 w-4 animate-spin" />
) : null} ) : (
{isThreadLoading ? ( <>
<Loader2 className="h-4 w-4 animate-spin" /> {/* MessagesSquare icon - hidden on hover if not selected */}
) : ( <MessagesSquare
<MessagesSquare className="h-4 w-4" /> className={`h-4 w-4 transition-opacity duration-150 ${
)} isSelected ? 'opacity-0' : 'opacity-100 group-hover/icon:opacity-0'
<span>{thread.projectName}</span> }`}
</Link> />
</SidebarMenuButton>
{/* Checkbox - appears on hover or when selected */}
<div
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-150 ${
isSelected
? 'opacity-100'
: 'opacity-0 group-hover/icon:opacity-100'
}`}
onClick={(e) => toggleThreadSelection(thread.threadId, e)}
>
<div
className={`h-4 w-4 border rounded cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-center ${
isSelected
? 'bg-primary border-primary'
: 'border-muted-foreground/30 bg-background'
}`}
>
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
</div>
</div>
</>
)}
</div>
<span className="ml-2">{thread.projectName}</span>
</Link>
</SidebarMenuButton>
</div>
)} )}
{state !== 'collapsed' && !isMultiSelectActive && ( {state !== 'collapsed' && !isSelected && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover> <SidebarMenuAction showOnHover className="group-hover:opacity-100">
<MoreHorizontal /> <MoreHorizontal />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</SidebarMenuAction> </SidebarMenuAction>