From 31ed528d84efca0de9b0c3c0b3828bef3bdf99c9 Mon Sep 17 00:00:00 2001 From: Potenz Date: Tue, 30 Sep 2025 22:21:30 +0530 Subject: [PATCH] feat(search-bar) = added in cloudpage --- apps/Backend/src/routes/cloud-storage.ts | 9 +- .../src/storage/cloudStorage-storage.ts | 27 +- .../cloud-storage/file-preview-modal.tsx | 74 ++- .../cloud-storage/files-section.tsx | 50 ++ .../components/cloud-storage/folder-panel.tsx | 6 +- .../components/cloud-storage/search-bar.tsx | 426 ++++++++++++++++++ .../Frontend/src/pages/cloud-storage-page.tsx | 68 ++- 7 files changed, 638 insertions(+), 22 deletions(-) create mode 100644 apps/Frontend/src/components/cloud-storage/search-bar.tsx diff --git a/apps/Backend/src/routes/cloud-storage.ts b/apps/Backend/src/routes/cloud-storage.ts index e616dc3..856539b 100644 --- a/apps/Backend/src/routes/cloud-storage.ts +++ b/apps/Backend/src/routes/cloud-storage.ts @@ -513,7 +513,14 @@ router.get( if (!q) return sendError(res, 400, "Missing search query parameter 'q'"); try { - const { data, total } = await storage.searchFolders(q, limit, offset); + const parentId = null; + + const { data, total } = await storage.searchFolders( + q, + limit, + offset, + parentId + ); return res.json({ error: false, data, totalCount: total }); } catch (err) { return sendError(res, 500, "Folder search failed"); diff --git a/apps/Backend/src/storage/cloudStorage-storage.ts b/apps/Backend/src/storage/cloudStorage-storage.ts index 914db83..53061d2 100644 --- a/apps/Backend/src/storage/cloudStorage-storage.ts +++ b/apps/Backend/src/storage/cloudStorage-storage.ts @@ -95,7 +95,8 @@ export interface IStorage { searchFolders( q: string, limit: number, - offset: number + offset: number, + parentId?: number | null ): Promise<{ data: CloudFolder[]; total: number }>; searchFiles( q: string, @@ -400,16 +401,34 @@ export const cloudStorageStorage: IStorage = { }, // --- SEARCH --- - async searchFolders(q: string, limit = 20, offset = 0) { + async searchFolders( + q: string, + limit = 20, + offset = 0, + parentId?: number | null + ) { + // Build where clause + const where: any = { + name: { contains: q, mode: "insensitive" }, + }; + + // If parentId is explicitly provided: + // - parentId === null -> top-level folders (parent IS NULL) + // - parentId === number -> children of that folder + // If parentId is undefined -> search across all folders (no parent filter) + if (parentId !== undefined) { + where.parentId = parentId; + } + const [folders, total] = await Promise.all([ db.cloudFolder.findMany({ - where: { name: { contains: q, mode: "insensitive" } }, + where, orderBy: { name: "asc" }, skip: offset, take: limit, }), db.cloudFolder.count({ - where: { name: { contains: q, mode: "insensitive" } }, + where, }), ]); return { data: folders as unknown as CloudFolder[], total }; diff --git a/apps/Frontend/src/components/cloud-storage/file-preview-modal.tsx b/apps/Frontend/src/components/cloud-storage/file-preview-modal.tsx index 797abca..b180d37 100644 --- a/apps/Frontend/src/components/cloud-storage/file-preview-modal.tsx +++ b/apps/Frontend/src/components/cloud-storage/file-preview-modal.tsx @@ -1,21 +1,32 @@ import React, { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; -import { apiRequest } from "@/lib/queryClient"; +import { apiRequest, queryClient } from "@/lib/queryClient"; import { toast } from "@/hooks/use-toast"; -import { Download, Maximize2, Minimize2, X } from "lucide-react"; +import { Download, Maximize2, Minimize2, Trash2, X } from "lucide-react"; +import { DeleteConfirmationDialog } from "../ui/deleteDialog"; +import { QueryClient } from "@tanstack/react-query"; +import { cloudFilesQueryKeyRoot } from "./files-section"; type Props = { fileId: number | null; isOpen: boolean; onClose: () => void; + onDeleted?: () => void; }; -export default function FilePreviewModal({ fileId, isOpen, onClose }: Props) { +export default function FilePreviewModal({ + fileId, + isOpen, + onClose, + onDeleted, +}: Props) { const [loading, setLoading] = useState(false); const [meta, setMeta] = useState(null); const [blobUrl, setBlobUrl] = useState(null); const [error, setError] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); + const [deleting, setDeleting] = useState(false); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); useEffect(() => { if (!isOpen || !fileId) return; @@ -107,6 +118,49 @@ export default function FilePreviewModal({ fileId, isOpen, onClose }: Props) { } } + async function confirmDelete() { + if (!fileId) return; + + setIsDeleteOpen(false); + setDeleting(true); + + try { + const res = await apiRequest( + "DELETE", + `/api/cloud-storage/files/${fileId}` + ); + const json = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(json?.message || `Delete failed (${res.status})`); + } + + toast({ + title: "Deleted", + description: `File "${meta?.name ?? `file-${fileId}`}" deleted.`, + }); + + // notify parent to refresh lists if they provided callback + if (typeof onDeleted === "function") { + try { + onDeleted(); + } catch (e) { + // ignore parent errors + } + } + + // close modal + onClose(); + } catch (err: any) { + toast({ + title: "Delete failed", + description: err?.message ?? String(err), + variant: "destructive", + }); + } finally { + setDeleting(false); + } + } + // container sizing classes const containerBase = "bg-white rounded-md p-3 flex flex-col overflow-hidden shadow-xl"; @@ -145,6 +199,13 @@ export default function FilePreviewModal({ fileId, isOpen, onClose }: Props) { + + + +
+ + + + + +
+ + +
+
+

+ Recent searches +

+
+ {recentTerms.length ? ( + recentTerms.map((t) => ( + setQ(t)} + > + {t} + + )) + ) : ( +
+ No recent searches +
+ )} +
+
+ +
+

Results

+ +
+ + {loading && ( + + Searching... + + )} + + {!loading && errMsg && ( + + {errMsg} + + )} + + {!loading && !results.length && debouncedQ && !errMsg && ( + + No results for "{debouncedQ}" + + )} + + {!loading && + results.map((r) => ( + { + if (r.kind === "folder") onOpenFolder(r.id); + else onSelectFile(r.id); + }} + > +
+ {r.kind === "folder" ? ( + + ) : ( + + )} +
+
+
{r.name}
+
+ {r.kind === "file" ? (r.mimeType ?? "file") : "Folder"} +
+
+ {r.kind === "file" && r.fileSize != null && ( +
+ {String(r.fileSize)} +
+ )} +
+ ))} +
+
+ +
+
+ {total} result(s) +
+
+ +
+ {page} / {totalPages} +
+ +
+
+
+
+ + ); +} diff --git a/apps/Frontend/src/pages/cloud-storage-page.tsx b/apps/Frontend/src/pages/cloud-storage-page.tsx index aae97ab..5dde447 100644 --- a/apps/Frontend/src/pages/cloud-storage-page.tsx +++ b/apps/Frontend/src/pages/cloud-storage-page.tsx @@ -10,6 +10,11 @@ import { useAuth } from "@/hooks/use-auth"; import RecentTopLevelFoldersCard, { recentTopLevelFoldersQueryKey, } from "@/components/cloud-storage/recent-top-level-folder-modal"; +import CloudSearchBar, { + cloudSearchQueryKeyRoot, +} from "@/components/cloud-storage/search-bar"; +import FilePreviewModal from "@/components/cloud-storage/file-preview-modal"; +import { cloudFilesQueryKeyRoot } from "@/components/cloud-storage/files-section"; export default function CloudStoragePage() { const { toast } = useToast(); @@ -28,6 +33,21 @@ export default function CloudStoragePage() { // New folder modal const [isNewFolderOpen, setIsNewFolderOpen] = useState(false); + // searchbar - file preview modal state + const [previewFileId, setPreviewFileId] = useState(null); + const [isPreviewOpen, setIsPreviewOpen] = useState(false); + + // searchbar - handlers + function handleOpenFolder(folderId: number | null) { + setPanelInitialFolderId(folderId); + setPanelOpen(true); + } + + function handleSelectFile(fileId: number) { + setPreviewFileId(fileId); + setIsPreviewOpen(true); + } + // create folder handler (page-level) async function handleCreateFolder(name: string) { try { @@ -72,21 +92,21 @@ export default function CloudStoragePage() {
- -
+ { + handleOpenFolder(folderId); + }} + onSelectFile={(fileId) => { + handleSelectFile(fileId); + }} + /> + {/* Recent folders card (delegated component) */} )} + {/* File preview modal */} + { + setIsPreviewOpen(false); + setPreviewFileId(null); + }} + onDeleted={() => { + // close preview (modal may already close, but be explicit) + setIsPreviewOpen(false); + setPreviewFileId(null); + + // refresh the recent folders card + qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); + + // invalidate file lists and search results + qc.invalidateQueries({ + queryKey: cloudFilesQueryKeyRoot, + exact: false, + }); + qc.invalidateQueries({ + queryKey: cloudSearchQueryKeyRoot, + exact: false, + }); + + // remount the recent card so it clears internal selection (UX nicety) + setRecentKey((k) => k + 1); + }} + /> {/* New folder modal (reusable) */}