Quick Search working again

This commit is contained in:
stephen 2026-01-17 16:12:37 +11:00
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.

View file

@ -25,6 +25,17 @@
- [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)
- [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
@ -369,4 +380,62 @@ 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
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`.

Binary file not shown.

View file

@ -1,6 +1,7 @@
'use client';
import { useState } from "react";
import { styled } from '@mui/material/styles';
import {
Button,
CircularProgress,
@ -16,10 +17,10 @@ import {
import {
DataGrid,
GridColDef,
GridToolbarContainer,
// Using the new non-deprecated QuickFilter components
Toolbar,
QuickFilter,
QuickFilterControl,
QuickFilterClear,
} from "@mui/x-data-grid";
import SyncIcon from "@mui/icons-material/Sync";
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 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';
@ -35,43 +37,64 @@ import { syncOneDrive } from "./sync-actions";
import { deleteFileNodeAction } from "./actions";
import { useRouter } from "next/navigation";
/**
* UPDATED TOOLBAR: Uses the new QuickFilter structure to avoid deprecation
*/
// --- 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 (
<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">
Library
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<QuickFilter>
<QuickFilterControl
render={(props) => (
<TextField
{...props}
variant="outlined"
size="small"
placeholder="Search files..."
sx={{ width: 350 }}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
},
}}
/>
)}
/>
</QuickFilter>
</Box>
</GridToolbarContainer>
{/* 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>
</Toolbar>
);
}
// --- 3. Main Dashboard View ---
interface DashboardViewProps {
initialFiles: any[];
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[] = [
{
field: "name",
@ -156,7 +171,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
width: 120,
valueGetter: (value, row) => row.metadata?.type || (row.isFolder ? "Folder" : "File"),
renderCell: (params) => (
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold', color: 'text.secondary' }}>
<Typography variant="caption" sx={{ textTransform: 'uppercase', fontWeight: 'bold' }}>
{params.value}
</Typography>
)
@ -167,6 +182,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
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",
@ -175,44 +196,26 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
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 && (
<>
<Tooltip title="View in Tab">
<IconButton size="small" color="info" onClick={() => handleViewInTab(params.row.id)}>
<OpenInNewIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Download to Folder">
<IconButton size="small" color="success" onClick={() => handleDownload(params.row.id)}>
<DownloadIcon fontSize="small" />
</IconButton>
</Tooltip>
<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) && (
<>
<Tooltip title="Edit Details">
<IconButton
size="small"
color="primary"
onClick={() => router.push(`/update/${params.row.id}`)}
>
<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>
<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>
@ -229,7 +232,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
startIcon={isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon />}
onClick={handleRefresh}
>
Refresh List
Refresh
</Button>
<Button
variant="contained"
@ -241,17 +244,22 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
</Button>
</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
rows={initialFiles}
columns={columns}
// --- THE FIX: showToolbar must be true, and toolbar slot must be assigned ---
showToolbar
slots={{ toolbar: CustomToolbar }}
disableRowSelectionOnClick
sx={{
border: 'none',
'& .MuiDataGrid-columnHeader': { bgcolor: '#f8f9fa' },
'& .MuiDataGrid-footerContainer': { borderTop: '1px solid #eee' }
initialState={{
columns: {
columnVisibilityModel: {
metadata_search: false,
},
},
}}
sx={{ border: 'none' }}
/>
</Box>
</Box>