diff options
| author | diogo464 <[email protected]> | 2025-08-11 16:48:14 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-11 16:48:14 +0100 |
| commit | 741d42c7ab635180ab416dc749e1bcb774a22f5e (patch) | |
| tree | 221f5f991293edfb6abfe50b7fc692caf9bca3cc | |
| parent | 83ff4ce9b3e93a361dd120ab2428e6d854af75bb (diff) | |
Simplify frontend by removing FileItem conversion layer
- Remove FileItem interface and 300+ lines of mock data
- Eliminate transformTreeNodes() conversion function
- Update component to use DriveTreeNode[] directly
- Rename /api/list to /api/tree with server-side tree building
- Add Drive_tree() function and DriveTreeNode types
- Significantly reduce code complexity and memory usage
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
| -rw-r--r-- | CLAUDE.md | 10 | ||||
| -rw-r--r-- | frontend/app/api/list/route.ts | 13 | ||||
| -rw-r--r-- | frontend/app/api/tree/route.ts | 13 | ||||
| -rw-r--r-- | frontend/file-drive.tsx | 554 | ||||
| -rw-r--r-- | frontend/lib/drive_server.ts | 51 | ||||
| -rw-r--r-- | frontend/lib/drive_shared.ts | 4 | ||||
| -rw-r--r-- | frontend/lib/drive_types.ts | 15 |
7 files changed, 230 insertions, 430 deletions
| @@ -17,11 +17,11 @@ | |||
| 17 | - `just logs` - View logs | 17 | - `just logs` - View logs |
| 18 | 18 | ||
| 19 | ## Current Implementation | 19 | ## Current Implementation |
| 20 | - Frontend directory: `frontend/` | 20 | - Frontend directory: `frontend/` (note: frontend code is in frontend/ directory) |
| 21 | - Drive functions: `frontend/lib/drive.ts` | 21 | - Drive functions: `frontend/lib/drive_server.ts` |
| 22 | - `Drive_ls(path, recursive)` - Lists files/directories | 22 | - `Drive_ls(path, recursive)` - Lists files/directories |
| 23 | - `Drive_blob_path(blob)` - Gets filesystem path for a blob ID | 23 | - `Drive_blob_path(blob)` - Gets filesystem path for a blob ID |
| 24 | - Current page shows basic Next.js template but calls Drive_ls("/", false) | 24 | - Current page shows v0 generated UI with mockData but has /api/list endpoint available |
| 25 | 25 | ||
| 26 | ## Tech Stack | 26 | ## Tech Stack |
| 27 | - Next.js 15.4.6 with App Router | 27 | - Next.js 15.4.6 with App Router |
| @@ -48,8 +48,8 @@ | |||
| 48 | 48 | ||
| 49 | ## API Endpoints | 49 | ## API Endpoints |
| 50 | 50 | ||
| 51 | ### Legacy Endpoints | 51 | ### Tree Endpoint |
| 52 | - `/api/list` - GET all files recursively from root | 52 | - `/api/tree` - GET filesystem tree with hierarchy |
| 53 | 53 | ||
| 54 | ### RESTful API - `/api/fs/[...path]` | 54 | ### RESTful API - `/api/fs/[...path]` |
| 55 | - **GET** `/api/fs/path/to/directory` - List directory contents | 55 | - **GET** `/api/fs/path/to/directory` - List directory contents |
diff --git a/frontend/app/api/list/route.ts b/frontend/app/api/list/route.ts deleted file mode 100644 index 425e89c..0000000 --- a/frontend/app/api/list/route.ts +++ /dev/null | |||
| @@ -1,13 +0,0 @@ | |||
| 1 | import { NextResponse } from 'next/server' | ||
| 2 | import { Drive_ls } from '@/lib/drive_server' | ||
| 3 | |||
| 4 | export async function GET() { | ||
| 5 | try { | ||
| 6 | const entries = await Drive_ls('/', true) | ||
| 7 | |||
| 8 | return NextResponse.json(entries) | ||
| 9 | } catch (error) { | ||
| 10 | console.error('Error listing files:', error) | ||
| 11 | throw error | ||
| 12 | } | ||
| 13 | } \ No newline at end of file | ||
diff --git a/frontend/app/api/tree/route.ts b/frontend/app/api/tree/route.ts new file mode 100644 index 0000000..ece8122 --- /dev/null +++ b/frontend/app/api/tree/route.ts | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | import { NextResponse } from 'next/server' | ||
| 2 | import { Drive_tree } from '@/lib/drive_server' | ||
| 3 | |||
| 4 | export async function GET() { | ||
| 5 | try { | ||
| 6 | const treeResponse = await Drive_tree() | ||
| 7 | |||
| 8 | return NextResponse.json(treeResponse) | ||
| 9 | } catch (error) { | ||
| 10 | console.error('Error building tree:', error) | ||
| 11 | throw error | ||
| 12 | } | ||
| 13 | } \ No newline at end of file | ||
diff --git a/frontend/file-drive.tsx b/frontend/file-drive.tsx index b38548a..ffcc6f5 100644 --- a/frontend/file-drive.tsx +++ b/frontend/file-drive.tsx | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | import type React from "react" | 3 | import type React from "react" |
| 4 | 4 | ||
| 5 | import { useState, useRef } from "react" | 5 | import { useState, useRef, useEffect } from "react" |
| 6 | import { | 6 | import { |
| 7 | ChevronRight, | 7 | ChevronRight, |
| 8 | ChevronDown, | 8 | ChevronDown, |
| @@ -36,332 +36,8 @@ import { Label } from "@/components/ui/label" | |||
| 36 | import { Checkbox } from "@/components/ui/checkbox" | 36 | import { Checkbox } from "@/components/ui/checkbox" |
| 37 | import { toast } from "@/hooks/use-toast" | 37 | import { toast } from "@/hooks/use-toast" |
| 38 | import HistoryView from "./history-view" | 38 | import HistoryView from "./history-view" |
| 39 | import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" | ||
| 39 | 40 | ||
| 40 | interface FileItem { | ||
| 41 | id: string | ||
| 42 | name: string | ||
| 43 | type: "file" | "folder" | ||
| 44 | size: number | ||
| 45 | modified: string | ||
| 46 | modifiedTime: string | ||
| 47 | modifiedBy: string | ||
| 48 | children?: FileItem[] | ||
| 49 | } | ||
| 50 | |||
| 51 | const mockData: FileItem[] = [ | ||
| 52 | { | ||
| 53 | id: "1", | ||
| 54 | name: "Documents and Important Files", | ||
| 55 | type: "folder", | ||
| 56 | size: 25728640, | ||
| 57 | modified: "2024-01-15", | ||
| 58 | modifiedTime: "14:32", | ||
| 59 | modifiedBy: "John Smith", | ||
| 60 | children: [ | ||
| 61 | { | ||
| 62 | id: "1-1", | ||
| 63 | name: "Work Projects and Client Reports", | ||
| 64 | type: "folder", | ||
| 65 | size: 18388608, | ||
| 66 | modified: "2024-01-10", | ||
| 67 | modifiedTime: "09:15", | ||
| 68 | modifiedBy: "Sarah Johnson", | ||
| 69 | children: [ | ||
| 70 | { | ||
| 71 | id: "1-1-1", | ||
| 72 | name: "Client ABC Corporation", | ||
| 73 | type: "folder", | ||
| 74 | size: 12388608, | ||
| 75 | modified: "2024-01-09", | ||
| 76 | modifiedTime: "16:45", | ||
| 77 | modifiedBy: "Mike Davis", | ||
| 78 | children: [ | ||
| 79 | { | ||
| 80 | id: "1-1-1-1", | ||
| 81 | name: "Quarterly Financial Analysis and Market Research Report Q4 2023.pdf", | ||
| 82 | type: "file", | ||
| 83 | size: 4194304, | ||
| 84 | modified: "2024-01-09", | ||
| 85 | modifiedTime: "16:30", | ||
| 86 | modifiedBy: "Mike Davis", | ||
| 87 | }, | ||
| 88 | { | ||
| 89 | id: "1-1-1-2", | ||
| 90 | name: "Strategic Business Development Plan and Implementation Timeline.docx", | ||
| 91 | type: "file", | ||
| 92 | size: 2097152, | ||
| 93 | modified: "2024-01-08", | ||
| 94 | modifiedTime: "11:20", | ||
| 95 | modifiedBy: "Emily Chen", | ||
| 96 | }, | ||
| 97 | { | ||
| 98 | id: "1-1-1-3", | ||
| 99 | name: "Meeting Notes and Action Items", | ||
| 100 | type: "folder", | ||
| 101 | size: 6097152, | ||
| 102 | modified: "2024-01-07", | ||
| 103 | modifiedTime: "13:45", | ||
| 104 | modifiedBy: "Alex Rodriguez", | ||
| 105 | children: [ | ||
| 106 | { | ||
| 107 | id: "1-1-1-3-1", | ||
| 108 | name: "Weekly Status Meeting Notes January 2024 - Detailed Summary.txt", | ||
| 109 | type: "file", | ||
| 110 | size: 1048576, | ||
| 111 | modified: "2024-01-07", | ||
| 112 | modifiedTime: "13:30", | ||
| 113 | modifiedBy: "Alex Rodriguez", | ||
| 114 | }, | ||
| 115 | { | ||
| 116 | id: "1-1-1-3-2", | ||
| 117 | name: "Project Kickoff Meeting Transcript and Stakeholder Feedback.docx", | ||
| 118 | type: "file", | ||
| 119 | size: 3048576, | ||
| 120 | modified: "2024-01-06", | ||
| 121 | modifiedTime: "15:10", | ||
| 122 | modifiedBy: "Lisa Wang", | ||
| 123 | }, | ||
| 124 | { | ||
| 125 | id: "1-1-1-3-3", | ||
| 126 | name: "Budget Review and Resource Allocation Discussion Points.xlsx", | ||
| 127 | type: "file", | ||
| 128 | size: 2000000, | ||
| 129 | modified: "2024-01-05", | ||
| 130 | modifiedTime: "10:25", | ||
| 131 | modifiedBy: "David Brown", | ||
| 132 | }, | ||
| 133 | ], | ||
| 134 | }, | ||
| 135 | ], | ||
| 136 | }, | ||
| 137 | { | ||
| 138 | id: "1-1-2", | ||
| 139 | name: "Internal Company Documentation and Policies", | ||
| 140 | type: "folder", | ||
| 141 | size: 6000000, | ||
| 142 | modified: "2024-01-08", | ||
| 143 | modifiedTime: "08:30", | ||
| 144 | modifiedBy: "HR Department", | ||
| 145 | children: [ | ||
| 146 | { | ||
| 147 | id: "1-1-2-1", | ||
| 148 | name: "Employee Handbook 2024 - Complete Guide with Benefits and Procedures.pdf", | ||
| 149 | type: "file", | ||
| 150 | size: 3500000, | ||
| 151 | modified: "2024-01-08", | ||
| 152 | modifiedTime: "08:15", | ||
| 153 | modifiedBy: "HR Department", | ||
| 154 | }, | ||
| 155 | { | ||
| 156 | id: "1-1-2-2", | ||
| 157 | name: "IT Security Policies and Data Protection Guidelines - Updated Version.docx", | ||
| 158 | type: "file", | ||
| 159 | size: 2500000, | ||
| 160 | modified: "2024-01-07", | ||
| 161 | modifiedTime: "17:40", | ||
| 162 | modifiedBy: "IT Security Team", | ||
| 163 | }, | ||
| 164 | ], | ||
| 165 | }, | ||
| 166 | ], | ||
| 167 | }, | ||
| 168 | { | ||
| 169 | id: "1-2", | ||
| 170 | name: "Personal Documents and Certificates", | ||
| 171 | type: "folder", | ||
| 172 | size: 7340032, | ||
| 173 | modified: "2024-01-12", | ||
| 174 | modifiedTime: "12:15", | ||
| 175 | modifiedBy: "John Smith", | ||
| 176 | children: [ | ||
| 177 | { | ||
| 178 | id: "1-2-1", | ||
| 179 | name: "Professional Resume and Cover Letter Templates - Updated 2024.docx", | ||
| 180 | type: "file", | ||
| 181 | size: 1048576, | ||
| 182 | modified: "2024-01-12", | ||
| 183 | modifiedTime: "12:00", | ||
| 184 | modifiedBy: "John Smith", | ||
| 185 | }, | ||
| 186 | { | ||
| 187 | id: "1-2-2", | ||
| 188 | name: "Educational Certificates and Professional Qualifications Portfolio.pdf", | ||
| 189 | type: "file", | ||
| 190 | size: 6291456, | ||
| 191 | modified: "2024-01-11", | ||
| 192 | modifiedTime: "14:30", | ||
| 193 | modifiedBy: "John Smith", | ||
| 194 | }, | ||
| 195 | ], | ||
| 196 | }, | ||
| 197 | ], | ||
| 198 | }, | ||
| 199 | { | ||
| 200 | id: "2", | ||
| 201 | name: "Media Files and Creative Assets", | ||
| 202 | type: "folder", | ||
| 203 | size: 152428800, | ||
| 204 | modified: "2024-01-14", | ||
| 205 | modifiedTime: "16:20", | ||
| 206 | modifiedBy: "Creative Team", | ||
| 207 | children: [ | ||
| 208 | { | ||
| 209 | id: "2-1", | ||
| 210 | name: "Photography and Visual Content", | ||
| 211 | type: "folder", | ||
| 212 | size: 75428800, | ||
| 213 | modified: "2024-01-14", | ||
| 214 | modifiedTime: "16:15", | ||
| 215 | modifiedBy: "Creative Team", | ||
| 216 | children: [ | ||
| 217 | { | ||
| 218 | id: "2-1-1", | ||
| 219 | name: "Travel and Vacation Photos Collection", | ||
| 220 | type: "folder", | ||
| 221 | size: 45428800, | ||
| 222 | modified: "2024-01-14", | ||
| 223 | modifiedTime: "16:10", | ||
| 224 | modifiedBy: "John Smith", | ||
| 225 | children: [ | ||
| 226 | { | ||
| 227 | id: "2-1-1-1", | ||
| 228 | name: "Summer Vacation 2023 - Beach Resort and Mountain Hiking Adventure Photos.jpg", | ||
| 229 | type: "file", | ||
| 230 | size: 15145728, | ||
| 231 | modified: "2024-01-14", | ||
| 232 | modifiedTime: "16:05", | ||
| 233 | modifiedBy: "John Smith", | ||
| 234 | }, | ||
| 235 | { | ||
| 236 | id: "2-1-1-2", | ||
| 237 | name: "European City Tour - Architecture and Cultural Landmarks Photography Collection.jpg", | ||
| 238 | type: "file", | ||
| 239 | size: 18283072, | ||
| 240 | modified: "2024-01-13", | ||
| 241 | modifiedTime: "19:30", | ||
| 242 | modifiedBy: "John Smith", | ||
| 243 | }, | ||
| 244 | { | ||
| 245 | id: "2-1-1-3", | ||
| 246 | name: "Wildlife Photography Safari - African Animals and Natural Landscapes.jpg", | ||
| 247 | type: "file", | ||
| 248 | size: 12000000, | ||
| 249 | modified: "2024-01-12", | ||
| 250 | modifiedTime: "20:45", | ||
| 251 | modifiedBy: "John Smith", | ||
| 252 | }, | ||
| 253 | ], | ||
| 254 | }, | ||
| 255 | { | ||
| 256 | id: "2-1-2", | ||
| 257 | name: "Professional Headshots and Corporate Event Photography - High Resolution.png", | ||
| 258 | type: "file", | ||
| 259 | size: 30000000, | ||
| 260 | modified: "2024-01-13", | ||
| 261 | modifiedTime: "11:15", | ||
| 262 | modifiedBy: "Professional Photographer", | ||
| 263 | }, | ||
| 264 | ], | ||
| 265 | }, | ||
| 266 | { | ||
| 267 | id: "2-2", | ||
| 268 | name: "Video Content and Multimedia Projects", | ||
| 269 | type: "folder", | ||
| 270 | size: 77000000, | ||
| 271 | modified: "2024-01-13", | ||
| 272 | modifiedTime: "14:20", | ||
| 273 | modifiedBy: "Video Production Team", | ||
| 274 | children: [ | ||
| 275 | { | ||
| 276 | id: "2-2-1", | ||
| 277 | name: "Corporate Training Videos and Educational Content Series - Complete Collection.mp4", | ||
| 278 | type: "file", | ||
| 279 | size: 45000000, | ||
| 280 | modified: "2024-01-13", | ||
| 281 | modifiedTime: "14:15", | ||
| 282 | modifiedBy: "Video Production Team", | ||
| 283 | }, | ||
| 284 | { | ||
| 285 | id: "2-2-2", | ||
| 286 | name: "Product Demo and Marketing Presentation Video - 4K Quality.mov", | ||
| 287 | type: "file", | ||
| 288 | size: 32000000, | ||
| 289 | modified: "2024-01-12", | ||
| 290 | modifiedTime: "16:30", | ||
| 291 | modifiedBy: "Marketing Team", | ||
| 292 | }, | ||
| 293 | ], | ||
| 294 | }, | ||
| 295 | ], | ||
| 296 | }, | ||
| 297 | { | ||
| 298 | id: "3", | ||
| 299 | name: "Development Projects and Source Code Repository", | ||
| 300 | type: "folder", | ||
| 301 | size: 89715200, | ||
| 302 | modified: "2024-01-16", | ||
| 303 | modifiedTime: "18:45", | ||
| 304 | modifiedBy: "Development Team", | ||
| 305 | children: [ | ||
| 306 | { | ||
| 307 | id: "3-1", | ||
| 308 | name: "Web Applications and Frontend Projects", | ||
| 309 | type: "folder", | ||
| 310 | size: 45000000, | ||
| 311 | modified: "2024-01-16", | ||
| 312 | modifiedTime: "18:40", | ||
| 313 | modifiedBy: "Frontend Team", | ||
| 314 | children: [ | ||
| 315 | { | ||
| 316 | id: "3-1-1", | ||
| 317 | name: "E-commerce Platform with React and Node.js - Full Stack Implementation.zip", | ||
| 318 | type: "file", | ||
| 319 | size: 25000000, | ||
| 320 | modified: "2024-01-16", | ||
| 321 | modifiedTime: "18:35", | ||
| 322 | modifiedBy: "Lead Developer", | ||
| 323 | }, | ||
| 324 | { | ||
| 325 | id: "3-1-2", | ||
| 326 | name: "Dashboard Analytics Tool with Real-time Data Visualization Components.zip", | ||
| 327 | type: "file", | ||
| 328 | size: 20000000, | ||
| 329 | modified: "2024-01-15", | ||
| 330 | modifiedTime: "22:10", | ||
| 331 | modifiedBy: "Data Team", | ||
| 332 | }, | ||
| 333 | ], | ||
| 334 | }, | ||
| 335 | { | ||
| 336 | id: "3-2", | ||
| 337 | name: "Mobile App Development and Cross-Platform Solutions.zip", | ||
| 338 | type: "file", | ||
| 339 | size: 44715200, | ||
| 340 | modified: "2024-01-14", | ||
| 341 | modifiedTime: "13:25", | ||
| 342 | modifiedBy: "Mobile Team", | ||
| 343 | }, | ||
| 344 | ], | ||
| 345 | }, | ||
| 346 | { | ||
| 347 | id: "4", | ||
| 348 | name: "Configuration Files and System Settings - Development Environment Setup.txt", | ||
| 349 | type: "file", | ||
| 350 | size: 4096, | ||
| 351 | modified: "2024-01-16", | ||
| 352 | modifiedTime: "09:30", | ||
| 353 | modifiedBy: "System Admin", | ||
| 354 | }, | ||
| 355 | { | ||
| 356 | id: "5", | ||
| 357 | name: "Database Backup and Migration Scripts - Production Environment.sql", | ||
| 358 | type: "file", | ||
| 359 | size: 8192, | ||
| 360 | modified: "2024-01-15", | ||
| 361 | modifiedTime: "23:45", | ||
| 362 | modifiedBy: "Database Admin", | ||
| 363 | }, | ||
| 364 | ] | ||
| 365 | 41 | ||
| 366 | function formatFileSize(bytes: number): string { | 42 | function formatFileSize(bytes: number): string { |
| 367 | if (bytes === 0) return "0 Bytes" | 43 | if (bytes === 0) return "0 Bytes" |
| @@ -371,22 +47,38 @@ function formatFileSize(bytes: number): string { | |||
| 371 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | 47 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] |
| 372 | } | 48 | } |
| 373 | 49 | ||
| 374 | function calculateTotalSize(items: FileItem[]): number { | 50 | function calculateTotalSize(items: DriveTreeNode[]): number { |
| 375 | return items.reduce((total, item) => { | 51 | return items.reduce((total, item) => { |
| 376 | if (item.type === "folder" && item.children) { | 52 | if (item.type === "dir" && item.children) { |
| 377 | return total + calculateTotalSize(item.children) | 53 | return total + calculateTotalSize(item.children) |
| 378 | } | 54 | } |
| 379 | return total + item.size | 55 | return total + (item.size || 0) |
| 380 | }, 0) | 56 | }, 0) |
| 381 | } | 57 | } |
| 382 | 58 | ||
| 59 | // Fetch data from /api/tree endpoint | ||
| 60 | async function fetchDriveTree(): Promise<DriveTreeResponse> { | ||
| 61 | const response = await fetch('/api/tree') | ||
| 62 | if (!response.ok) { | ||
| 63 | throw new Error(`Failed to fetch drive tree: ${response.statusText}`) | ||
| 64 | } | ||
| 65 | return await response.json() | ||
| 66 | } | ||
| 67 | |||
| 68 | // Convert UNIX timestamp to date string | ||
| 69 | function formatDate(timestamp: number): string { | ||
| 70 | return new Date(timestamp * 1000).toISOString().split('T')[0] // YYYY-MM-DD | ||
| 71 | } | ||
| 72 | |||
| 383 | export default function FileDrive() { | 73 | export default function FileDrive() { |
| 384 | const [files, setFiles] = useState<FileItem[]>(mockData) | 74 | const [files, setFiles] = useState<DriveTreeNode[]>([]) |
| 75 | const [loading, setLoading] = useState(true) | ||
| 76 | const [error, setError] = useState<string | null>(null) | ||
| 385 | const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set()) | 77 | const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set()) |
| 386 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) | 78 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) |
| 387 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) | 79 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) |
| 388 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) | 80 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) |
| 389 | const [currentItem, setCurrentItem] = useState<FileItem | null>(null) | 81 | const [currentItem, setCurrentItem] = useState<DriveTreeNode | null>(null) |
| 390 | const [newName, setNewName] = useState("") | 82 | const [newName, setNewName] = useState("") |
| 391 | const fileInputRef = useRef<HTMLInputElement>(null) | 83 | const fileInputRef = useRef<HTMLInputElement>(null) |
| 392 | 84 | ||
| @@ -395,9 +87,28 @@ export default function FileDrive() { | |||
| 395 | const [uploadToFolder, setUploadToFolder] = useState<string | null>(null) | 87 | const [uploadToFolder, setUploadToFolder] = useState<string | null>(null) |
| 396 | 88 | ||
| 397 | const maxStorage = 1073741824 // 1GB | 89 | const maxStorage = 1073741824 // 1GB |
| 398 | const usedStorage = calculateTotalSize(files) | 90 | const usedStorage = 0;//calculateTotalSize(files) |
| 399 | const storagePercentage = (usedStorage / maxStorage) * 100 | 91 | const storagePercentage = (usedStorage / maxStorage) * 100 |
| 400 | 92 | ||
| 93 | // Load drive data on component mount | ||
| 94 | useEffect(() => { | ||
| 95 | async function loadDriveData() { | ||
| 96 | try { | ||
| 97 | setLoading(true) | ||
| 98 | setError(null) | ||
| 99 | const treeResponse = await fetchDriveTree() | ||
| 100 | setFiles(treeResponse.root) | ||
| 101 | } catch (err) { | ||
| 102 | setError(err instanceof Error ? err.message : 'Failed to load drive data') | ||
| 103 | console.error('Error loading drive data:', err) | ||
| 104 | } finally { | ||
| 105 | setLoading(false) | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | loadDriveData() | ||
| 110 | }, []) | ||
| 111 | |||
| 401 | const toggleFolder = (folderId: string) => { | 112 | const toggleFolder = (folderId: string) => { |
| 402 | const newExpanded = new Set(expandedFolders) | 113 | const newExpanded = new Set(expandedFolders) |
| 403 | if (newExpanded.has(folderId)) { | 114 | if (newExpanded.has(folderId)) { |
| @@ -419,36 +130,36 @@ export default function FileDrive() { | |||
| 419 | } | 130 | } |
| 420 | 131 | ||
| 421 | const selectAll = () => { | 132 | const selectAll = () => { |
| 422 | const getAllIds = (items: FileItem[]): string[] => { | 133 | const getAllPaths = (items: DriveTreeNode[]): string[] => { |
| 423 | const ids: string[] = [] | 134 | const paths: string[] = [] |
| 424 | items.forEach((item) => { | 135 | items.forEach((item) => { |
| 425 | ids.push(item.id) | 136 | paths.push(item.path) |
| 426 | if (item.children) { | 137 | if (item.children) { |
| 427 | ids.push(...getAllIds(item.children)) | 138 | paths.push(...getAllPaths(item.children)) |
| 428 | } | 139 | } |
| 429 | }) | 140 | }) |
| 430 | return ids | 141 | return paths |
| 431 | } | 142 | } |
| 432 | setSelectedFiles(new Set(getAllIds(files))) | 143 | setSelectedFiles(new Set(getAllPaths(files))) |
| 433 | } | 144 | } |
| 434 | 145 | ||
| 435 | const deselectAll = () => { | 146 | const deselectAll = () => { |
| 436 | setSelectedFiles(new Set()) | 147 | setSelectedFiles(new Set()) |
| 437 | } | 148 | } |
| 438 | 149 | ||
| 439 | const openRenameDialog = (item: FileItem) => { | 150 | const openRenameDialog = (item: DriveTreeNode) => { |
| 440 | setCurrentItem(item) | 151 | setCurrentItem(item) |
| 441 | setNewName(item.name) | 152 | setNewName(item.name) |
| 442 | setRenameDialogOpen(true) | 153 | setRenameDialogOpen(true) |
| 443 | } | 154 | } |
| 444 | 155 | ||
| 445 | const openInfoDialog = (item: FileItem) => { | 156 | const openInfoDialog = (item: DriveTreeNode) => { |
| 446 | setCurrentItem(item) | 157 | setCurrentItem(item) |
| 447 | setInfoDialogOpen(true) | 158 | setInfoDialogOpen(true) |
| 448 | } | 159 | } |
| 449 | 160 | ||
| 450 | const copyPermalink = (item: FileItem) => { | 161 | const copyPermalink = (item: DriveTreeNode) => { |
| 451 | const permalink = `${window.location.origin}/drive/file/${item.id}` | 162 | const permalink = `${window.location.origin}/drive/file/${item.path}` |
| 452 | navigator.clipboard.writeText(permalink).then(() => { | 163 | navigator.clipboard.writeText(permalink).then(() => { |
| 453 | toast({ | 164 | toast({ |
| 454 | title: "Link copied!", | 165 | title: "Link copied!", |
| @@ -459,9 +170,9 @@ export default function FileDrive() { | |||
| 459 | 170 | ||
| 460 | const handleRename = () => { | 171 | const handleRename = () => { |
| 461 | if (currentItem && newName.trim()) { | 172 | if (currentItem && newName.trim()) { |
| 462 | const renameInArray = (items: FileItem[]): FileItem[] => { | 173 | const renameInArray = (items: DriveTreeNode[]): DriveTreeNode[] => { |
| 463 | return items.map((item) => { | 174 | return items.map((item) => { |
| 464 | if (item.id === currentItem.id) { | 175 | if (item.path === currentItem.path) { |
| 465 | return { ...item, name: newName.trim() } | 176 | return { ...item, name: newName.trim() } |
| 466 | } | 177 | } |
| 467 | if (item.children) { | 178 | if (item.children) { |
| @@ -485,22 +196,22 @@ export default function FileDrive() { | |||
| 485 | const uploadedFiles = event.target.files | 196 | const uploadedFiles = event.target.files |
| 486 | if (uploadedFiles) { | 197 | if (uploadedFiles) { |
| 487 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ | 198 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ |
| 488 | id: `upload-${Date.now()}-${index}`, | 199 | path: `/upload-${Date.now()}-${index}`, |
| 489 | name: file.name, | 200 | name: file.name, |
| 490 | type: "file" as const, | 201 | type: "file" as const, |
| 202 | lastmod: Math.floor(Date.now() / 1000), | ||
| 203 | blob: null, | ||
| 491 | size: file.size, | 204 | size: file.size, |
| 492 | modified: new Date().toISOString().split("T")[0], | 205 | author: "Current User", |
| 493 | modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }), | ||
| 494 | modifiedBy: "Current User", | ||
| 495 | })) | 206 | })) |
| 496 | setFiles([...files, ...newFiles]) | 207 | setFiles([...files, ...newFiles]) |
| 497 | } | 208 | } |
| 498 | } | 209 | } |
| 499 | 210 | ||
| 500 | const deleteItems = (itemIds: string[]) => { | 211 | const deleteItems = (itemPaths: string[]) => { |
| 501 | const deleteFromArray = (items: FileItem[]): FileItem[] => { | 212 | const deleteFromArray = (items: DriveTreeNode[]): DriveTreeNode[] => { |
| 502 | return items.filter((item) => { | 213 | return items.filter((item) => { |
| 503 | if (itemIds.includes(item.id)) return false | 214 | if (itemPaths.includes(item.path)) return false |
| 504 | if (item.children) { | 215 | if (item.children) { |
| 505 | item.children = deleteFromArray(item.children) | 216 | item.children = deleteFromArray(item.children) |
| 506 | } | 217 | } |
| @@ -510,7 +221,7 @@ export default function FileDrive() { | |||
| 510 | setFiles(deleteFromArray(files)) | 221 | setFiles(deleteFromArray(files)) |
| 511 | // Remove deleted items from selection | 222 | // Remove deleted items from selection |
| 512 | const newSelected = new Set(selectedFiles) | 223 | const newSelected = new Set(selectedFiles) |
| 513 | itemIds.forEach((id) => newSelected.delete(id)) | 224 | itemPaths.forEach((path) => newSelected.delete(path)) |
| 514 | setSelectedFiles(newSelected) | 225 | setSelectedFiles(newSelected) |
| 515 | } | 226 | } |
| 516 | 227 | ||
| @@ -526,27 +237,27 @@ export default function FileDrive() { | |||
| 526 | // Could also redirect to logout endpoint | 237 | // Could also redirect to logout endpoint |
| 527 | } | 238 | } |
| 528 | 239 | ||
| 529 | const handleFolderUpload = (event: React.ChangeEvent<HTMLInputElement>, folderId: string) => { | 240 | const handleFolderUpload = (event: React.ChangeEvent<HTMLInputElement>, folderPath: string) => { |
| 530 | const uploadedFiles = event.target.files | 241 | const uploadedFiles = event.target.files |
| 531 | if (uploadedFiles) { | 242 | if (uploadedFiles) { |
| 532 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ | 243 | const newFiles = Array.from(uploadedFiles).map((file, index) => ({ |
| 533 | id: `upload-${Date.now()}-${index}`, | 244 | path: `${folderPath}/${file.name}`, |
| 534 | name: file.name, | 245 | name: file.name, |
| 535 | type: "file" as const, | 246 | type: "file" as const, |
| 247 | lastmod: Math.floor(Date.now() / 1000), | ||
| 248 | blob: null, | ||
| 536 | size: file.size, | 249 | size: file.size, |
| 537 | modified: new Date().toISOString().split("T")[0], | 250 | author: "Current User", |
| 538 | modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }), | ||
| 539 | modifiedBy: "Current User", | ||
| 540 | })) | 251 | })) |
| 541 | 252 | ||
| 542 | // Add files to the specific folder | 253 | // Add files to the specific folder |
| 543 | const addToFolder = (items: FileItem[]): FileItem[] => { | 254 | const addToFolder = (items: DriveTreeNode[]): DriveTreeNode[] => { |
| 544 | return items.map((item) => { | 255 | return items.map((item) => { |
| 545 | if (item.id === folderId && item.type === "folder") { | 256 | if (item.path === folderPath && item.type === "dir") { |
| 546 | return { | 257 | return { |
| 547 | ...item, | 258 | ...item, |
| 548 | children: [...(item.children || []), ...newFiles], | 259 | children: [...(item.children || []), ...newFiles], |
| 549 | size: item.size + newFiles.reduce((total, file) => total + file.size, 0), | 260 | size: (item.size || 0) + newFiles.reduce((total, file) => total + file.size, 0), |
| 550 | } | 261 | } |
| 551 | } | 262 | } |
| 552 | if (item.children) { | 263 | if (item.children) { |
| @@ -567,33 +278,33 @@ export default function FileDrive() { | |||
| 567 | setUploadToFolder(null) | 278 | setUploadToFolder(null) |
| 568 | } | 279 | } |
| 569 | 280 | ||
| 570 | const openFolderUpload = (folderId: string) => { | 281 | const openFolderUpload = (folderPath: string) => { |
| 571 | setUploadToFolder(folderId) | 282 | setUploadToFolder(folderPath) |
| 572 | // Trigger file input click after state is set | 283 | // Trigger file input click after state is set |
| 573 | setTimeout(() => { | 284 | setTimeout(() => { |
| 574 | const input = document.getElementById(`folder-upload-${folderId}`) as HTMLInputElement | 285 | const input = document.getElementById(`folder-upload-${folderPath}`) as HTMLInputElement |
| 575 | input?.click() | 286 | input?.click() |
| 576 | }, 0) | 287 | }, 0) |
| 577 | } | 288 | } |
| 578 | 289 | ||
| 579 | const renderFileRow = (item: FileItem, level = 0): React.ReactNode[] => { | 290 | const renderFileRow = (item: DriveTreeNode, level = 0): React.ReactNode[] => { |
| 580 | const isExpanded = expandedFolders.has(item.id) | 291 | const isExpanded = expandedFolders.has(item.path) |
| 581 | const isSelected = selectedFiles.has(item.id) | 292 | const isSelected = selectedFiles.has(item.path) |
| 582 | const rows: React.ReactNode[] = [] | 293 | const rows: React.ReactNode[] = [] |
| 583 | 294 | ||
| 584 | rows.push( | 295 | rows.push( |
| 585 | <TableRow key={item.id} className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`}> | 296 | <TableRow key={item.path} className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`}> |
| 586 | <TableCell className="w-[40px]"> | 297 | <TableCell className="w-[40px]"> |
| 587 | <Checkbox checked={isSelected} onCheckedChange={() => toggleFileSelection(item.id)} /> | 298 | <Checkbox checked={isSelected} onCheckedChange={() => toggleFileSelection(item.path)} /> |
| 588 | </TableCell> | 299 | </TableCell> |
| 589 | <TableCell className="font-medium"> | 300 | <TableCell className="font-medium"> |
| 590 | <div className="flex items-center gap-2" style={{ paddingLeft: `${level * 20}px` }}> | 301 | <div className="flex items-center gap-2" style={{ paddingLeft: `${level * 20}px` }}> |
| 591 | {item.type === "folder" && ( | 302 | {item.type === "dir" && ( |
| 592 | <Button variant="ghost" size="sm" className="h-4 w-4 p-0" onClick={() => toggleFolder(item.id)}> | 303 | <Button variant="ghost" size="sm" className="h-4 w-4 p-0" onClick={() => toggleFolder(item.path)}> |
| 593 | {isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />} | 304 | {isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />} |
| 594 | </Button> | 305 | </Button> |
| 595 | )} | 306 | )} |
| 596 | {item.type === "folder" ? ( | 307 | {item.type === "dir" ? ( |
| 597 | <Folder className="h-4 w-4 text-blue-500" /> | 308 | <Folder className="h-4 w-4 text-blue-500" /> |
| 598 | ) : ( | 309 | ) : ( |
| 599 | <File className="h-4 w-4 text-gray-500" /> | 310 | <File className="h-4 w-4 text-gray-500" /> |
| @@ -601,8 +312,8 @@ export default function FileDrive() { | |||
| 601 | <span>{item.name}</span> | 312 | <span>{item.name}</span> |
| 602 | </div> | 313 | </div> |
| 603 | </TableCell> | 314 | </TableCell> |
| 604 | <TableCell>{formatFileSize(item.size)}</TableCell> | 315 | <TableCell>{formatFileSize(item.size || 0)}</TableCell> |
| 605 | <TableCell>{item.modified}</TableCell> | 316 | <TableCell>{formatDate(item.lastmod)}</TableCell> |
| 606 | <TableCell> | 317 | <TableCell> |
| 607 | <DropdownMenu> | 318 | <DropdownMenu> |
| 608 | <DropdownMenuTrigger asChild> | 319 | <DropdownMenuTrigger asChild> |
| @@ -611,9 +322,9 @@ export default function FileDrive() { | |||
| 611 | </Button> | 322 | </Button> |
| 612 | </DropdownMenuTrigger> | 323 | </DropdownMenuTrigger> |
| 613 | <DropdownMenuContent align="end"> | 324 | <DropdownMenuContent align="end"> |
| 614 | {item.type === "folder" && ( | 325 | {item.type === "dir" && ( |
| 615 | <> | 326 | <> |
| 616 | <DropdownMenuItem onClick={() => openFolderUpload(item.id)}> | 327 | <DropdownMenuItem onClick={() => openFolderUpload(item.path)}> |
| 617 | <Upload className="mr-2 h-4 w-4" /> | 328 | <Upload className="mr-2 h-4 w-4" /> |
| 618 | Upload to Folder | 329 | Upload to Folder |
| 619 | </DropdownMenuItem> | 330 | </DropdownMenuItem> |
| @@ -636,7 +347,7 @@ export default function FileDrive() { | |||
| 636 | <DropdownMenuItem | 347 | <DropdownMenuItem |
| 637 | onClick={() => { | 348 | onClick={() => { |
| 638 | if (selectedFiles.size > 0) { | 349 | if (selectedFiles.size > 0) { |
| 639 | console.log("Moving selected files to:", item.type === "folder" ? item.id : "parent of " + item.id) | 350 | console.log("Moving selected files to:", item.type === "dir" ? item.path : "parent of " + item.path) |
| 640 | setSelectedFiles(new Set()) | 351 | setSelectedFiles(new Set()) |
| 641 | } | 352 | } |
| 642 | }} | 353 | }} |
| @@ -646,7 +357,7 @@ export default function FileDrive() { | |||
| 646 | <Move className="mr-2 h-4 w-4" /> | 357 | <Move className="mr-2 h-4 w-4" /> |
| 647 | Move Here {selectedFiles.size > 0 && `(${selectedFiles.size})`} | 358 | Move Here {selectedFiles.size > 0 && `(${selectedFiles.size})`} |
| 648 | </DropdownMenuItem> | 359 | </DropdownMenuItem> |
| 649 | <DropdownMenuItem onClick={() => deleteItems([item.id])} className="text-red-600"> | 360 | <DropdownMenuItem onClick={() => deleteItems([item.path])} className="text-red-600"> |
| 650 | <Trash2 className="mr-2 h-4 w-4" /> | 361 | <Trash2 className="mr-2 h-4 w-4" /> |
| 651 | Delete | 362 | Delete |
| 652 | </DropdownMenuItem> | 363 | </DropdownMenuItem> |
| @@ -656,7 +367,7 @@ export default function FileDrive() { | |||
| 656 | </TableRow>, | 367 | </TableRow>, |
| 657 | ) | 368 | ) |
| 658 | 369 | ||
| 659 | if (item.type === "folder" && item.children && isExpanded) { | 370 | if (item.type === "dir" && item.children && isExpanded) { |
| 660 | item.children.forEach((child) => { | 371 | item.children.forEach((child) => { |
| 661 | rows.push(...renderFileRow(child, level + 1)) | 372 | rows.push(...renderFileRow(child, level + 1)) |
| 662 | }) | 373 | }) |
| @@ -716,8 +427,28 @@ export default function FileDrive() { | |||
| 716 | 427 | ||
| 717 | {currentView === "drive" ? ( | 428 | {currentView === "drive" ? ( |
| 718 | <> | 429 | <> |
| 430 | {/* Loading State */} | ||
| 431 | {loading && ( | ||
| 432 | <div className="flex items-center justify-center py-8"> | ||
| 433 | <div className="text-center"> | ||
| 434 | <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div> | ||
| 435 | <p className="text-sm text-muted-foreground">Loading drive contents...</p> | ||
| 436 | </div> | ||
| 437 | </div> | ||
| 438 | )} | ||
| 439 | |||
| 440 | {/* Error State */} | ||
| 441 | {error && ( | ||
| 442 | <div className="bg-red-50 border border-red-200 rounded-lg p-4"> | ||
| 443 | <div className="text-sm text-red-800"> | ||
| 444 | <strong>Error loading drive data:</strong> {error} | ||
| 445 | </div> | ||
| 446 | </div> | ||
| 447 | )} | ||
| 448 | |||
| 719 | {/* Storage Info */} | 449 | {/* Storage Info */} |
| 720 | <div className="bg-card rounded-lg border p-4"> | 450 | {!loading && !error && ( |
| 451 | <div className="bg-card rounded-lg border p-4"> | ||
| 721 | <div className="flex items-center justify-between mb-2"> | 452 | <div className="flex items-center justify-between mb-2"> |
| 722 | <span className="text-sm font-medium">Storage Usage</span> | 453 | <span className="text-sm font-medium">Storage Usage</span> |
| 723 | <span className="text-sm text-muted-foreground"> | 454 | <span className="text-sm text-muted-foreground"> |
| @@ -729,10 +460,11 @@ export default function FileDrive() { | |||
| 729 | <span>{storagePercentage.toFixed(1)}% used</span> | 460 | <span>{storagePercentage.toFixed(1)}% used</span> |
| 730 | <span>{formatFileSize(maxStorage - usedStorage)} available</span> | 461 | <span>{formatFileSize(maxStorage - usedStorage)} available</span> |
| 731 | </div> | 462 | </div> |
| 732 | </div> | 463 | </div> |
| 464 | )} | ||
| 733 | 465 | ||
| 734 | {/* Bulk Actions */} | 466 | {/* Bulk Actions */} |
| 735 | {selectedFiles.size > 0 && ( | 467 | {!loading && !error && selectedFiles.size > 0 && ( |
| 736 | <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center justify-between"> | 468 | <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center justify-between"> |
| 737 | <div className="flex items-center gap-4"> | 469 | <div className="flex items-center gap-4"> |
| 738 | <span className="text-sm font-medium text-blue-900"> | 470 | <span className="text-sm font-medium text-blue-900"> |
| @@ -757,7 +489,8 @@ export default function FileDrive() { | |||
| 757 | )} | 489 | )} |
| 758 | 490 | ||
| 759 | {/* File Table */} | 491 | {/* File Table */} |
| 760 | <div className="border rounded-lg"> | 492 | {!loading && !error && ( |
| 493 | <div className="border rounded-lg"> | ||
| 761 | <Table> | 494 | <Table> |
| 762 | <TableHeader> | 495 | <TableHeader> |
| 763 | <TableRow> | 496 | <TableRow> |
| @@ -767,34 +500,34 @@ export default function FileDrive() { | |||
| 767 | selectedFiles.size > 0 && | 500 | selectedFiles.size > 0 && |
| 768 | selectedFiles.size === | 501 | selectedFiles.size === |
| 769 | (() => { | 502 | (() => { |
| 770 | const getAllIds = (items: FileItem[]): string[] => { | 503 | const getAllPaths = (items: DriveTreeNode[]): string[] => { |
| 771 | const ids: string[] = [] | 504 | const paths: string[] = [] |
| 772 | items.forEach((item) => { | 505 | items.forEach((item) => { |
| 773 | ids.push(item.id) | 506 | paths.push(item.path) |
| 774 | if (item.children) { | 507 | if (item.children) { |
| 775 | ids.push(...getAllIds(item.children)) | 508 | paths.push(...getAllPaths(item.children)) |
| 776 | } | 509 | } |
| 777 | }) | 510 | }) |
| 778 | return ids | 511 | return paths |
| 779 | } | 512 | } |
| 780 | return getAllIds(files).length | 513 | return getAllPaths(files).length |
| 781 | })() | 514 | })() |
| 782 | } | 515 | } |
| 783 | indeterminate={ | 516 | indeterminate={ |
| 784 | selectedFiles.size > 0 && | 517 | selectedFiles.size > 0 && |
| 785 | selectedFiles.size < | 518 | selectedFiles.size < |
| 786 | (() => { | 519 | (() => { |
| 787 | const getAllIds = (items: FileItem[]): string[] => { | 520 | const getAllPaths = (items: DriveTreeNode[]): string[] => { |
| 788 | const ids: string[] = [] | 521 | const paths: string[] = [] |
| 789 | items.forEach((item) => { | 522 | items.forEach((item) => { |
| 790 | ids.push(item.id) | 523 | paths.push(item.path) |
| 791 | if (item.children) { | 524 | if (item.children) { |
| 792 | ids.push(...getAllIds(item.children)) | 525 | paths.push(...getAllPaths(item.children)) |
| 793 | } | 526 | } |
| 794 | }) | 527 | }) |
| 795 | return ids | 528 | return paths |
| 796 | } | 529 | } |
| 797 | return getAllIds(files).length | 530 | return getAllPaths(files).length |
| 798 | })() | 531 | })() |
| 799 | } | 532 | } |
| 800 | onCheckedChange={(checked) => { | 533 | onCheckedChange={(checked) => { |
| @@ -814,7 +547,8 @@ export default function FileDrive() { | |||
| 814 | </TableHeader> | 547 | </TableHeader> |
| 815 | <TableBody>{files.flatMap((file) => renderFileRow(file))}</TableBody> | 548 | <TableBody>{files.flatMap((file) => renderFileRow(file))}</TableBody> |
| 816 | </Table> | 549 | </Table> |
| 817 | </div> | 550 | </div> |
| 551 | )} | ||
| 818 | </> | 552 | </> |
| 819 | ) : ( | 553 | ) : ( |
| 820 | <HistoryView /> | 554 | <HistoryView /> |
| @@ -824,7 +558,7 @@ export default function FileDrive() { | |||
| 824 | <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}> | 558 | <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}> |
| 825 | <DialogContent> | 559 | <DialogContent> |
| 826 | <DialogHeader> | 560 | <DialogHeader> |
| 827 | <DialogTitle>Rename {currentItem?.type === "folder" ? "Folder" : "File"}</DialogTitle> | 561 | <DialogTitle>Rename {currentItem?.type === "dir" ? "Folder" : "File"}</DialogTitle> |
| 828 | </DialogHeader> | 562 | </DialogHeader> |
| 829 | <div className="space-y-4"> | 563 | <div className="space-y-4"> |
| 830 | <div> | 564 | <div> |
| @@ -858,12 +592,12 @@ export default function FileDrive() { | |||
| 858 | <DialogContent className="max-w-md"> | 592 | <DialogContent className="max-w-md"> |
| 859 | <DialogHeader> | 593 | <DialogHeader> |
| 860 | <DialogTitle className="flex items-center gap-2"> | 594 | <DialogTitle className="flex items-center gap-2"> |
| 861 | {currentItem?.type === "folder" ? ( | 595 | {currentItem?.type === "dir" ? ( |
| 862 | <Folder className="h-5 w-5 text-blue-500" /> | 596 | <Folder className="h-5 w-5 text-blue-500" /> |
| 863 | ) : ( | 597 | ) : ( |
| 864 | <File className="h-5 w-5 text-gray-500" /> | 598 | <File className="h-5 w-5 text-gray-500" /> |
| 865 | )} | 599 | )} |
| 866 | {currentItem?.type === "folder" ? "Folder" : "File"} Information | 600 | {currentItem?.type === "dir" ? "Folder" : "File"} Information |
| 867 | </DialogTitle> | 601 | </DialogTitle> |
| 868 | </DialogHeader> | 602 | </DialogHeader> |
| 869 | {currentItem && ( | 603 | {currentItem && ( |
| @@ -875,25 +609,23 @@ export default function FileDrive() { | |||
| 875 | </div> | 609 | </div> |
| 876 | <div> | 610 | <div> |
| 877 | <Label className="text-sm font-medium text-muted-foreground">Size</Label> | 611 | <Label className="text-sm font-medium text-muted-foreground">Size</Label> |
| 878 | <p className="text-sm">{formatFileSize(currentItem.size)}</p> | 612 | <p className="text-sm">{formatFileSize(currentItem.size || 0)}</p> |
| 879 | </div> | 613 | </div> |
| 880 | <div> | 614 | <div> |
| 881 | <Label className="text-sm font-medium text-muted-foreground">Modified</Label> | 615 | <Label className="text-sm font-medium text-muted-foreground">Modified</Label> |
| 882 | <p className="text-sm"> | 616 | <p className="text-sm">{formatDate(currentItem.lastmod)}</p> |
| 883 | {currentItem.modified} at {currentItem.modifiedTime} | ||
| 884 | </p> | ||
| 885 | </div> | 617 | </div> |
| 886 | <div> | 618 | <div> |
| 887 | <Label className="text-sm font-medium text-muted-foreground">Modified By</Label> | 619 | <Label className="text-sm font-medium text-muted-foreground">Modified By</Label> |
| 888 | <p className="text-sm">{currentItem.modifiedBy}</p> | 620 | <p className="text-sm">{currentItem.author}</p> |
| 889 | </div> | 621 | </div> |
| 890 | <div> | 622 | <div> |
| 891 | <Label className="text-sm font-medium text-muted-foreground">Type</Label> | 623 | <Label className="text-sm font-medium text-muted-foreground">Type</Label> |
| 892 | <p className="text-sm capitalize">{currentItem.type}</p> | 624 | <p className="text-sm capitalize">{currentItem.type}</p> |
| 893 | </div> | 625 | </div> |
| 894 | <div> | 626 | <div> |
| 895 | <Label className="text-sm font-medium text-muted-foreground">ID</Label> | 627 | <Label className="text-sm font-medium text-muted-foreground">Path</Label> |
| 896 | <p className="text-sm font-mono text-xs">{currentItem.id}</p> | 628 | <p className="text-sm font-mono text-xs">{currentItem.path}</p> |
| 897 | </div> | 629 | </div> |
| 898 | </div> | 630 | </div> |
| 899 | <div className="flex justify-end"> | 631 | <div className="flex justify-end"> |
| @@ -908,10 +640,10 @@ export default function FileDrive() { | |||
| 908 | <input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileUpload} /> | 640 | <input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileUpload} /> |
| 909 | {/* Hidden file inputs for folder uploads */} | 641 | {/* Hidden file inputs for folder uploads */} |
| 910 | {(() => { | 642 | {(() => { |
| 911 | const getAllFolders = (items: FileItem[]): FileItem[] => { | 643 | const getAllFolders = (items: DriveTreeNode[]): DriveTreeNode[] => { |
| 912 | const folders: FileItem[] = [] | 644 | const folders: DriveTreeNode[] = [] |
| 913 | items.forEach((item) => { | 645 | items.forEach((item) => { |
| 914 | if (item.type === "folder") { | 646 | if (item.type === "dir") { |
| 915 | folders.push(item) | 647 | folders.push(item) |
| 916 | if (item.children) { | 648 | if (item.children) { |
| 917 | folders.push(...getAllFolders(item.children)) | 649 | folders.push(...getAllFolders(item.children)) |
| @@ -922,12 +654,12 @@ export default function FileDrive() { | |||
| 922 | } | 654 | } |
| 923 | return getAllFolders(files).map((folder) => ( | 655 | return getAllFolders(files).map((folder) => ( |
| 924 | <input | 656 | <input |
| 925 | key={folder.id} | 657 | key={folder.path} |
| 926 | id={`folder-upload-${folder.id}`} | 658 | id={`folder-upload-${folder.path}`} |
| 927 | type="file" | 659 | type="file" |
| 928 | multiple | 660 | multiple |
| 929 | className="hidden" | 661 | className="hidden" |
| 930 | onChange={(e) => handleFolderUpload(e, folder.id)} | 662 | onChange={(e) => handleFolderUpload(e, folder.path)} |
| 931 | /> | 663 | /> |
| 932 | )) | 664 | )) |
| 933 | })()} | 665 | })()} |
diff --git a/frontend/lib/drive_server.ts b/frontend/lib/drive_server.ts index 2f80002..7e2193c 100644 --- a/frontend/lib/drive_server.ts +++ b/frontend/lib/drive_server.ts | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | import { spawnSync } from 'child_process' | 1 | import { spawnSync } from 'child_process' |
| 2 | import { DriveLsEntry } from './drive_types' | 2 | import { DriveLsEntry, DriveTreeNode, DriveTreeResponse } from './drive_types' |
| 3 | import { Drive_split_path, Drive_basename } from './drive_shared' | ||
| 3 | 4 | ||
| 4 | /** | 5 | /** |
| 5 | * Server-only drive functions that use Node.js APIs | 6 | * Server-only drive functions that use Node.js APIs |
| @@ -94,4 +95,52 @@ export async function Drive_mkdir(path: string, email: string) { | |||
| 94 | if (result.status !== 0) { | 95 | if (result.status !== 0) { |
| 95 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); | 96 | throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`); |
| 96 | } | 97 | } |
| 98 | } | ||
| 99 | |||
| 100 | /// builds a filesystem tree from Drive_ls entries | ||
| 101 | export async function Drive_tree(): Promise<DriveTreeResponse> { | ||
| 102 | const entries = await Drive_ls('/', true); | ||
| 103 | |||
| 104 | const nodesMap = new Map<string, DriveTreeNode>(); | ||
| 105 | const rootNodes: DriveTreeNode[] = []; | ||
| 106 | |||
| 107 | // First pass: create all nodes | ||
| 108 | entries.forEach(entry => { | ||
| 109 | const node: DriveTreeNode = { | ||
| 110 | path: entry.path, | ||
| 111 | name: Drive_basename(entry.path), | ||
| 112 | type: entry.type, | ||
| 113 | lastmod: entry.lastmod, | ||
| 114 | blob: entry.blob, | ||
| 115 | size: entry.size, | ||
| 116 | author: entry.author, | ||
| 117 | children: entry.type === "dir" ? [] : undefined | ||
| 118 | }; | ||
| 119 | nodesMap.set(entry.path, node); | ||
| 120 | }); | ||
| 121 | |||
| 122 | // Second pass: build hierarchy | ||
| 123 | entries.forEach(entry => { | ||
| 124 | const pathParts = Drive_split_path(entry.path); | ||
| 125 | const node = nodesMap.get(entry.path)!; | ||
| 126 | |||
| 127 | if (pathParts.length === 1) { | ||
| 128 | // Root level item | ||
| 129 | rootNodes.push(node); | ||
| 130 | } else { | ||
| 131 | // Find parent path by reconstructing it | ||
| 132 | const parentParts = pathParts.slice(0, -1); | ||
| 133 | const parentPath = '/' + parentParts.join('/'); | ||
| 134 | const parent = nodesMap.get(parentPath); | ||
| 135 | |||
| 136 | if (parent && parent.children) { | ||
| 137 | parent.children.push(node); | ||
| 138 | } else { | ||
| 139 | // If parent not found, add to root | ||
| 140 | rootNodes.push(node); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | }); | ||
| 144 | |||
| 145 | return { root: rootNodes }; | ||
| 97 | } \ No newline at end of file | 146 | } \ No newline at end of file |
diff --git a/frontend/lib/drive_shared.ts b/frontend/lib/drive_shared.ts index 99cb5ed..c9d9ac9 100644 --- a/frontend/lib/drive_shared.ts +++ b/frontend/lib/drive_shared.ts | |||
| @@ -12,4 +12,8 @@ export function Drive_parent(path: string): string | null { | |||
| 12 | if (parts.length <= 1) | 12 | if (parts.length <= 1) |
| 13 | return null; | 13 | return null; |
| 14 | return parts[parts.length - 2]; | 14 | return parts[parts.length - 2]; |
| 15 | } | ||
| 16 | |||
| 17 | export function Drive_split_path(path: string): string[] { | ||
| 18 | return path.split('/').filter(part => part.length > 0); | ||
| 15 | } \ No newline at end of file | 19 | } \ No newline at end of file |
diff --git a/frontend/lib/drive_types.ts b/frontend/lib/drive_types.ts index 2bbedf8..ced8271 100644 --- a/frontend/lib/drive_types.ts +++ b/frontend/lib/drive_types.ts | |||
| @@ -6,3 +6,18 @@ export interface DriveLsEntry { | |||
| 6 | size: number | null | 6 | size: number | null |
| 7 | author: string | 7 | author: string |
| 8 | } | 8 | } |
| 9 | |||
| 10 | export interface DriveTreeNode { | ||
| 11 | path: string | ||
| 12 | name: string | ||
| 13 | type: "dir" | "file" | ||
| 14 | lastmod: number | ||
| 15 | blob: string | null | ||
| 16 | size: number | null | ||
| 17 | author: string | ||
| 18 | children?: DriveTreeNode[] // Only for directories | ||
| 19 | } | ||
| 20 | |||
| 21 | export interface DriveTreeResponse { | ||
| 22 | root: DriveTreeNode[] | ||
| 23 | } | ||
