feat: Add Sync All button to projects settings page with tests

- Adds syncAllMutation calling trpc.syncAllProjects once for all cards
- Shows Sync All button in header when ≥1 project exists (hidden otherwise)
- Propagates isPending to ProjectCard.syncAllPending to disable per-card sync
- On success: toasts all-success or failure count; invalidates listProjects + getProjectSyncStatus per project
- On network error: toasts Sync failed with err.message
- Adds unit tests for ProjectSyncManager.syncAllProjects: empty list, all succeed, partial failure, result shape, and failure counting logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 14:39:58 +01:00
parent d867a5f397
commit 7a4d0d2582
2 changed files with 206 additions and 2 deletions

View File

@@ -19,6 +19,24 @@ function ProjectsSettingsPage() {
const projectsQuery = trpc.listProjects.useQuery()
const utils = trpc.useUtils()
const syncAllMutation = trpc.syncAllProjects.useMutation({
onSuccess: (results) => {
const failed = results.filter(r => !r.success)
if (failed.length === 0) {
toast.success('All projects synced.')
} else {
toast.error(`${failed.length} project(s) failed to sync — check project names in the list.`)
}
void utils.listProjects.invalidate()
results.forEach(r => {
void utils.getProjectSyncStatus.invalidate({ id: r.projectId })
})
},
onError: (err) => {
toast.error(`Sync failed: ${err.message}`)
},
})
const deleteMutation = trpc.deleteProject.useMutation({
onSuccess: () => {
void utils.listProjects.invalidate()
@@ -43,7 +61,18 @@ function ProjectsSettingsPage() {
return (
<div className="space-y-6">
<div className="flex justify-end">
<div className="flex items-center justify-end gap-2">
{(projects?.length ?? 0) > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => syncAllMutation.mutate()}
disabled={syncAllMutation.isPending}
>
<RefreshCw className={syncAllMutation.isPending ? 'animate-spin mr-2 h-4 w-4' : 'mr-2 h-4 w-4'} />
{syncAllMutation.isPending ? 'Syncing…' : 'Sync All'}
</Button>
)}
<Button size="sm" onClick={() => setRegisterOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Register Project
@@ -72,6 +101,7 @@ function ProjectsSettingsPage() {
<ProjectCard
key={project.id}
project={project}
syncAllPending={syncAllMutation.isPending}
onDelete={() => {
if (window.confirm(`Delete project "${project.name}"? This will also remove the cloned repository.`)) {
deleteMutation.mutate({ id: project.id })
@@ -108,9 +138,11 @@ function formatRelativeTime(date: Date | string | null): string {
function ProjectCard({
project,
onDelete,
syncAllPending,
}: {
project: { id: string; name: string; url: string; defaultBranch: string; lastFetchedAt: Date | null }
onDelete: () => void
syncAllPending?: boolean
}) {
const [editing, setEditing] = useState(false)
const [editValue, setEditValue] = useState(project.defaultBranch)
@@ -215,7 +247,7 @@ function ProjectCard({
size="icon"
className="shrink-0 text-muted-foreground"
onClick={() => syncMutation.mutate({ id: project.id })}
disabled={syncMutation.isPending}
disabled={syncMutation.isPending || syncAllPending}
title="Sync from remote"
>
<RefreshCw className={`h-4 w-4 ${syncMutation.isPending ? 'animate-spin' : ''}`} />