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 (
+
+ } onClick={() => router.back()} sx={{ mb: 2 }}>
+ Back
+
+
+
+ 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 }
+ }}
+ >
+
+ {folders.map((f: any) => (
+
+ ))}
+
+
+ {/* Custom Metadata Section */}
+
+
+
+ Custom Attributes
+
+ }
+ size="small"
+ onClick={() => setCustomMetadata([...customMetadata, { key: "", value: "" }])}
+ >
+ Add Field
+
+
+
+
+ {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 } }}
+ />
+
+ }
+ onClick={handleUpdate}
+ disabled={loading}
+ sx={{ py: 1.5, fontWeight: 'bold' }}
+ >
+ {loading ? "Saving..." : "Save Changes"}
+
+
+
+ );
+}
\ 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 },
+ }}
>
-
+
{folders.map((f: any) => (
))}
- 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'}