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:
committed by
GitHub
parent
bec5f6b8f5
commit
dd40b653d6
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user