mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: implement document preview functionality in DocumentsSidebar and enhance mobile experience with vaul drawer
This commit is contained in:
parent
37e018e94f
commit
838d38a67a
6 changed files with 101 additions and 38 deletions
|
|
@ -38,7 +38,6 @@ import {
|
|||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
|
|
@ -234,6 +233,7 @@ export function DocumentsTableShell({
|
|||
mentionedDocIds,
|
||||
onToggleChatMention,
|
||||
isSearchMode = false,
|
||||
onOpenInTab,
|
||||
}: {
|
||||
documents: Document[];
|
||||
loading: boolean;
|
||||
|
|
@ -253,6 +253,8 @@ export function DocumentsTableShell({
|
|||
onToggleChatMention?: (doc: Document, mentioned: boolean) => void;
|
||||
/** Whether results are filtered by a search query or type filters */
|
||||
isSearchMode?: boolean;
|
||||
/** When provided, desktop "Preview" opens a document tab instead of the popup dialog */
|
||||
onOpenInTab?: (doc: Document) => void;
|
||||
}) {
|
||||
const t = useTranslations("documents");
|
||||
const { openDialog } = useDocumentUploadDialog();
|
||||
|
|
@ -742,9 +744,9 @@ export function DocumentsTableShell({
|
|||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={() => handleViewDocument(doc)}>
|
||||
<DropdownMenuItem onClick={() => onOpenInTab ? onOpenInTab(doc) : handleViewDocument(doc)}>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
Open
|
||||
</DropdownMenuItem>
|
||||
{isEditable && (
|
||||
<DropdownMenuItem
|
||||
|
|
@ -923,26 +925,18 @@ export function DocumentsTableShell({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Document Content Viewer */}
|
||||
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DialogContent className="max-w-4xl max-w-[92%] md:max-w-4xl max-h-[75vh] md:max-h-[80vh] flex flex-col overflow-hidden pb-0 p-3 md:p-6 gap-2 md:gap-4">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="text-sm md:text-lg leading-tight pr-6">
|
||||
{/* Document Content Viewer (mobile drawer) */}
|
||||
<Drawer open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DrawerContent className="max-h-[85vh] flex flex-col">
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="text-left shrink-0">
|
||||
<DrawerTitle className="text-base leading-tight break-words">
|
||||
{viewingDoc?.title}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div
|
||||
onScroll={handlePreviewScroll}
|
||||
className={[
|
||||
"overflow-y-auto flex-1 min-h-0 px-1 md:px-6 select-text",
|
||||
"max-md:text-xs",
|
||||
"max-md:[&_h1]:text-base! max-md:[&_h1]:mt-3!",
|
||||
"max-md:[&_h2]:text-sm! max-md:[&_h2]:mt-2!",
|
||||
"max-md:[&_h3]:text-xs! max-md:[&_h3]:mt-2!",
|
||||
"max-md:[&_h4]:text-xs!",
|
||||
"max-md:[&_td]:text-[11px]! max-md:[&_td]:px-2! max-md:[&_td]:py-1.5!",
|
||||
"max-md:[&_th]:text-[11px]! max-md:[&_th]:px-2! max-md:[&_th]:py-1.5!",
|
||||
].join(" ")}
|
||||
className="overflow-y-auto flex-1 min-h-0 px-4 pb-6 select-text text-xs [&_h1]:text-base! [&_h1]:mt-3! [&_h2]:text-sm! [&_h2]:mt-2! [&_h3]:text-xs! [&_h3]:mt-2! [&_h4]:text-xs! [&_td]:text-[11px]! [&_td]:px-2! [&_td]:py-1.5! [&_th]:text-[11px]! [&_th]:px-2! [&_th]:py-1.5!"
|
||||
style={{
|
||||
maskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
|
|
@ -956,8 +950,8 @@ export function DocumentsTableShell({
|
|||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
{/* Document Metadata Viewer (Ctrl+Click) */}
|
||||
<JsonMetadataViewer
|
||||
|
|
@ -1027,7 +1021,7 @@ export function DocumentsTableShell({
|
|||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
Preview
|
||||
Open
|
||||
</Button>
|
||||
{mobileActionDoc &&
|
||||
EDITABLE_DOCUMENT_TYPES.includes(
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuContent align="end" className="w-40" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuItem onClick={() => onPreview(doc)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Open
|
||||
|
|
@ -243,7 +243,7 @@ export const DocumentNode = React.memo(function DocumentNode({
|
|||
</ContextMenuTrigger>
|
||||
|
||||
{contextMenuOpen && (
|
||||
<ContextMenuContent className="w-40">
|
||||
<ContextMenuContent className="w-40" onClick={(e) => e.stopPropagation()}>
|
||||
<ContextMenuItem onClick={() => onPreview(doc)}>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
Open
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ function MobileEditorDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ function MobileHitlEditDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useParams } from "next/navigation";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
|
||||
import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters";
|
||||
import {
|
||||
|
|
@ -27,6 +28,14 @@ import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
|
|||
import { FolderTreeView } from "@/components/documents/FolderTreeView";
|
||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerHandle,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
|
||||
|
|
@ -34,7 +43,9 @@ import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
|||
import { useDocumentSearch } from "@/hooks/use-document-search";
|
||||
import { useDocuments } from "@/hooks/use-documents";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { foldersApiService } from "@/lib/apis/folders-api.service";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||
import { queries } from "@/zero/queries/index";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
|
@ -81,6 +92,8 @@ export function DocumentsSidebar({
|
|||
const { data: connectors } = useAtomValue(connectorsAtom);
|
||||
const connectorCount = connectors?.length ?? 0;
|
||||
|
||||
const isMobileLayout = useIsMobile();
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebouncedValue(search, 250);
|
||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||
|
|
@ -333,6 +346,31 @@ export function DocumentsSidebar({
|
|||
[]
|
||||
);
|
||||
|
||||
// Document popup viewer state (for tree view "Open" and mobile preview)
|
||||
const [viewingDoc, setViewingDoc] = useState<DocumentNodeDoc | null>(null);
|
||||
const [viewingContent, setViewingContent] = useState<string>("");
|
||||
const [viewingLoading, setViewingLoading] = useState(false);
|
||||
|
||||
const handleViewDocumentPopup = useCallback(async (doc: DocumentNodeDoc) => {
|
||||
setViewingDoc(doc);
|
||||
setViewingLoading(true);
|
||||
try {
|
||||
const fullDoc = await documentsApiService.getDocument({ id: doc.id });
|
||||
setViewingContent(fullDoc.content);
|
||||
} catch (err) {
|
||||
console.error("[DocumentsSidebar] Failed to fetch document content:", err);
|
||||
setViewingContent("Failed to load document content.");
|
||||
} finally {
|
||||
setViewingLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCloseViewer = useCallback(() => {
|
||||
setViewingDoc(null);
|
||||
setViewingContent("");
|
||||
setViewingLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleToggleChatMention = useCallback(
|
||||
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
|
||||
if (isMentioned) {
|
||||
|
|
@ -622,6 +660,11 @@ export function DocumentsSidebar({
|
|||
mentionedDocIds={mentionedDocIds}
|
||||
onToggleChatMention={handleToggleChatMention}
|
||||
isSearchMode={isSearchMode || activeTypes.length > 0}
|
||||
onOpenInTab={!isMobileLayout ? (doc) => openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
}) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FolderTreeView
|
||||
|
|
@ -636,18 +679,24 @@ export function DocumentsSidebar({
|
|||
onMoveFolder={handleMoveFolder}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
onPreviewDocument={(doc) => {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
if (isMobileLayout) {
|
||||
handleViewDocumentPopup(doc);
|
||||
} else {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onEditDocument={(doc) => {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
if (!isMobileLayout) {
|
||||
openDocumentTab({
|
||||
documentId: doc.id,
|
||||
searchSpaceId,
|
||||
title: doc.title,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDeleteDocument={(doc) => handleDeleteDocument(doc.id)}
|
||||
onMoveDocument={handleMoveDocument}
|
||||
|
|
@ -675,6 +724,26 @@ export function DocumentsSidebar({
|
|||
parentFolderName={createFolderParentName}
|
||||
onConfirm={handleCreateFolderConfirm}
|
||||
/>
|
||||
|
||||
<Drawer open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DrawerContent className="max-h-[85vh] flex flex-col">
|
||||
<DrawerHandle />
|
||||
<DrawerHeader className="text-left shrink-0">
|
||||
<DrawerTitle className="text-base leading-tight break-words">
|
||||
{viewingDoc?.title}
|
||||
</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="overflow-y-auto flex-1 min-h-0 px-4 pb-6 select-text text-xs [&_h1]:text-base! [&_h1]:mt-3! [&_h2]:text-sm! [&_h2]:mt-2! [&_h3]:text-xs! [&_h3]:mt-2!">
|
||||
{viewingLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ function MobileReportDrawer() {
|
|||
shouldScaleBackground={false}
|
||||
>
|
||||
<DrawerContent
|
||||
className="h-[95vh] max-h-[95vh] z-80 bg-sidebar overflow-hidden"
|
||||
className="h-[90vh] max-h-[90vh] z-80 bg-sidebar overflow-hidden"
|
||||
overlayClassName="z-80"
|
||||
>
|
||||
<DrawerHandle />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue