Skip to content

Commit b0ab561

Browse files
met les options de selection de project multiples, deletes, mettre en pr
1 parent 2869c1e commit b0ab561

File tree

2 files changed

+119
-7
lines changed

2 files changed

+119
-7
lines changed

src/app/(app)/projects/actions.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {
88
createProject,
99
updateProjectGithubRepo,
1010
updateProjectVisibility,
11+
deleteProject,
12+
getProjectMemberRole,
13+
getProjectByUuid,
1114
} from "@/lib/db";
1215
import { auth } from "@/lib/authEdge";
1316
import { Octokit } from "octokit";
@@ -154,3 +157,56 @@ export async function importFlowUpProjectsAction(): Promise<{
154157
return { success: false, successCount: 0, errorCount: 0, error: error.message || "An unexpected error occurred during bulk import." };
155158
}
156159
}
160+
161+
export async function batchUpdateProjectsAction(
162+
projectUuids: string[],
163+
action: 'makePrivate' | 'makePublic' | 'delete'
164+
): Promise<{ successCount: number; errorCount: number; errors: string[] }> {
165+
const session = await auth();
166+
if (!session?.user?.uuid) {
167+
throw new Error("Authentication required.");
168+
}
169+
const userUuid = session.user.uuid;
170+
171+
let successCount = 0;
172+
let errorCount = 0;
173+
const errors: string[] = [];
174+
175+
for (const uuid of projectUuids) {
176+
try {
177+
const project = await getProjectByUuid(uuid);
178+
if (!project) {
179+
errors.push(`Project with ID ${uuid} not found.`);
180+
errorCount++;
181+
continue;
182+
}
183+
184+
const role = await getProjectMemberRole(uuid, userUuid);
185+
if (role !== 'owner') {
186+
errors.push(`You do not have permission to modify "${project.name}".`);
187+
errorCount++;
188+
continue;
189+
}
190+
191+
if (action === 'makePrivate') {
192+
await updateProjectVisibility(uuid, true);
193+
successCount++;
194+
} else if (action === 'makePublic') {
195+
await updateProjectVisibility(uuid, false);
196+
successCount++;
197+
} else if (action === 'delete') {
198+
await deleteProject(uuid);
199+
successCount++;
200+
}
201+
} catch (e: any) {
202+
errors.push(`Failed to update project ${uuid}: ${e.message}`);
203+
errorCount++;
204+
}
205+
}
206+
207+
if (successCount > 0) {
208+
revalidatePath('/projects');
209+
}
210+
211+
return { successCount, errorCount, errors };
212+
}

src/app/(app)/projects/page.tsx

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
12
'use client';
23

34
import { Button } from "@/components/ui/button";
45
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5-
import { PlusCircle, Search, Filter, FolderKanban, Flame, MoreHorizontal, Copy, Link as LinkIcon, ChevronDown, Github, Loader2 } from "lucide-react";
6+
import { PlusCircle, Search, Filter, FolderKanban, Flame, MoreHorizontal, Copy, Link as LinkIcon, ChevronDown, Github, Loader2, X, Trash2, EyeOff, Eye } from "lucide-react";
67
import Link from "next/link";
78
import { Input } from "@/components/ui/input";
89
import { useEffect, useState, useCallback, useActionState, useTransition } from "react";
910
import type { Project, DuplicateProjectFormState } from "@/types";
1011
import { useAuth } from "@/hooks/useAuth";
1112
import { Skeleton } from "@/components/ui/skeleton";
1213
import { useRouter } from "next/navigation";
13-
import { fetchProjectsAction, getLinkableGithubReposAction, createProjectFromRepoAction, importFlowUpProjectsAction } from "./actions";
14+
import { fetchProjectsAction, getLinkableGithubReposAction, createProjectFromRepoAction, importFlowUpProjectsAction, batchUpdateProjectsAction } from "./actions";
1415
import type { LinkableGithubRepo } from "./actions";
1516
import { duplicateProjectAction } from "./[id]/actions";
1617
import { cn } from "@/lib/utils";
@@ -39,6 +40,9 @@ export default function ProjectsPage() {
3940
const [linkableRepos, setLinkableRepos] = useState<LinkableGithubRepo[]>([]);
4041
const [isLoadingRepos, setIsLoadingRepos] = useState(false);
4142
const [repoToImport, setRepoToImport] = useState<LinkableGithubRepo | null>(null);
43+
44+
const [selectedProjects, setSelectedProjects] = useState<string[]>([]);
45+
const [isBatchProcessing, startBatchTransition] = useTransition();
4246

4347
const loadProjects = useCallback(async () => {
4448
if (user && !authLoading) {
@@ -103,7 +107,6 @@ export default function ProjectsPage() {
103107
});
104108
};
105109

106-
107110
useEffect(() => {
108111
if (!isDuplicating && duplicateFormState) {
109112
if (duplicateFormState.message && !duplicateFormState.error) {
@@ -117,6 +120,36 @@ export default function ProjectsPage() {
117120
}
118121
}, [duplicateFormState, isDuplicating, toast, loadProjects]);
119122

123+
const handleSelectProject = (projectId: string, isSelected: boolean) => {
124+
if (isSelected) {
125+
setSelectedProjects(prev => [...prev, projectId]);
126+
} else {
127+
setSelectedProjects(prev => prev.filter(id => id !== projectId));
128+
}
129+
};
130+
131+
const handleSelectAll = (isSelected: boolean) => {
132+
if (isSelected) {
133+
setSelectedProjects(filteredProjects.map(p => p.uuid));
134+
} else {
135+
setSelectedProjects([]);
136+
}
137+
};
138+
139+
const handleBatchAction = (action: 'makePrivate' | 'makePublic' | 'delete') => {
140+
startBatchTransition(async () => {
141+
const result = await batchUpdateProjectsAction(selectedProjects, action);
142+
if (result.successCount > 0) {
143+
toast({ title: 'Batch Action Complete', description: `${result.successCount} projects updated successfully.` });
144+
}
145+
if (result.errorCount > 0) {
146+
toast({ variant: 'destructive', title: 'Batch Action Failed', description: `${result.errorCount} projects failed to update. ${result.errors.join(' ')}` });
147+
}
148+
setSelectedProjects([]);
149+
await loadProjects();
150+
});
151+
};
152+
120153
if (authLoading && isLoadingProjects) {
121154
return (
122155
<div className="space-y-6">
@@ -154,6 +187,8 @@ export default function ProjectsPage() {
154187
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
155188
(project.description && project.description.toLowerCase().includes(searchTerm.toLowerCase()))
156189
);
190+
191+
const allSelected = filteredProjects.length > 0 && selectedProjects.length === filteredProjects.length;
157192

158193
return (
159194
<div className="space-y-6">
@@ -194,6 +229,19 @@ export default function ProjectsPage() {
194229
</Button>
195230
</div>
196231
</div>
232+
{selectedProjects.length > 0 && (
233+
<div className="mt-4 p-2 border rounded-lg bg-muted/50 flex flex-col sm:flex-row items-center justify-between gap-2">
234+
<div className="flex items-center gap-2">
235+
<Checkbox id="select-all" checked={allSelected} onCheckedChange={(checked) => handleSelectAll(Boolean(checked))} />
236+
<Label htmlFor="select-all" className="text-sm font-medium">{selectedProjects.length} selected</Label>
237+
</div>
238+
<div className="flex items-center gap-2">
239+
<Button size="sm" variant="outline" onClick={() => handleBatchAction('makePublic')} disabled={isBatchProcessing}><Eye className="mr-2 h-4 w-4" /> Make Public</Button>
240+
<Button size="sm" variant="outline" onClick={() => handleBatchAction('makePrivate')} disabled={isBatchProcessing}><EyeOff className="mr-2 h-4 w-4" /> Make Private</Button>
241+
<Button size="sm" variant="destructive" onClick={() => handleBatchAction('delete')} disabled={isBatchProcessing}><Trash2 className="mr-2 h-4 w-4" /> Delete</Button>
242+
</div>
243+
</div>
244+
)}
197245
</CardHeader>
198246
<CardContent>
199247
{isLoadingProjects ? (
@@ -231,11 +279,19 @@ export default function ProjectsPage() {
231279
<Dialog key={project.uuid} open={projectToDuplicate?.uuid === project.uuid} onOpenChange={(open) => !open && setProjectToDuplicate(null)}>
232280
<Card
233281
className={cn(
234-
"hover:shadow-lg transition-shadow flex flex-col",
235-
project.isUrgent && "border-destructive ring-1 ring-destructive"
282+
"hover:shadow-lg transition-shadow flex flex-col relative",
283+
project.isUrgent && "border-destructive ring-1 ring-destructive",
284+
selectedProjects.includes(project.uuid) && "ring-2 ring-primary border-primary"
236285
)}
237286
>
238-
<CardHeader className="flex-grow">
287+
<div className="absolute top-2 left-2 z-10">
288+
<Checkbox
289+
id={`select-${project.uuid}`}
290+
checked={selectedProjects.includes(project.uuid)}
291+
onCheckedChange={(checked) => handleSelectProject(project.uuid, Boolean(checked))}
292+
/>
293+
</div>
294+
<CardHeader className="flex-grow pt-8">
239295
<div className="flex justify-between items-start">
240296
<CardTitle className="hover:text-primary">
241297
<Link href={`/projects/${project.uuid}`}>{project.name}</Link>
@@ -248,7 +304,7 @@ export default function ProjectsPage() {
248304
</CardHeader>
249305
<CardContent>
250306
<div className="text-xs text-muted-foreground space-y-0.5">
251-
<p>Owner: <span className="font-medium text-foreground">{project.ownerUuid === user?.uuid ? 'You' : 'Other'}</span></p>
307+
<p>Owner: <span className="font-medium text-foreground">{project.ownerUuid === user?.uuid ? 'You' : project.ownerName || 'Other'}</span></p>
252308
<p>Updated: <span className="font-medium text-foreground">{new Date(project.updatedAt).toLocaleDateString()}</span></p>
253309
<p>Status: <span className={cn("font-medium", project.isPrivate ? "text-foreground" : "text-green-600")}>{project.isPrivate ? 'Private' : 'Public'}</span></p>
254310
</div>

0 commit comments

Comments
 (0)