SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%

feat: regrouper les dossiers racine par espace partagé (explorateur, grille, arbre latéral)

Simon-Pierre Boucher committed 25 days ago (Sep 7, 2026) parent 3114277

5 changed files +56 −8

modified app/api/folders/route.ts +2 −0
@@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
19 19 },
20 20 },
21 21 _count: { select: { files: true } },
22 + sharedSpace: { select: { id: true, name: true } },
22 23 },
23 24 where: { parentId: null },
24 25 orderBy: { name: "asc" },
@@ -42,6 +43,7 @@ export async function GET(request: NextRequest) {
42 43 where,
43 44 include: {
44 45 _count: { select: { files: true, children: true } },
46 + sharedSpace: { select: { id: true, name: true } },
45 47 },
46 48 orderBy: { name: "asc" },
47 49 });
modified components/files/FileExplorer.tsx +34 −7
@@ -26,10 +26,25 @@ interface FolderData {
26 26 name: string;
27 27 parentId: string | null;
28 28 sharedSpaceId?: string | null;
29 + sharedSpace?: { id: string; name: string } | null;
29 30 createdAt: string;
30 31 _count?: { files: number; children: number };
31 32 }
32 33
34 +/** À la racine, les dossiers de niveau 1 de chaque espace partagé sont regroupés sous le nom de l'espace. */
35 +function groupFoldersBySpace(folders: FolderData[], enabled: boolean): { key: string; label: string | null; folders: FolderData[] }[] {
36 + if (!enabled) return [{ key: "all", label: null, folders }];
37 + const groups = new Map<string, { key: string; label: string | null; folders: FolderData[] }>();
38 + for (const f of folders) {
39 + const key = f.sharedSpace?.id ?? "__perso";
40 + if (!groups.has(key)) groups.set(key, { key, label: f.sharedSpace?.name ?? "Mes dossiers", folders: [] });
41 + groups.get(key)!.folders.push(f);
42 + }
43 + const arr = Array.from(groups.values());
44 + if (arr.length <= 1) return [{ key: "all", label: null, folders }];
45 + return arr.sort((a, b) => (a.key === "__perso" ? -1 : b.key === "__perso" ? 1 : (a.label ?? "").localeCompare(b.label ?? "", "fr")));
46 +}
47 +
33 48 interface FileExplorerProps {
34 49 folderId?: string | null;
35 50 /** Nom du dossier courant (affiché dans la zone d'upload / overlay) */
@@ -239,6 +254,7 @@ export default function FileExplorer({ folderId, folderName }: FileExplorerProps
239 254 /* ---------- Tri ---------- */
240 255 const dirMul = sortDir === "asc" ? 1 : -1;
241 256 const sortedFolders = useMemo(() => [...folders].sort((a, b) => dirMul * a.name.localeCompare(b.name, "fr", { numeric: true })), [folders, dirMul]);
257 + const folderGroups = useMemo(() => groupFoldersBySpace(sortedFolders, !folderId), [sortedFolders, folderId]);
242 258 const sortedFiles = useMemo(() => [...files].sort((a, b) => {
243 259 let r = 0;
244 260 switch (sortKey) {
@@ -682,8 +698,17 @@ export default function FileExplorer({ folderId, folderName }: FileExplorerProps
682 698 <div className="card overflow-clip">
683 699 <ListHeader />
684 700 <div className="divide-y divide-line/70 dark:divide-white/5">
685 − {sortedFolders.map((folder) => (
686 − <FolderCard key={folder.id} folder={folder} onRefresh={handleRefresh} viewMode="list" focusable />
701 + {folderGroups.map((g) => (
702 + <div key={g.key} className="divide-y divide-line/70 dark:divide-white/5">
703 + {g.label && (
704 + <div className="flex items-center gap-2 px-4 py-2 bg-gray-50/80 dark:bg-white/[0.03] text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500 dark:text-gray-400">
705 + <Users className="w-3.5 h-3.5 text-blue-500" /> {g.label} <span className="text-gray-400 font-normal normal-case tracking-normal">· {g.folders.length} dossier{g.folders.length > 1 ? "s" : ""}</span>
706 + </div>
707 + )}
708 + {g.folders.map((folder) => (
709 + <FolderCard key={folder.id} folder={folder} onRefresh={handleRefresh} viewMode="list" focusable />
710 + ))}
711 + </div>
687 712 ))}
688 713 {sortedFiles.map((file) => (
689 714 <FileCard
@@ -704,16 +729,18 @@ export default function FileExplorer({ folderId, folderName }: FileExplorerProps
704 729 </div>
705 730 ) : (
706 731 <div className="space-y-6">
707 − {sortedFolders.length > 0 && (
708 − <section>
709 − <h3 className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-2.5">Dossiers</h3>
732 + {folderGroups.map((g) => g.folders.length > 0 && (
733 + <section key={g.key}>
734 + <h3 className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-2.5 flex items-center gap-2">
735 + {g.label ? <><Users className="w-3.5 h-3.5 text-blue-500" /> {g.label}</> : "Dossiers"}
736 + </h3>
710 737 <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 lg:grid-cols-4 xl:grid-cols-5">
711 − {sortedFolders.map((folder) => (
738 + {g.folders.map((folder) => (
712 739 <FolderCard key={folder.id} folder={folder} onRefresh={handleRefresh} viewMode="grid" focusable />
713 740 ))}
714 741 </div>
715 742 </section>
716 − )}
743 + ))}
717 744 {sortedFiles.length > 0 && (
718 745 <section>
719 746 <h3 className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-2.5">Fichiers</h3>
modified components/layout/FolderTree.tsx +20 −1
@@ -9,6 +9,7 @@ import { FOLDERS_CHANGED_EVENT, handleInternalDrop, hasInternalDrag, readFolderI
9 9 interface TreeFolder {
10 10 id: string;
11 11 name: string;
12 + sharedSpace?: { id: string; name: string } | null;
12 13 children?: TreeFolder[];
13 14 _count?: { files: number };
14 15 }
@@ -152,5 +153,23 @@ export default function FolderTree({ onNavigate }: FolderTreeProps) {
152 153
153 154 if (tree.length === 0) return <p className="px-3 py-2 text-[12px] text-gray-400">Aucun dossier</p>;
154 155
155 − return <div className="space-y-px">{tree.map((n) => renderNode(n, 0))}</div>;
156 + // Regroupement des racines par espace partagé (dossiers personnels d'abord)
157 + const groups = new Map<string, { label: string | null; nodes: TreeFolder[] }>();
158 + for (const n of tree) {
159 + const key = n.sharedSpace?.id ?? "__perso";
160 + if (!groups.has(key)) groups.set(key, { label: n.sharedSpace?.name ?? null, nodes: [] });
161 + groups.get(key)!.nodes.push(n);
162 + }
163 + const ordered = Array.from(groups.entries()).sort(([a, ga], [b, gb]) => (a === "__perso" ? -1 : b === "__perso" ? 1 : (ga.label ?? "").localeCompare(gb.label ?? "", "fr")));
164 + if (ordered.length <= 1) return <div className="space-y-px">{tree.map((n) => renderNode(n, 0))}</div>;
165 + return (
166 + <div className="space-y-1">
167 + {ordered.map(([key, g]) => (
168 + <div key={key}>
169 + <div className="px-3 pt-2 pb-1 text-[10.5px] font-semibold uppercase tracking-[0.1em] text-gray-400 dark:text-gray-500 truncate" title={g.label ?? "Mes dossiers"}>{g.label ?? "Mes dossiers"}</div>
170 + <div className="space-y-px">{g.nodes.map((n) => renderNode(n, 0))}</div>
171 + </div>
172 + ))}
173 + </div>
174 + );
156 175 }
modified prisma/dev.db-shm +0 −0

Binary file not shown.

modified prisma/dev.db-wal +0 −0

Binary file not shown.