From 936bd70682a2069a044a7b95160a25df50049f9a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:34:02 +0530 Subject: [PATCH] refactor: simplify dashboard layout by removing unused navigation props and implementing a new DocumentsSidebar for better document management --- .../[search_space_id]/client-layout.tsx | 2 - .../editor/[documentId]/page.tsx | 8 +- .../dashboard/[search_space_id]/layout.tsx | 35 +- .../assistant-ui/connector-popup.tsx | 1 - .../tabs/active-connectors-tab.tsx | 34 +- .../views/youtube-crawler-view.tsx | 4 - .../components/dashboard-breadcrumb.tsx | 4 - .../layout/providers/LayoutDataProvider.tsx | 28 +- .../layout/ui/shell/LayoutShell.tsx | 22 ++ .../layout/ui/sidebar/DocumentsSidebar.tsx | 333 ++++++++++++++++++ .../components/layout/ui/sidebar/index.ts | 1 + 11 files changed, 388 insertions(+), 84 deletions(-) create mode 100644 surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 83a579970..1d713930a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -27,8 +27,6 @@ export function DashboardClientLayout({ }: { children: React.ReactNode; searchSpaceId: string; - navSecondary?: any[]; - navMain?: any[]; }) { const t = useTranslations("dashboard"); const router = useRouter(); diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index 765bbf098..4503ee34d 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -256,7 +256,7 @@ export default function EditorPage() { setHasUnsavedChanges(false); toast.success("Note created successfully! Reindexing in background..."); - router.push(`/dashboard/${searchSpaceId}/documents`); + router.push(`/dashboard/${searchSpaceId}/new-chat`); } else { // Existing document — save const response = await authenticatedFetch( @@ -277,7 +277,7 @@ export default function EditorPage() { setHasUnsavedChanges(false); toast.success("Document saved! Reindexing in background..."); - router.push(`/dashboard/${searchSpaceId}/documents`); + router.push(`/dashboard/${searchSpaceId}/new-chat`); } } catch (error) { console.error("Error saving document:", error); @@ -298,7 +298,7 @@ export default function EditorPage() { if (hasUnsavedChanges) { setShowUnsavedDialog(true); } else { - router.push(`/dashboard/${searchSpaceId}/documents`); + router.push(`/dashboard/${searchSpaceId}/new-chat`); } }; @@ -311,7 +311,7 @@ export default function EditorPage() { router.push(pendingNavigation); setPendingNavigation(null); } else { - router.push(`/dashboard/${searchSpaceId}/documents`); + router.push(`/dashboard/${searchSpaceId}/new-chat`); } }; diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index e434d85fb..1bf9609d5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -10,43 +10,10 @@ export default function DashboardLayout({ params: Promise<{ search_space_id: string }>; children: React.ReactNode; }) { - // Use React.use to unwrap the params Promise const { search_space_id } = use(params); - const customNavSecondary = [ - { - title: `All Search Spaces`, - url: `#`, - icon: "Info", - }, - { - title: `All Search Spaces`, - url: "/dashboard", - icon: "Undo2", - }, - ]; - - const customNavMain = [ - { - title: "Chat", - url: `/dashboard/${search_space_id}/new-chat`, - icon: "MessageCircle", - items: [], - }, - { - title: "Documents", - url: `/dashboard/${search_space_id}/documents`, - icon: "SquareLibrary", - items: [], - }, - ]; - return ( - + {children} ); diff --git a/surfsense_web/components/assistant-ui/connector-popup.tsx b/surfsense_web/components/assistant-ui/connector-popup.tsx index 332694676..0a2146539 100644 --- a/surfsense_web/components/assistant-ui/connector-popup.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup.tsx @@ -415,7 +415,6 @@ export const ConnectorIndicator: FC<{ hideTrigger?: boolean }> = ({ hideTrigger activeDocumentTypes={activeDocumentTypes} connectors={connectors as SearchSourceConnector[]} indexingConnectorIds={indexingConnectorIds} - searchSpaceId={searchSpaceId} onTabChange={handleTabChange} onManage={handleStartEdit} onViewAccountsList={handleViewAccountsList} diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx index 639dab2aa..ccf1476dd 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/active-connectors-tab.tsx @@ -1,18 +1,13 @@ "use client"; -import { ArrowRight, Cable } from "lucide-react"; -import { useRouter } from "next/navigation"; +import { Cable } from "lucide-react"; import type { FC } from "react"; -import { useState } from "react"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; -import { Switch } from "@/components/ui/switch"; import { TabsContent } from "@/components/ui/tabs"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; -import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types"; -import { connectorsApiService } from "@/lib/apis/connectors-api.service"; import { cn } from "@/lib/utils"; import { COMPOSIO_CONNECTORS, OAUTH_CONNECTORS } from "../constants/connector-constants"; import { getDocumentCountForConnector } from "../utils/connector-document-mapping"; @@ -25,37 +20,21 @@ interface ActiveConnectorsTabProps { activeDocumentTypes: Array<[string, number]>; connectors: SearchSourceConnector[]; indexingConnectorIds: Set; - searchSpaceId: string; onTabChange: (value: string) => void; onManage?: (connector: SearchSourceConnector) => void; onViewAccountsList?: (connectorType: string, connectorTitle: string) => void; } -/** - * Check if a connector type is indexable - */ -function isIndexableConnector(connectorType: string): boolean { - const nonIndexableTypes = ["MCP_CONNECTOR"]; - return !nonIndexableTypes.includes(connectorType); -} - export const ActiveConnectorsTab: FC = ({ searchQuery, hasSources, activeDocumentTypes, connectors, indexingConnectorIds, - searchSpaceId, - onTabChange, + onTabChange: _onTabChange, onManage, onViewAccountsList, }) => { - const router = useRouter(); - - const handleViewAllDocuments = () => { - router.push(`/dashboard/${searchSpaceId}/documents`); - }; - // Convert activeDocumentTypes array to Record for utility function const documentTypeCounts = activeDocumentTypes.reduce( (acc, [docType, count]) => { @@ -300,15 +279,6 @@ export const ActiveConnectorsTab: FC = ({

Documents

-
{standaloneDocuments.map((doc) => ( diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx index 0798ecfdb..7e246f847 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/youtube-crawler-view.tsx @@ -3,7 +3,6 @@ import { TagInput, type Tag as TagType } from "emblor"; import { useAtom } from "jotai"; import { ArrowLeft } from "lucide-react"; -import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { type FC, useState } from "react"; import { toast } from "sonner"; @@ -24,7 +23,6 @@ interface YouTubeCrawlerViewProps { export const YouTubeCrawlerView: FC = ({ searchSpaceId, onBack }) => { const t = useTranslations("add_youtube"); - const router = useRouter(); const [videoTags, setVideoTags] = useState([]); const [activeTagIndex, setActiveTagIndex] = useState(null); const [error, setError] = useState(null); @@ -74,9 +72,7 @@ export const YouTubeCrawlerView: FC = ({ searchSpaceId, toast(t("success_toast"), { description: t("success_toast_desc"), }); - // Close the popup and navigate to documents onBack(); - router.push(`/dashboard/${searchSpaceId}/documents`); }, onError: (error: unknown) => { const errorMessage = error instanceof Error ? error.message : t("error_generic"); diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index d907ca6d1..4030c34ee 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -118,7 +118,6 @@ export function DashboardBreadcrumb() { // Handle editor sub-sections (document ID) if (section === "editor") { - // Handle special cases for editor let documentLabel: string; if (subSection === "new") { documentLabel = "New Note"; @@ -128,11 +127,9 @@ export function DashboardBreadcrumb() { breadcrumbs.push({ label: t("documents"), - href: `/dashboard/${segments[1]}/documents`, }); breadcrumbs.push({ label: sectionLabel, - href: `/dashboard/${segments[1]}/documents`, }); breadcrumbs.push({ label: documentLabel }); return breadcrumbs; @@ -148,7 +145,6 @@ export function DashboardBreadcrumb() { const documentLabel = documentLabels[subSection] || subSection; breadcrumbs.push({ label: t("documents"), - href: `/dashboard/${segments[1]}/documents`, }); breadcrumbs.push({ label: documentLabel }); return breadcrumbs; diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 0f6aaaf0d..ea081b5cd 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -114,6 +114,9 @@ export function LayoutDataProvider({ const [isInboxSidebarOpen, setIsInboxSidebarOpen] = useState(false); const [isInboxDocked, setIsInboxDocked] = useState(false); + // Documents sidebar state + const [isDocumentsSidebarOpen, setIsDocumentsSidebarOpen] = useState(false); + // Search space dialog state const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false); @@ -301,9 +304,9 @@ export function LayoutDataProvider({ }, { title: "Documents", - url: `/dashboard/${searchSpaceId}/documents`, + url: "#documents", icon: SquareLibrary, - isActive: pathname?.includes("/documents"), + isActive: isDocumentsSidebarOpen, }, { title: "Announcements", @@ -313,7 +316,7 @@ export function LayoutDataProvider({ badge: announcementUnreadCount > 0 ? formatInboxCount(announcementUnreadCount) : undefined, }, ], - [searchSpaceId, pathname, isInboxSidebarOpen, totalUnreadCount, announcementUnreadCount] + [pathname, isInboxSidebarOpen, isDocumentsSidebarOpen, totalUnreadCount, announcementUnreadCount] ); // Handlers @@ -411,6 +414,19 @@ export function LayoutDataProvider({ if (!prev) { setIsAllSharedChatsSidebarOpen(false); setIsAllPrivateChatsSidebarOpen(false); + setIsDocumentsSidebarOpen(false); + } + return !prev; + }); + return; + } + // Handle documents specially - toggle sidebar instead of navigating + if (item.url === "#documents") { + setIsDocumentsSidebarOpen((prev) => { + if (!prev) { + setIsInboxSidebarOpen(false); + setIsAllSharedChatsSidebarOpen(false); + setIsAllPrivateChatsSidebarOpen(false); } return !prev; }); @@ -515,12 +531,14 @@ export function LayoutDataProvider({ setIsAllSharedChatsSidebarOpen(true); setIsAllPrivateChatsSidebarOpen(false); setIsInboxSidebarOpen(false); + setIsDocumentsSidebarOpen(false); }, []); const handleViewAllPrivateChats = useCallback(() => { setIsAllPrivateChatsSidebarOpen(true); setIsAllSharedChatsSidebarOpen(false); setIsInboxSidebarOpen(false); + setIsDocumentsSidebarOpen(false); }, []); // Delete handlers @@ -651,6 +669,10 @@ export function LayoutDataProvider({ onOpenChange: setIsAllPrivateChatsSidebarOpen, searchSpaceId, }} + documentsPanel={{ + open: isDocumentsSidebarOpen, + onOpenChange: setIsDocumentsSidebarOpen, + }} > {children} diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index 107e15d2b..ce310816c 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -13,6 +13,7 @@ import { IconRail } from "../icon-rail"; import { AllPrivateChatsSidebar, AllSharedChatsSidebar, + DocumentsSidebar, InboxSidebar, MobileSidebar, MobileSidebarTrigger, @@ -94,6 +95,10 @@ interface LayoutShellProps { onOpenChange: (open: boolean) => void; searchSpaceId: string; }; + documentsPanel?: { + open: boolean; + onOpenChange: (open: boolean) => void; + }; } export function LayoutShell({ @@ -133,6 +138,7 @@ export function LayoutShell({ isLoadingChats = false, allSharedChatsPanel, allPrivateChatsPanel, + documentsPanel, }: LayoutShellProps) { const isMobile = useIsMobile(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); @@ -211,6 +217,14 @@ export function LayoutShell({ /> )} + {/* Mobile Documents Sidebar - slide-out panel */} + {documentsPanel && ( + + )} + {/* Mobile All Shared Chats - slide-out panel */} {allSharedChatsPanel && ( )} + {/* Documents Sidebar - slide-out panel */} + {documentsPanel && ( + + )} + {/* All Shared Chats - slide-out panel */} {allSharedChatsPanel && ( (value: T, delay = 250) { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const t = setTimeout(() => setDebounced(value), delay); + return () => clearTimeout(t); + }, [value, delay]); + return debounced; +} + +interface DocumentsSidebarProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps) { + const t = useTranslations("documents"); + const tSidebar = useTranslations("sidebar"); + const params = useParams(); + const isMobile = !useMediaQuery("(min-width: 640px)"); + const searchSpaceId = Number(params.search_space_id); + + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounced(search, 250); + const [activeTypes, setActiveTypes] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({ + document_type: true, + created_by: false, + created_at: true, + status: true, + }); + const [pageIndex, setPageIndex] = useState(0); + const [sortKey, setSortKey] = useState("created_at"); + const [sortDesc, setSortDesc] = useState(true); + const [selectedIds, setSelectedIds] = useState>(new Set()); + const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom); + + const { + documents: realtimeDocuments, + typeCounts: realtimeTypeCounts, + loading: realtimeLoading, + error: realtimeError, + } = useDocuments(searchSpaceId, activeTypes); + + const isSearchMode = !!debouncedSearch.trim(); + + const searchQueryParams = useMemo( + () => ({ + search_space_id: searchSpaceId, + page: pageIndex, + page_size: PAGE_SIZE, + title: debouncedSearch.trim(), + ...(activeTypes.length > 0 && { document_types: activeTypes }), + }), + [searchSpaceId, pageIndex, activeTypes, debouncedSearch] + ); + + const { + data: searchResponse, + isLoading: isSearchLoading, + refetch: refetchSearch, + error: searchError, + } = useQuery({ + queryKey: cacheKeys.documents.globalQueryParams(searchQueryParams), + queryFn: () => documentsApiService.searchDocuments({ queryParams: searchQueryParams }), + staleTime: 30 * 1000, + enabled: !!searchSpaceId && isSearchMode && open, + }); + + const sortedRealtimeDocuments = useMemo(() => { + const docs = [...realtimeDocuments]; + docs.sort((a, b) => { + const av = a[sortKey] ?? ""; + const bv = b[sortKey] ?? ""; + let cmp: number; + if (sortKey === "created_at") { + cmp = new Date(av as string).getTime() - new Date(bv as string).getTime(); + } else { + cmp = String(av).localeCompare(String(bv)); + } + return sortDesc ? -cmp : cmp; + }); + return docs; + }, [realtimeDocuments, sortKey, sortDesc]); + + const paginatedRealtimeDocuments = useMemo(() => { + const start = pageIndex * PAGE_SIZE; + const end = start + PAGE_SIZE; + return sortedRealtimeDocuments.slice(start, end); + }, [sortedRealtimeDocuments, pageIndex]); + + const displayDocs = isSearchMode + ? (searchResponse?.items || []).map((item) => ({ + id: item.id, + search_space_id: item.search_space_id, + document_type: item.document_type, + title: item.title, + created_by_id: item.created_by_id ?? null, + created_by_name: item.created_by_name ?? null, + created_by_email: item.created_by_email ?? null, + created_at: item.created_at, + status: ( + item as { + status?: { state: "ready" | "pending" | "processing" | "failed"; reason?: string }; + } + ).status ?? { state: "ready" as const }, + })) + : paginatedRealtimeDocuments; + + const displayTotal = isSearchMode ? searchResponse?.total || 0 : sortedRealtimeDocuments.length; + const loading = isSearchMode ? isSearchLoading : realtimeLoading; + const error = isSearchMode ? searchError : realtimeError; + const pageEnd = Math.min((pageIndex + 1) * PAGE_SIZE, displayTotal); + + const onToggleType = (type: DocumentTypeEnum, checked: boolean) => { + setActiveTypes((prev) => { + if (checked) { + return prev.includes(type) ? prev : [...prev, type]; + } + return prev.filter((t) => t !== type); + }); + setPageIndex(0); + setSelectedIds(new Set()); + }; + + const onBulkDelete = async () => { + if (selectedIds.size === 0) { + toast.error(t("no_rows_selected")); + return; + } + + const allDocs = isSearchMode + ? (searchResponse?.items || []).map((item) => ({ + id: item.id, + status: (item as { status?: { state: string } }).status, + })) + : sortedRealtimeDocuments.map((doc) => ({ id: doc.id, status: doc.status })); + + const selectedDocs = allDocs.filter((doc) => selectedIds.has(doc.id)); + const deletableIds = selectedDocs + .filter((doc) => doc.status?.state !== "pending" && doc.status?.state !== "processing") + .map((doc) => doc.id); + const inProgressCount = selectedIds.size - deletableIds.length; + + if (inProgressCount > 0) { + toast.warning( + `${inProgressCount} document(s) are pending or processing and cannot be deleted.` + ); + } + + if (deletableIds.length === 0) return; + + try { + let conflictCount = 0; + const results = await Promise.all( + deletableIds.map(async (id) => { + try { + await deleteDocumentMutation({ id }); + return true; + } catch (error: unknown) { + const status = + (error as { response?: { status?: number } })?.response?.status ?? + (error as { status?: number })?.status; + if (status === 409) conflictCount++; + return false; + } + }) + ); + const okCount = results.filter((r) => r === true).length; + if (okCount === deletableIds.length) { + toast.success(t("delete_success_count", { count: okCount })); + } else if (conflictCount > 0) { + toast.error(`${conflictCount} document(s) started processing. Please try again later.`); + } else { + toast.error(t("delete_partial_failed")); + } + if (isSearchMode) await refetchSearch(); + setSelectedIds(new Set()); + } catch (e) { + console.error(e); + toast.error(t("delete_error")); + } + }; + + const handleDeleteDocument = useCallback( + async (id: number): Promise => { + try { + await deleteDocumentMutation({ id }); + toast.success(t("delete_success") || "Document deleted"); + if (isSearchMode) await refetchSearch(); + return true; + } catch (e) { + console.error("Error deleting document:", e); + return false; + } + }, + [deleteDocumentMutation, isSearchMode, refetchSearch, t] + ); + + const handleSortChange = useCallback((key: SortKey) => { + setSortKey((currentKey) => { + if (currentKey === key) { + setSortDesc((v) => !v); + return currentKey; + } + setSortDesc(false); + return key; + }); + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: Reset page on search change + useEffect(() => { + setPageIndex(0); + }, [debouncedSearch]); + + useEffect(() => { + if (!open) return; + const panelWidth = isMobile ? window.innerWidth : 720; + const isNarrow = panelWidth < 600; + setColumnVisibility((prev) => ({ ...prev, created_by: !isNarrow, created_at: !isNarrow })); + }, [open, isMobile]); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape" && open) { + onOpenChange(false); + } + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [open, onOpenChange]); + + const documentsContent = ( + <> +
+
+
+ {isMobile && ( + + )} + +

{t("title") || "Documents"}

+
+
+
+ +
+ + + + + setPageIndex(0)} + onPrev={() => setPageIndex((i) => Math.max(0, i - 1))} + onNext={() => setPageIndex((i) => (pageEnd < displayTotal ? i + 1 : i))} + onLast={() => setPageIndex(Math.max(0, Math.ceil(displayTotal / PAGE_SIZE) - 1))} + canPrev={pageIndex > 0} + canNext={pageEnd < displayTotal} + /> +
+ + ); + + return ( + + {documentsContent} + + ); +} diff --git a/surfsense_web/components/layout/ui/sidebar/index.ts b/surfsense_web/components/layout/ui/sidebar/index.ts index d9c5edee5..8d295c776 100644 --- a/surfsense_web/components/layout/ui/sidebar/index.ts +++ b/surfsense_web/components/layout/ui/sidebar/index.ts @@ -1,6 +1,7 @@ export { AllPrivateChatsSidebar } from "./AllPrivateChatsSidebar"; export { AllSharedChatsSidebar } from "./AllSharedChatsSidebar"; export { ChatListItem } from "./ChatListItem"; +export { DocumentsSidebar } from "./DocumentsSidebar"; export { InboxSidebar } from "./InboxSidebar"; export { MobileSidebar, MobileSidebarTrigger } from "./MobileSidebar"; export { NavSection } from "./NavSection";