diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts index 9edb37e..9c47fdc 100644 --- a/src/app/dashboard/actions.ts +++ b/src/app/dashboard/actions.ts @@ -24,42 +24,32 @@ export async function getFileNodes() { /** * 2. DELETE: Remove from OneDrive and Database - * Improved to ensure DB removal even if OneDrive API returns errors. */ export async function deleteFileAction(fileId: string) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - // 1. Find the node to check permissions const node = await prisma.fileNode.findUnique({ where: { id: fileId }, }); if (!node) { - // If not in DB, trigger a refresh anyway to clear the UI revalidatePath("/dashboard"); return { success: true, message: "Item already removed from database" }; } - // @ts-ignore - role added via auth callbacks + // @ts-ignore const isAdmin = session.user.role === "ADMIN"; const isOwner = node.ownerId === session.user.id; if (!isAdmin && !isOwner) { - throw new Error("Permission Denied: You do not have authority to delete this item."); + throw new Error("Permission Denied."); } - if (node.isFolder && !isAdmin) { - throw new Error("Security Restriction: Only Administrators can delete folders."); - } - - // 2. OneDrive Deletion Attempt try { const accessToken = await getFreshAccessToken(session.user.id); - if (accessToken) { const rootFolder = "WebCalibre"; - // We target the folder on OneDrive by the ID stored in our DB const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${node.id}`; const onedriveRes = await fetch(onedrivePath, { @@ -68,27 +58,19 @@ export async function deleteFileAction(fileId: string) { }); if (!onedriveRes.ok && onedriveRes.status !== 404) { - const errData = await onedriveRes.json().catch(() => ({})); - console.warn("OneDrive Deletion Warning (Cloud record might still exist):", errData); + console.warn("OneDrive Deletion Warning: Cloud record might still exist."); } } } catch (cloudError) { - // We catch cloud errors but DO NOT throw them, so we can proceed to delete the DB record - console.error("Cloud communication failed, proceeding with DB cleanup:", cloudError); + console.error("Cloud cleanup failed:", cloudError); } - // 3. Database Deletion (The Source of Truth for your UI) try { - await prisma.fileNode.delete({ - where: { id: fileId } - }); - + await prisma.fileNode.delete({ where: { id: fileId } }); revalidatePath("/dashboard"); revalidatePath("/upload"); - return { success: true }; - } catch (dbError: any) { - console.error("Database deletion failed:", dbError); + } catch (dbError) { throw new Error("Failed to remove the record from the database."); } } @@ -99,7 +81,6 @@ export async function deleteFileAction(fileId: string) { export async function moveNodeAction(nodeId: string, newParentId: string | null) { const session = await auth(); if (!session?.user?.id) throw new Error("Unauthorized"); - if (nodeId === newParentId) throw new Error("Cannot move to self."); try { @@ -107,10 +88,79 @@ export async function moveNodeAction(nodeId: string, newParentId: string | null) where: { id: nodeId }, data: { parentId: newParentId } }); - revalidatePath("/dashboard"); return { success: true }; } catch (error) { throw new Error("Move failed."); } +} + +/** + * 4. UPDATE & REPLACE: Full update of metadata and OneDrive content + * Uses FormData to handle the binary file upload and text fields. + */ +export async function updateFileFullAction(formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const id = formData.get("id") as string; + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const parentIdRaw = formData.get("parentId") as string; + const metadataStr = formData.get("metadata") as string; + const newFile = formData.get("file") as File | null; + + // Handle the "root" placeholder back to null for Prisma + const parentId = parentIdRaw === "root" ? null : parentIdRaw; + let metadata = JSON.parse(metadataStr); + + try { + const accessToken = await getFreshAccessToken(session.user.id); + if (!accessToken) throw new Error("Access token expired."); + + // 1. OneDrive Overwrite (if file is provided) + if (newFile && newFile.size > 0) { + const rootFolder = "WebCalibre"; + // PUT request to the specific file ID path overwrites content + const onedrivePath = `https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}/${id}:/content`; + + const uploadRes = await fetch(onedrivePath, { + method: "PUT", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": newFile.type + }, + body: Buffer.from(await newFile.arrayBuffer()), + }); + + if (!uploadRes.ok) { + const err = await uploadRes.json(); + throw new Error(`OneDrive error: ${err.error?.message}`); + } + + // Update metadata with new file properties + metadata.type = newFile.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'; + metadata.mimeType = newFile.type; + } + + // 2. Database Update + await prisma.fileNode.update({ + where: { id }, + data: { + name, + description, + parentId, + metadata, + // If file changed, update size; otherwise keep existing + size: newFile ? BigInt(newFile.size) : undefined, + updatedAt: new Date(), + } + }); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error: any) { + console.error("Full Update Action Failure:", error); + throw new Error(error.message || "Failed to update record."); + } } \ No newline at end of file diff --git a/src/app/dashboard/dashboard-view.tsx b/src/app/dashboard/dashboard-view.tsx index d326916..0c4ef56 100644 --- a/src/app/dashboard/dashboard-view.tsx +++ b/src/app/dashboard/dashboard-view.tsx @@ -1,8 +1,18 @@ -"use client"; +'use client'; + +//src/app/dashboard/dashboard-view.tsx import { useState } from "react"; import { - Button, CircularProgress, Box, Chip, IconButton, Tooltip, Typography, Stack, TextField, InputAdornment + Button, + CircularProgress, + Box, + Chip, + IconButton, + Typography, + Stack, + TextField, + InputAdornment } from "@mui/material"; import { DataGrid, @@ -17,9 +27,10 @@ import RefreshIcon from "@mui/icons-material/Refresh"; import FolderIcon from "@mui/icons-material/Folder"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; import DeleteIcon from "@mui/icons-material/Delete"; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import EditIcon from "@mui/icons-material/Edit"; import SearchIcon from '@mui/icons-material/Search'; import CancelIcon from '@mui/icons-material/Cancel'; + import { syncOneDrive } from "./sync-actions"; import { deleteFileAction } from "./actions"; import { useRouter } from "next/navigation"; @@ -30,7 +41,6 @@ function CustomToolbar() { Library - ( @@ -139,10 +149,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps field: "type", headerName: "Type", width: 120, - // Fixed: Pulling type from metadata since it's not a top-level property - valueGetter: (value, row) => { - return row.metadata?.type || (row.isFolder ? "Folder" : "File"); - }, + valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"), renderCell: (params) => ( {params.value} @@ -152,21 +159,40 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps { field: "size", headerName: "Size", - width: 120, + width: 100, renderCell: (params) => params.row.isFolder ? "--" : `${(Number(params.value) / 1024 / 1024).toFixed(2)} MB` }, { field: "actions", headerName: "Actions", - width: 100, + width: 120, align: 'right', - renderCell: (params) => ( - (isAdmin || params.row.ownerId === user?.id) && ( - handleDelete(params.row.id, params.row.name)}> - - - ) - ) + renderCell: (params) => { + const isOwner = params.row.ownerId === user?.id; + if (isAdmin || isOwner) { + return ( + + router.push(`/update/${params.row.id}`)} + title="Edit Details" + > + + + handleDelete(params.row.id, params.row.name)} + title="Delete" + > + + + + ); + } + return null; + } }, { field: "metadata_search", diff --git a/src/app/dashboard/sync-actions.ts b/src/app/dashboard/sync-actions.ts index 9a4e2e2..afcebfc 100644 --- a/src/app/dashboard/sync-actions.ts +++ b/src/app/dashboard/sync-actions.ts @@ -1,5 +1,4 @@ 'use server'; - import { auth } from "@/auth"; import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; @@ -7,64 +6,37 @@ import { getFreshAccessToken } from "@/lib/auth-utils"; export async function syncOneDrive() { const session = await auth(); - if (!session?.user?.id) throw new Error("Unauthorized"); try { const accessToken = await getFreshAccessToken(session.user.id); - - if (!accessToken) { - throw new Error("Microsoft account not linked properly or session expired."); - } - const response = await fetch("https://graph.microsoft.com/v1.0/me/drive/root:/WebCalibre:/children", { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json" - }, + headers: { Authorization: `Bearer ${accessToken}` }, }); - if (!response.ok) { - const errorData = await response.json(); - if (response.status === 404) { - return { success: true, count: 0 }; - } - throw new Error(errorData.error?.message || "Failed to fetch data from OneDrive"); - } + if (!response.ok) return { success: true, count: 0 }; const data = await response.json(); let syncedCount = 0; - - // Regular expression to identify if a string is a UUID const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; for (const item of data.value) { const isFolder = !!item.folder; - /** - * FILTER: If the item is a folder and the name is a UUID, skip it. - * These are storage containers created by the upload action, not user-facing folders. - */ + // Only skip if it's a folder AND it's a UUID (storage container) + // If a user named a file with a UUID, we still want it. if (isFolder && uuidRegex.test(item.name)) { continue; } - const extension = isFolder - ? 'FOLDER' - : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); - - const fileSize = item.size ? BigInt(item.size) : BigInt(0); + const extension = isFolder ? 'FOLDER' : (item.name.split('.').pop()?.toUpperCase() || 'UNKNOWN'); await prisma.fileNode.upsert({ - where: { oneDriveId: item.id }, + where: { oneDriveId: item.id }, // Primary match update: { name: item.name, - size: fileSize, + size: BigInt(item.size || 0), isFolder: isFolder, - metadata: { - type: extension, - mimeType: item.file?.mimeType || null - }, path: item.parentReference?.path + '/' + item.name, updatedAt: new Date(), }, @@ -72,14 +44,11 @@ export async function syncOneDrive() { id: crypto.randomUUID(), oneDriveId: item.id, name: item.name, - size: fileSize, + size: BigInt(item.size || 0), isFolder: isFolder, path: item.parentReference?.path + '/' + item.name, ownerId: session.user.id, - metadata: { - type: extension, - mimeType: item.file?.mimeType || null - }, + metadata: { type: extension, mimeType: item.file?.mimeType || null }, } }); syncedCount++; @@ -87,9 +56,7 @@ export async function syncOneDrive() { revalidatePath('/dashboard'); return { success: true, count: syncedCount }; - } catch (error: any) { - console.error("Sync Process Failure:", error.message); - throw new Error(error.message || "An unexpected error occurred during sync."); + throw new Error(error.message); } } \ No newline at end of file diff --git a/src/app/dashboard/upload/page.tsx b/src/app/dashboard/upload/page.tsx deleted file mode 100644 index 16540d5..0000000 --- a/src/app/dashboard/upload/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Box, Button, Typography, Paper, LinearProgress, Container } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import { uploadFileAction } from "../upload-actions"; - -export default function UploadPage() { - const [file, setFile] = useState(null); - const [loading, setLoading] = useState(false); - - const handleUpload = async () => { - if (!file) return; - setLoading(true); - - const formData = new FormData(); - formData.append("file", file); - - try { - await uploadFileAction(formData); - alert("Book added to WebCalibre!"); - setFile(null); - } catch (err) { - alert("Upload failed. Check console."); - } finally { - setLoading(false); - } - }; - - return ( - - - - Upload to Library - - - Files will be saved in your OneDrive "WebCalibre" folder. - - - - setFile(e.target.files?.[0] || null)} - /> - - - - {loading && } - - - - - ); -} \ No newline at end of file diff --git a/src/app/update/[id]/_actions.ts b/src/app/update/[id]/_actions.ts new file mode 100644 index 0000000..59a0890 --- /dev/null +++ b/src/app/update/[id]/_actions.ts @@ -0,0 +1,49 @@ +'use server'; + +//src/app/update/[id]/_actions.ts + +import { auth } from "@/auth"; +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function updateFileAction(formData: FormData) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + const id = formData.get("id") as string; + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const parentIdRaw = formData.get("parentId") as string; + const customMetadataRaw = formData.get("customMetadata") as string; + + const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; + const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; + + try { + // 1. Get existing record to preserve system metadata (like mimeType) + const existing = await prisma.fileNode.findUnique({ where: { id } }); + const existingMetadata = (existing?.metadata as Record) || {}; + + // 2. Update the record + await prisma.fileNode.update({ + where: { id }, + data: { + name, + description, + parentId, + metadata: { + ...customMetadata, // User's new keys + type: name.split('.').pop()?.toUpperCase() || existingMetadata.type || "FILE", + mimeType: existingMetadata.mimeType // Preserve the original mimeType + } + } + }); + + revalidatePath("/dashboard"); + revalidatePath(`/update/${id}`); + return { success: true }; + } catch (error: any) { + console.error("Update error:", error); + return { success: false, message: error.message }; + } +} \ No newline at end of file diff --git a/src/app/update/[id]/page.tsx b/src/app/update/[id]/page.tsx new file mode 100644 index 0000000..4a12575 --- /dev/null +++ b/src/app/update/[id]/page.tsx @@ -0,0 +1,34 @@ +import { auth } from "@/auth"; +import { redirect, notFound } from "next/navigation"; +import { prisma } from "@/lib/prisma"; +import { Container } from "@mui/material"; +import UpdateView from "./update-view"; + +// Note: params is now handled as a Promise +export default async function UpdatePage({ params }: { params: Promise<{ id: string }> }) { + const session = await auth(); + if (!session) redirect("/"); + + // 1. Await the params to get the actual ID + const { id } = await params; + + // 2. Fetch the specific file using the awaited ID + const fileNode = await prisma.fileNode.findUnique({ + where: { id: id } + }); + + if (!fileNode) notFound(); + + // Fetch folders for the destination dropdown + const folders = await prisma.fileNode.findMany({ + where: { isFolder: true }, + orderBy: { name: 'asc' }, + select: { id: true, name: true } + }); + + return ( + + + + ); +} \ No newline at end of file diff --git a/src/app/update/[id]/update-view.tsx b/src/app/update/[id]/update-view.tsx new file mode 100644 index 0000000..a3d9007 --- /dev/null +++ b/src/app/update/[id]/update-view.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useState } from "react"; +import { + Box, Button, Typography, Paper, Stack, + TextField, MenuItem, IconButton, Grid, Divider +} from "@mui/material"; +import SaveIcon from "@mui/icons-material/Save"; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; +import FolderIcon from "@mui/icons-material/Folder"; +import { useRouter } from "next/navigation"; +import { updateFileAction } from "./_actions"; + +interface MetadataPair { + key: string; + value: string; +} + +export default function UpdateView({ fileNode, folders }: any) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + // 1. Initialize Basic Info + const [name, setName] = useState(fileNode.name); + const [description, setDescription] = useState(fileNode.description || ""); + const [parentId, setParentId] = useState(fileNode.parentId || ""); + + // 2. Parse existing JSON metadata into Key/Value array for the UI + // We filter out 'type' and 'mimeType' as they are system-managed + const initialMetadata = Object.entries(fileNode.metadata || {}) + .filter(([key]) => !['type', 'mimeType'].includes(key)) + .map(([key, value]) => ({ key, value: String(value) })); + + const [customMetadata, setCustomMetadata] = useState(initialMetadata); + + const handleUpdate = async () => { + setLoading(true); + const formData = new FormData(); + formData.append("id", fileNode.id); + formData.append("name", name); + formData.append("description", description); + formData.append("parentId", parentId); + + // Convert array back to object for storage + const metadataObj = customMetadata.reduce((acc, curr) => { + if (curr.key.trim()) acc[curr.key.trim()] = curr.value; + return acc; + }, {} as Record); + + formData.append("customMetadata", JSON.stringify(metadataObj)); + + const res = await updateFileAction(formData); + if (res.success) { + router.push("/dashboard"); + router.refresh(); + } else { + alert("Update failed"); + setLoading(false); + } + }; + + return ( + + + + + Edit File Details + + + + {/* Name Field */} + setName(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + {/* Folder Select */} + setParentId(e.target.value)} + slotProps={{ + select: { displayEmpty: true }, + inputLabel: { shrink: true } + }} + > + -- Root -- + {folders.map((f: any) => ( + {f.name} + ))} + + + {/* Custom Metadata Section */} + + + + Custom Attributes + + + + + + {customMetadata.map((row, index) => ( + + + { + const updated = [...customMetadata]; + updated[index].key = e.target.value; + setCustomMetadata(updated); + }} + /> + + + { + const updated = [...customMetadata]; + updated[index].value = e.target.value; + setCustomMetadata(updated); + }} + /> + + + setCustomMetadata(customMetadata.filter((_, i) => i !== index))}> + + + + + ))} + + + + setDescription(e.target.value)} + slotProps={{ inputLabel: { shrink: true } }} + /> + + + + + ); +} \ No newline at end of file diff --git a/src/app/upload/_actions.ts b/src/app/upload/_actions.ts index 1d18ce6..096fbb0 100644 --- a/src/app/upload/_actions.ts +++ b/src/app/upload/_actions.ts @@ -1,11 +1,59 @@ 'use server'; - import { auth } from "@/auth"; import { getFreshAccessToken } from "@/lib/auth-utils"; import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; -// createFolderAction remains the same... +export async function createFolderAction(name: string, parentId?: string | null) { + const session = await auth(); + if (!session?.user?.id) throw new Error("Unauthorized"); + + try { + const accessToken = await getFreshAccessToken(session.user.id); + const rootFolder = "WebCalibre"; + const internalId = crypto.randomUUID(); + + const onedriveRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + name: internalId, + folder: {}, + "@microsoft.graph.conflictBehavior": "rename" + }) + }); + + if (!onedriveRes.ok) { + const errorData = await onedriveRes.json(); + throw new Error(errorData.error?.message || "OneDrive folder creation failed"); + } + + // CAPTURE Microsoft's ID + const onedriveData = await onedriveRes.json(); + + const newNode = await prisma.fileNode.create({ + data: { + id: internalId, + oneDriveId: onedriveData.id, // SAVED HERE + name: name, + isFolder: true, + path: `/${rootFolder}/${internalId}`, + ownerId: session.user.id, + parentId: parentId || null, + metadata: { type: "FOLDER" } + } + }); + + revalidatePath("/upload"); + revalidatePath("/dashboard"); + return { success: true, node: newNode }; + } catch (error: any) { + throw new Error(error.message || "Failed to create folder"); + } +} export async function uploadFileAction(formData: FormData) { const session = await auth(); @@ -13,9 +61,8 @@ export async function uploadFileAction(formData: FormData) { const file = formData.get("file") as File; const description = formData.get("description") as string || ""; - const parentId = formData.get("parentId") as string | null; - - // Parse the dynamic metadata from the client + const parentIdRaw = formData.get("parentId") as string | null; + const parentId = (parentIdRaw === "" || parentIdRaw === "root") ? null : parentIdRaw; const customMetadataRaw = formData.get("customMetadata") as string; const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {}; @@ -25,52 +72,60 @@ export async function uploadFileAction(formData: FormData) { const rootFolder = "WebCalibre"; const internalId = crypto.randomUUID(); - // 1. Create OneDrive Storage Folder + // 1. Create Storage Container const createSubFolderRes = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:/${rootFolder}:/children`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ name: internalId, folder: {} }) }); - - if (!createSubFolderRes.ok) throw new Error("Storage directory creation failed"); const subFolderData = await createSubFolderRes.json(); - // 2. Upload Session & File Transfer (Existing logic is fine) + // 2. Upload File const sessionUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${subFolderData.id}:/${encodeURIComponent(file.name)}:/createUploadSession`; const sessionRes = await fetch(sessionUrl, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ item: { "@microsoft.graph.conflictBehavior": "replace" } }) }); + const { uploadUrl } = await sessionRes.json(); const buffer = Buffer.from(await file.arrayBuffer()); - await fetch(uploadUrl, { + + const uploadRes = await fetch(uploadUrl, { method: "PUT", - headers: { "Content-Length": `${file.size}`, "Content-Range": `bytes 0-${file.size - 1}/${file.size}` }, + headers: { + "Content-Length": `${file.size}`, + "Content-Range": `bytes 0-${file.size - 1}/${file.size}` + }, body: buffer }); - // 3. Final Database Record with Merged Metadata + // 3. GET THE FINAL FILE ID FROM MICROSOFT + const uploadedFileData = await uploadRes.json(); + const oneDriveId = uploadedFileData.id; + const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN"; await prisma.fileNode.create({ data: { id: internalId, + oneDriveId: oneDriveId, // SAVED HERE name: file.name, description: description, size: BigInt(file.size), isFolder: false, path: `/${rootFolder}/${internalId}/${file.name}`, ownerId: session.user.id, - parentId: parentId || null, + parentId: parentId, metadata: { - ...customMetadata, // User's dynamic keys (Latitude, Author, etc.) - type: extension, // System keys (preserved for UI icons) + ...customMetadata, + type: extension, mimeType: file.type } } }); revalidatePath("/dashboard"); + revalidatePath("/upload"); return { success: true }; } \ No newline at end of file diff --git a/src/app/upload/upload-view.tsx b/src/app/upload/upload-view.tsx index e277828..e92f4fd 100644 --- a/src/app/upload/upload-view.tsx +++ b/src/app/upload/upload-view.tsx @@ -1,28 +1,20 @@ 'use client'; -import { useState } from "react"; +import { useState, useRef } from "react"; import { - Box, Button, Typography, Paper, LinearProgress, Stack, - TextField, MenuItem, IconButton, Tooltip, Divider, - Grid + Box, Button, Typography, Paper, Stack, + TextField, MenuItem, IconButton, Divider, + Grid, CircularProgress } from "@mui/material"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import FolderIcon from "@mui/icons-material/Folder"; +import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import AssignmentIcon from '@mui/icons-material/Assignment'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { useRouter } from "next/navigation"; import { uploadFileAction, createFolderAction } from "./_actions"; -const formatFileSize = (bytes: number) => { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -}; - interface MetadataPair { key: string; value: string; @@ -30,19 +22,37 @@ interface MetadataPair { export default function UploadView({ user, folders = [] }: any) { const router = useRouter(); + const fileInputRef = useRef(null); const [file, setFile] = useState(null); const [description, setDescription] = useState(""); const [parentId, setParentId] = useState(""); const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle'); - - // Dynamic Metadata State const [customMetadata, setCustomMetadata] = useState([]); + // Folder Creation State const [showFolderInput, setShowFolderInput] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const handleCreateFolder = async () => { + if (!newFolderName.trim()) return; + setIsCreatingFolder(true); + try { + // FIX: Pass the current parentId to the action so it nests correctly + const result = await createFolderAction(newFolderName, parentId); + if (result.success) { + setNewFolderName(""); + setShowFolderInput(false); + router.refresh(); + } + } catch (err: any) { + alert(err.message || "Failed to create folder"); + } finally { + setIsCreatingFolder(false); + } + }; + const addMetadataRow = () => setCustomMetadata([...customMetadata, { key: "", value: "" }]); const removeMetadataRow = (index: number) => { @@ -64,7 +74,6 @@ export default function UploadView({ user, folders = [] }: any) { formData.append("description", description); formData.append("parentId", parentId); - // Pass custom metadata as a JSON string const metadataObj = customMetadata.reduce((acc, curr) => { if (curr.key.trim()) acc[curr.key.trim()] = curr.value; return acc; @@ -79,6 +88,7 @@ export default function UploadView({ user, folders = [] }: any) { setFile(null); setDescription(""); setCustomMetadata([]); + router.push("/dashboard"); router.refresh(); } } catch (err) { @@ -94,42 +104,97 @@ export default function UploadView({ user, folders = [] }: any) { - {/* Section 1: Destination (Condensed for brevity, same as your original) */} + {/* 1. Destination */} 1. Destination + setParentId(e.target.value)} + size="small" + slotProps={{ + select: { displayEmpty: true }, + inputLabel: { shrink: true }, + }} > - -- Root -- + -- Root (Main Folder) -- {folders.map((f: any) => ( {f.name} ))} - setShowFolderInput(!showFolderInput)} sx={{ border: '1px solid #ccc', borderRadius: 1 }}> + setShowFolderInput(!showFolderInput)} + sx={{ border: '1px solid #ccc', borderRadius: 1 }} + > + + {showFolderInput && ( + + + {parentId ? `Create inside current selection` : `Create at Root`} + + + setNewFolderName(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleCreateFolder()} + /> + + + + )} - {/* Section 2: Upload Area */} + {/* 2. Upload Area */} - + 2. Upload File - setFile(e.target.files?.[0] || null)} /> - + + setFile(e.target.files?.[0] || null)} + /> + + - {/* Section 3: Dynamic Metadata Area */} + {/* 3. Custom Attributes */} @@ -140,16 +205,12 @@ export default function UploadView({ user, folders = [] }: any) { - - Add specific details like Author, Latitude, Aperture, or Part Number. - - {customMetadata.map((row, index) => ( updateMetadataRow(index, 'key', e.target.value)} /> @@ -180,8 +241,9 @@ export default function UploadView({ user, folders = [] }: any) { variant="contained" size="large" fullWidth disabled={!file || status === 'uploading'} onClick={handleUpload} + sx={{ py: 2, fontWeight: 'bold' }} > - {status === 'uploading' ? 'Processing...' : 'Add to Library'} + {status === 'uploading' ? 'Uploading to OneDrive...' : 'Start Upload'}