Task attempt 9280e32b-e17b-492e-9446-2d9765f1b1b6 - Final changes
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, };
|
||||
|
||||
Reference in New Issue
Block a user