/** * RepositoryDropdown - Dropdown for selecting repository groups. * * Features: * - Shows repository groups (not individual worktrees) * - Displays worktree count and total sessions * - Click outside to close * - Keyboard navigation (Escape to close) * - Filter out already selected items */ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useStore } from '@renderer/store'; import { ChevronDown, FolderOpen, GitBranch } from 'lucide-react'; import type { RepositoryDropdownItem } from '@renderer/components/settings/hooks/useSettingsConfig'; interface RepositoryDropdownProps { /** Callback when a repository is selected */ onSelect: (item: RepositoryDropdownItem) => void; /** IDs of items to exclude from the list */ excludeIds?: string[]; /** Placeholder text */ placeholder?: string; /** Whether the dropdown is disabled */ disabled?: boolean; /** Whether to drop up instead of down */ dropUp?: boolean; /** Custom class for the container */ className?: string; } export const RepositoryDropdown = ({ onSelect, excludeIds = [], placeholder = 'Select repository...', disabled = false, dropUp = false, className = '', }: Readonly): React.JSX.Element => { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); // Get repository groups from store const repositoryGroups = useStore((state) => state.repositoryGroups); const fetchRepositoryGroups = useStore((state) => state.fetchRepositoryGroups); // Fetch data if not loaded useEffect(() => { if (repositoryGroups.length === 0) { void fetchRepositoryGroups(); } }, [repositoryGroups.length, fetchRepositoryGroups]); // Convert repository groups to dropdown items const allItems = useMemo((): RepositoryDropdownItem[] => { return repositoryGroups.map((group) => ({ id: group.id, name: group.name, path: group.worktrees[0]?.path ?? '', worktreeCount: group.worktrees.length, totalSessions: group.totalSessions, })); }, [repositoryGroups]); // Filter out excluded items const availableItems = useMemo(() => { return allItems.filter((item) => !excludeIds.includes(item.id)); }, [allItems, excludeIds]); // Close dropdown on outside click useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isOpen]); // Close on escape useEffect(() => { const handleEscape = (event: KeyboardEvent): void => { if (event.key === 'Escape') { setIsOpen(false); } }; if (isOpen) { document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); } }, [isOpen]); const handleSelect = (item: RepositoryDropdownItem): void => { onSelect(item); setIsOpen(false); }; const isEmpty = availableItems.length === 0; return (
{/* Trigger Button */} {/* Dropdown Menu */} {isOpen && !isEmpty && (
{availableItems.map((item) => ( handleSelect(item)} /> ))}
)}
); }; /** * Individual item in the dropdown. */ const RepositoryDropdownItemComponentInner = ({ item, onSelect, }: Readonly<{ item: RepositoryDropdownItem; onSelect: () => void; }>): React.JSX.Element => { return ( ); }; const RepositoryDropdownItemComponent = React.memo(RepositoryDropdownItemComponentInner); /** * Selected repository item with remove button. */ const SelectedRepositoryItemInner = ({ item, onRemove, disabled = false, }: Readonly<{ item: RepositoryDropdownItem; onRemove: () => void; disabled?: boolean; }>): React.JSX.Element => { return (
{item.name} {item.worktreeCount > 1 && ( {item.worktreeCount} )}
{item.path}
); }; export const SelectedRepositoryItem = React.memo(SelectedRepositoryItemInner);