288 lines
No EOL
8.9 KiB
TypeScript
288 lines
No EOL
8.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from "react";
|
|
import { styled } from '@mui/material/styles';
|
|
import {
|
|
Button,
|
|
CircularProgress,
|
|
Box,
|
|
Chip,
|
|
IconButton,
|
|
Typography,
|
|
Stack,
|
|
TextField,
|
|
InputAdornment,
|
|
Tooltip,
|
|
} 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 EditIcon from "@mui/icons-material/Edit";
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
import DownloadIcon from '@mui/icons-material/Download';
|
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
|
|
import { syncOneDrive } from "./sync-actions";
|
|
import { deleteFileNodeAction } from "./actions";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
// --- 1. Styled Component for Search Placement ---
|
|
|
|
const StyledQuickFilter = styled(QuickFilter)({
|
|
marginLeft: 'auto', // Pushes the search box to the right side of the toolbar
|
|
});
|
|
|
|
// --- 2. Custom Toolbar Component ---
|
|
|
|
function CustomToolbar() {
|
|
return (
|
|
<Toolbar >
|
|
<Box sx={{
|
|
display: 'flex',
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
p: 2,
|
|
borderBottom: '1px solid',
|
|
borderColor: 'divider'
|
|
}}>
|
|
<Typography variant="h6" fontWeight="bold" color="primary">
|
|
Library
|
|
</Typography>
|
|
|
|
{/* The 'expanded' prop ensures the search input is always visible by default */}
|
|
<StyledQuickFilter expanded>
|
|
<QuickFilterControl
|
|
render={({ ref, ...other }) => (
|
|
<TextField
|
|
{...other}
|
|
sx={{ width: 300 }}
|
|
inputRef={ref}
|
|
placeholder="Search library..."
|
|
size="small"
|
|
slotProps={{
|
|
input: {
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon fontSize="small" />
|
|
</InputAdornment>
|
|
),
|
|
endAdornment: other.value ? (
|
|
<InputAdornment position="end">
|
|
<QuickFilterClear
|
|
edge="end"
|
|
size="small"
|
|
material={{ sx: { marginRight: -0.75 } }}
|
|
>
|
|
<CancelIcon fontSize="small" />
|
|
</QuickFilterClear>
|
|
</InputAdornment>
|
|
) : null,
|
|
// Ensure other props are spread correctly
|
|
...other.slotProps?.input,
|
|
},
|
|
...other.slotProps,
|
|
}}
|
|
/>
|
|
)}
|
|
/>
|
|
</StyledQuickFilter>
|
|
</Box>
|
|
</Toolbar>
|
|
);
|
|
}
|
|
|
|
// --- 3. Main Dashboard View ---
|
|
|
|
interface DashboardViewProps {
|
|
initialFiles: any[];
|
|
user?: {
|
|
id?: string;
|
|
role?: string;
|
|
};
|
|
}
|
|
|
|
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";
|
|
const [lastSynced, setLastSynced] = useState<Date | null>(new Date()); // Defaults to 'Just now' on load
|
|
|
|
const getVirtualPath = (parentId: string | null): string => {
|
|
if (!parentId) return "WebCalibre";
|
|
const parent = initialFiles.find((f) => f.id === parentId);
|
|
if (!parent) return "WebCalibre";
|
|
const prefix = parent.parentId ? `${getVirtualPath(parent.parentId)} / ` : "";
|
|
return `${prefix}${parent.name}`;
|
|
};
|
|
|
|
const handleSync = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await syncOneDrive();
|
|
setLastSynced(new Date()); // Update the time
|
|
router.refresh();
|
|
} catch (error) {
|
|
console.error("Sync failed:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = () => {
|
|
setIsRefreshing(true);
|
|
router.refresh();
|
|
setTimeout(() => setIsRefreshing(false), 800);
|
|
};
|
|
|
|
const handleDelete = async (id: string, name: string) => {
|
|
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
|
|
try {
|
|
await deleteFileNodeAction(id);
|
|
router.refresh();
|
|
} catch (error: any) {
|
|
alert(error.message || "Failed to delete file");
|
|
}
|
|
};
|
|
|
|
const columns: GridColDef[] = [
|
|
{
|
|
field: "name",
|
|
headerName: "Name",
|
|
flex: 1.5,
|
|
minWidth: 250,
|
|
renderCell: (params) => (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: '100%' }}>
|
|
{params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />}
|
|
<Typography variant="body2">{params.value}</Typography>
|
|
</Box>
|
|
)
|
|
},
|
|
{
|
|
field: "parentId",
|
|
headerName: "Location",
|
|
flex: 1,
|
|
renderCell: (params) => <Chip label={getVirtualPath(params.value)} size="small" variant="outlined" />
|
|
},
|
|
{ field: "description", headerName: "Description", flex: 1 },
|
|
{
|
|
field: "type",
|
|
headerName: "Type",
|
|
width: 120,
|
|
valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"),
|
|
renderCell: (params) => (
|
|
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold' }}>
|
|
{params.value}
|
|
</Typography>
|
|
)
|
|
},
|
|
{
|
|
field: "size",
|
|
headerName: "Size",
|
|
width: 100,
|
|
renderCell: (params) => params.row.isFolder ? "--" : `${(Number(params.value) / 1024 / 1024).toFixed(2)} MB`
|
|
},
|
|
{
|
|
field: "metadata_search",
|
|
headerName: "Search Metadata",
|
|
width: 0,
|
|
valueGetter: (value, row) => row.metadata ? JSON.stringify(row.metadata) : ""
|
|
},
|
|
{
|
|
field: "actions",
|
|
headerName: "Actions",
|
|
width: 180,
|
|
align: 'right',
|
|
renderCell: (params) => {
|
|
const isOwner = params.row.ownerId === user?.id;
|
|
const isFolder = params.row.isFolder;
|
|
return (
|
|
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
|
|
{!isFolder && (
|
|
<>
|
|
<IconButton size="small" color="info" onClick={() => window.open(`/api/download?id=${params.row.id}&mode=inline`, '_blank')}>
|
|
<OpenInNewIcon fontSize="small" />
|
|
</IconButton>
|
|
<IconButton size="small" color="success" onClick={() => window.location.href = `/api/download?id=${params.row.id}&mode=attachment`}>
|
|
<DownloadIcon fontSize="small" />
|
|
</IconButton>
|
|
</>
|
|
)}
|
|
{(isAdmin || isOwner) && (
|
|
<>
|
|
<IconButton size="small" color="primary" onClick={() => router.push(`/update/${params.row.id}`)}>
|
|
<EditIcon fontSize="small" />
|
|
</IconButton>
|
|
<IconButton size="small" color="error" onClick={() => handleDelete(params.row.id, params.row.name)}>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|
|
}
|
|
];
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: 2 }}>
|
|
{lastSynced && (
|
|
<Typography
|
|
variant="caption"
|
|
color="text.secondary"
|
|
sx={{ fontStyle: 'italic' }}
|
|
suppressHydrationWarning
|
|
>
|
|
Last synced: {lastSynced.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</Typography>
|
|
)}
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
|
|
onClick={handleRefresh}
|
|
>
|
|
Refresh
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <SyncIcon />}
|
|
onClick={handleSync}
|
|
disabled={loading}
|
|
>
|
|
Sync OneDrive
|
|
</Button>
|
|
</Box>
|
|
|
|
<Box sx={{ height: 750, width: "100%", bgcolor: 'background.paper', borderRadius: 3, boxShadow: 1, overflow: 'hidden' }}>
|
|
<DataGrid
|
|
rows={initialFiles}
|
|
columns={columns}
|
|
// --- THE FIX: showToolbar must be true, and toolbar slot must be assigned ---
|
|
showToolbar
|
|
slots={{ toolbar: CustomToolbar }}
|
|
disableRowSelectionOnClick
|
|
initialState={{
|
|
columns: {
|
|
columnVisibilityModel: {
|
|
metadata_search: false,
|
|
},
|
|
},
|
|
}}
|
|
sx={{ border: 'none' }}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
} |