Quick Search working again
This commit is contained in:
parent
e15373c169
commit
c1a9bb6465
5 changed files with 6736 additions and 77 deletions
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -25,6 +25,17 @@
|
||||||
- [7.1. Pro-Tip: Update your package.json](#71-pro-tip-update-your-packagejson)
|
- [7.1. Pro-Tip: Update your package.json](#71-pro-tip-update-your-packagejson)
|
||||||
- [8. Where we are up to 8/1/2026](#8-where-we-are-up-to-812026)
|
- [8. Where we are up to 8/1/2026](#8-where-we-are-up-to-812026)
|
||||||
- [9. Testing Creating And Uploading Folders and Files](#9-testing-creating-and-uploading-folders-and-files)
|
- [9. Testing Creating And Uploading Folders and Files](#9-testing-creating-and-uploading-folders-and-files)
|
||||||
|
- [10. 📚 WebCalibre Development Notes 17/1/2026](#10--webcalibre-development-notes-1712026)
|
||||||
|
- [10.1. 🚀 Project Overview](#101--project-overview)
|
||||||
|
- [10.2. 🛠 Critical Technical Learnings](#102--critical-technical-learnings)
|
||||||
|
- [10.2.1. MUI X v8 DataGrid Toolbar Fix](#1021-mui-x-v8-datagrid-toolbar-fix)
|
||||||
|
- [10.2.2. Global "Hidden" Search](#1022-global-hidden-search)
|
||||||
|
- [10.2.3. Server-Side Extraction](#1023-server-side-extraction)
|
||||||
|
- [10.3. 📑 Essential Commands](#103--essential-commands)
|
||||||
|
- [10.4. 📅 Future Roadmap](#104--future-roadmap)
|
||||||
|
- [10.4.1. Phase 1: Metadata Utility (In Progress)](#1041-phase-1-metadata-utility-in-progress)
|
||||||
|
- [10.4.2. Phase 2: UI Enhancements](#1042-phase-2-ui-enhancements)
|
||||||
|
- [10.5. 💡 Reminders](#105--reminders)
|
||||||
|
|
||||||
# 1. Reference
|
# 1. Reference
|
||||||
|
|
||||||
|
|
@ -370,3 +381,61 @@ For download there 2 function we need to implement
|
||||||
2) if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf
|
2) if the file is a pdf open the file in another tab in the browser as most browser support reading a pdf
|
||||||
|
|
||||||
So we need to implement 2 new actions, downLoad file and open pdf to read. Does this make sense ?
|
So we need to implement 2 new actions, downLoad file and open pdf to read. Does this make sense ?
|
||||||
|
|
||||||
|
# 10. 📚 WebCalibre Development Notes 17/1/2026
|
||||||
|
|
||||||
|
## 10.1. 🚀 Project Overview
|
||||||
|
A Next.js (App Router) dashboard for managing a OneDrive-synced file library.
|
||||||
|
Uses **Prisma + PostgreSQL** for metadata storage and **MUI X v8 (DataGrid)** for the frontend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10.2. 🛠 Critical Technical Learnings
|
||||||
|
|
||||||
|
### 10.2.1. MUI X v8 DataGrid Toolbar Fix
|
||||||
|
* **Problem:** The Search Bar and "Library" title often disappear even when defined in `slots`.
|
||||||
|
* **Solution:** - You MUST include the `showToolbar` prop on the `<DataGrid />`.
|
||||||
|
- Use the **Persistent Quick Filter** model: set the `expanded` prop on the `<QuickFilter />` component within the custom toolbar to ensure it doesn't collapse.
|
||||||
|
- Use `slotProps.textField` (not `input`) to customize the search input in MUI v7/v8.
|
||||||
|
|
||||||
|
### 10.2.2. Global "Hidden" Search
|
||||||
|
* To allow the search bar to find "Author", "ISBN", or "Dimensions" without cluttering the UI:
|
||||||
|
- Create a column (e.g., `metadata_search`) with `width: 0`.
|
||||||
|
- Use a `valueGetter` to return `JSON.stringify(row.metadata)`.
|
||||||
|
- Hide the column in `initialState.columns.columnVisibilityModel`.
|
||||||
|
- The DataGrid filtering engine will now index this hidden string.
|
||||||
|
|
||||||
|
### 10.2.3. Server-Side Extraction
|
||||||
|
* Metadata extraction from file binaries (PDF, EPUB, Images) **must** occur on the server.
|
||||||
|
* Libraries like `sharp` and `pdf-parse` are Node.js-based and will crash if imported into client components.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10.3. 📑 Essential Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `npx tsc --noEmit` | **Run this frequently.** Validates types across the entire project. Finds errors your IDE might miss. |
|
||||||
|
| `npx prisma generate` | Updates the Prisma Client types after schema changes. |
|
||||||
|
| `npx prisma db push` | Pushes schema changes to the PostgreSQL database. |
|
||||||
|
| `npm run dev` | Starts the development server. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10.4. 📅 Future Roadmap
|
||||||
|
|
||||||
|
### 10.4.1. Phase 1: Metadata Utility (In Progress)
|
||||||
|
- [ ] Implement `src/lib/metadata-extractor.ts`.
|
||||||
|
- [ ] Integrate extraction into the `syncOneDrive` Server Action.
|
||||||
|
- [ ] Supported formats: PDF (`pdf-parse`), EPUB (`node-epub-utils`), Images (`sharp`).
|
||||||
|
|
||||||
|
### 10.4.2. Phase 2: UI Enhancements
|
||||||
|
- [ ] Add "Last Synced" timestamp state to the dashboard header.
|
||||||
|
- [ ] Add dedicated sortable columns for "Author" and "File Type".
|
||||||
|
- [ ] Implement folder-specific row styling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10.5. 💡 Reminders
|
||||||
|
* **Hard Refresh:** If the DataGrid UI behaves weirdly after code changes, use `Cmd + Shift + R` or `Ctrl + F5`.
|
||||||
|
* **Z-Index:** The search bar animation uses `gridArea: '1 / 1'`. If icons overlap, check the styled-components logic in `dashboard-view.tsx`.
|
||||||
BIN
docs/notes.pdf
BIN
docs/notes.pdf
Binary file not shown.
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
|
@ -16,10 +17,10 @@ import {
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
DataGrid,
|
||||||
GridColDef,
|
GridColDef,
|
||||||
GridToolbarContainer,
|
Toolbar,
|
||||||
// Using the new non-deprecated QuickFilter components
|
|
||||||
QuickFilter,
|
QuickFilter,
|
||||||
QuickFilterControl,
|
QuickFilterControl,
|
||||||
|
QuickFilterClear,
|
||||||
} from "@mui/x-data-grid";
|
} from "@mui/x-data-grid";
|
||||||
import SyncIcon from "@mui/icons-material/Sync";
|
import SyncIcon from "@mui/icons-material/Sync";
|
||||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
|
@ -28,6 +29,7 @@ import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||||
|
|
||||||
|
|
@ -35,43 +37,64 @@ import { syncOneDrive } from "./sync-actions";
|
||||||
import { deleteFileNodeAction } from "./actions";
|
import { deleteFileNodeAction } from "./actions";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
/**
|
// --- 1. Styled Component for Search Placement ---
|
||||||
* UPDATED TOOLBAR: Uses the new QuickFilter structure to avoid deprecation
|
|
||||||
*/
|
const StyledQuickFilter = styled(QuickFilter)({
|
||||||
|
marginLeft: 'auto', // Pushes the search box to the right side of the toolbar
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 2. Custom Toolbar Component ---
|
||||||
|
|
||||||
function CustomToolbar() {
|
function CustomToolbar() {
|
||||||
return (
|
return (
|
||||||
<GridToolbarContainer sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<Toolbar sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
||||||
<Typography variant="h6" fontWeight="bold" color="primary">
|
<Typography variant="h6" fontWeight="bold" color="primary">
|
||||||
Library
|
Library
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<QuickFilter>
|
{/* The 'expanded' prop ensures the search input is always visible by default */}
|
||||||
<QuickFilterControl
|
<StyledQuickFilter expanded>
|
||||||
render={(props) => (
|
<QuickFilterControl
|
||||||
<TextField
|
render={({ ref, ...other }) => (
|
||||||
{...props}
|
<TextField
|
||||||
variant="outlined"
|
{...other}
|
||||||
size="small"
|
sx={{ width: 300 }}
|
||||||
placeholder="Search files..."
|
inputRef={ref}
|
||||||
sx={{ width: 350 }}
|
placeholder="Search library..."
|
||||||
slotProps={{
|
size="small"
|
||||||
input: {
|
slotProps={{
|
||||||
startAdornment: (
|
input: {
|
||||||
<InputAdornment position="start">
|
startAdornment: (
|
||||||
<SearchIcon fontSize="small" />
|
<InputAdornment position="start">
|
||||||
</InputAdornment>
|
<SearchIcon fontSize="small" />
|
||||||
),
|
</InputAdornment>
|
||||||
},
|
),
|
||||||
}}
|
endAdornment: other.value ? (
|
||||||
/>
|
<InputAdornment position="end">
|
||||||
)}
|
<QuickFilterClear
|
||||||
/>
|
edge="end"
|
||||||
</QuickFilter>
|
size="small"
|
||||||
</Box>
|
material={{ sx: { marginRight: -0.75 } }}
|
||||||
</GridToolbarContainer>
|
>
|
||||||
|
<CancelIcon fontSize="small" />
|
||||||
|
</QuickFilterClear>
|
||||||
|
</InputAdornment>
|
||||||
|
) : null,
|
||||||
|
// Ensure other props are spread correctly
|
||||||
|
...other.slotProps?.input,
|
||||||
|
},
|
||||||
|
...other.slotProps,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledQuickFilter>
|
||||||
|
</Toolbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 3. Main Dashboard View ---
|
||||||
|
|
||||||
interface DashboardViewProps {
|
interface DashboardViewProps {
|
||||||
initialFiles: any[];
|
initialFiles: any[];
|
||||||
user?: {
|
user?: {
|
||||||
|
|
@ -122,14 +145,6 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = (id: string) => {
|
|
||||||
window.location.href = `/api/download?id=${id}&mode=attachment`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewInTab = (id: string) => {
|
|
||||||
window.open(`/api/download?id=${id}&mode=inline`, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: "name",
|
field: "name",
|
||||||
|
|
@ -156,7 +171,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
width: 120,
|
width: 120,
|
||||||
valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"),
|
valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"),
|
||||||
renderCell: (params) => (
|
renderCell: (params) => (
|
||||||
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold', color: 'text.secondary' }}>
|
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold' }}>
|
||||||
{params.value}
|
{params.value}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
|
|
@ -167,6 +182,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
width: 100,
|
width: 100,
|
||||||
renderCell: (params) => params.row.isFolder ? "--" : `${(Number(params.value) / 1024 / 1024).toFixed(2)} MB`
|
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",
|
field: "actions",
|
||||||
headerName: "Actions",
|
headerName: "Actions",
|
||||||
|
|
@ -175,44 +196,26 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
renderCell: (params) => {
|
renderCell: (params) => {
|
||||||
const isOwner = params.row.ownerId === user?.id;
|
const isOwner = params.row.ownerId === user?.id;
|
||||||
const isFolder = params.row.isFolder;
|
const isFolder = params.row.isFolder;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
|
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
|
||||||
{!isFolder && (
|
{!isFolder && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="View in Tab">
|
<IconButton size="small" color="info" onClick={() => window.open(`/api/download?id=${params.row.id}&mode=inline`, '_blank')}>
|
||||||
<IconButton size="small" color="info" onClick={() => handleViewInTab(params.row.id)}>
|
<OpenInNewIcon fontSize="small" />
|
||||||
<OpenInNewIcon fontSize="small" />
|
</IconButton>
|
||||||
</IconButton>
|
<IconButton size="small" color="success" onClick={() => window.location.href = `/api/download?id=${params.row.id}&mode=attachment`}>
|
||||||
</Tooltip>
|
<DownloadIcon fontSize="small" />
|
||||||
<Tooltip title="Download to Folder">
|
</IconButton>
|
||||||
<IconButton size="small" color="success" onClick={() => handleDownload(params.row.id)}>
|
|
||||||
<DownloadIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isAdmin || isOwner) && (
|
{(isAdmin || isOwner) && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title="Edit Details">
|
<IconButton size="small" color="primary" onClick={() => router.push(`/update/${params.row.id}`)}>
|
||||||
<IconButton
|
<EditIcon fontSize="small" />
|
||||||
size="small"
|
</IconButton>
|
||||||
color="primary"
|
<IconButton size="small" color="error" onClick={() => handleDelete(params.row.id, params.row.name)}>
|
||||||
onClick={() => router.push(`/update/${params.row.id}`)}
|
<DeleteIcon fontSize="small" />
|
||||||
>
|
</IconButton>
|
||||||
<EditIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete">
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
onClick={() => handleDelete(params.row.id, params.row.name)}
|
|
||||||
>
|
|
||||||
<DeleteIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
@ -229,7 +232,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
|
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
>
|
>
|
||||||
Refresh List
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
|
@ -241,17 +244,22 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ height: 700, width: "100%", bgcolor: 'background.paper', borderRadius: 3, boxShadow: 1, overflow: 'hidden' }}>
|
<Box sx={{ height: 750, width: "100%", bgcolor: 'background.paper', borderRadius: 3, boxShadow: 1, overflow: 'hidden' }}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={initialFiles}
|
rows={initialFiles}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
// --- THE FIX: showToolbar must be true, and toolbar slot must be assigned ---
|
||||||
|
showToolbar
|
||||||
slots={{ toolbar: CustomToolbar }}
|
slots={{ toolbar: CustomToolbar }}
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
sx={{
|
initialState={{
|
||||||
border: 'none',
|
columns: {
|
||||||
'& .MuiDataGrid-columnHeader': { bgcolor: '#f8f9fa' },
|
columnVisibilityModel: {
|
||||||
'& .MuiDataGrid-footerContainer': { borderTop: '1px solid #eee' }
|
metadata_search: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
sx={{ border: 'none' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue