import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { resolveDocumentAssetUrl } from '../asset_manager'; import { getTagColorStyle } from '../utils/colors'; import { DownloadIcon, FolderIcon, EditIcon } from '../ui/icons'; const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag']; const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt }) => { const url = useMemo( () => resolveDocumentAssetUrl(document, 'thumbnail', { ensureAssetUrl, getAsset: getDocumentAsset, }), [document, ensureAssetUrl, getDocumentAsset], ); return (
{url ? ( {alt event.preventDefault()} /> ) : (
DOC
)}
); }; const DocumentsTable = ({ currentFolderName, breadcrumbs, onRefresh, onShowSkeuoWorkspace = () => {}, onRequestCreateFolder, creatingFolder = false, subfolders, documents, searchResults, isFilterActive, onFolderSelect, onFolderDrop, onFolderDragOver, onFolderDragLeave, onFolderDragStart, onFolderDragEnd, draggedFolderId, onFolderDelete, selectedFolderIds = [], onFolderRowClick, onDocumentRowClick, onDocumentOpen, selectedDocumentIds, focusedDocumentId, focusedRowKey, draggingDocumentIds = [], onDocumentDragStart, onDocumentDragEnd, onDocumentDelete, onFolderRename, onDocumentRename, tagLookupById, onDocumentListFocus, onDocumentListKeyDown, onFocusedRowChange, ensureAssetUrl = null, getDocumentAsset = () => null, getDownloadHref, onTagClick, isSearchLoading = false, onDocumentTagDrop, }) => { const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; const selectedSet = useMemo( () => new Set(selectedDocumentIds), [selectedDocumentIds], ); const selectedFolderSet = useMemo( () => new Set(selectedFolderIds || []), [selectedFolderIds], ); const draggingSet = useMemo( () => new Set(draggingDocumentIds || []), [draggingDocumentIds], ); const scrollRef = useRef(null); const isTagDragEvent = useCallback((event) => { const types = Array.from(event.dataTransfer?.types || []); return TAG_MIME_TYPES.some((type) => types.includes(type)); }, []); const ensureFocusedRowVisible = useCallback(() => { if (!focusedRowKey) return; const container = scrollRef.current; if (!container) return; let selector = null; if (focusedRowKey.startsWith('document:')) { selector = `#document-row-${focusedRowKey.slice('document:'.length)}`; } else if (focusedRowKey.startsWith('folder:')) { selector = `#folder-row-${focusedRowKey.slice('folder:'.length)}`; } if (!selector) { return; } const row = container.querySelector(selector); if (!row || !container.contains(row)) { return; } const header = container.querySelector('thead'); const headerHeight = header ? header.getBoundingClientRect().height : 0; const rowTop = row.offsetTop; const rowBottom = rowTop + row.offsetHeight; const visibleTop = container.scrollTop + headerHeight; const visibleBottom = container.scrollTop + container.clientHeight; if (rowTop < visibleTop) { container.scrollTop = Math.max(rowTop - headerHeight, 0); return; } if (rowBottom > visibleBottom) { const nextScrollTop = rowBottom - container.clientHeight; container.scrollTop = Math.max(nextScrollTop, 0); } }, [focusedRowKey]); useEffect(() => { ensureFocusedRowVisible(); }, [ensureFocusedRowVisible]); const activeDescendantId = useMemo(() => { if (!focusedRowKey) return undefined; if (focusedRowKey.startsWith('document:')) { return `document-row-${focusedRowKey.slice('document:'.length)}`; } if (focusedRowKey.startsWith('folder:')) { return `folder-row-${focusedRowKey.slice('folder:'.length)}`; } return undefined; }, [focusedRowKey]); const handleDocumentTagDragOver = useCallback( (event) => { if (!isTagDragEvent(event)) { return; } event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; event.currentTarget.classList.add('tag-drop-target'); }, [isTagDragEvent], ); const handleDocumentTagDragLeave = useCallback( (event) => { if (!isTagDragEvent(event)) { return; } if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) { return; } event.currentTarget.classList.remove('tag-drop-target'); }, [isTagDragEvent], ); const handleDocumentTagDrop = useCallback( (event, documentId) => { if (!isTagDragEvent(event)) { return; } event.preventDefault(); event.stopPropagation(); event.currentTarget.classList.remove('tag-drop-target'); const payload = event.dataTransfer.getData('application/x-papercrate-tag') || event.dataTransfer.getData('text/papercrate-tag'); if (!payload) { return; } try { const parsed = JSON.parse(payload); if (parsed?.id && onDocumentTagDrop) { onDocumentTagDrop(documentId, parsed); } } catch ( // eslint-disable-next-line no-empty error ) {} }, [isTagDragEvent, onDocumentTagDrop], ); return (
{showingSearchResults && (
Search results
)}
{ if (event.target === scrollRef.current) { onDocumentListFocus?.(); } }} onKeyDown={(event) => { if (event.target !== scrollRef.current) { return; } if (onDocumentListKeyDown) { onDocumentListKeyDown(event); } }} aria-activedescendant={activeDescendantId} > {!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; const isSelectedFolder = selectedFolderSet.has(folder.id); return ( { onFolderRowClick?.(folder.id, event); const shouldNavigate = !event.defaultPrevented && !event.metaKey && !event.ctrlKey && !event.shiftKey; if (shouldNavigate) { onFolderSelect(folder.id); } if (scrollRef.current) { scrollRef.current.focus({ preventScroll: true }); } onFocusedRowChange?.(`folder:${folder.id}`); }} onDoubleClick={(event) => { event.preventDefault(); 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 isDraggingDoc = draggingSet.has(doc.id); const rowClasses = ['document']; if (isSelected) rowClasses.push('selected'); if (isDraggingDoc) rowClasses.push('is-dragging'); const downloadHref = getDownloadHref?.(doc) || null; return ( onDocumentRowClick(doc.id, event)} onDoubleClick={() => onDocumentOpen(doc.id)} draggable onDragStart={(event) => onDocumentDragStart(event, doc)} onDragEnd={onDocumentDragEnd} onDragOver={handleDocumentTagDragOver} onDragLeave={handleDocumentTagDragLeave} onDrop={(event) => handleDocumentTagDrop(event, doc.id)} > ); })}
Preview Name Type Updated Actions
{folder.name} {folder.id !== 'root' && ( )}
Folder
{doc.title || doc.original_name}
{(doc.tags || []).length > 0 && (
{(doc.tags || []).map((tag) => { const colorSource = tag?.color || tagLookupById?.get(tag.id)?.color; const style = getTagColorStyle(colorSource); return ( { event.stopPropagation(); if (onTagClick) { onTagClick(tag.id); } }} role="button" tabIndex={0} draggable onDragStart={(event) => { event.stopPropagation(); try { if (event.dataTransfer) { event.dataTransfer.effectAllowed = 'copyMove'; } const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id, }); event.dataTransfer?.setData('application/x-papercrate-tag', payload); event.dataTransfer?.setData('text/papercrate-tag', payload); event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); } catch ( // eslint-disable-next-line no-empty error ) {} }} onDragEnd={(event) => { event.stopPropagation(); }} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.stopPropagation(); if (onTagClick) { onTagClick(tag.id); } } }} > {tag.label} ); })}
)}
{doc.content_type || 'Document'} {doc.updated_at ? new Date(doc.updated_at).toLocaleString() : '—'}
{downloadHref ? ( event.stopPropagation()} onAuxClick={(event) => event.stopPropagation()} onContextMenu={(event) => event.stopPropagation()} > Download ) : ( No download )}
)}
{rows.length === 0 && showingSearchResults && !isSearchLoading && (
No documents match the current filters.
)} {showingSearchResults && rows.length > 0 && (
Showing {rows.length} document{rows.length === 1 ? '' : 's'} in {currentFolderName} and subfolders.
)}
); }; export default DocumentsTable; export { DocumentThumbnailImage };