import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { createRoot } from 'react-dom/client'; import axios from 'axios'; import { HashRouter, Navigate, Route, Routes, useNavigate, useParams, } from 'react-router-dom'; import './styles.css'; const API_ROOT = (process.env.API_BASE_URL || '').replace(/\/$/, ''); const api = axios.create({ baseURL: API_ROOT ? `${API_ROOT}/api` : '/api', withCredentials: true, }); const DEFAULT_FOLDER_NAME = 'All Documents'; const hasFiles = (event) => Array.from(event.dataTransfer?.types || []).includes('Files'); const IconChevronRight = ({ className }) => ( ); const IconTrash = ({ className }) => ( ); const IconDownload = ({ className }) => ( ); const IconFolder = ({ className }) => ( ); const createRootNode = () => ({ id: 'root', name: DEFAULT_FOLDER_NAME, parentId: null, children: [], expanded: true, loaded: false, }); const StatusBanner = ({ status }) => { if (!status) return null; return
{status.message}
; }; const DropOverlay = ({ active, folderName }) => (
Drop files to upload to {folderName}
); const LoginView = ({ onSubmit, status }) => (

Paperless-NEO

Authenticate to manage your documents.

); const FolderNode = ({ node, depth, isSelected, onToggle, onSelect, onDrop, onDragOver, onDragLeave, onDelete, renderChildren, onFolderDragStart, onFolderDragEnd, draggingFolderId, }) => { const canToggle = node.id === 'root' || node.children.length > 0 || !node.loaded; const icon = canToggle ? : null; const canDrag = node.id !== 'root'; const isDragging = draggingFolderId === node.id; const handleToggleClick = (event) => { event.stopPropagation(); if (canToggle) { onToggle(node.id); } }; return (
  • onSelect(node.id)} onDragOver={(event) => onDragOver(event, node.id)} onDragLeave={onDragLeave} onDrop={(event) => onDrop(event, node.id)} onDragStart={(event) => { if (!canDrag || !onFolderDragStart) return; onFolderDragStart(event, node.id); }} onDragEnd={(event) => { if (onFolderDragEnd) { onFolderDragEnd(event); } }} > {icon} {node.name} {node.id !== 'root' && ( )}
    {node.expanded && node.children.length > 0 && ( )}
  • ); }; const FilterBar = ({ query, onQueryChange, tags, activeTagIds, onToggleTag, onClear, hasFilters, }) => (
    onQueryChange(event.target.value)} />
    {tags.length ? ( tags.map((tag) => ( )) ) : ( No tags yet )}
    {hasFilters && ( )}
    ); const DocumentsTable = ({ currentFolderName, breadcrumbs, onRefresh, subfolders, documents, searchResults, isFilterActive, onFolderSelect, onFolderDrop, onFolderDragOver, onFolderDragLeave, onFolderDragStart, onFolderDragEnd, draggedFolderId, onFolderDelete, onDocumentRowClick, onDocumentOpen, selectedDocumentIds, focusedDocumentId, draggingDocumentIds = [], onDocumentDragStart, onDocumentDragEnd, onDownload, filterBar, }) => { const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; const selectedSet = useMemo( () => new Set(selectedDocumentIds), [selectedDocumentIds], ); const draggingSet = useMemo( () => new Set(draggingDocumentIds || []), [draggingDocumentIds], ); return (
    {showingSearchResults && (
    Search results
    )}
    {filterBar}
    {!showingSearchResults && !subfolders.length && rows.length === 0 ? (
    Drop files anywhere or onto a folder to upload documents.
    ) : ( {!showingSearchResults && subfolders.map((folder) => { const canDragFolder = folder.id !== 'root'; const isDraggingFolder = draggedFolderId === folder.id; return ( onFolderSelect(folder.id)} onDragOver={(event) => onFolderDragOver(event, folder.id)} onDragLeave={onFolderDragLeave} onDrop={(event) => onFolderDrop(event, folder.id)} draggable={canDragFolder} onDragStart={(event) => { if (canDragFolder) { onFolderDragStart(event, folder.id); } }} onDragEnd={(event) => { if (canDragFolder) { onFolderDragEnd(event); } }} > ); })} {rows.map((doc) => { const isSelected = selectedSet.has(doc.id); const rowClasses = ['document']; if (isSelected) rowClasses.push('selected'); if (focusedDocumentId === doc.id) rowClasses.push('focused'); if (draggingSet.has(doc.id)) { rowClasses.push('dragging'); } return ( onDocumentRowClick(doc.id, event)} onDoubleClick={(event) => { event.stopPropagation(); if (onDocumentOpen) { onDocumentOpen(doc.id); } }} draggable onDragStart={(event) => onDocumentDragStart(event, doc.id)} onDragEnd={onDocumentDragEnd} > ); })}
    Preview Name Type Updated Actions
    {folder.name} Folder
    {doc.thumbnail ? ( {`Thumbnail ) : (
    DOC
    )}
    {doc.title || doc.original_name} {(doc.tags || []).length > 0 && (
    {(doc.tags || []).map((tag) => ( {tag.label} ))}
    )}
    {doc.content_type || 'Document'} { doc.updated_at ? new Date(doc.updated_at).toLocaleString() : '—' }
    )}
    {rows.length === 0 && isFilterActive && (
    No documents match the current filters.
    )} {showingSearchResults && rows.length > 0 && (
    Showing {rows.length} document{rows.length === 1 ? '' : 's'} in {currentFolderName} and subfolders.
    )}
    ); }; const computeStackAngle = (docId, index) => { if (index === 0) return 0; let hash = 0; const source = docId || `stack-${index}`; for (let i = 0; i < source.length; i += 1) { hash = (hash * 31 + source.charCodeAt(i)) % 997; } const magnitude = Math.max(3, (hash % 13) + 3); // 3..15 const sign = index % 2 === 0 ? 1 : -1; return magnitude * sign; }; const MAX_PREVIEW_STACK_ITEMS = 15; const PreviewStack = ({ items = [], maxItems = MAX_PREVIEW_STACK_ITEMS, emptyMessage = 'Preview unavailable', onItemActivate, onOpenPreview, activeItemId = null, }) => { if (!items.length) { return {emptyMessage}; } const limited = items.slice(0, maxItems); const hasMultiple = limited.length > 1; const preparedItems = useMemo( () => limited.map((entry, index) => ({ entry, angle: index === 0 ? 0 : computeStackAngle(entry.id, index), offset: 0, })), [limited], ); return (
    {preparedItems.map(({ entry, angle, offset }, index) => { const transform = hasMultiple ? `translate(-50%, -50%) rotate(${angle}deg)` : 'translate(-50%, -50%)'; const isFront = index === 0; return (
    {entry.alt { event.stopPropagation(); if (isFront && onOpenPreview) { onOpenPreview(entry.id); } else if (onItemActivate) { onItemActivate(entry.id); } }} onKeyDown={(event) => { if (!onItemActivate && !onOpenPreview) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); if (isFront && onOpenPreview) { onOpenPreview(entry.id); } else { onItemActivate?.(entry.id); } } }} />
    ); })}
    ); }; const DetailPanel = ({ selectedDocuments = [], detailMap = new Map(), tags = [], onDownload, onTagAdd, onTagRemove, onRegenerateThumbnails, previewEntry, onOpenPreview, onBulkTagAdd, onBulkTagRemove, onBulkMove, onBulkReanalyze, folderOptions = [], defaultMoveTarget = 'root', onPromoteSelection, activePreviewId = null, }) => { const lookup = detailMap && typeof detailMap.get === 'function' ? detailMap : new Map(); const selectedCount = selectedDocuments.length; const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null; const detail = singleDoc ? lookup.get(singleDoc.id) || null : null; const handlePreviewActivate = useCallback( (docId) => { if (!docId) return; onPromoteSelection?.(docId); }, [onPromoteSelection], ); const makePreviewItem = useCallback((doc, detailEntry, fallbackUrl = null) => { if (!doc) return null; const url = doc.thumbnail?.url || detailEntry?.document?.thumbnail?.url || fallbackUrl; if (!url) return null; const asset = detailEntry?.assets?.find((item) => item.asset_type === 'thumbnail'); const width = asset?.width || 0; const height = asset?.height || 0; const orientation = width > 0 && height > 0 ? (width >= height ? 'landscape' : 'portrait') : 'landscape'; return { id: doc.id, url, orientation, alt: doc.title || doc.original_name || 'Document preview', }; }, []); const stackDocuments = useMemo(() => { if (!selectedDocuments.length) return []; const seen = new Set(); const ordered = []; for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) { const doc = selectedDocuments[index]; if (!doc?.id || seen.has(doc.id)) continue; seen.add(doc.id); ordered.push(doc); if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) { break; } } return ordered; }, [selectedDocuments]); const singlePreviewItems = useMemo(() => { if (!singleDoc) return []; const item = makePreviewItem(singleDoc, detail, previewEntry?.url || null); return item ? [item] : []; }, [singleDoc, detail, previewEntry, makePreviewItem]); const stackPreviews = useMemo( () => stackDocuments .map((doc) => makePreviewItem(doc, lookup.get(doc.id))) .filter(Boolean), [stackDocuments, lookup, makePreviewItem], ); const commonTags = useMemo(() => { if (selectedCount < 2) return []; const tagSets = selectedDocuments.map((doc) => new Set((doc.tags || []).map((tag) => tag.label))); if (!tagSets.length) return []; const intersection = new Set(tagSets[0]); tagSets.slice(1).forEach((set) => { [...intersection].forEach((label) => { if (!set.has(label)) { intersection.delete(label); } }); }); return [...intersection]; }, [selectedDocuments, selectedCount]); const stackTotalSizeBytes = useMemo(() => { if (!stackPreviews.length) return 0; return stackPreviews.reduce((sum, item) => { const detailEntry = lookup.get(item.id); const bytes = detailEntry?.current_version?.size_bytes || 0; return sum + (typeof bytes === 'number' ? bytes : 0); }, 0); }, [stackPreviews, lookup]); const renderSingle = () => { if (!singleDoc) { return

    Select a document to view metadata, tags and actions.

    ; } if (!detail) { return

    Loading details…

    ; } const displayName = singleDoc.title || singleDoc.original_name; return ( <>

    {displayName}

    Uploaded:{' '} {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
    Size:{' '} {detail.current_version ? `${(detail.current_version.size_bytes / 1024).toFixed(1)} KB` : '—'}
    Type: {singleDoc.content_type || 'Unknown'}
    Issued:{' '} {detail.document.issued_at ? new Date(detail.document.issued_at).toLocaleString() : '—'}
    Original filename:{' '} {singleDoc.original_name}
    Tags
    {detail.document.tags?.length ? ( detail.document.tags.map((tag) => ( {tag.label}{' '} )) ) : ( No tags yet. )}
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onTagAdd(singleDoc, value, input); }} > {tags.map((tag) => (
    {detail.document.metadata && Object.keys(detail.document.metadata).length > 0 && (
    Metadata
    {JSON.stringify(detail.document.metadata, null, 2)}
    )} ); }; const renderBulk = () => { const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`; const sizeLabel = stackTotalSizeBytes ? `${(stackTotalSizeBytes / 1024).toFixed(1)} KB` : '—'; return ( <>

    {countLabel}

    Total size (stack): {sizeLabel}
    Common tags:{' '} {commonTags.length ? commonTags.join(', ') : 'None'}
    {commonTags.length > 0 && (
    {commonTags.map((label) => ( {label} ))}
    )}
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onBulkTagAdd?.({ label: value, input }); }} >
    { event.preventDefault(); const input = event.currentTarget.elements.tag; const value = input.value.trim(); if (!value) return; onBulkTagRemove?.({ label: value, input }); }} >
    {tags.map((tag) => (
    ); }; return ( ); }; const PreviewWorkspace = ({ document, detail, previewEntry, onClose, onDownload, onRegenerateThumbnails, }) => { if (!document) { return null; } const title = detail?.document?.title || document.title || detail?.document?.original_name || document.original_name; const mime = previewEntry?.contentType || document.content_type || 'application/pdf'; return (

    {title}

    {document.content_type || 'Document'} {detail?.current_version?.size_bytes ? ` · ${(detail.current_version.size_bytes / 1024 / 1024).toFixed(2)} MB` : ''}
    {!previewEntry?.url ? (
    Loading preview…
    ) : (