diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx deleted file mode 100644 index 4ed451850..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ /dev/null @@ -1,1162 +0,0 @@ -"use client"; - -import { useAtomValue, useSetAtom } from "jotai"; -import { - AlertCircle, - CheckCircle2, - ChevronDown, - ChevronUp, - Clock, - Eye, - FileText, - FileX, - MoreHorizontal, - Network, - PenLine, - SearchX, - Trash2, - User, -} from "lucide-react"; -import { useTranslations } from "next-intl"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { toast } from "sonner"; -import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; -import { membersAtom } from "@/atoms/members/members-query.atoms"; -import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup"; -import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; -import { MarkdownViewer } from "@/components/markdown-viewer"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Drawer, - DrawerContent, - DrawerHandle, - DrawerHeader, - DrawerTitle, -} from "@/components/ui/drawer"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Spinner } from "@/components/ui/spinner"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useLongPress } from "@/hooks/use-long-press"; -import { documentsApiService } from "@/lib/apis/documents-api.service"; -import { getDocumentTypeIcon } from "./DocumentTypeIcon"; -import type { Document, DocumentStatus } from "./types"; - -const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const; -const NON_DELETABLE_DOCUMENT_TYPES = ["SURFSENSE_DOCS"] as const; - -function getInitials(name: string): string { - const parts = name.trim().split(/\s+/); - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase(); - } - return name.slice(0, 2).toUpperCase(); -} - -function StatusIndicator({ status }: { status?: DocumentStatus }) { - const state = status?.state ?? "ready"; - - switch (state) { - case "pending": - return ( - - -
- -
-
- Pending: waiting to be synced -
- ); - case "processing": - return ( - - -
- -
-
- Syncing -
- ); - case "failed": - return ( - - -
- -
-
- - {status?.reason || "Processing failed"} - -
- ); - case "ready": - return ( - - -
- -
-
- Ready -
- ); - } -} - -export type SortKey = keyof Pick; - -function sortDocuments(docs: Document[], key: SortKey, desc: boolean): Document[] { - const sorted = [...docs].sort((a, b) => { - const av = a[key] ?? ""; - const bv = b[key] ?? ""; - if (key === "created_at") - return new Date(av as string).getTime() - new Date(bv as string).getTime(); - return String(av).localeCompare(String(bv)); - }); - return desc ? sorted.reverse() : sorted; -} - -function formatAbsoluteDate(dateStr: string): string { - const date = new Date(dateStr); - return date.toLocaleString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }); -} - -function SortableHeader({ - children, - sortKey, - currentSortKey, - sortDesc, - onSort, - icon, -}: { - children: React.ReactNode; - sortKey: SortKey; - currentSortKey: SortKey; - sortDesc: boolean; - onSort: (key: SortKey) => void; - icon?: React.ReactNode; -}) { - const isActive = currentSortKey === sortKey; - return ( - - ); -} - -function MobileCardWrapper({ - onLongPress, - children, -}: { - onLongPress: () => void; - children: React.ReactNode; -}) { - const { handlers, wasLongPress } = useLongPress(onLongPress); - - return ( - // biome-ignore lint/a11y/useSemanticElements: touch-only long-press wrapper for mobile -
{ - handlers.onTouchEnd(); - if (wasLongPress()) { - e.preventDefault(); - } - }} - onContextMenu={(e) => e.preventDefault()} - > - {children} -
- ); -} - -export function DocumentsTableShell({ - documents, - loading, - error, - sortKey, - sortDesc, - onSortChange, - deleteDocument, - bulkDeleteDocuments, - searchSpaceId, - hasMore = false, - loadingMore = false, - onLoadMore, - mentionedDocIds, - onToggleChatMention, - isSearchMode = false, - onOpenInTab, -}: { - documents: Document[]; - loading: boolean; - error: boolean; - sortKey: SortKey; - sortDesc: boolean; - onSortChange: (key: SortKey) => void; - deleteDocument: (id: number) => Promise; - bulkDeleteDocuments?: (ids: number[]) => Promise<{ success: number; failed: number }>; - searchSpaceId: string; - hasMore?: boolean; - loadingMore?: boolean; - onLoadMore?: () => void; - /** IDs of documents currently mentioned as chips in the chat composer */ - mentionedDocIds?: Set; - /** Toggle a document's mention in the chat (add if not mentioned, remove if mentioned) */ - 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(); - - const [viewingDoc, setViewingDoc] = useState(null); - const [viewingContent, setViewingContent] = useState(""); - const [viewingLoading, setViewingLoading] = useState(false); - - const [metadataDoc, setMetadataDoc] = useState(null); - const [metadataJson, setMetadataJson] = useState | null>(null); - const [metadataLoading, setMetadataLoading] = useState(false); - const [previewScrollPos, setPreviewScrollPos] = useState<"top" | "middle" | "bottom">("top"); - const previewRafRef = useRef(); - const handlePreviewScroll = useCallback((e: React.UIEvent) => { - const el = e.currentTarget; - if (previewRafRef.current) return; - previewRafRef.current = requestAnimationFrame(() => { - const atTop = el.scrollTop <= 2; - const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2; - setPreviewScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle"); - previewRafRef.current = undefined; - }); - }, []); - useEffect( - () => () => { - if (previewRafRef.current) cancelAnimationFrame(previewRafRef.current); - }, - [] - ); - - const [deleteDoc, setDeleteDoc] = useState(null); - const [isDeleting, setIsDeleting] = useState(false); - const [mobileActionDoc, setMobileActionDoc] = useState(null); - const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false); - const [isBulkDeleting, setIsBulkDeleting] = useState(false); - const openEditor = useSetAtom(openEditorPanelAtom); - const [openMenuDocId, setOpenMenuDocId] = useState(null); - - const { data: members } = useAtomValue(membersAtom); - const memberMap = useMemo(() => { - const map = new Map(); - if (members) { - for (const m of members) { - map.set(m.user_id, { - name: m.user_display_name || m.user_email || "Unknown", - email: m.user_email || undefined, - avatarUrl: m.user_avatar_url || undefined, - }); - } - } - return map; - }, [members]); - - const desktopSentinelRef = useRef(null); - const mobileSentinelRef = useRef(null); - const desktopScrollRef = useRef(null); - const mobileScrollRef = useRef(null); - - useEffect(() => { - if (!onLoadMore || !hasMore || loadingMore) return; - - const observers: IntersectionObserver[] = []; - - const observe = (root: HTMLElement | null, sentinel: HTMLElement | null) => { - if (!root || !sentinel) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries.some((e) => e.isIntersecting)) onLoadMore(); - }, - { root, rootMargin: "150px", threshold: 0 } - ); - observer.observe(sentinel); - observers.push(observer); - }; - - observe(desktopScrollRef.current, desktopSentinelRef.current); - observe(mobileScrollRef.current, mobileSentinelRef.current); - - return () => { - for (const o of observers) o.disconnect(); - }; - }, [onLoadMore, hasMore, loadingMore]); - - const handleViewDocument = useCallback(async (doc: Document) => { - setViewingDoc(doc); - const preview = doc.content_preview || doc.content; - if (preview) { - setViewingContent(preview); - return; - } - setViewingLoading(true); - try { - const fullDoc = await documentsApiService.getDocument({ id: doc.id }); - setViewingContent(fullDoc.content_preview || fullDoc.content); - } catch (err) { - console.error("[DocumentsTableShell] Failed to fetch document content:", err); - setViewingContent("Failed to load document content."); - } finally { - setViewingLoading(false); - } - }, []); - - const handleCloseViewer = useCallback(() => { - setViewingDoc(null); - setViewingContent(""); - setViewingLoading(false); - }, []); - - const handleViewMetadata = useCallback(async (doc: Document) => { - setMetadataDoc(doc); - setMetadataLoading(true); - try { - const fullDoc = await documentsApiService.getDocument({ id: doc.id }); - setMetadataJson(fullDoc.document_metadata ?? {}); - } catch (err) { - console.error("[DocumentsTableShell] Failed to fetch document metadata:", err); - setMetadataJson({ error: "Failed to load document metadata" }); - } finally { - setMetadataLoading(false); - } - }, []); - - const handleDeleteFromMenu = useCallback(async () => { - if (!deleteDoc) return; - setIsDeleting(true); - try { - const ok = await deleteDocument(deleteDoc.id); - if (!ok) toast.error("Failed to delete document"); - } catch (error: unknown) { - console.error("Error deleting document:", error); - const status = - (error as { response?: { status?: number } })?.response?.status ?? - (error as { status?: number })?.status; - if (status === 409) { - toast.error("Document is now being processed. Please try again later."); - } else { - toast.error("Failed to delete document"); - } - } finally { - setIsDeleting(false); - setDeleteDoc(null); - } - }, [deleteDoc, deleteDocument]); - - const sorted = React.useMemo( - () => sortDocuments(documents, sortKey, sortDesc), - [documents, sortKey, sortDesc] - ); - - const isSelectable = (doc: Document) => { - const state = doc.status?.state; - return state !== "pending" && state !== "processing"; - }; - - const hasChatMode = !!onToggleChatMention && !!mentionedDocIds; - - const selectableDocs = sorted.filter(isSelectable); - const allMentionedOnPage = - hasChatMode && - selectableDocs.length > 0 && - selectableDocs.every((d) => mentionedDocIds.has(d.id)); - const someMentionedOnPage = - hasChatMode && selectableDocs.some((d) => mentionedDocIds.has(d.id)) && !allMentionedOnPage; - - const toggleAll = (checked: boolean) => { - if (!onToggleChatMention) return; - for (const doc of selectableDocs) { - const isMentioned = mentionedDocIds?.has(doc.id) ?? false; - if (checked && !isMentioned) { - onToggleChatMention(doc, false); - } else if (!checked && isMentioned) { - onToggleChatMention(doc, true); - } - } - }; - - const onSortHeader = (key: SortKey) => onSortChange(key); - - const deletableSelectedIds = React.useMemo(() => { - if (!mentionedDocIds || mentionedDocIds.size === 0) return []; - return sorted - .filter((doc) => { - if (!mentionedDocIds.has(doc.id)) return false; - const state = doc.status?.state; - return ( - state !== "pending" && - state !== "processing" && - !NON_DELETABLE_DOCUMENT_TYPES.includes( - doc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number] - ) - ); - }) - .map((doc) => doc.id); - }, [sorted, mentionedDocIds]); - - const hasDeletableSelection = deletableSelectedIds.length > 0; - - const handleBulkDelete = useCallback(async () => { - if (deletableSelectedIds.length === 0) return; - setIsBulkDeleting(true); - try { - if (bulkDeleteDocuments) { - const { success, failed } = await bulkDeleteDocuments(deletableSelectedIds); - if (success > 0) { - toast.success(`Deleted ${success} document${success !== 1 ? "s" : ""}`); - } - if (failed > 0) { - toast.error(`Failed to delete ${failed} document${failed !== 1 ? "s" : ""}`); - } - } else { - const results = await Promise.allSettled( - deletableSelectedIds.map((id) => deleteDocument(id)) - ); - const successCount = results.filter( - (r) => r.status === "fulfilled" && r.value === true - ).length; - const failCount = deletableSelectedIds.length - successCount; - if (successCount > 0) { - toast.success(`Deleted ${successCount} document${successCount !== 1 ? "s" : ""}`); - } - if (failCount > 0) { - toast.error(`Failed to delete ${failCount} document${failCount !== 1 ? "s" : ""}`); - } - } - } catch { - toast.error("Failed to delete documents"); - } - setIsBulkDeleting(false); - setBulkDeleteConfirmOpen(false); - }, [deletableSelectedIds, bulkDeleteDocuments, deleteDocument]); - - const bulkDeleteBar = hasDeletableSelection ? ( -
- -
- ) : null; - - return ( -
- {/* Desktop Table View */} -
- - - - -
- toggleAll(!!v)} - aria-label={hasChatMode ? "Toggle all for chat" : "Select all"} - className="shrink-0" - /> -
-
- - } - > - Document - - - - - - - - - - - - -
-
-
- {loading ? ( -
- - - {[65, 80, 45, 72, 55, 88, 40, 60, 50, 75].map((widthPercent) => ( - - -
- -
-
- - - - - - - - - -
- ))} -
-
-
- ) : error ? ( -
-
- -

{t("error_loading")}

-
-
- ) : sorted.length === 0 ? ( -
- {isSearchMode ? ( -
- -
-

- No matching documents -

-

- Try a different search term or adjust your filters. -

-
-
- ) : ( -
-
- -
-
-

{t("no_documents")}

-

- Get started by uploading your first document. -

-
- -
- )} -
- ) : ( -
- {bulkDeleteBar} - - - {sorted.map((doc) => { - const isMentioned = mentionedDocIds?.has(doc.id) ?? false; - const canInteract = isSelectable(doc); - const isBeingProcessed = - doc.status?.state === "pending" || doc.status?.state === "processing"; - const isFileFailed = - doc.document_type === "FILE" && doc.status?.state === "failed"; - const isEditable = EDITABLE_DOCUMENT_TYPES.includes( - doc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number] - ); - const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes( - doc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number] - ); - const isMenuOpen = openMenuDocId === doc.id; - const handleRowToggle = () => { - if (canInteract && onToggleChatMention) { - onToggleChatMention(doc, isMentioned); - } - }; - const handleRowClick = (e: React.MouseEvent) => { - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - e.stopPropagation(); - handleViewMetadata(doc); - return; - } - handleRowToggle(); - }; - return ( - - e.stopPropagation()} - > -
- {(() => { - const state = doc.status?.state ?? "ready"; - if (state === "pending" || state === "processing") { - return ; - } - if (state === "failed") { - if (isMentioned) { - return ( - handleRowToggle()} - aria-label="Remove from chat" - className="shrink-0" - /> - ); - } - return ( - <> - - - - - handleRowToggle()} - aria-label="Add to chat" - className="shrink-0" - /> - - - ); - } - return ( - handleRowToggle()} - aria-label={isMentioned ? "Remove from chat" : "Add to chat"} - className="shrink-0" - /> - ); - })()} -
-
- - - {doc.title} - - - - - {getDocumentTypeIcon(doc.document_type, "h-4 w-4")} - - - e.stopPropagation()} - > -
- {(() => { - const member = doc.created_by_id - ? memberMap.get(doc.created_by_id) - : null; - const displayName = - member?.name || - doc.created_by_name || - doc.created_by_email || - "Unknown"; - const avatarUrl = member?.avatarUrl; - const email = member?.email || doc.created_by_email || displayName; - return ( - - - - - {avatarUrl && ( - - )} - - {getInitials(displayName)} - - - - - {email} - - ); - })()} -
- setOpenMenuDocId(open ? doc.id : null)} - > - - - - - - onOpenInTab ? onOpenInTab(doc) : handleViewDocument(doc) - } - disabled={isBeingProcessed} - > - - Open - - {isEditable && ( - { - if (!(isBeingProcessed || isFileFailed)) { - openEditor({ - documentId: doc.id, - searchSpaceId: Number(searchSpaceId), - title: doc.title, - }); - } - }} - disabled={isBeingProcessed || isFileFailed} - > - - Edit - - )} - {shouldShowDelete && ( - !isBeingProcessed && setDeleteDoc(doc)} - disabled={isBeingProcessed} - className="" - > - - Delete - - )} - - -
-
-
- - ); - })} - -
- {hasMore &&
} -
- )} -
- - {/* Mobile Card View */} - {loading ? ( -
- {[70, 85, 55, 78, 62, 90].map((widthPercent) => ( -
-
- -
- -
- - -
-
- ))} -
- ) : error ? ( -
-
- -

{t("error_loading")}

-
-
- ) : sorted.length === 0 ? ( -
- {isSearchMode ? ( -
- -
-

No matching documents

-

- Try a different search term or adjust your filters. -

-
-
- ) : ( -
-
- -
-
-

{t("no_documents")}

-

- Get started by uploading your first document. -

-
- -
- )} -
- ) : ( -
- {bulkDeleteBar} - {sorted.map((doc) => { - const isMentioned = mentionedDocIds?.has(doc.id) ?? false; - const statusState = doc.status?.state ?? "ready"; - const showCheckbox = statusState === "ready"; - const canInteract = showCheckbox; - const handleCardClick = (e?: React.MouseEvent) => { - if (e && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - e.stopPropagation(); - handleViewMetadata(doc); - return; - } - if (canInteract && onToggleChatMention) { - onToggleChatMention(doc, isMentioned); - } - }; - return ( - setMobileActionDoc(doc)}> -
- {canInteract && hasChatMode && ( -
-
- ); - })} - {hasMore &&
} -
- )} - - {/* Document Content Viewer (mobile drawer) */} - !open && handleCloseViewer()}> - - - - - {viewingDoc?.title} - - -
- {viewingLoading ? ( -
- -
- ) : ( - <> - - {viewingDoc && ( -
- -
- )} - - )} -
-
-
- - {/* Document Metadata Viewer (Ctrl+Click) */} - { - if (!open) { - setMetadataDoc(null); - setMetadataJson(null); - setMetadataLoading(false); - } - }} - /> - - {/* Delete Confirmation Dialog */} - !open && setDeleteDoc(null)}> - - - Delete document? - - This action cannot be undone. This will permanently delete this document from your - search space. - - - - Cancel - { - e.preventDefault(); - handleDeleteFromMenu(); - }} - disabled={isDeleting} - className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - Delete - {isDeleting && } - - - - - - {/* Mobile Document Actions Drawer */} - !open && setMobileActionDoc(null)}> - - - - {mobileActionDoc?.title} -
-

- Owner:{" "} - {mobileActionDoc?.created_by_name || mobileActionDoc?.created_by_email || "—"} -

-

- Created:{" "} - {mobileActionDoc ? formatAbsoluteDate(mobileActionDoc.created_at) : ""} -

-
-
-
- - {mobileActionDoc && - EDITABLE_DOCUMENT_TYPES.includes( - mobileActionDoc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number] - ) && ( - - )} - {mobileActionDoc && - !NON_DELETABLE_DOCUMENT_TYPES.includes( - mobileActionDoc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number] - ) && ( - - )} -
-
-
- - {/* Bulk Delete Confirmation Dialog */} - !open && !isBulkDeleting && setBulkDeleteConfirmOpen(false)} - > - - - - Delete {deletableSelectedIds.length} document - {deletableSelectedIds.length !== 1 ? "s" : ""}? - - - This action cannot be undone.{" "} - {deletableSelectedIds.length === 1 - ? "This document" - : `These ${deletableSelectedIds.length} documents`}{" "} - will be permanently deleted from your search space. - - - - Cancel - { - e.preventDefault(); - handleBulkDelete(); - }} - disabled={isBulkDeleting} - className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - Delete - {isBulkDeleting && } - - - - -
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx deleted file mode 100644 index 03d5066b2..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/PaginationControls.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { ChevronFirst, ChevronLast, ChevronLeft, ChevronRight } from "lucide-react"; -import { motion } from "motion/react"; -import { Button } from "@/components/ui/button"; - -const PAGE_SIZE = 50; - -export function PaginationControls({ - pageIndex, - total, - onFirst, - onPrev, - onNext, - onLast, - canPrev, - canNext, -}: { - pageIndex: number; - total: number; - onFirst: () => void; - onPrev: () => void; - onNext: () => void; - onLast: () => void; - canPrev: boolean; - canNext: boolean; -}) { - const start = pageIndex * PAGE_SIZE + 1; - const end = Math.min((pageIndex + 1) * PAGE_SIZE, total); - - return ( - - {/* Range indicator */} - - {start}-{end} of {total} - - - {/* Navigation buttons */} -
- - - - -
-
- ); -} - -export { PAGE_SIZE }; diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx deleted file mode 100644 index 5b7451c61..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx +++ /dev/null @@ -1,225 +0,0 @@ -"use client"; - -import { useSetAtom } from "jotai"; -import { MoreHorizontal, PenLine, Trash2 } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; -import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import type { Document } from "./types"; - -const EDITABLE_DOCUMENT_TYPES = ["FILE", "NOTE"] as const; - -// SURFSENSE_DOCS are system-managed and cannot be deleted -const NON_DELETABLE_DOCUMENT_TYPES = ["SURFSENSE_DOCS"] as const; - -export function RowActions({ - document, - deleteDocument, - searchSpaceId, -}: { - document: Document; - deleteDocument: (id: number) => Promise; - searchSpaceId: string; -}) { - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const openEditorPanel = useSetAtom(openEditorPanelAtom); - - const isEditable = EDITABLE_DOCUMENT_TYPES.includes( - document.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number] - ); - - const isBeingProcessed = - document.status?.state === "pending" || document.status?.state === "processing"; - - const shouldShowDelete = !NON_DELETABLE_DOCUMENT_TYPES.includes( - document.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number] - ); - - const isEditDisabled = isBeingProcessed; - const isDeleteDisabled = isBeingProcessed; - - const handleDelete = async () => { - setIsDeleting(true); - try { - const ok = await deleteDocument(document.id); - if (!ok) toast.error("Failed to delete document"); - // Note: Success toast is handled by the mutation atom's onSuccess callback - // Cache is updated optimistically by the mutation, no need to refresh - } catch (error: unknown) { - console.error("Error deleting document:", error); - // Check for 409 Conflict (document started processing after UI loaded) - const status = - (error as { response?: { status?: number } })?.response?.status ?? - (error as { status?: number })?.status; - if (status === 409) { - toast.error("Document is now being processed. Please try again later."); - } else { - toast.error("Failed to delete document"); - } - } finally { - setIsDeleting(false); - setIsDeleteOpen(false); - } - }; - - const handleEdit = () => { - openEditorPanel({ - documentId: document.id, - searchSpaceId: Number(searchSpaceId), - title: document.title, - }); - }; - - return ( - <> - {/* Desktop Actions */} -
- {isEditable ? ( - - - - - - !isEditDisabled && handleEdit()} - disabled={isEditDisabled} - className={ - isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "" - } - > - - Edit - - {shouldShowDelete && ( - !isDeleteDisabled && setIsDeleteOpen(true)} - disabled={isDeleteDisabled} - className={ - isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "" - } - > - - Delete - - )} - - - ) : ( - shouldShowDelete && ( - - ) - )} -
- - {/* Mobile Actions Dropdown */} -
- {isEditable ? ( - - - - - - !isEditDisabled && handleEdit()} - disabled={isEditDisabled} - className={ - isEditDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "" - } - > - - Edit - - {shouldShowDelete && ( - !isDeleteDisabled && setIsDeleteOpen(true)} - disabled={isDeleteDisabled} - className={ - isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : "" - } - > - - Delete - - )} - - - ) : ( - shouldShowDelete && ( - - ) - )} -
- - - - - Delete document? - - This action cannot be undone. This will permanently delete this document from your - search space. - - - - Cancel - { - e.preventDefault(); - handleDelete(); - }} - disabled={isDeleting} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - {isDeleting ? "Deleting" : "Delete"} - - - - - - ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/types.ts b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/types.ts deleted file mode 100644 index 88914bd4f..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type DocumentType = string; - -export type DocumentStatus = { - state: "ready" | "pending" | "processing" | "failed"; - reason?: string; -}; - -export type Document = { - id: number; - title: string; - document_type: DocumentType; - document_metadata?: any; - content?: string; - content_preview?: string; - created_at: string; - search_space_id: number; - created_by_id?: string | null; - created_by_name?: string | null; - created_by_email?: string | null; - status?: DocumentStatus; -}; - -export type ColumnVisibility = { - document_type: boolean; - created_by: boolean; - created_at: boolean; - status: boolean; -}; 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 55fc99150..c08871445 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 @@ -2,7 +2,7 @@ import { Search, Unplug } from "lucide-react"; import type { FC } from "react"; -import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; +import { getDocumentTypeLabel } from "@/components/documents/DocumentTypeIcon"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { TabsContent } from "@/components/ui/tabs"; diff --git a/surfsense_web/components/documents/DocumentNode.tsx b/surfsense_web/components/documents/DocumentNode.tsx index 0a361fc2b..83fcac814 100644 --- a/surfsense_web/components/documents/DocumentNode.tsx +++ b/surfsense_web/components/documents/DocumentNode.tsx @@ -13,7 +13,7 @@ import { } from "lucide-react"; import React, { useCallback, useRef, useState } from "react"; import { useDrag } from "react-dnd"; -import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; +import { getDocumentTypeIcon } from "@/components/documents/DocumentTypeIcon"; import { ExportContextItems, ExportDropdownItems } from "@/components/shared/ExportMenuItems"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx b/surfsense_web/components/documents/DocumentTypeIcon.tsx similarity index 100% rename from surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon.tsx rename to surfsense_web/components/documents/DocumentTypeIcon.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/components/documents/DocumentsFilters.tsx similarity index 100% rename from surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx rename to surfsense_web/components/documents/DocumentsFilters.tsx diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index 8bd7d64ea..64da1e832 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -7,7 +7,7 @@ import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import { DocumentsFilters } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters"; +import { DocumentsFilters } from "@/components/documents/DocumentsFilters"; import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom"; import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index c5d0cd84c..3dc6d2c01 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -21,7 +21,7 @@ import { import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; -import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; +import { getDocumentTypeLabel } from "@/components/documents/DocumentTypeIcon"; import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom"; import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs"; diff --git a/surfsense_web/components/ui/tooltip.tsx b/surfsense_web/components/ui/tooltip.tsx index fb6dd17e7..bcf1c72f8 100644 --- a/surfsense_web/components/ui/tooltip.tsx +++ b/surfsense_web/components/ui/tooltip.tsx @@ -72,7 +72,7 @@ function TooltipContent({ data-slot="tooltip-content" sideOffset={sideOffset} className={cn( - "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-balance pointer-events-none select-none", + "bg-black text-white font-medium shadow-xl px-3 py-1.5 dark:bg-zinc-800 dark:text-zinc-50 border-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md text-xs text-pretty pointer-events-none select-none", className )} {...props}