Improve branch select (#22)

* Task attempt e5665be6-2bdc-4ec9-8e7d-8e3e1c684d54 - Final changes

* Task attempt e5665be6-2bdc-4ec9-8e7d-8e3e1c684d54 - Final changes

* Task attempt e5665be6-2bdc-4ec9-8e7d-8e3e1c684d54 - Final changes

* Cargo fmt

* Clippy

* Prettier
This commit is contained in:
Louis Knight-Webb
2025-06-30 15:34:18 +01:00
committed by GitHub
parent bec5f6b8f5
commit dd40b653d6
5 changed files with 486 additions and 40 deletions

View File

@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
import { useState, useMemo } from 'react';
import {
History,
Settings2,
@@ -7,13 +8,20 @@ import {
GitCompare,
ExternalLink,
GitBranch as GitBranchIcon,
Search,
Plus,
Check,
X,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuLabel,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
@@ -91,6 +99,102 @@ export function TaskDetailsToolbar({
onSetIsHoveringDevServer,
}: TaskDetailsToolbarProps) {
const { config } = useConfig();
const [branchSearchTerm, setBranchSearchTerm] = useState('');
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
const [newBranchName, setNewBranchName] = useState('');
const [baseBranchForNew, setBaseBranchForNew] = useState<string>('');
const [showBaseBranchDropdown, setShowBaseBranchDropdown] = useState(false);
const [baseBranchSearchTerm, setBaseBranchSearchTerm] = useState('');
// Filter branches based on search term
const filteredBranches = useMemo(() => {
if (!branchSearchTerm.trim()) {
return branches;
}
return branches.filter((branch) =>
branch.name.toLowerCase().includes(branchSearchTerm.toLowerCase())
);
}, [branches, branchSearchTerm]);
// Filter branches for base branch selection
const filteredBaseBranches = useMemo(() => {
if (!baseBranchSearchTerm.trim()) {
return branches;
}
return branches.filter((branch) =>
branch.name.toLowerCase().includes(baseBranchSearchTerm.toLowerCase())
);
}, [branches, baseBranchSearchTerm]);
// Get display name for selected branch
const selectedBranchDisplayName = useMemo(() => {
if (!selectedBranch) return 'current';
// For remote branches, show just the branch name without the remote prefix
if (selectedBranch.includes('/')) {
const parts = selectedBranch.split('/');
return parts[parts.length - 1];
}
return selectedBranch;
}, [selectedBranch]);
// Get display name for base branch
const baseBranchDisplayName = useMemo(() => {
if (!baseBranchForNew) return 'Current branch';
// For remote branches, show just the branch name without the remote prefix
if (baseBranchForNew.includes('/')) {
const parts = baseBranchForNew.split('/');
return parts[parts.length - 1];
}
return baseBranchForNew;
}, [baseBranchForNew]);
// Handle creating new branch
const handleCreateBranch = async () => {
if (!newBranchName.trim()) return;
try {
const response = await fetch(`/api/projects/${projectId}/branches`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newBranchName.trim(),
base_branch: baseBranchForNew || null,
}),
});
const result = await response.json();
if (result.success) {
// Select the newly created branch
onSetSelectedBranch(result.data.name);
// Reset form
setIsCreatingBranch(false);
setNewBranchName('');
setBaseBranchForNew('');
setBranchSearchTerm('');
setShowBaseBranchDropdown(false);
setBaseBranchSearchTerm('');
} else {
alert(`Failed to create branch: ${result.message}`);
}
} catch (error) {
console.error('Failed to create branch:', error);
alert('Failed to create branch. Please try again.');
}
};
// Cancel creating branch
const handleCancelCreateBranch = () => {
setIsCreatingBranch(false);
setNewBranchName('');
setBaseBranchForNew('');
setShowBaseBranchDropdown(false);
setBaseBranchSearchTerm('');
};
return (
<div className="px-6 pb-4">
@@ -219,9 +323,12 @@ export function TaskDetailsToolbar({
<Button
variant="outline"
size="sm"
className="rounded-none border-x-0 px-2"
className="rounded-none border-x-0 px-3 max-w-32"
>
<GitBranchIcon className="h-4 w-4" />
<GitBranchIcon className="h-4 w-4 mr-1 flex-shrink-0" />
<span className="truncate text-xs">
{selectedBranchDisplayName}
</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
@@ -230,36 +337,240 @@ export function TaskDetailsToolbar({
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="center" className="w-56">
{branches.map((branch) => (
<DropdownMenuItem
key={branch.name}
onClick={() => onSetSelectedBranch(branch.name)}
className={
selectedBranch === branch.name ? 'bg-accent' : ''
}
>
<div className="flex items-center justify-between w-full">
<span
className={branch.is_current ? 'font-medium' : ''}
>
{branch.name}
</span>
<div className="flex gap-1">
{branch.is_current && (
<span className="text-xs bg-green-100 text-green-800 px-1 rounded">
current
</span>
)}
{branch.is_remote && (
<span className="text-xs bg-blue-100 text-blue-800 px-1 rounded">
remote
</span>
)}
<DropdownMenuContent align="center" className="w-80">
{!isCreatingBranch ? (
<>
<div className="p-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search branches..."
value={branchSearchTerm}
onChange={(e) =>
setBranchSearchTerm(e.target.value)
}
className="pl-8"
/>
</div>
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsCreatingBranch(true);
setBaseBranchForNew(
branches.find((b) => b.is_current)?.name || ''
);
}}
className="text-blue-600 hover:text-blue-700"
>
<Plus className="h-4 w-4 mr-2" />
Create new branch...
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="max-h-64 overflow-y-auto">
{filteredBranches.length === 0 ? (
<div className="p-2 text-sm text-muted-foreground text-center">
No branches found
</div>
) : (
filteredBranches.map((branch) => (
<DropdownMenuItem
key={branch.name}
onClick={() => {
onSetSelectedBranch(branch.name);
setBranchSearchTerm('');
}}
className={
selectedBranch === branch.name
? 'bg-accent'
: ''
}
>
<div className="flex items-center justify-between w-full">
<span
className={
branch.is_current ? 'font-medium' : ''
}
>
{branch.name}
</span>
<div className="flex gap-1">
{branch.is_current && (
<span className="text-xs bg-green-100 text-green-800 px-1 rounded">
current
</span>
)}
{branch.is_remote && (
<span className="text-xs bg-blue-100 text-blue-800 px-1 rounded">
remote
</span>
)}
</div>
</div>
</DropdownMenuItem>
))
)}
</div>
</>
) : (
<>
<DropdownMenuLabel>Create New Branch</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="p-3 space-y-3">
<div>
<label className="text-sm font-medium">
Branch name
</label>
<Input
placeholder="feature/my-feature"
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleCreateBranch();
} else if (e.key === 'Escape') {
handleCancelCreateBranch();
}
}}
className="mt-1"
autoFocus
/>
</div>
<div>
<label className="text-sm font-medium">
Base branch
</label>
<DropdownMenu
open={showBaseBranchDropdown}
onOpenChange={setShowBaseBranchDropdown}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="mt-1 w-full justify-between"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<span className="truncate">
{baseBranchDisplayName}
</span>
<GitBranchIcon className="h-4 w-4 ml-2 flex-shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-80">
<div className="p-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search branches..."
value={baseBranchSearchTerm}
onChange={(e) =>
setBaseBranchSearchTerm(e.target.value)
}
className="pl-8"
/>
</div>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setBaseBranchForNew('');
setShowBaseBranchDropdown(false);
setBaseBranchSearchTerm('');
}}
className={
!baseBranchForNew ? 'bg-accent' : ''
}
>
<div className="flex items-center justify-between w-full">
<span className="font-medium">
Current branch
</span>
<span className="text-xs bg-green-100 text-green-800 px-1 rounded">
default
</span>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="max-h-48 overflow-y-auto">
{filteredBaseBranches.length === 0 ? (
<div className="p-2 text-sm text-muted-foreground text-center">
No branches found
</div>
) : (
filteredBaseBranches.map((branch) => (
<DropdownMenuItem
key={branch.name}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setBaseBranchForNew(branch.name);
setShowBaseBranchDropdown(false);
setBaseBranchSearchTerm('');
}}
className={
baseBranchForNew === branch.name
? 'bg-accent'
: ''
}
>
<div className="flex items-center justify-between w-full">
<span
className={
branch.is_current
? 'font-medium'
: ''
}
>
{branch.name}
</span>
<div className="flex gap-1">
{branch.is_current && (
<span className="text-xs bg-green-100 text-green-800 px-1 rounded">
current
</span>
)}
{branch.is_remote && (
<span className="text-xs bg-blue-100 text-blue-800 px-1 rounded">
remote
</span>
)}
</div>
</div>
</DropdownMenuItem>
))
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex gap-2 pt-2">
<Button
size="sm"
onClick={handleCreateBranch}
disabled={!newBranchName.trim()}
className="flex-1"
>
<Check className="h-4 w-4 mr-1" />
Create
</Button>
<Button
size="sm"
variant="outline"
onClick={handleCancelCreateBranch}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>