mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 09:24:09 +00:00
a0bf5b5bf5
* 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.
90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
import type { ReactNode } from 'react';
|
|
import { Command } from '@/components/ui/command';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import type { NotificationItem } from '@/components/dashboard/types';
|
|
import { SidebarActions } from './SidebarActions';
|
|
import { SidebarActivityTicker } from './SidebarActivityTicker';
|
|
import { SidebarBrand } from './SidebarBrand';
|
|
import { SidebarBulkBar } from './SidebarBulkBar';
|
|
import { SidebarFilterChips, type FilterCounts } from './SidebarFilterChips';
|
|
import { SidebarSearch } from './SidebarSearch';
|
|
import { StackList, type StackListProps } from './StackList';
|
|
import type { FilterChip } from './sidebar-types';
|
|
import type { BulkAction } from '@/hooks/useBulkStackActions';
|
|
|
|
export interface StackSidebarProps {
|
|
isDarkMode: boolean;
|
|
nodeSwitcherSlot: ReactNode;
|
|
createStackSlot: ReactNode | null;
|
|
onScan: () => void;
|
|
isScanning: boolean;
|
|
canCreate: boolean;
|
|
searchQuery: string;
|
|
onSearchChange: (v: string) => void;
|
|
filterChip: FilterChip;
|
|
filterCounts: FilterCounts;
|
|
onFilterChipChange: (chip: FilterChip) => void;
|
|
list: StackListProps;
|
|
notifications: NotificationItem[];
|
|
tickerConnected: boolean;
|
|
onOpenActivity: () => void;
|
|
bulkMode: boolean;
|
|
selectedFiles: Set<string>;
|
|
isPaid: boolean;
|
|
onToggleBulkMode: () => void;
|
|
onToggleSelect: (file: string) => void;
|
|
onClearSelection: () => void;
|
|
onBulkAction: (action: BulkAction) => void;
|
|
}
|
|
|
|
export function StackSidebar(props: StackSidebarProps) {
|
|
const {
|
|
isDarkMode, nodeSwitcherSlot, createStackSlot, onScan, isScanning, canCreate,
|
|
searchQuery, onSearchChange, filterChip, filterCounts, onFilterChipChange,
|
|
list, notifications, tickerConnected, onOpenActivity,
|
|
bulkMode, selectedFiles, isPaid, onToggleBulkMode, onToggleSelect, onClearSelection, onBulkAction,
|
|
} = props;
|
|
|
|
return (
|
|
<div className="w-64 border-r border-glass-border bg-sidebar backdrop-blur-md flex flex-col">
|
|
<SidebarBrand isDarkMode={isDarkMode} />
|
|
<div className="px-4 pt-2 pb-0">{nodeSwitcherSlot}</div>
|
|
{canCreate && createStackSlot !== null && (
|
|
<SidebarActions
|
|
createStackSlot={createStackSlot}
|
|
onScan={onScan}
|
|
isScanning={isScanning}
|
|
bulkMode={bulkMode}
|
|
onToggleBulkMode={onToggleBulkMode}
|
|
/>
|
|
)}
|
|
<Command shouldFilter={false} className="bg-transparent flex-1 flex flex-col overflow-hidden">
|
|
<SidebarSearch value={searchQuery} onValueChange={onSearchChange} />
|
|
<SidebarFilterChips
|
|
active={filterChip}
|
|
counts={filterCounts}
|
|
onChange={onFilterChipChange}
|
|
/>
|
|
{selectedFiles.size > 0 && (
|
|
<SidebarBulkBar
|
|
selectedCount={selectedFiles.size}
|
|
isPaid={isPaid}
|
|
onAction={onBulkAction}
|
|
onClear={onClearSelection}
|
|
/>
|
|
)}
|
|
<ScrollArea className="flex-1 px-2 pb-2">
|
|
<div data-stacks-loaded={list.isLoading ? 'false' : 'true'}>
|
|
<StackList {...list} bulkMode={bulkMode} selectedFiles={selectedFiles} onToggleSelect={onToggleSelect} />
|
|
</div>
|
|
</ScrollArea>
|
|
</Command>
|
|
<SidebarActivityTicker
|
|
notifications={notifications}
|
|
connected={tickerConnected}
|
|
onNavigate={onOpenActivity}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|