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
@@ -90,6 +90,7 @@ fn main() {
|
||||
vibe_kanban::models::project::SearchResult::decl(),
|
||||
vibe_kanban::models::project::SearchMatchType::decl(),
|
||||
vibe_kanban::models::project::GitBranch::decl(),
|
||||
vibe_kanban::models::project::CreateBranch::decl(),
|
||||
vibe_kanban::models::task::CreateTask::decl(),
|
||||
vibe_kanban::models::task::CreateTaskAndStart::decl(),
|
||||
vibe_kanban::models::task::TaskStatus::decl(),
|
||||
|
||||
@@ -77,6 +77,15 @@ pub struct GitBranch {
|
||||
pub name: String,
|
||||
pub is_current: bool,
|
||||
pub is_remote: bool,
|
||||
#[ts(type = "Date")]
|
||||
pub last_commit_date: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct CreateBranch {
|
||||
pub name: String,
|
||||
pub base_branch: Option<String>,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
@@ -219,15 +228,28 @@ impl Project {
|
||||
let current_branch = self.get_current_branch().unwrap_or_default();
|
||||
let mut branches = Vec::new();
|
||||
|
||||
// Helper function to get last commit date for a branch
|
||||
let get_last_commit_date = |branch: &git2::Branch| -> Result<DateTime<Utc>, git2::Error> {
|
||||
if let Some(target) = branch.get().target() {
|
||||
if let Ok(commit) = repo.find_commit(target) {
|
||||
let timestamp = commit.time().seconds();
|
||||
return Ok(DateTime::from_timestamp(timestamp, 0).unwrap_or_else(Utc::now));
|
||||
}
|
||||
}
|
||||
Ok(Utc::now()) // Default to now if we can't get the commit date
|
||||
};
|
||||
|
||||
// Get local branches
|
||||
let local_branches = repo.branches(Some(BranchType::Local))?;
|
||||
for branch_result in local_branches {
|
||||
let (branch, _) = branch_result?;
|
||||
if let Some(name) = branch.name()? {
|
||||
let last_commit_date = get_last_commit_date(&branch)?;
|
||||
branches.push(GitBranch {
|
||||
name: name.to_string(),
|
||||
is_current: name == current_branch,
|
||||
is_remote: false,
|
||||
last_commit_date,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -239,30 +261,83 @@ impl Project {
|
||||
if let Some(name) = branch.name()? {
|
||||
// Skip remote HEAD references
|
||||
if !name.ends_with("/HEAD") {
|
||||
let last_commit_date = get_last_commit_date(&branch)?;
|
||||
branches.push(GitBranch {
|
||||
name: name.to_string(),
|
||||
is_current: false,
|
||||
is_remote: true,
|
||||
last_commit_date,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort branches: current first, then local, then remote
|
||||
// Sort branches: current first, then by most recent commit date
|
||||
branches.sort_by(|a, b| {
|
||||
if a.is_current && !b.is_current {
|
||||
std::cmp::Ordering::Less
|
||||
} else if !a.is_current && b.is_current {
|
||||
std::cmp::Ordering::Greater
|
||||
} else if !a.is_remote && b.is_remote {
|
||||
std::cmp::Ordering::Less
|
||||
} else if a.is_remote && !b.is_remote {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
a.name.cmp(&b.name)
|
||||
// Sort by most recent commit date (newest first)
|
||||
b.last_commit_date.cmp(&a.last_commit_date)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
pub fn create_branch(
|
||||
&self,
|
||||
branch_name: &str,
|
||||
base_branch: Option<&str>,
|
||||
) -> Result<GitBranch, git2::Error> {
|
||||
let repo = Repository::open(&self.git_repo_path)?;
|
||||
|
||||
// Get the base branch reference - default to current branch if not specified
|
||||
let base_branch_name = match base_branch {
|
||||
Some(name) => name.to_string(),
|
||||
None => self
|
||||
.get_current_branch()
|
||||
.unwrap_or_else(|_| "HEAD".to_string()),
|
||||
};
|
||||
|
||||
// Find the base commit
|
||||
let base_commit = if base_branch_name == "HEAD" {
|
||||
repo.head()?.peel_to_commit()?
|
||||
} else {
|
||||
// Try to find the branch as local first, then remote
|
||||
let base_ref = if let Ok(local_ref) =
|
||||
repo.find_reference(&format!("refs/heads/{}", base_branch_name))
|
||||
{
|
||||
local_ref
|
||||
} else if let Ok(remote_ref) =
|
||||
repo.find_reference(&format!("refs/remotes/{}", base_branch_name))
|
||||
{
|
||||
remote_ref
|
||||
} else {
|
||||
return Err(git2::Error::from_str(&format!(
|
||||
"Base branch '{}' not found",
|
||||
base_branch_name
|
||||
)));
|
||||
};
|
||||
base_ref.peel_to_commit()?
|
||||
};
|
||||
|
||||
// Create the new branch
|
||||
let _new_branch = repo.branch(branch_name, &base_commit, false)?;
|
||||
|
||||
// Get the commit date for the new branch (same as base commit)
|
||||
let last_commit_date = {
|
||||
let timestamp = base_commit.time().seconds();
|
||||
DateTime::from_timestamp(timestamp, 0).unwrap_or_else(Utc::now)
|
||||
};
|
||||
|
||||
Ok(GitBranch {
|
||||
name: branch_name.to_string(),
|
||||
is_current: false,
|
||||
is_remote: false,
|
||||
last_commit_date,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use uuid::Uuid;
|
||||
|
||||
use crate::models::{
|
||||
project::{
|
||||
CreateProject, GitBranch, Project, ProjectWithBranch, SearchMatchType, SearchResult,
|
||||
UpdateProject,
|
||||
CreateBranch, CreateProject, GitBranch, Project, ProjectWithBranch, SearchMatchType,
|
||||
SearchResult, UpdateProject,
|
||||
},
|
||||
ApiResponse,
|
||||
};
|
||||
@@ -94,6 +94,60 @@ pub async fn get_project_branches(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_project_branch(
|
||||
Path(id): Path<Uuid>,
|
||||
Extension(pool): Extension<SqlitePool>,
|
||||
Json(payload): Json<CreateBranch>,
|
||||
) -> Result<ResponseJson<ApiResponse<GitBranch>>, StatusCode> {
|
||||
// Validate branch name
|
||||
if payload.name.trim().is_empty() {
|
||||
return Ok(ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some("Branch name cannot be empty".to_string()),
|
||||
}));
|
||||
}
|
||||
|
||||
// Check if branch name contains invalid characters
|
||||
if payload.name.contains(' ') {
|
||||
return Ok(ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some("Branch name cannot contain spaces".to_string()),
|
||||
}));
|
||||
}
|
||||
|
||||
match Project::find_by_id(&pool, id).await {
|
||||
Ok(Some(project)) => {
|
||||
match project.create_branch(&payload.name, payload.base_branch.as_deref()) {
|
||||
Ok(branch) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(branch),
|
||||
message: Some(format!("Branch '{}' created successfully", payload.name)),
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to create branch '{}' for project {}: {}",
|
||||
payload.name,
|
||||
id,
|
||||
e
|
||||
);
|
||||
Ok(ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some(format!("Failed to create branch: {}", e)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => Err(StatusCode::NOT_FOUND),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch project: {}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_project(
|
||||
Extension(pool): Extension<SqlitePool>,
|
||||
Json(payload): Json<CreateProject>,
|
||||
@@ -436,6 +490,9 @@ pub fn projects_router() -> Router {
|
||||
get(get_project).put(update_project).delete(delete_project),
|
||||
)
|
||||
.route("/projects/:id/with-branch", get(get_project_with_branch))
|
||||
.route("/projects/:id/branches", get(get_project_branches))
|
||||
.route(
|
||||
"/projects/:id/branches",
|
||||
get(get_project_branches).post(create_project_branch),
|
||||
)
|
||||
.route("/projects/:id/search", get(search_project_files))
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,7 +36,9 @@ export type SearchResult = { path: string, is_file: boolean, match_type: SearchM
|
||||
|
||||
export type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
|
||||
|
||||
export type GitBranch = { name: string, is_current: boolean, is_remote: boolean, };
|
||||
export type GitBranch = { name: string, is_current: boolean, is_remote: boolean, last_commit_date: Date, };
|
||||
|
||||
export type CreateBranch = { name: string, base_branch: string | null, };
|
||||
|
||||
export type CreateTask = { project_id: string, title: string, description: string | null, };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user