* All tasks are complete. Here's a summary of the changes made:
## Summary
Successfully refactored the project branches API to fetch branches using repo IDs instead of project IDs.
### Files Created
- `frontend/src/hooks/useRepoBranches.ts` - New hook that fetches branches for a single repository using `repoApi.getBranches(repoId)`
### Files Modified
1. **`frontend/src/hooks/index.ts`** - Added export for `useRepoBranches`
2. **`frontend/src/components/dialogs/tasks/GitActionsDialog.tsx`**
- Removed `useState` for `repoBranches` and `loadingBranches`
- Removed `useEffect` that called `projectsApi.getBranches(effectiveProjectId)`
- Removed `useMemo` that filtered branches by `selectedRepoId`
- Now uses `useRepoBranches(selectedRepoId)` to fetch branches directly for the selected repo from the task attempt
3. **`frontend/src/components/dialogs/tasks/CreatePRDialog.tsx`**
- Removed `projectId` from props interface and component destructuring
- Removed `useState` for `branches` and `branchesLoading`
- Removed branch-fetching logic from useEffect
- Now uses `useRepoBranches(repoId)` to fetch branches directly for the specified repo
- Split useEffect into two: one for form initialization, one for setting default base branch
4. **`frontend/src/components/tasks/Toolbar/GitOperations.tsx`**
- Removed `projectId` from `CreatePRDialog.show()` call
### Benefits
- More efficient - fetches only branches for the specific repo needed instead of all project repos
- Better caching - React Query caches branches per repo for efficient reuse
- Cleaner code - removed manual filtering logic from components
- Type safety - hooks return `GitBranch[]` directly instead of `RepositoryBranches[]`
* All tasks complete. Here's a summary of the changes made:
## Summary
Successfully decoupled the branches API from project IDs.
### Changes Made
**1. `frontend/src/hooks/useBranches.ts`** - Refactored to accept `repos: Array<{ id: string; name: string }>` instead of `projectId`
- Now uses `useQueries` to fetch branches for multiple repos in parallel
- Reuses the same query keys as `useRepoBranches` for cache consistency
**2. `frontend/src/hooks/useRepoBranchSelection.ts`** - Refactored to accept `repos: Repo[]` instead of `projectId`
- Removed internal fetch of project repos (caller now provides them)
- Removed `projectRepos` from return value
- Uses refactored `useBranches(repos)`
**3. `frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx`**
- Added `useQuery` to fetch project repos separately
- Passes `repos` to `useRepoBranchSelection`
**4. `frontend/src/components/dialogs/tasks/TaskFormDialog.tsx`**
- Already fetched `projectRepos`
- Updated to pass `projectRepos` to refactored `useBranches`
**5. `frontend/src/components/layout/Navbar.tsx`**
- Replaced `useBranches(projectId)` with `useQuery` for `projectsApi.getRepositories`
- Only needed repo count, not branches
**6. `frontend/src/components/projects/ProjectCard.tsx`**
- Replaced `useBranches(project.id)` with `useQuery` for `projectsApi.getRepositories`
- Only needed repo count, not branches
### Benefits
- Branch hooks no longer require a project ID
- Repos can come from any source (project, attempt, or future sources without projects)
- Better separation of concerns
- Enables future task attempts without projects attached
* Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a
* The fix is already applied. Type checks pass with `Repo[]` instead of the loose `Array<{ id: string; name: string }>` type.
This makes it explicit that `useBranches` expects actual `Repo` objects (with `Repo.id` being the repository's UUID), not project-repo junction records.
* All done. The refactor is complete:
**Summary of changes:**
1. **Created `useProjectRepos` hook** (`frontend/src/hooks/useProjectRepos.ts`) - A reusable hook that fetches project repositories with proper query key management
2. **Updated 5 files** to use the new hook instead of duplicating the `useQuery` pattern:
- `CreateAttemptDialog.tsx`
- `TaskFormDialog.tsx`
- `Navbar.tsx`
- `ProjectCard.tsx`
- `NoServerContent.tsx`
3. **Earlier refactors** (from before the summary):
- `useBranches.ts` - Now accepts `repos: Repo[]` instead of `projectId`
- `useRepoBranchSelection.ts` - Now accepts `repos: Repo[]` instead of `projectId`
This enables future task attempts without projects attached, as branch fetching is now decoupled from project IDs.
* No more usages. The branches in `ProjectTasks.tsx` are used for the diffs panel - showing available branches for git operations when viewing an attempt's diffs.
* Cleanup script changes for task attempt 6b5b9208-565d-4620-9fe3-20791db77d7a
* All done. The refactor is complete:
**Summary of changes:**
`GitOperations` now fetches its own branches using `useRepoBranches(selectedRepoId)` instead of receiving them as a prop. This:
1. Removes prop drilling through `ProjectTasks` → `DiffsPanelContainer` → `DiffsPanel` → `GitOperations`
2. Removes the need for `ProjectTasks` to fetch all project repos just to get branches for the selected repo
3. Makes `GitOperations` self-contained - it already has `selectedRepoId` from `useAttemptRepo`, so it can fetch its own branches
4. Simplifies `GitActionsDialog` which no longer needs to fetch branches either
* All done. Here's a summary of the changes:
**Removed `projectId` from GitOperations flow:**
1. **`useRebase.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)`
2. **`useChangeTargetBranch.ts`** - Changed param from `projectId` to `repoId`, updated invalidation to use `repoBranchKeys.byRepo(repoId)`
3. **`useMerge.ts`** - Updated invalidation to use `repoBranchKeys.all` instead of `['projectBranches']`
4. **`useGitOperations.ts`** - Changed param from `projectId` to `repoId`
5. **`GitOperations.tsx`** - Removed `projectId` prop, now passes `selectedRepoId` to `useGitOperations`
6. **`GitActionsDialog.tsx`** - Removed `projectId` from props and internal logic
7. **`DiffsPanelContainer`** (in ProjectTasks.tsx) - Removed `projectId` prop
8. **Callers of `GitActionsDialog.show`** - Removed `projectId` from `NextActionCard.tsx` and `actions-dropdown.tsx`
* The cleanup is complete. Here's a summary of what was done:
**Removed dead project branches code:**
1. **Backend** (`crates/server/src/routes/projects.rs`):
- Removed `get_project_branches` handler and route
- Removed `RepositoryBranches` and `ProjectBranchesResponse` structs
- Fixed unused `Serialize` import warning
2. **Type Generation** (`crates/server/src/bin/generate_types.rs`):
- Removed exports for `ProjectBranchesResponse` and `RepositoryBranches`
3. **Frontend Hooks** (`frontend/src/hooks/useBranches.ts`):
- Defined local `RepositoryBranches` type instead of importing from shared/types
4. **Frontend API** (`frontend/src/lib/api.ts`):
- Removed `projectsApi.getBranches` method
- Removed `RepositoryBranches` and `ProjectBranchesResponse` imports
Both TypeScript and Rust compilation pass without errors.
* Done. Removed the `RepositoryBranches` type and simplified `useBranches` to return `Map<string, GitBranch[]>`. Changes:
1. **`useBranches.ts`** - Returns `Map<string, GitBranch[]>` instead of `RepositoryBranches[]`
2. **`useRepoBranchSelection.ts`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()`
3. **`TaskFormDialog.tsx`** - Uses `branchMap.get(repo.id) ?? []` instead of `.find()`
Type checks pass.
* Done. Deleted `useBranches` entirely and consolidated everything into `useRepoBranchSelection`. Summary:
1. **Deleted** `frontend/src/hooks/useBranches.ts`
2. **Updated** `useRepoBranchSelection.ts` - inlined the `useQueries` logic directly
3. **Updated** `hooks/index.ts` - removed `useBranches` export
4. **Updated** `TaskFormDialog.tsx` - now uses `useRepoBranchSelection` instead of duplicating the config-building logic
Type checks pass.
* types
212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
import {
|
|
Card,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card.tsx';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu.tsx';
|
|
import { Button } from '@/components/ui/button.tsx';
|
|
import {
|
|
Calendar,
|
|
Edit,
|
|
ExternalLink,
|
|
FolderOpen,
|
|
Link2,
|
|
MoreHorizontal,
|
|
Trash2,
|
|
Unlink,
|
|
} from 'lucide-react';
|
|
import { Project } from 'shared/types';
|
|
import { useEffect, useRef } from 'react';
|
|
import { useOpenProjectInEditor } from '@/hooks/useOpenProjectInEditor';
|
|
import { useNavigateWithSearch, useProjectRepos } from '@/hooks';
|
|
import { projectsApi } from '@/lib/api';
|
|
import { LinkProjectDialog } from '@/components/dialogs/projects/LinkProjectDialog';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useProjectMutations } from '@/hooks/useProjectMutations';
|
|
|
|
type Props = {
|
|
project: Project;
|
|
isFocused: boolean;
|
|
fetchProjects: () => void;
|
|
setError: (error: string) => void;
|
|
onEdit: (project: Project) => void;
|
|
};
|
|
|
|
function ProjectCard({
|
|
project,
|
|
isFocused,
|
|
fetchProjects,
|
|
setError,
|
|
onEdit,
|
|
}: Props) {
|
|
const navigate = useNavigateWithSearch();
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const handleOpenInEditor = useOpenProjectInEditor(project);
|
|
const { t } = useTranslation('projects');
|
|
|
|
const { data: repos } = useProjectRepos(project.id);
|
|
const isSingleRepoProject = repos?.length === 1;
|
|
|
|
const { unlinkProject } = useProjectMutations({
|
|
onUnlinkSuccess: () => {
|
|
fetchProjects();
|
|
},
|
|
onUnlinkError: (error) => {
|
|
console.error('Failed to unlink project:', error);
|
|
setError('Failed to unlink project');
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isFocused && ref.current) {
|
|
ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
ref.current.focus();
|
|
}
|
|
}, [isFocused]);
|
|
|
|
const handleDelete = async (id: string, name: string) => {
|
|
if (
|
|
!confirm(
|
|
`Are you sure you want to delete "${name}"? This action cannot be undone.`
|
|
)
|
|
)
|
|
return;
|
|
|
|
try {
|
|
await projectsApi.delete(id);
|
|
fetchProjects();
|
|
} catch (error) {
|
|
console.error('Failed to delete project:', error);
|
|
setError('Failed to delete project');
|
|
}
|
|
};
|
|
|
|
const handleEdit = (project: Project) => {
|
|
onEdit(project);
|
|
};
|
|
|
|
const handleOpenInIDE = () => {
|
|
handleOpenInEditor();
|
|
};
|
|
|
|
const handleLinkProject = async () => {
|
|
try {
|
|
await LinkProjectDialog.show({
|
|
projectId: project.id,
|
|
projectName: project.name,
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to link project:', error);
|
|
}
|
|
};
|
|
|
|
const handleUnlinkProject = () => {
|
|
const confirmed = window.confirm(
|
|
`Are you sure you want to unlink "${project.name}"? The local project will remain, but it will no longer be linked to the remote project.`
|
|
);
|
|
if (confirmed) {
|
|
unlinkProject.mutate(project.id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
className={`hover:shadow-md transition-shadow cursor-pointer focus:ring-2 focus:ring-primary outline-none border`}
|
|
onClick={() => navigate(`/projects/${project.id}/tasks`)}
|
|
tabIndex={isFocused ? 0 : -1}
|
|
ref={ref}
|
|
>
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<CardTitle className="text-lg">{project.name}</CardTitle>
|
|
<div className="flex items-center gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
navigate(`/projects/${project.id}`);
|
|
}}
|
|
>
|
|
<ExternalLink className="mr-2 h-4 w-4" />
|
|
{t('viewProject')}
|
|
</DropdownMenuItem>
|
|
{isSingleRepoProject && (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleOpenInIDE();
|
|
}}
|
|
>
|
|
<FolderOpen className="mr-2 h-4 w-4" />
|
|
{t('openInIDE')}
|
|
</DropdownMenuItem>
|
|
)}
|
|
{project.remote_project_id ? (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleUnlinkProject();
|
|
}}
|
|
>
|
|
<Unlink className="mr-2 h-4 w-4" />
|
|
{t('unlinkFromOrganization')}
|
|
</DropdownMenuItem>
|
|
) : (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleLinkProject();
|
|
}}
|
|
>
|
|
<Link2 className="mr-2 h-4 w-4" />
|
|
{t('linkToOrganization')}
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEdit(project);
|
|
}}
|
|
>
|
|
<Edit className="mr-2 h-4 w-4" />
|
|
{t('common:buttons.edit')}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleDelete(project.id, project.name);
|
|
}}
|
|
className="text-destructive"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
{t('common:buttons.delete')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
<CardDescription className="flex items-center">
|
|
<Calendar className="mr-1 h-3 w-3" />
|
|
{t('createdDate', {
|
|
date: new Date(project.created_at).toLocaleDateString(),
|
|
})}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export default ProjectCard;
|