mirror of https://github.com/kortix-ai/suna.git
Merge pull request #461 from kortix-ai/refactor/multi-select-hover-checkbox
Refactor multi-select: replace icon with checkbox on hover for minima…
This commit is contained in:
commit
7d0296b23b
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue