feat: regrouper les dossiers racine par espace partagé (explorateur, grille, arbre latéral)
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.