import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { resolveDocumentAssetUrl } from '../asset_manager'; import { getTagColorStyle } from '../utils/colors'; import { DownloadIcon, FolderIcon } from '../ui/icons'; const FilterBar = ({ query, onQueryChange, tags, activeTagIds, onToggleTag, onClear, hasFilters, }) => (
onQueryChange(event.target.value)} />
{tags.length ? ( tags.map((tag) => { const isActive = activeTagIds.includes(tag.id); const style = getTagColorStyle(tag.color); const buttonStyle = style ? { ...style, opacity: isActive ? 1 : 0.95, boxShadow: isActive ? '0 0 0 1px var(--shadow-soft)' : undefined, } : undefined; return ( ); }) ) : ( No tags yet )}
{hasFilters && ( )}
); const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt }) => { const url = useMemo( () => resolveDocumentAssetUrl(document, 'thumbnail', { ensureAssetUrl, getAsset: getDocumentAsset, }), [document, ensureAssetUrl, getDocumentAsset], ); if (url) { return ( {alt ); } return
DOC
; }; const DocumentsTable = ({ currentFolderName, breadcrumbs, onRefresh, onShowSkeuoWorkspace = () => {}, onRequestCreateFolder, creatingFolder = false, subfolders, documents, searchResults, isFilterActive, onFolderSelect, onFolderDrop, onFolderDragOver, onFolderDragLeave, onFolderDragStart, onFolderDragEnd, draggedFolderId, onFolderDelete, onDocumentRowClick, onDocumentOpen, selectedDocumentIds, focusedDocumentId, focusedRowKey, draggingDocumentIds = [], onDocumentDragStart, onDocumentDragEnd, onDocumentDelete, filterBar, tagLookupById, onDocumentListFocus, onDocumentListKeyDown, onFocusedRowChange, ensureAssetUrl = null, getDocumentAsset = () => null, getDownloadHref, }) => { const showingSearchResults = searchResults !== null; const rows = showingSearchResults ? searchResults : documents; const selectedSet = useMemo( () => new Set(selectedDocumentIds), [selectedDocumentIds], ); const draggingSet = useMemo( () => new Set(draggingDocumentIds || []), [draggingDocumentIds], ); const scrollRef = useRef(null); 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]); return (
{showingSearchResults && (
Search results
)}
{filterBar}
{ 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; return ( { onFolderSelect(folder.id); if (scrollRef.current) { scrollRef.current.focus({ preventScroll: true }); } onFocusedRowChange?.(`folder:${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} > ); })}
Preview Name Type Updated Actions
{folder.name} 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 ( {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 && isFilterActive && (
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 { FilterBar, DocumentThumbnailImage };