feat: bulk select sessions in sidebar
Select toggle in header enters selection mode. Click toggles, shift-click ranges, parent auto-includes subtasks, Cmd/Ctrl+A selects visible, Esc exits, Cmd/Ctrl+Backspace deletes. Bulk bar: move to folder (existing or new inline), archive/delete via existing confirm + "Never ask" pref. Cross-scope forbidden — new scope clears previous. Also: web sidebar header folds toggle into action row (no separate chrome row, same position on toggle); sidebar min width bumped 250→280 so icons fit; minimal-mode hover reveal padding trimmed.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
RiAddLine,
|
||||
RiCloseLine,
|
||||
RiDeleteBinLine,
|
||||
RiFolderLine,
|
||||
} from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
|
||||
|
||||
type Props = {
|
||||
selectedCount: number;
|
||||
scopeKey: string | null;
|
||||
scopeFolders: SessionFolder[];
|
||||
archivedBucket: boolean;
|
||||
onMoveToFolder: (folderId: string) => void;
|
||||
onCreateFolderAndMove: () => void;
|
||||
onRemoveFromFolder: () => void;
|
||||
canRemoveFromFolder: boolean;
|
||||
onDelete: () => void;
|
||||
onDone: () => void;
|
||||
};
|
||||
|
||||
export const BulkActionBar: React.FC<Props> = ({
|
||||
selectedCount,
|
||||
scopeKey,
|
||||
scopeFolders,
|
||||
archivedBucket,
|
||||
onMoveToFolder,
|
||||
onCreateFolderAndMove,
|
||||
onRemoveFromFolder,
|
||||
canRemoveFromFolder,
|
||||
onDelete,
|
||||
onDone,
|
||||
}) => {
|
||||
const canMoveToFolder = Boolean(scopeKey) && !archivedBucket;
|
||||
const destructiveLabel = archivedBucket ? 'Delete' : 'Archive';
|
||||
const iconButtonClass = 'inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50';
|
||||
const destructiveIconButtonClass = 'inline-flex h-7 w-7 items-center justify-center rounded-md text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50';
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-1 border-t border-border px-2.5 py-1.5">
|
||||
<span className="typography-ui-label text-muted-foreground whitespace-nowrap">
|
||||
{selectedCount} selected
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
{canMoveToFolder ? (
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={iconButtonClass}
|
||||
aria-label="Move to folder"
|
||||
>
|
||||
<RiFolderLine className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}><p>Move to folder</p></TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="min-w-[180px]">
|
||||
{scopeFolders.length === 0 ? (
|
||||
<DropdownMenuItem disabled className="text-muted-foreground">
|
||||
No folders yet
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
scopeFolders.map((folder) => (
|
||||
<DropdownMenuItem key={folder.id} onClick={() => onMoveToFolder(folder.id)}>
|
||||
<span className="flex-1 truncate">{folder.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onCreateFolderAndMove}>
|
||||
<RiAddLine className="mr-1 h-4 w-4" />
|
||||
New folder...
|
||||
</DropdownMenuItem>
|
||||
{canRemoveFromFolder ? (
|
||||
<DropdownMenuItem
|
||||
onClick={onRemoveFromFolder}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<RiCloseLine className="mr-1 h-4 w-4" />
|
||||
Remove from folder
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
className={cn(destructiveIconButtonClass)}
|
||||
aria-label={destructiveLabel}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}><p>{destructiveLabel}</p></TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDone}
|
||||
className={iconButtonClass}
|
||||
aria-label="Exit selection"
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}><p>Exit selection</p></TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user