Files
Netcatty/components/GroupTreeItem.tsx
T
bincxz 59aa2f564b feat: implement TopTabs and VaultView components with session and workspace management
- Added TopTabs component for managing terminal sessions and workspaces.
- Integrated drag-and-drop functionality for session organization.
- Created VaultView component for managing hosts, SSH keys, snippets, and port forwarding.
- Implemented group management for hosts with the ability to create, delete, and move groups.
- Added initial data for hosts and snippets in defaultData.ts.
- Defined storage keys for persistent data management in storageKeys.ts.
2025-12-07 22:57:54 +08:00

105 lines
3.8 KiB
TypeScript

import React, { useMemo } from 'react';
import { ChevronRight, Folder, FolderOpen, FolderPlus, Plus } from 'lucide-react';
import { GroupNode } from '../types';
import { cn } from '../lib/utils';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from './ui/context-menu';
interface GroupTreeItemProps {
node: GroupNode;
depth: number;
expandedPaths: Set<string>;
onToggle: (path: string) => void;
onSelectGroup: (path: string) => void;
selectedGroup: string | null;
onEditGroup: (path: string) => void;
onNewHost: (path: string) => void;
onNewSubfolder: (path: string) => void;
}
export const GroupTreeItem: React.FC<GroupTreeItemProps> = ({
node,
depth,
expandedPaths,
onToggle,
onSelectGroup,
selectedGroup,
onEditGroup,
onNewHost,
onNewSubfolder,
}) => {
const isExpanded = expandedPaths.has(node.path);
const hasChildren = node.children && Object.keys(node.children).length > 0;
const paddingLeft = `${depth * 12 + 12}px`;
const isSelected = selectedGroup === node.path;
const childNodes = useMemo(() => {
return node.children
? (Object.values(node.children) as unknown as GroupNode[]).sort((a, b) => a.name.localeCompare(b.name))
: [];
}, [node.children]);
return (
<Collapsible open={isExpanded} onOpenChange={() => onToggle(node.path)}>
<ContextMenu>
<ContextMenuTrigger>
<CollapsibleTrigger asChild>
<div
className={cn(
"flex items-center py-1.5 pr-2 text-sm font-medium cursor-pointer transition-colors select-none group relative rounded-r-md",
isSelected ? "bg-primary/10 text-primary border-l-2 border-primary" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
)}
style={{ paddingLeft }}
onClick={() => onSelectGroup(node.path)}
>
<div className="mr-1.5 flex-shrink-0 w-4 h-4 flex items-center justify-center">
{hasChildren && (
<div className={cn("transition-transform duration-200", isExpanded ? "rotate-90" : "")}>
<ChevronRight size={12} />
</div>
)}
</div>
<div className="mr-2 text-primary/80 group-hover:text-primary transition-colors">
{isExpanded ? <FolderOpen size={16} /> : <Folder size={16} />}
</div>
<span className="truncate flex-1">{node.name}</span>
{node.hosts.length > 0 && (
<span className="text-[10px] opacity-70 bg-background/50 px-1.5 rounded-full border border-border">
{node.hosts.length}
</span>
)}
</div>
</CollapsibleTrigger>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => onNewHost(node.path)}>
<Plus className="mr-2 h-4 w-4" /> New Host
</ContextMenuItem>
<ContextMenuItem onClick={() => onNewSubfolder(node.path)}>
<FolderPlus className="mr-2 h-4 w-4" /> New Subfolder
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{hasChildren && (
<CollapsibleContent>
{childNodes.map((child) => (
<GroupTreeItem
key={child.path}
node={child}
depth={depth + 1}
expandedPaths={expandedPaths}
onToggle={onToggle}
onSelectGroup={onSelectGroup}
selectedGroup={selectedGroup}
onEditGroup={onEditGroup}
onNewHost={onNewHost}
onNewSubfolder={onNewSubfolder}
/>
))}
</CollapsibleContent>
)}
</Collapsible>
);
};