Task attempt 9280e32b-e17b-492e-9446-2d9765f1b1b6 - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-24 23:49:08 +01:00
parent 332c04938a
commit 557e8930f7
7 changed files with 121 additions and 37 deletions

View File

@@ -80,6 +80,7 @@ fn main() {
vibe_kanban::executor::ExecutorConstants::decl(),
vibe_kanban::models::project::CreateProject::decl(),
vibe_kanban::models::project::Project::decl(),
vibe_kanban::models::project::ProjectWithBranch::decl(),
vibe_kanban::models::project::UpdateProject::decl(),
vibe_kanban::models::project::SearchResult::decl(),
vibe_kanban::models::project::SearchMatchType::decl(),

View File

@@ -1,4 +1,5 @@
use chrono::{DateTime, Utc};
use git2::Repository;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use ts_rs::TS;
@@ -38,6 +39,22 @@ pub struct UpdateProject {
pub dev_script: Option<String>,
}
#[derive(Debug, Serialize, TS)]
#[ts(export)]
pub struct ProjectWithBranch {
pub id: Uuid,
pub name: String,
pub git_repo_path: String,
pub setup_script: Option<String>,
pub dev_script: Option<String>,
pub current_branch: Option<String>,
#[ts(type = "Date")]
pub created_at: DateTime<Utc>,
#[ts(type = "Date")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, TS)]
#[ts(export)]
pub struct SearchResult {
@@ -162,4 +179,30 @@ impl Project {
Ok(result.count > 0)
}
pub fn get_current_branch(&self) -> Result<String, git2::Error> {
let repo = Repository::open(&self.git_repo_path)?;
let head = repo.head()?;
if let Some(branch_name) = head.shorthand() {
Ok(branch_name.to_string())
} else {
Ok("HEAD".to_string())
}
}
pub fn with_branch_info(self) -> ProjectWithBranch {
let current_branch = self.get_current_branch().ok();
ProjectWithBranch {
id: self.id,
name: self.name,
git_repo_path: self.git_repo_path,
setup_script: self.setup_script,
dev_script: self.dev_script,
current_branch,
created_at: self.created_at,
updated_at: self.updated_at,
}
}
}

View File

@@ -10,7 +10,7 @@ use std::collections::HashMap;
use uuid::Uuid;
use crate::models::{
project::{CreateProject, Project, SearchMatchType, SearchResult, UpdateProject},
project::{CreateProject, Project, ProjectWithBranch, SearchMatchType, SearchResult, UpdateProject},
ApiResponse,
};
@@ -48,6 +48,24 @@ pub async fn get_project(
}
}
pub async fn get_project_with_branch(
Path(id): Path<Uuid>,
Extension(pool): Extension<SqlitePool>,
) -> Result<ResponseJson<ApiResponse<ProjectWithBranch>>, StatusCode> {
match Project::find_by_id(&pool, id).await {
Ok(Some(project)) => Ok(ResponseJson(ApiResponse {
success: true,
data: Some(project.with_branch_info()),
message: None,
})),
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>,
@@ -388,5 +406,6 @@ pub fn projects_router() -> Router {
"/projects/:id",
get(get_project).put(update_project).delete(delete_project),
)
.route("/projects/:id/with-branch", get(get_project_with_branch))
.route("/projects/:id/search", get(search_project_files))
}

View File

@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Project, ApiResponse } from 'shared/types'
import { ProjectWithBranch, ApiResponse } from 'shared/types'
import { ProjectForm } from './project-form'
import { makeRequest } from '@/lib/api'
import { ArrowLeft, Edit, Trash2, Calendar, Clock, AlertCircle, Loader2, CheckSquare } from 'lucide-react'
@@ -16,7 +16,7 @@ interface ProjectDetailProps {
export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
const navigate = useNavigate()
const [project, setProject] = useState<Project | null>(null)
const [project, setProject] = useState<ProjectWithBranch | null>(null)
const [loading, setLoading] = useState(false)
const [showEditForm, setShowEditForm] = useState(false)
const [error, setError] = useState('')
@@ -25,8 +25,8 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
setLoading(true)
setError('')
try {
const response = await makeRequest(`/api/projects/${projectId}`)
const data: ApiResponse<Project> = await response.json()
const response = await makeRequest(`/api/projects/${projectId}/with-branch`)
const data: ApiResponse<ProjectWithBranch> = await response.json()
if (data.success && data.data) {
setProject(data.data)
} else {
@@ -109,7 +109,14 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
Back to Projects
</Button>
<div>
<h1 className="text-2xl font-bold">{project.name}</h1>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{project.name}</h1>
{project.current_branch && (
<span className="text-sm text-muted-foreground bg-muted px-2 py-1 rounded-md">
{project.current_branch}
</span>
)}
</div>
<p className="text-sm text-muted-foreground">Project details and settings</p>
</div>
</div>

View File

@@ -22,6 +22,7 @@ interface FileSearchTextareaProps {
disabled?: boolean
className?: string
projectId?: string
onKeyDown?: (e: React.KeyboardEvent) => void
}
export function FileSearchTextarea({
@@ -31,7 +32,8 @@ export function FileSearchTextarea({
rows = 3,
disabled = false,
className,
projectId
projectId,
onKeyDown
}: FileSearchTextareaProps) {
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<FileSearchResult[]>([])
@@ -109,34 +111,39 @@ export function FileSearchTextarea({
// Handle keyboard navigation
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (!showDropdown || searchResults.length === 0) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex(prev =>
prev < searchResults.length - 1 ? prev + 1 : 0
)
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex(prev =>
prev > 0 ? prev - 1 : searchResults.length - 1
)
break
case 'Enter':
if (selectedIndex >= 0) {
// Handle dropdown navigation first
if (showDropdown && searchResults.length > 0) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
selectFile(searchResults[selectedIndex])
}
break
case 'Escape':
e.preventDefault()
setShowDropdown(false)
setSearchQuery('')
setAtSymbolPosition(-1)
break
setSelectedIndex(prev =>
prev < searchResults.length - 1 ? prev + 1 : 0
)
return
case 'ArrowUp':
e.preventDefault()
setSelectedIndex(prev =>
prev > 0 ? prev - 1 : searchResults.length - 1
)
return
case 'Enter':
if (selectedIndex >= 0) {
e.preventDefault()
selectFile(searchResults[selectedIndex])
return
}
break
case 'Escape':
e.preventDefault()
setShowDropdown(false)
setSearchQuery('')
setAtSymbolPosition(-1)
return
}
}
// Call the passed onKeyDown handler
onKeyDown?.(e)
}
// Select a file and insert it into the text

View File

@@ -17,7 +17,7 @@ import { TaskDetailsPanel } from "@/components/tasks/TaskDetailsPanel";
import type {
TaskStatus,
TaskWithAttemptStatus,
Project,
ProjectWithBranch,
ExecutorConfig,
CreateTaskAndStart,
} from "shared/types";
@@ -38,7 +38,7 @@ export function ProjectTasks() {
}>();
const navigate = useNavigate();
const [tasks, setTasks] = useState<Task[]>([]);
const [project, setProject] = useState<Project | null>(null);
const [project, setProject] = useState<ProjectWithBranch | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
@@ -92,10 +92,10 @@ export function ProjectTasks() {
const fetchProject = async () => {
try {
const response = await makeRequest(`/api/projects/${projectId}`);
const response = await makeRequest(`/api/projects/${projectId}/with-branch`);
if (response.ok) {
const result: ApiResponse<Project> = await response.json();
const result: ApiResponse<ProjectWithBranch> = await response.json();
if (result.success && result.data) {
setProject(result.data);
}
@@ -349,6 +349,11 @@ export function ProjectTasks() {
<div className="px-8 my-12 flex flex-row">
<div className="w-full flex items-center gap-3">
<h1 className="text-2xl font-bold">{project?.name || "Project"}</h1>
{project?.current_branch && (
<span className="text-sm text-muted-foreground bg-muted px-2 py-1 rounded-md">
{project.current_branch}
</span>
)}
<Button
variant="ghost"
size="sm"

View File

@@ -28,6 +28,8 @@ export type CreateProject = { name: string, git_repo_path: string, use_existing_
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, created_at: Date, updated_at: Date, };
export type ProjectWithBranch = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, current_branch: string | null, created_at: Date, updated_at: Date, };
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, };
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };