(files: string[])
| 199 | } |
| 200 | |
| 201 | function buildTree(files: string[]): TreeNode[] { |
| 202 | const root: TreeNode[] = []; |
| 203 | |
| 204 | for (const filePath of files) { |
| 205 | const parts = filePath.split('/').filter(Boolean); |
| 206 | let current = root; |
| 207 | |
| 208 | for (let i = 0; i < parts.length; i++) { |
| 209 | const part = parts[i]; |
| 210 | const isLast = i === parts.length - 1; |
| 211 | // For leaf nodes (files) we MUST store the original full path from data.files, |
| 212 | // otherwise onFileClick won't match `n.file === path` in the graph data. |
| 213 | const nodePath = isLast ? filePath : parts.slice(0, i + 1).join('/'); |
| 214 | |
| 215 | let node = current.find(n => n.name === part); |
| 216 | if (!node) { |
| 217 | node = { name: part, path: nodePath, isDir: !isLast, children: [] }; |
| 218 | current.push(node); |
| 219 | } |
| 220 | current = node.children; |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | // Sort: folders first, then files, each alphabetically |
| 225 | const sortNodes = (nodes: TreeNode[]): TreeNode[] => |
| 226 | nodes |
| 227 | .sort((a, b) => { |
| 228 | if (a.isDir && !b.isDir) return -1; |
| 229 | if (!a.isDir && b.isDir) return 1; |
| 230 | return a.name.localeCompare(b.name); |
| 231 | }) |
| 232 | .map(n => ({ ...n, children: sortNodes(n.children) })); |
| 233 | |
| 234 | return sortNodes(root); |
| 235 | } |
| 236 | |
| 237 | // ─── Tree Item ──────────────────────────────────────────────────────────────── |
| 238 | function TreeItem({ |
no test coverage detected