Compare commits

...
Author SHA1 Message Date
Cursor AgentandMarc Seitz f38e5aad8e feat: include folders in all documents search results with path breadcrumbs
- API: search folders by name when query is present, build parent path
  breadcrumbs (folderList), exclude hidden folders and children of hidden
  folders, return matching folders alongside documents
- SWR hook: add FolderWithCountAndPath type, expose searchFolders from
  useDocuments hook
- Documents page: display search-matched folders when filtered instead
  of empty array, pass correct loading state for filtered folders
- Folder card: render path breadcrumb (Home > parent > folder) when
  search query is active, matching the existing document card pattern
- Pagination: add optional extraInfo prop showing folder match count

Co-authored-by: Marc Seitz <mfts@users.noreply.github.com>
2026-03-04 13:13:13 +00:00
6 changed files with 127 additions and 9 deletions
+2 -2
View File
@@ -31,7 +31,7 @@ import { mutate } from "swr";
import { moveDocumentToFolder } from "@/lib/documents/move-documents";
import { moveFolderToFolder } from "@/lib/documents/move-folder";
import { DataroomFolderWithCount } from "@/lib/swr/use-dataroom";
import { FolderWithCount } from "@/lib/swr/use-documents";
import { FolderWithCount, FolderWithCountAndPath } from "@/lib/swr/use-documents";
import { DocumentWithLinksAndLinkCountAndViewCount } from "@/lib/types";
import { useMediaQuery } from "@/lib/utils/use-media-query";
@@ -75,7 +75,7 @@ export function DocumentsList({
loading,
foldersLoading,
}: {
folders: FolderWithCount[] | undefined;
folders: FolderWithCount[] | FolderWithCountAndPath[] | undefined;
documents: DocumentWithLinksAndLinkCountAndViewCount[] | undefined;
teamInfo: TeamContextType | null;
folderPathName?: string[];
+41 -3
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
@@ -5,9 +6,11 @@ import { useEffect, useRef, useState } from "react";
import { TeamContextType } from "@/context/team-context";
import {
BetweenHorizontalStartIcon,
ChevronRight,
ClipboardCopyIcon,
CopyIcon,
EyeOffIcon,
FolderIcon,
FolderInputIcon,
FolderPenIcon,
MoreVertical,
@@ -19,8 +22,8 @@ import { mutate } from "swr";
import { getFolderColorClasses, getFolderIcon } from "@/lib/constants/folder-constants";
import { DataroomFolderWithCount } from "@/lib/swr/use-dataroom";
import { FolderWithCount } from "@/lib/swr/use-documents";
import { timeAgo } from "@/lib/utils";
import { FolderWithCount, FolderWithCountAndPath } from "@/lib/swr/use-documents";
import { getBreadcrumbPath, timeAgo } from "@/lib/utils";
import {
HIERARCHICAL_DISPLAY_STYLE,
useHierarchicalDisplayName,
@@ -42,7 +45,7 @@ import { AddFolderToDataroomModal } from "./add-folder-to-dataroom-modal";
import { MoveToFolderModal } from "./move-folder-modal";
type FolderCardProps = {
folder: FolderWithCount | DataroomFolderWithCount;
folder: FolderWithCount | FolderWithCountAndPath | DataroomFolderWithCount;
teamInfo: TeamContextType | null;
isDataroom?: boolean;
dataroomId?: string;
@@ -65,6 +68,10 @@ export default function FolderCard({
onDelete,
}: FolderCardProps) {
const router = useRouter();
const queryParams = router.query;
const searchQuery = queryParams["search"];
const sortQuery = queryParams["sort"];
const folderList = "folderList" in folder ? folder.folderList : undefined;
const [moveFolderOpen, setMoveFolderOpen] = useState<boolean>(false);
const [openFolder, setOpenFolder] = useState<boolean>(false);
const [menuOpen, setMenuOpen] = useState<boolean>(false);
@@ -237,6 +244,37 @@ export default function FolderCard({
{folder._count.childFolders === 1 ? "Folder" : "Folders"}
</p>
</div>
{searchQuery && folderList !== undefined ? (
<div className="relative z-10 mt-1 flex flex-wrap items-center space-x-1 text-xs leading-5 text-muted-foreground">
{getBreadcrumbPath(folderList).map((segment, index) => (
<p
className="inset-2 flex items-center gap-x-1 truncate"
key={segment.pathLink}
>
{index !== 0 && <ChevronRight className="h-3 w-3" />}
<FolderIcon className="h-3 w-3" />
<Link
href={segment.pathLink}
onClick={(e) => e.stopPropagation()}
className="relative z-10 hover:underline"
>
{segment.name}
</Link>
</p>
))}
<p className="inset-2 flex items-center gap-x-1 truncate">
<ChevronRight className="h-3 w-3" />
<FolderIcon className="h-3 w-3" />
<Link
href={folderPath}
onClick={(e) => e.stopPropagation()}
className="relative z-10 hover:underline"
>
{folder.name}
</Link>
</p>
</div>
) : null}
</div>
</div>
+3
View File
@@ -24,6 +24,7 @@ interface PaginationProps {
onPageSizeChange: (size: number) => void;
totalShownItems: number;
itemName: string;
extraInfo?: string;
}
export function Pagination({
@@ -35,6 +36,7 @@ export function Pagination({
onPageSizeChange,
totalShownItems,
itemName,
extraInfo,
}: PaginationProps) {
return (
<Card className="mt-4 p-4">
@@ -42,6 +44,7 @@ export function Pagination({
<div className="flex-1 text-sm text-muted-foreground">
Showing <span className="font-bold">{totalShownItems}</span> of{" "}
{totalItems} {itemName}
{extraInfo && <span className="ml-2">· {extraInfo}</span>}
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="hidden items-center space-x-2 sm:flex">
+6
View File
@@ -29,6 +29,7 @@ export default function useDocuments() {
const { data, isValidating, error } = useSWR<{
documents: DocumentWithLinksAndLinkCountAndViewCount[];
folders?: FolderWithCountAndPath[];
pagination?: {
total: number;
pages: number;
@@ -47,6 +48,7 @@ export default function useDocuments() {
return {
documents: data?.documents || [],
searchFolders: data?.folders,
pagination: data?.pagination,
isValidating,
loading: !data && !error,
@@ -85,6 +87,10 @@ export type FolderWithCount = Folder & {
};
};
export type FolderWithCountAndPath = FolderWithCount & {
folderList: string[];
};
export function useFolder({ name }: { name: string[] }) {
const teamInfo = useTeam();
const router = useRouter();
@@ -248,8 +248,68 @@ export default async function handle(
);
}
let matchingFolders: any[] = [];
if (query) {
const folders = await prisma.folder.findMany({
where: {
teamId,
hiddenInAllDocuments: false,
name: {
contains: query,
mode: "insensitive",
},
OR: [
{ parentId: null },
{
parentFolder: {
hiddenInAllDocuments: false,
},
},
],
},
include: {
_count: {
select: {
documents: true,
childFolders: true,
},
},
},
orderBy: { name: "asc" },
});
matchingFolders = await Promise.all(
folders.map(async (folder) => {
const folderNames: string[] = [];
const parentPath = folder.path.substring(
0,
folder.path.lastIndexOf("/"),
);
if (parentPath) {
const pathSegments = parentPath.split("/").filter(Boolean);
if (pathSegments.length > 0) {
const parentPaths = pathSegments.map((_, index) =>
"/" + pathSegments.slice(0, index + 1).join("/"),
);
const parentFolders = await prisma.folder.findMany({
where: {
teamId,
path: { in: parentPaths },
},
select: { path: true, name: true },
orderBy: { path: "asc" },
});
folderNames.push(...parentFolders.map((f) => f.name));
}
}
return { ...folder, folderList: folderNames };
}),
);
}
return res.status(200).json({
documents: documentsWithFolderList,
...(query && { folders: matchingFolders }),
...(usePagination && {
pagination: {
total: totalDocuments,
+15 -4
View File
@@ -32,8 +32,14 @@ export default function Documents() {
}
const { folders, loading: foldersLoading } = useRootFolders();
const { documents, pagination, isValidating, isFiltered, loading } =
useDocuments();
const {
documents,
searchFolders,
pagination,
isValidating,
isFiltered,
loading,
} = useDocuments();
const { folders: hiddenFolders, documents: hiddenDocuments } =
useHiddenDocuments();
@@ -55,7 +61,7 @@ export default function Documents() {
});
};
const displayFolders = isFiltered ? [] : folders;
const displayFolders = isFiltered ? searchFolders ?? [] : folders;
return (
<AppLayout>
@@ -121,7 +127,7 @@ export default function Documents() {
folders={displayFolders}
teamInfo={teamInfo}
loading={loading}
foldersLoading={foldersLoading}
foldersLoading={isFiltered ? loading : foldersLoading}
/>
{isFiltered && pagination && (
@@ -134,6 +140,11 @@ export default function Documents() {
onPageChange={updatePagination}
onPageSizeChange={(size) => updatePagination(undefined, size)}
itemName="documents"
extraInfo={
searchFolders && searchFolders.length > 0
? `${searchFolders.length} folder${searchFolders.length > 1 ? "s" : ""} found`
: undefined
}
/>
)}
</div>