Files
sencho/frontend/src/components/sidebar/SidebarActions.tsx
T
Anso a0bf5b5bf5 feat(sidebar): bulk stack operations (#854)
* feat(sidebar): bulk stack operations (select, start/stop/restart/update)

- Add ⊞ bulk mode toggle in SidebarActions (cyan active state, tooltip "Bulk
  mode (B)"); keyboard shortcut B toggles, Esc exits, Ctrl+A selects all
  visible (chip-filtered) stacks
- Reserved checkbox column in StackRow becomes visible and interactive in bulk
  mode; clicking a row in bulk mode toggles selection instead of opening the
  stack; kebab and context-menu still work in either mode
- SidebarBulkBar appears below filter chips when >=1 stack selected: shows
  count, Start / Stop / Restart / Update actions; Update is disabled with a
  Skipper TierBadge for Community licenses
- useBulkStackActions hook fans out operations via Promise.allSettled and
  surfaces an aggregate toast ("3 of 4 restarted; 1 failed: plex")
- Bulk update enforced Skipper-gated frontend-side (isPaid check in hook) and
  sends x-bulk-mode header for backend defense-in-depth
- Extract isInputFocused / isPaletteOpen to lib/keyboard-guards.ts; both
  useStackKeyboardShortcuts and the new bulk keyboard effect now share the
  same guards instead of duplicating the logic
- chipFilteredFiles captured via useRef in bulk keyboard effect so the listener
  is not torn down and re-added on every status-poll cycle

* fix(sidebar): separate TooltipProviders for bulk and scan icon buttons

Wrapping both icon buttons in a single TooltipProvider made them
render as one flex child, collapsing the gap-2 between them.
Splitting into two independent TooltipProviders restores the 8px
gap and right padding of the scan button.
2026-04-30 19:53:12 -04:00

60 lines
2.0 KiB
TypeScript

import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { FolderSearch, Loader2, LayoutList } from 'lucide-react';
import { cn } from '@/lib/utils';
interface SidebarActionsProps {
createStackSlot: ReactNode;
onScan: () => void;
isScanning: boolean;
bulkMode: boolean;
onToggleBulkMode: () => void;
}
export function SidebarActions({ createStackSlot, onScan, isScanning, bulkMode, onToggleBulkMode }: SidebarActionsProps) {
return (
<div className="p-4 flex gap-2">
<div className="flex-1">{createStackSlot}</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className={cn('rounded-lg shrink-0 shadow-btn-glow', bulkMode && 'border-brand/40 text-brand bg-brand/10')}
onClick={onToggleBulkMode}
aria-pressed={bulkMode}
>
<LayoutList className="w-4 h-4" strokeWidth={1.5} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Bulk mode (B)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className="rounded-lg shrink-0 shadow-btn-glow"
onClick={onScan}
disabled={isScanning}
>
{isScanning
? <Loader2 className="w-4 h-4 animate-spin" strokeWidth={1.5} />
: <FolderSearch className="w-4 h-4" strokeWidth={1.5} />}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Scan stacks folder</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}