metadata functionality is working
This commit is contained in:
parent
c71e0bf602
commit
b557f75bea
7 changed files with 271 additions and 401 deletions
|
|
@ -352,3 +352,7 @@ Project-1 ID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1 parentID 0371
|
|||
AS-NZS3000-2018.pdf parentID c8eebe0e-6e90-4cfb-8e44-c05f7064f3b1
|
||||
|
||||
Now we will go now and add metadata
|
||||
|
||||
Now we have the metadata along with some great filters on the data board page
|
||||
|
||||
Now we are going to implement an an update of a file or folder stored. 12/1/2026
|
||||
BIN
docs/notes.pdf
BIN
docs/notes.pdf
Binary file not shown.
102
package-lock.json
generated
102
package-lock.json
generated
|
|
@ -13,9 +13,9 @@
|
|||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@mui/material": "^7.3.7",
|
||||
"@mui/material-nextjs": "^7.3.6",
|
||||
"@mui/x-data-grid": "^8.23.0",
|
||||
"@mui/x-data-grid": "^8.24.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"next": "16.1.1",
|
||||
|
|
@ -1333,9 +1333,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz",
|
||||
"integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.7.tgz",
|
||||
"integrity": "sha512-8jWwS6FweMkpyRkrJooamUGe1CQfO1yJ+lM43IyUJbrhHW/ObES+6ry4vfGi8EKaldHL3t3BG1bcLcERuJPcjg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
|
@ -1369,22 +1369,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz",
|
||||
"integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.7.tgz",
|
||||
"integrity": "sha512-6bdIxqzeOtBAj2wAsfhWCYyMKPLkRO9u/2o5yexcL0C3APqyy91iGSWgT3H7hg+zR2XgE61+WAu12wXPON8b6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/core-downloads-tracker": "^7.3.6",
|
||||
"@mui/system": "^7.3.6",
|
||||
"@mui/types": "^7.4.9",
|
||||
"@mui/utils": "^7.3.6",
|
||||
"@mui/core-downloads-tracker": "^7.3.7",
|
||||
"@mui/system": "^7.3.7",
|
||||
"@mui/types": "^7.4.10",
|
||||
"@mui/utils": "^7.3.7",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.2.0",
|
||||
"react-is": "^19.2.3",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1397,7 +1397,7 @@
|
|||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^7.3.6",
|
||||
"@mui/material-pigment-css": "^7.3.7",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
|
|
@ -1459,13 +1459,13 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.6.tgz",
|
||||
"integrity": "sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.7.tgz",
|
||||
"integrity": "sha512-w7r1+CYhG0syCAQUWAuV5zSaU2/67WA9JXUderdb7DzCIJdp/5RmJv6L85wRjgKCMsxFF0Kfn0kPgPbPgw/jdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/utils": "^7.3.6",
|
||||
"@mui/utils": "^7.3.7",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1486,16 +1486,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.6.tgz",
|
||||
"integrity": "sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.7.tgz",
|
||||
"integrity": "sha512-y/QkNXv6cF6dZ5APztd/dFWfQ6LHKPx3skyYO38YhQD4+Cxd6sFAL3Z38WMSSC8LQz145Mpp3CcLrSCLKPwYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
"csstype": "^3.1.3",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1520,18 +1520,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz",
|
||||
"integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz",
|
||||
"integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/private-theming": "^7.3.6",
|
||||
"@mui/styled-engine": "^7.3.6",
|
||||
"@mui/types": "^7.4.9",
|
||||
"@mui/utils": "^7.3.6",
|
||||
"@mui/private-theming": "^7.3.7",
|
||||
"@mui/styled-engine": "^7.3.7",
|
||||
"@mui/types": "^7.4.10",
|
||||
"@mui/utils": "^7.3.7",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"csstype": "^3.2.3",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1560,9 +1560,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.9.tgz",
|
||||
"integrity": "sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==",
|
||||
"version": "7.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.10.tgz",
|
||||
"integrity": "sha512-0+4mSjknSu218GW3isRqoxKRTOrTLd/vHi/7UC4+wZcUrOAqD9kRk7UQRL1mcrzqRoe7s3UT6rsRpbLkW5mHpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
|
|
@ -1577,17 +1577,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "7.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.6.tgz",
|
||||
"integrity": "sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz",
|
||||
"integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/types": "^7.4.9",
|
||||
"@mui/types": "^7.4.10",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.2.0"
|
||||
"react-is": "^19.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
|
@ -1613,15 +1613,15 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.23.0.tgz",
|
||||
"integrity": "sha512-fv8AiibTgwPKJsXtYdc5bYDDW3EQp/ieQ0iUejvZ2JbYikMIPTYjI4pHy7zW3F3RDsi6DGQnw5AW6LeoDcT0fA==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.24.0.tgz",
|
||||
"integrity": "sha512-OCTrskRlxwJZ+vYWbgQnQPXWNMA/nY/RIkoESjbCG38N9fJicVCdyjszjcQn9V1NQH4fwkMlIN+DVU6zzbqJxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/utils": "^7.3.5",
|
||||
"@mui/x-internals": "8.23.0",
|
||||
"@mui/x-virtualizer": "0.3.0",
|
||||
"@mui/x-internals": "8.24.0",
|
||||
"@mui/x-virtualizer": "0.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
|
|
@ -1651,9 +1651,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/x-internals": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.23.0.tgz",
|
||||
"integrity": "sha512-FN7wdqwTxqq1tJBYVz8TA/HMcViuaHS0Jphr4pEjT/8Iuf94Yt3P82WbsTbXyYrgOQDQl07UqE7qWcJetRcHcg==",
|
||||
"version": "8.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.24.0.tgz",
|
||||
"integrity": "sha512-tHPk8eUcuwvhNsAc4zcia8y+issjC+bLGssrQQSdLLeLIf9ewFbcwAURNTSnNhreyj/4RJOemVrSxKXpjkdVvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
|
|
@ -1673,14 +1673,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mui/x-virtualizer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.3.0.tgz",
|
||||
"integrity": "sha512-ScQ3xullKQAtbIT9N7PVhgW9BDTo2Hu8fr/+N08TdRMJcq8bsVJB25mlcHk6zxjlgCuojqaeGfw71S1FOvyCXQ==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.3.1.tgz",
|
||||
"integrity": "sha512-MhQ7U/zmSofBxeCRMiolhZWJ80RUJMFFMRqeehJu5GqscnudiVcplZb0eNsAkz9IjnJTyrjyLS0PY+kn78enOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@mui/utils": "^7.3.5",
|
||||
"@mui/x-internals": "8.23.0"
|
||||
"@mui/x-internals": "8.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@
|
|||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.6",
|
||||
"@mui/material": "^7.3.6",
|
||||
"@mui/material": "^7.3.7",
|
||||
"@mui/material-nextjs": "^7.3.6",
|
||||
"@mui/x-data-grid": "^8.23.0",
|
||||
"@mui/x-data-grid": "^8.24.0",
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"next": "16.1.1",
|
||||
|
|
|
|||
|
|
@ -2,18 +2,69 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button, CircularProgress, Box, Chip, IconButton, Tooltip, Typography
|
||||
Button, CircularProgress, Box, Chip, IconButton, Tooltip, Typography, Stack, TextField, InputAdornment
|
||||
} from "@mui/material";
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
Toolbar,
|
||||
QuickFilter,
|
||||
QuickFilterControl,
|
||||
QuickFilterClear,
|
||||
} from "@mui/x-data-grid";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
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 { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import { syncOneDrive } from "./sync-actions";
|
||||
import { deleteFileAction } from "./actions"; // Un-commented and assumed active
|
||||
import { deleteFileAction } from "./actions";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function CustomToolbar() {
|
||||
return (
|
||||
<Toolbar sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" fontWeight="bold" color="primary">
|
||||
Library
|
||||
</Typography>
|
||||
|
||||
<QuickFilter sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<QuickFilterControl
|
||||
render={({ ref, ...controlProps }, state) => (
|
||||
<TextField
|
||||
{...controlProps}
|
||||
inputRef={ref}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
placeholder="Search files and metadata..."
|
||||
sx={{ width: 350 }}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: state.value ? (
|
||||
<InputAdornment position="end">
|
||||
<QuickFilterClear size="small">
|
||||
<CancelIcon fontSize="small" />
|
||||
</QuickFilterClear>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</QuickFilter>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
interface DashboardViewProps {
|
||||
initialFiles: any[];
|
||||
user?: {
|
||||
|
|
@ -26,19 +77,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const isAdmin = user?.role === "ADMIN";
|
||||
|
||||
/**
|
||||
* Recursive function to build the virtual path for the "Location" column.
|
||||
* Ensures we see "Projects / Project-1" instead of UUIDs or "ROOT".
|
||||
*/
|
||||
const getVirtualPath = (parentId: string | null): string => {
|
||||
if (!parentId) return "WebCalibre";
|
||||
const parent = initialFiles.find((f) => f.id === parentId);
|
||||
if (!parent) return "WebCalibre";
|
||||
|
||||
// Recursive lookup for breadcrumbs
|
||||
const prefix = parent.parentId ? `${getVirtualPath(parent.parentId)} / ` : "";
|
||||
return `${prefix}${parent.name}`;
|
||||
};
|
||||
|
|
@ -50,7 +94,6 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error("Sync failed:", error);
|
||||
alert("Failed to sync OneDrive");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -59,13 +102,11 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
const handleRefresh = () => {
|
||||
setIsRefreshing(true);
|
||||
router.refresh();
|
||||
// Visual feedback for the refresh action
|
||||
setTimeout(() => setIsRefreshing(false), 800);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string, name: string) => {
|
||||
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
|
||||
|
||||
try {
|
||||
await deleteFileAction(id);
|
||||
router.refresh();
|
||||
|
|
@ -82,49 +123,28 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
minWidth: 250,
|
||||
renderCell: (params) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
{params.row.isFolder ? (
|
||||
<FolderIcon sx={{ color: '#FFB020' }} /> // Folder yellow
|
||||
) : (
|
||||
<InsertDriveFileIcon color="action" />
|
||||
)}
|
||||
<Typography variant="body2" sx={{ fontWeight: params.row.isFolder ? 600 : 400 }}>
|
||||
{params.value}
|
||||
</Typography>
|
||||
{params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />}
|
||||
<Typography variant="body2">{params.value}</Typography>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: "parentId",
|
||||
headerName: "Location (Project)",
|
||||
headerName: "Location",
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
renderCell: (params) => {
|
||||
const path = getVirtualPath(params.value);
|
||||
return (
|
||||
<Tooltip title={path}>
|
||||
<Chip
|
||||
label={path}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color={path === "WebCalibre" ? "default" : "primary"}
|
||||
sx={{ maxWidth: '100%' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
renderCell: (params) => <Chip label={getVirtualPath(params.value)} size="small" variant="outlined" />
|
||||
},
|
||||
{ field: "description", headerName: "Description", flex: 1 },
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 100,
|
||||
width: 120,
|
||||
// Fixed: Pulling type from metadata since it's not a top-level property
|
||||
valueGetter: (value, row) => {
|
||||
// Priority: Metadata -> isFolder check -> fallback
|
||||
const type = row.metadata?.type || (row.isFolder ? "FOLDER" : "FILE");
|
||||
// Ensure we don't display a UUID here if the sync was messy
|
||||
return type.length > 10 && row.isFolder ? "FOLDER" : type;
|
||||
return row.metadata?.type || (row.isFolder ? "Folder" : "File");
|
||||
},
|
||||
renderCell: (params) => (
|
||||
<Typography variant="caption" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
|
||||
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold', color: 'text.secondary' }}>
|
||||
{params.value}
|
||||
</Typography>
|
||||
)
|
||||
|
|
@ -133,40 +153,31 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
field: "size",
|
||||
headerName: "Size",
|
||||
width: 120,
|
||||
valueGetter: (value, row) => {
|
||||
if (row.isFolder) return null;
|
||||
return value;
|
||||
},
|
||||
renderCell: (params) => {
|
||||
if (params.value === null) return "--";
|
||||
const mb = (Number(params.value) / 1024 / 1024).toFixed(2);
|
||||
return `${mb} MB`;
|
||||
}
|
||||
renderCell: (params) => params.row.isFolder ? "--" : `${(Number(params.value) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{
|
||||
field: "actions",
|
||||
headerName: "Actions",
|
||||
width: 100,
|
||||
sortable: false,
|
||||
align: 'right',
|
||||
headerAlign: 'right',
|
||||
renderCell: (params) => {
|
||||
const isOwner = params.row.ownerId === user?.id;
|
||||
if (isAdmin || isOwner) {
|
||||
return (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(params.row.id, params.row.name);
|
||||
}}
|
||||
>
|
||||
renderCell: (params) => (
|
||||
(isAdmin || params.row.ownerId === user?.id) && (
|
||||
<IconButton size="small" color="error" onClick={() => handleDelete(params.row.id, params.row.name)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
field: "metadata_search",
|
||||
headerName: "Metadata Search",
|
||||
width: 0,
|
||||
valueGetter: (value, row) => {
|
||||
if (!row.metadata) return "";
|
||||
return Object.entries(row.metadata)
|
||||
.filter(([k]) => k !== 'type' && k !== 'mimeType')
|
||||
.map(([k, v]) => `${k}:${v}`)
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -174,53 +185,32 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
return (
|
||||
<Box className="space-y-4">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mb: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
{isRefreshing ? "Refreshing..." : "Refresh List"}
|
||||
<Button variant="outlined" startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />} onClick={handleRefresh}>
|
||||
Refresh List
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
|
||||
onClick={handleSync}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Syncing..." : "Sync OneDrive"}
|
||||
<Button variant="contained" startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />} onClick={handleSync} disabled={loading}>
|
||||
Sync OneDrive
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
height: 700,
|
||||
width: "100%",
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 3,
|
||||
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<Box sx={{ height: 700, width: "100%", bgcolor: 'background.paper', borderRadius: 3, boxShadow: 1, overflow: 'hidden' }}>
|
||||
<DataGrid
|
||||
rows={initialFiles}
|
||||
columns={columns}
|
||||
pageSizeOptions={[10, 25, 50]}
|
||||
slots={{ toolbar: CustomToolbar }}
|
||||
showToolbar
|
||||
disableRowSelectionOnClick
|
||||
initialState={{
|
||||
pagination: { paginationModel: { pageSize: 10 } },
|
||||
sorting: {
|
||||
sortModel: [{ field: 'name', sort: 'asc' }],
|
||||
columns: {
|
||||
columnVisibilityModel: {
|
||||
metadata_search: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
disableRowSelectionOnClick
|
||||
sx={{
|
||||
border: 'none',
|
||||
'& .MuiDataGrid-columnHeaders': {
|
||||
bgcolor: '#f8f9fa',
|
||||
borderBottom: '1px solid #eee',
|
||||
},
|
||||
'& .MuiDataGrid-cell': {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeaders': { bgcolor: '#f8f9fa' },
|
||||
'& .MuiDataGrid-toolbarContainer': { borderBottom: '1px solid #eee' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -5,42 +5,8 @@ import { getFreshAccessToken } from "@/lib/auth-utils";
|
|||
import { prisma } from "@/lib/prisma";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
/**
|
||||
* Creates a virtual folder in the database.
|
||||
* We explicitly set the metadata type to "FOLDER" so the Dashboard icon
|
||||
* and Location logic work immediately.
|
||||
*/
|
||||
export async function createFolderAction(name: string, parentId: string | null = null) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error("Unauthorized");
|
||||
// createFolderAction remains the same...
|
||||
|
||||
// We generate a manual UUID because the schema requires 'id' but has no default generator
|
||||
const internalId = crypto.randomUUID();
|
||||
|
||||
await prisma.fileNode.create({
|
||||
data: {
|
||||
id: internalId,
|
||||
name: name,
|
||||
isFolder: true,
|
||||
path: `/virtual/${name}`,
|
||||
ownerId: session.user.id,
|
||||
parentId: parentId || null,
|
||||
metadata: {
|
||||
type: "FOLDER",
|
||||
mimeType: "inode/directory"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/upload");
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to OneDrive into a unique UUID folder
|
||||
* and links it to a virtual parent (Project/Folder) in the DB.
|
||||
*/
|
||||
export async function uploadFileAction(formData: FormData) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error("Unauthorized");
|
||||
|
|
@ -49,78 +15,57 @@ export async function uploadFileAction(formData: FormData) {
|
|||
const description = formData.get("description") as string || "";
|
||||
const parentId = formData.get("parentId") as string | null;
|
||||
|
||||
// Parse the dynamic metadata from the client
|
||||
const customMetadataRaw = formData.get("customMetadata") as string;
|
||||
const customMetadata = customMetadataRaw ? JSON.parse(customMetadataRaw) : {};
|
||||
|
||||
if (!file) throw new Error("No file selected");
|
||||
|
||||
// Use the utility to ensure we have a valid JWT (fixing the "no dots" error)
|
||||
const accessToken = await getFreshAccessToken(session.user.id);
|
||||
const rootFolder = "WebCalibre";
|
||||
const internalId = crypto.randomUUID();
|
||||
|
||||
// 1. Create the unique UUID folder on OneDrive inside WebCalibre
|
||||
// 1. Create OneDrive Storage Folder
|
||||
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: {},
|
||||
"@microsoft.graph.conflictBehavior": "fail"
|
||||
})
|
||||
headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name: internalId, folder: {} })
|
||||
});
|
||||
|
||||
if (!createSubFolderRes.ok) {
|
||||
const err = await createSubFolderRes.json();
|
||||
console.error("OneDrive Folder Creation Error:", err);
|
||||
throw new Error("Storage directory creation failed");
|
||||
}
|
||||
|
||||
if (!createSubFolderRes.ok) throw new Error("Storage directory creation failed");
|
||||
const subFolderData = await createSubFolderRes.json();
|
||||
|
||||
// 2. Create Upload Session for the file
|
||||
// 2. Upload Session & File Transfer (Existing logic is fine)
|
||||
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"
|
||||
},
|
||||
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());
|
||||
|
||||
// 3. Perform the actual upload
|
||||
const uploadRes = await fetch(uploadUrl, {
|
||||
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
|
||||
});
|
||||
|
||||
if (!uploadRes.ok) throw new Error("OneDrive stream failed");
|
||||
const driveItem = await uploadRes.json();
|
||||
|
||||
// 4. Record in Database with full metadata and virtual hierarchy
|
||||
// 3. Final Database Record with Merged Metadata
|
||||
const extension = file.name.split('.').pop()?.toUpperCase() || "UNKNOWN";
|
||||
|
||||
await prisma.fileNode.create({
|
||||
data: {
|
||||
id: internalId, // Matches the folder name on OneDrive
|
||||
oneDriveId: driveItem.id,
|
||||
id: internalId,
|
||||
name: file.name,
|
||||
description: description,
|
||||
size: BigInt(file.size),
|
||||
isFolder: false,
|
||||
path: `/${rootFolder}/${internalId}/${file.name}`,
|
||||
ownerId: session.user.id,
|
||||
parentId: parentId || null, // Virtual link to the Project folder
|
||||
parentId: parentId || null,
|
||||
metadata: {
|
||||
type: extension,
|
||||
...customMetadata, // User's dynamic keys (Latitude, Author, etc.)
|
||||
type: extension, // System keys (preserved for UI icons)
|
||||
mimeType: file.type
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@ import { useState } from "react";
|
|||
import {
|
||||
Box, Button, Typography, Paper, LinearProgress, Stack,
|
||||
TextField, MenuItem, IconButton, Tooltip, Divider,
|
||||
InputAdornment
|
||||
Grid
|
||||
} 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 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";
|
||||
|
||||
/**
|
||||
* Format bytes to human readable string (MiB/KiB)
|
||||
*/
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
|
|
@ -24,73 +23,66 @@ const formatFileSize = (bytes: number) => {
|
|||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
interface Folder {
|
||||
id: string;
|
||||
name: string;
|
||||
parentId: string | null;
|
||||
interface MetadataPair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface UploadViewProps {
|
||||
user: any;
|
||||
folders?: Folder[];
|
||||
}
|
||||
|
||||
export default function UploadView({ user, folders = [] }: UploadViewProps) {
|
||||
export default function UploadView({ user, folders = [] }: any) {
|
||||
const router = useRouter();
|
||||
|
||||
// Form State
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [description, setDescription] = useState("");
|
||||
const [parentId, setParentId] = useState("");
|
||||
const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle');
|
||||
|
||||
// Folder Creation State
|
||||
// Dynamic Metadata State
|
||||
const [customMetadata, setCustomMetadata] = useState<MetadataPair[]>([]);
|
||||
|
||||
const [showFolderInput, setShowFolderInput] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||
|
||||
const MAX_FILE_SIZE = 150 * 1024 * 1024; // 150MB Limit
|
||||
const addMetadataRow = () => setCustomMetadata([...customMetadata, { key: "", value: "" }]);
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
if (!newFolderName.trim()) return;
|
||||
setIsCreatingFolder(true);
|
||||
try {
|
||||
// Creates folder nested under current selection if parentId exists
|
||||
await createFolderAction(newFolderName, parentId || null);
|
||||
setNewFolderName("");
|
||||
setShowFolderInput(false);
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
alert("Error creating folder");
|
||||
} finally {
|
||||
setIsCreatingFolder(false);
|
||||
}
|
||||
const removeMetadataRow = (index: number) => {
|
||||
setCustomMetadata(customMetadata.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateMetadataRow = (index: number, field: 'key' | 'value', val: string) => {
|
||||
const updated = [...customMetadata];
|
||||
updated[index][field] = val;
|
||||
setCustomMetadata(updated);
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) return;
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
alert(`File is too large! Max allowed: ${formatFileSize(MAX_FILE_SIZE)}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('uploading');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
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;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
formData.append("customMetadata", JSON.stringify(metadataObj));
|
||||
|
||||
try {
|
||||
const result = await uploadFileAction(formData);
|
||||
if (result.success) {
|
||||
setStatus('success');
|
||||
setFile(null);
|
||||
setDescription("");
|
||||
setCustomMetadata([]);
|
||||
router.refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Upload failed. Check server logs.");
|
||||
alert("Upload failed.");
|
||||
setStatus('idle');
|
||||
}
|
||||
};
|
||||
|
|
@ -102,156 +94,95 @@ export default function UploadView({ user, folders = [] }: UploadViewProps) {
|
|||
</Typography>
|
||||
|
||||
<Stack spacing={4} sx={{ mt: 4 }}>
|
||||
|
||||
{/* Section 1: Destination Folder */}
|
||||
{/* Section 1: Destination (Condensed for brevity, same as your original) */}
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom fontWeight="700" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FolderIcon color="primary" /> 1. Destination
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
label="Target Project / Folder"
|
||||
select fullWidth label="Target Project / Folder"
|
||||
value={parentId}
|
||||
onChange={(e) => setParentId(e.target.value)}
|
||||
disabled={status === 'uploading'}
|
||||
helperText="Files will be virtually organized into this folder."
|
||||
>
|
||||
<MenuItem value=""><em>-- Root (Main Library) --</em></MenuItem>
|
||||
{folders.map((f) => (
|
||||
<MenuItem key={f.id} value={f.id} sx={{ pl: f.parentId ? 4 : 2 }}>
|
||||
{f.parentId ? `↳ ${f.name}` : f.name}
|
||||
</MenuItem>
|
||||
<MenuItem value=""><em>-- Root --</em></MenuItem>
|
||||
{folders.map((f: any) => (
|
||||
<MenuItem key={f.id} value={f.id}>{f.name}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<Tooltip title="Create New Folder Inside Selected">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowFolderInput(!showFolderInput)}
|
||||
sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, width: 56, height: 56 }}
|
||||
>
|
||||
<IconButton color="primary" onClick={() => setShowFolderInput(!showFolderInput)} sx={{ border: '1px solid #ccc', borderRadius: 1 }}>
|
||||
<CreateNewFolderIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
|
||||
{showFolderInput && (
|
||||
<Stack direction="row" spacing={1} sx={{ mt: 2, p: 2, bgcolor: 'action.hover', borderRadius: 2 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="New folder name..."
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
autoFocus
|
||||
disabled={isCreatingFolder}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCreateFolder}
|
||||
disabled={isCreatingFolder || !newFolderName.trim()}
|
||||
>
|
||||
{isCreatingFolder ? "..." : "Create"}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Section 2: File Upload Area */}
|
||||
{/* Section 2: Upload Area */}
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom fontWeight="700" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CloudUploadIcon color="primary" /> 2. Upload File
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 5,
|
||||
mt: 1,
|
||||
border: '2px dashed',
|
||||
borderColor: file ? 'primary.main' : 'divider',
|
||||
borderRadius: 3,
|
||||
textAlign: 'center',
|
||||
bgcolor: file ? 'rgba(25, 118, 210, 0.04)' : '#fafafa',
|
||||
'&:hover': { bgcolor: 'rgba(0, 0, 0, 0.02)' }
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file" id="file-input" hidden
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] || null);
|
||||
setStatus('idle');
|
||||
}}
|
||||
/>
|
||||
<input type="file" id="file-input" hidden onChange={(e) => setFile(e.target.files?.[0] || null)} />
|
||||
<label htmlFor="file-input">
|
||||
<Button
|
||||
variant={file ? "outlined" : "contained"}
|
||||
component="span"
|
||||
startIcon={<CloudUploadIcon />}
|
||||
size="large"
|
||||
disabled={status === 'uploading'}
|
||||
>
|
||||
{file ? "Change File" : "Choose Document"}
|
||||
<Button variant="outlined" component="span" fullWidth sx={{ p: 4, borderStyle: 'dashed' }}>
|
||||
{file ? file.name : "Select File"}
|
||||
</Button>
|
||||
</label>
|
||||
|
||||
{file && (
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2" color="primary.main" fontWeight="bold">
|
||||
{file.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatFileSize(file.size)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Section 3: Notes */}
|
||||
{/* Section 3: Dynamic Metadata Area */}
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom fontWeight="700" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AssignmentIcon color="primary" /> 3. Metadata
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="h6" fontWeight="700" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<AssignmentIcon color="primary" /> 3. Custom Attributes
|
||||
</Typography>
|
||||
<Button startIcon={<AddCircleOutlineIcon />} size="small" onClick={addMetadataRow}>
|
||||
Add Field
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
|
||||
Add specific details like Author, Latitude, Aperture, or Part Number.
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={2}>
|
||||
{customMetadata.map((row, index) => (
|
||||
<Grid container spacing={1} key={index} alignItems="center">
|
||||
<Grid item xs={5}>
|
||||
<TextField
|
||||
label="Description / Notes"
|
||||
multiline rows={3} fullWidth
|
||||
placeholder="Add keywords or a brief summary..."
|
||||
fullWidth size="small" placeholder="Key (e.g. Latitude)"
|
||||
value={row.key} onChange={(e) => updateMetadataRow(index, 'key', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
fullWidth size="small" placeholder="Value"
|
||||
value={row.value} onChange={(e) => updateMetadataRow(index, 'value', e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
<IconButton color="error" onClick={() => removeMetadataRow(index)}>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
<TextField
|
||||
label="General Description"
|
||||
multiline rows={2} fullWidth
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={status === 'uploading'}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Progress & Actions */}
|
||||
<Box>
|
||||
{status === 'uploading' && (
|
||||
<Box mb={2}>
|
||||
<LinearProgress sx={{ borderRadius: 5, height: 10 }} />
|
||||
<Typography variant="caption" color="primary" sx={{ mt: 1, display: 'block', textAlign: 'center' }}>
|
||||
Transferring to OneDrive and updating Library...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="contained" size="large" fullWidth
|
||||
disabled={!file || status === 'uploading'}
|
||||
onClick={handleUpload}
|
||||
sx={{ py: 2, fontWeight: 'bold' }}
|
||||
>
|
||||
{status === 'uploading' ? 'Uploading...' : 'Add to Library'}
|
||||
{status === 'uploading' ? 'Processing...' : 'Add to Library'}
|
||||
</Button>
|
||||
|
||||
{status === 'success' && (
|
||||
<Typography align="center" color="success.main" fontWeight="bold" sx={{ mt: 2 }}>
|
||||
✅ File processed and assigned to project!
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue