import React from 'react'; import { RiGitBranchLine, RiArrowDownSLine, RiAddLine, RiCloseLine, RiLoader4Line, RiArrowLeftLine, } from '@remixicon/react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@/components/ui/command'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { GitRemote } from '@/lib/api/types'; interface BranchInfo { ahead?: number; behind?: number; } interface BranchSelectorProps { currentBranch: string | null | undefined; localBranches: string[]; remoteBranches: string[]; branchInfo: Record | undefined; onCheckout: (branch: string) => void; onCreate: (name: string, remote?: GitRemote) => Promise; remotes?: GitRemote[]; disabled?: boolean; tooltipDelayMs?: number; } const sanitizeBranchNameInput = (value: string): string => { return value .trim() .replace(/\s+/g, '-') .replace(/[^A-Za-z0-9._/-]/g, '-') .replace(/-+/g, '-') .replace(/\/{2,}/g, '/') .replace(/\/-+/g, '/') .replace(/-+\//g, '/') .replace(/^[-/]+/, '') .replace(/[-/]+$/, ''); }; export const BranchSelector: React.FC = ({ currentBranch, localBranches, remoteBranches, branchInfo, onCheckout, onCreate, remotes = [], disabled = false, tooltipDelayMs = 1000, }) => { const [isOpen, setIsOpen] = React.useState(false); const [search, setSearch] = React.useState(''); const [showCreate, setShowCreate] = React.useState(false); const [showRemoteSelect, setShowRemoteSelect] = React.useState(false); const [newBranchName, setNewBranchName] = React.useState(''); const [isCreating, setIsCreating] = React.useState(false); const createInputRef = React.useRef(null); const hasMultipleRemotes = remotes.length > 1; const sanitizedNewBranch = React.useMemo( () => sanitizeBranchNameInput(newBranchName), [newBranchName] ); const filteredLocal = React.useMemo(() => { const term = search.toLowerCase(); if (!term) return localBranches; return localBranches.filter((b) => b.toLowerCase().includes(term)); }, [search, localBranches]); const filteredRemote = React.useMemo(() => { const term = search.toLowerCase(); if (!term) return remoteBranches; return remoteBranches.filter((b) => b.toLowerCase().includes(term)); }, [search, remoteBranches]); const handleCheckout = (branch: string) => { if (branch === currentBranch) { setIsOpen(false); return; } onCheckout(branch); setIsOpen(false); setSearch(''); }; const handleShowCreate = () => { setShowCreate(true); setTimeout(() => createInputRef.current?.focus(), 50); }; const handleCreate = async () => { if (!sanitizedNewBranch || isCreating) return; // If multiple remotes, show remote selection first if (hasMultipleRemotes) { setShowRemoteSelect(true); return; } // Single or no remote - proceed directly setIsCreating(true); try { await onCreate(sanitizedNewBranch, remotes[0]); setNewBranchName(''); setShowCreate(false); setIsOpen(false); } finally { setIsCreating(false); } }; const handleSelectRemote = async (remote: GitRemote) => { if (!sanitizedNewBranch || isCreating) return; setIsCreating(true); try { await onCreate(sanitizedNewBranch, remote); setNewBranchName(''); setShowCreate(false); setShowRemoteSelect(false); setIsOpen(false); } finally { setIsCreating(false); } }; const handleBackFromRemoteSelect = () => { setShowRemoteSelect(false); }; const handleCancelCreate = () => { setNewBranchName(''); setShowCreate(false); setShowRemoteSelect(false); }; React.useEffect(() => { if (!isOpen) { setSearch(''); setShowCreate(false); setShowRemoteSelect(false); setNewBranchName(''); } }, [isOpen]); return ( Current branch No branches found. {showRemoteSelect ? ( // Remote selection step
Push {sanitizedNewBranch} to:
{remotes.map((remote) => ( ))}
) : !showCreate ? ( Create new branch... ) : (
setNewBranchName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleCreate(); } else if (e.key === 'Escape') { e.preventDefault(); handleCancelCreate(); } }} className="flex-1 min-w-0 bg-transparent typography-meta outline-none placeholder:text-muted-foreground" />
)}
{filteredLocal.map((branch) => ( handleCheckout(branch)} > {branch} {(branchInfo?.[branch]?.ahead || branchInfo?.[branch]?.behind) && ( {branchInfo[branch].ahead || 0} ahead ยท{' '} {branchInfo[branch].behind || 0} behind )} {currentBranch === branch && ( Current )} ))} {filteredLocal.length === 0 && ( No local branches )} {filteredRemote.map((branch) => ( handleCheckout(branch)} > {branch} ))} {filteredRemote.length === 0 && ( No remote branches )}
); };